1use chrono::{Datelike, NaiveDate};
12use rust_decimal::Decimal;
13use rust_decimal_macros::dec;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
18#[serde(tag = "type", rename_all = "snake_case")]
19pub enum FiscalCalendarType {
20 #[default]
22 CalendarYear,
23 CustomYearStart {
25 start_month: u8,
27 start_day: u8,
29 },
30 FourFourFive(FourFourFiveConfig),
32 ThirteenPeriod(ThirteenPeriodConfig),
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct FourFourFiveConfig {
39 pub pattern: WeekPattern,
41 pub anchor: FourFourFiveAnchor,
43 pub leap_week_placement: LeapWeekPlacement,
45}
46
47impl Default for FourFourFiveConfig {
48 fn default() -> Self {
49 Self {
50 pattern: WeekPattern::FourFourFive,
51 anchor: FourFourFiveAnchor::LastSaturdayOf(1), leap_week_placement: LeapWeekPlacement::Q4Period3,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum WeekPattern {
61 FourFourFive,
63 FourFiveFour,
65 FiveFourFour,
67}
68
69impl WeekPattern {
70 pub fn weeks_per_period(&self) -> [u8; 3] {
72 match self {
73 WeekPattern::FourFourFive => [4, 4, 5],
74 WeekPattern::FourFiveFour => [4, 5, 4],
75 WeekPattern::FiveFourFour => [5, 4, 4],
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(tag = "type", content = "month", rename_all = "snake_case")]
83pub enum FourFourFiveAnchor {
84 FirstSundayOf(u8),
86 LastSaturdayOf(u8),
88 NearestSaturdayTo(u8),
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum LeapWeekPlacement {
96 Q4Period3,
98 Q1Period1,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct ThirteenPeriodConfig {
105 pub year_start_day: u16,
107 pub year_start_month: u8,
109}
110
111impl Default for ThirteenPeriodConfig {
112 fn default() -> Self {
113 Self {
114 year_start_day: 1, year_start_month: 1, }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct FiscalCalendar {
123 pub calendar_type: FiscalCalendarType,
125 pub name: String,
127}
128
129impl Default for FiscalCalendar {
130 fn default() -> Self {
131 Self {
132 calendar_type: FiscalCalendarType::CalendarYear,
133 name: "Calendar Year".to_string(),
134 }
135 }
136}
137
138impl FiscalCalendar {
139 pub fn calendar_year() -> Self {
141 Self::default()
142 }
143
144 pub fn custom_year_start(start_month: u8, start_day: u8) -> Self {
146 Self {
147 calendar_type: FiscalCalendarType::CustomYearStart {
148 start_month,
149 start_day,
150 },
151 name: format!("Fiscal Year ({})", month_name(start_month)),
152 }
153 }
154
155 pub fn four_four_five(config: FourFourFiveConfig) -> Self {
157 Self {
158 calendar_type: FiscalCalendarType::FourFourFive(config),
159 name: "Retail 4-4-5".to_string(),
160 }
161 }
162
163 pub fn thirteen_period(config: ThirteenPeriodConfig) -> Self {
165 Self {
166 calendar_type: FiscalCalendarType::ThirteenPeriod(config),
167 name: "13-Period".to_string(),
168 }
169 }
170
171 pub fn fiscal_year(&self, date: NaiveDate) -> i32 {
173 match &self.calendar_type {
174 FiscalCalendarType::CalendarYear => date.year(),
175 FiscalCalendarType::CustomYearStart {
176 start_month,
177 start_day,
178 } => {
179 let year_start =
180 NaiveDate::from_ymd_opt(date.year(), *start_month as u32, *start_day as u32)
181 .unwrap_or_else(|| {
182 NaiveDate::from_ymd_opt(date.year(), *start_month as u32, 1).unwrap()
183 });
184 if date >= year_start {
185 date.year()
186 } else {
187 date.year() - 1
188 }
189 }
190 FiscalCalendarType::FourFourFive(_) | FiscalCalendarType::ThirteenPeriod(_) => {
191 date.year()
193 }
194 }
195 }
196
197 pub fn fiscal_period(&self, date: NaiveDate) -> u8 {
199 match &self.calendar_type {
200 FiscalCalendarType::CalendarYear => date.month() as u8,
201 FiscalCalendarType::CustomYearStart {
202 start_month,
203 start_day: _,
204 } => {
205 let month = date.month() as u8;
206 if month >= *start_month {
207 month - start_month + 1
208 } else {
209 12 - start_month + month + 1
210 }
211 }
212 FiscalCalendarType::ThirteenPeriod(_) => {
213 let day_of_year = date.ordinal();
215 ((day_of_year - 1) / 28 + 1).min(13) as u8
216 }
217 FiscalCalendarType::FourFourFive(config) => {
218 let weeks = config.pattern.weeks_per_period();
220 let week_of_year = (date.ordinal() as u8 - 1) / 7 + 1;
221 let mut cumulative = 0u8;
222 for (quarter, _) in (0..4).enumerate() {
223 for (period_in_q, &period_weeks) in weeks.iter().enumerate() {
224 cumulative += period_weeks;
225 if week_of_year <= cumulative {
226 return (quarter * 3 + period_in_q + 1) as u8;
227 }
228 }
229 }
230 12 }
232 }
233 }
234}
235
236fn month_name(month: u8) -> &'static str {
238 match month {
239 1 => "January",
240 2 => "February",
241 3 => "March",
242 4 => "April",
243 5 => "May",
244 6 => "June",
245 7 => "July",
246 8 => "August",
247 9 => "September",
248 10 => "October",
249 11 => "November",
250 12 => "December",
251 _ => "Unknown",
252 }
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257pub struct FiscalPeriod {
258 pub year: i32,
260 pub period: u8,
262 pub start_date: NaiveDate,
264 pub end_date: NaiveDate,
266 pub period_type: FiscalPeriodType,
268 pub is_year_end: bool,
270 pub status: PeriodStatus,
272}
273
274impl FiscalPeriod {
275 pub fn monthly(year: i32, month: u8) -> Self {
277 let start_date = NaiveDate::from_ymd_opt(year, month as u32, 1).unwrap();
278 let end_date = if month == 12 {
279 NaiveDate::from_ymd_opt(year + 1, 1, 1)
280 .unwrap()
281 .pred_opt()
282 .unwrap()
283 } else {
284 NaiveDate::from_ymd_opt(year, month as u32 + 1, 1)
285 .unwrap()
286 .pred_opt()
287 .unwrap()
288 };
289
290 Self {
291 year,
292 period: month,
293 start_date,
294 end_date,
295 period_type: FiscalPeriodType::Monthly,
296 is_year_end: month == 12,
297 status: PeriodStatus::Open,
298 }
299 }
300
301 pub fn quarterly(year: i32, quarter: u8) -> Self {
303 let start_month = (quarter - 1) * 3 + 1;
304 let end_month = quarter * 3;
305
306 let start_date = NaiveDate::from_ymd_opt(year, start_month as u32, 1).unwrap();
307 let end_date = if end_month == 12 {
308 NaiveDate::from_ymd_opt(year + 1, 1, 1)
309 .unwrap()
310 .pred_opt()
311 .unwrap()
312 } else {
313 NaiveDate::from_ymd_opt(year, end_month as u32 + 1, 1)
314 .unwrap()
315 .pred_opt()
316 .unwrap()
317 };
318
319 Self {
320 year,
321 period: quarter,
322 start_date,
323 end_date,
324 period_type: FiscalPeriodType::Quarterly,
325 is_year_end: quarter == 4,
326 status: PeriodStatus::Open,
327 }
328 }
329
330 pub fn days(&self) -> i64 {
332 (self.end_date - self.start_date).num_days() + 1
333 }
334
335 pub fn key(&self) -> String {
337 format!("{}-{:02}", self.year, self.period)
338 }
339
340 pub fn contains(&self, date: NaiveDate) -> bool {
342 date >= self.start_date && date <= self.end_date
343 }
344
345 pub fn from_calendar(calendar: &FiscalCalendar, date: NaiveDate) -> Self {
350 let fiscal_year = calendar.fiscal_year(date);
351 let period_num = calendar.fiscal_period(date);
352
353 match &calendar.calendar_type {
354 FiscalCalendarType::CalendarYear => Self::monthly(fiscal_year, period_num),
355 FiscalCalendarType::CustomYearStart {
356 start_month,
357 start_day,
358 } => {
359 let period_start_month = if *start_month + period_num - 1 > 12 {
361 start_month + period_num - 1 - 12
362 } else {
363 start_month + period_num - 1
364 };
365 let period_year = if *start_month + period_num - 1 > 12 {
366 fiscal_year + 1
367 } else {
368 fiscal_year
369 };
370
371 let start_date = if period_num == 1 {
372 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, *start_day as u32)
373 .unwrap_or_else(|| {
374 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, 1).unwrap()
375 })
376 } else {
377 NaiveDate::from_ymd_opt(period_year, period_start_month as u32, 1).unwrap()
378 };
379
380 let end_date = if period_num == 12 {
381 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, *start_day as u32)
383 .unwrap_or_else(|| {
384 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, 1)
385 .unwrap()
386 })
387 .pred_opt()
388 .unwrap()
389 } else {
390 let next_month = if period_start_month == 12 {
391 1
392 } else {
393 period_start_month + 1
394 };
395 let next_year = if period_start_month == 12 {
396 period_year + 1
397 } else {
398 period_year
399 };
400 NaiveDate::from_ymd_opt(next_year, next_month as u32, 1)
401 .unwrap()
402 .pred_opt()
403 .unwrap()
404 };
405
406 Self {
407 year: fiscal_year,
408 period: period_num,
409 start_date,
410 end_date,
411 period_type: FiscalPeriodType::Monthly,
412 is_year_end: period_num == 12,
413 status: PeriodStatus::Open,
414 }
415 }
416 FiscalCalendarType::FourFourFive(config) => {
417 let weeks = config.pattern.weeks_per_period();
419 let quarter = (period_num - 1) / 3;
420 let period_in_quarter = (period_num - 1) % 3;
421 let period_weeks = weeks[period_in_quarter as usize];
422
423 let year_start = NaiveDate::from_ymd_opt(fiscal_year, 1, 1).unwrap();
425
426 let mut weeks_before = 0u32;
428 for _ in 0..quarter {
429 for &w in &weeks {
430 weeks_before += w as u32;
431 }
432 }
433 for p in 0..period_in_quarter {
434 weeks_before += weeks[p as usize] as u32;
435 }
436
437 let start_date = year_start + chrono::Duration::weeks(weeks_before as i64);
438 let end_date = start_date + chrono::Duration::weeks(period_weeks as i64)
439 - chrono::Duration::days(1);
440
441 Self {
442 year: fiscal_year,
443 period: period_num,
444 start_date,
445 end_date,
446 period_type: FiscalPeriodType::FourWeek,
447 is_year_end: period_num == 12,
448 status: PeriodStatus::Open,
449 }
450 }
451 FiscalCalendarType::ThirteenPeriod(_) => {
452 let year_start = NaiveDate::from_ymd_opt(fiscal_year, 1, 1).unwrap();
454 let start_date = year_start + chrono::Duration::days((period_num as i64 - 1) * 28);
455 let end_date = start_date + chrono::Duration::days(27);
456
457 Self {
458 year: fiscal_year,
459 period: period_num,
460 start_date,
461 end_date,
462 period_type: FiscalPeriodType::FourWeek,
463 is_year_end: period_num == 13,
464 status: PeriodStatus::Open,
465 }
466 }
467 }
468 }
469}
470
471#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
473pub enum FiscalPeriodType {
474 Monthly,
476 Quarterly,
478 FourWeek,
480 Special,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
486pub enum PeriodStatus {
487 Open,
489 SoftClosed,
491 Closed,
493 Locked,
495}
496
497#[derive(Debug, Clone, PartialEq, Eq, Hash)]
499pub enum CloseTask {
500 RunDepreciation,
502 PostInventoryRevaluation,
504 ReconcileArToGl,
506 ReconcileApToGl,
508 ReconcileFaToGl,
510 ReconcileInventoryToGl,
512 PostAccruedExpenses,
514 PostAccruedRevenue,
516 PostPrepaidAmortization,
518 AllocateCorporateOverhead,
520 PostIntercompanySettlements,
522 RevalueForeignCurrency,
524 CalculateTaxProvision,
526 TranslateForeignSubsidiaries,
528 EliminateIntercompany,
530 GenerateTrialBalance,
532 GenerateFinancialStatements,
534 CloseIncomeStatement,
536 PostRetainedEarningsRollforward,
538 Custom(String),
540}
541
542impl CloseTask {
543 pub fn is_year_end_only(&self) -> bool {
545 matches!(
546 self,
547 CloseTask::CloseIncomeStatement | CloseTask::PostRetainedEarningsRollforward
548 )
549 }
550
551 pub fn name(&self) -> &str {
553 match self {
554 CloseTask::RunDepreciation => "Run Depreciation",
555 CloseTask::PostInventoryRevaluation => "Post Inventory Revaluation",
556 CloseTask::ReconcileArToGl => "Reconcile AR to GL",
557 CloseTask::ReconcileApToGl => "Reconcile AP to GL",
558 CloseTask::ReconcileFaToGl => "Reconcile FA to GL",
559 CloseTask::ReconcileInventoryToGl => "Reconcile Inventory to GL",
560 CloseTask::PostAccruedExpenses => "Post Accrued Expenses",
561 CloseTask::PostAccruedRevenue => "Post Accrued Revenue",
562 CloseTask::PostPrepaidAmortization => "Post Prepaid Amortization",
563 CloseTask::AllocateCorporateOverhead => "Allocate Corporate Overhead",
564 CloseTask::PostIntercompanySettlements => "Post IC Settlements",
565 CloseTask::RevalueForeignCurrency => "Revalue Foreign Currency",
566 CloseTask::CalculateTaxProvision => "Calculate Tax Provision",
567 CloseTask::TranslateForeignSubsidiaries => "Translate Foreign Subs",
568 CloseTask::EliminateIntercompany => "Eliminate Intercompany",
569 CloseTask::GenerateTrialBalance => "Generate Trial Balance",
570 CloseTask::GenerateFinancialStatements => "Generate Financials",
571 CloseTask::CloseIncomeStatement => "Close Income Statement",
572 CloseTask::PostRetainedEarningsRollforward => "Post RE Rollforward",
573 CloseTask::Custom(name) => name,
574 }
575 }
576}
577
578#[derive(Debug, Clone, PartialEq, Eq)]
580pub enum CloseTaskStatus {
581 Pending,
583 InProgress,
585 Completed,
587 CompletedWithWarnings(Vec<String>),
589 Failed(String),
591 Skipped(String),
593}
594
595#[derive(Debug, Clone)]
597pub struct CloseTaskResult {
598 pub task: CloseTask,
600 pub company_code: String,
602 pub fiscal_period: FiscalPeriod,
604 pub status: CloseTaskStatus,
606 pub started_at: Option<NaiveDate>,
608 pub completed_at: Option<NaiveDate>,
610 pub journal_entries_created: u32,
612 pub total_amount: Decimal,
614 pub notes: Vec<String>,
616}
617
618impl CloseTaskResult {
619 pub fn new(task: CloseTask, company_code: String, fiscal_period: FiscalPeriod) -> Self {
621 Self {
622 task,
623 company_code,
624 fiscal_period,
625 status: CloseTaskStatus::Pending,
626 started_at: None,
627 completed_at: None,
628 journal_entries_created: 0,
629 total_amount: Decimal::ZERO,
630 notes: Vec::new(),
631 }
632 }
633
634 pub fn is_success(&self) -> bool {
636 matches!(
637 self.status,
638 CloseTaskStatus::Completed | CloseTaskStatus::CompletedWithWarnings(_)
639 )
640 }
641}
642
643#[derive(Debug, Clone)]
645pub struct AccrualDefinition {
646 pub accrual_id: String,
648 pub company_code: String,
650 pub description: String,
652 pub accrual_type: AccrualType,
654 pub expense_revenue_account: String,
656 pub accrual_account: String,
658 pub calculation_method: AccrualCalculationMethod,
660 pub fixed_amount: Option<Decimal>,
662 pub percentage_rate: Option<Decimal>,
664 pub base_account: Option<String>,
666 pub frequency: AccrualFrequency,
668 pub auto_reverse: bool,
670 pub cost_center: Option<String>,
672 pub is_active: bool,
674 pub effective_from: NaiveDate,
676 pub effective_to: Option<NaiveDate>,
678}
679
680impl AccrualDefinition {
681 pub fn new(
683 accrual_id: String,
684 company_code: String,
685 description: String,
686 accrual_type: AccrualType,
687 expense_revenue_account: String,
688 accrual_account: String,
689 ) -> Self {
690 Self {
691 accrual_id,
692 company_code,
693 description,
694 accrual_type,
695 expense_revenue_account,
696 accrual_account,
697 calculation_method: AccrualCalculationMethod::FixedAmount,
698 fixed_amount: None,
699 percentage_rate: None,
700 base_account: None,
701 frequency: AccrualFrequency::Monthly,
702 auto_reverse: true,
703 cost_center: None,
704 is_active: true,
705 effective_from: NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
706 effective_to: None,
707 }
708 }
709
710 pub fn with_fixed_amount(mut self, amount: Decimal) -> Self {
712 self.calculation_method = AccrualCalculationMethod::FixedAmount;
713 self.fixed_amount = Some(amount);
714 self
715 }
716
717 pub fn with_percentage(mut self, rate: Decimal, base_account: &str) -> Self {
719 self.calculation_method = AccrualCalculationMethod::PercentageOfBase;
720 self.percentage_rate = Some(rate);
721 self.base_account = Some(base_account.to_string());
722 self
723 }
724
725 pub fn is_effective_on(&self, date: NaiveDate) -> bool {
727 if !self.is_active {
728 return false;
729 }
730 if date < self.effective_from {
731 return false;
732 }
733 if let Some(end) = self.effective_to {
734 if date > end {
735 return false;
736 }
737 }
738 true
739 }
740}
741
742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
744pub enum AccrualType {
745 AccruedExpense,
747 AccruedRevenue,
749 PrepaidExpense,
751 DeferredRevenue,
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Eq)]
757pub enum AccrualCalculationMethod {
758 FixedAmount,
760 PercentageOfBase,
762 DaysBased,
764 Manual,
766}
767
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
770pub enum AccrualFrequency {
771 Monthly,
773 Quarterly,
775 Annually,
777}
778
779#[derive(Debug, Clone)]
781pub struct OverheadAllocation {
782 pub allocation_id: String,
784 pub source_company: String,
786 pub source_cost_center: String,
788 pub source_account: String,
790 pub allocation_basis: AllocationBasis,
792 pub targets: Vec<AllocationTarget>,
794 pub description: String,
796 pub is_active: bool,
798}
799
800#[derive(Debug, Clone, PartialEq, Eq)]
802pub enum AllocationBasis {
803 Revenue,
805 Headcount,
807 DirectCosts,
809 SquareFootage,
811 FixedPercentage,
813 Custom(String),
815}
816
817#[derive(Debug, Clone)]
819pub struct AllocationTarget {
820 pub company_code: String,
822 pub cost_center: String,
824 pub account: String,
826 pub percentage: Option<Decimal>,
828 pub driver_value: Option<Decimal>,
830}
831
832#[derive(Debug, Clone)]
834pub struct CloseSchedule {
835 pub schedule_id: String,
837 pub company_code: String,
839 pub period_type: FiscalPeriodType,
841 pub tasks: Vec<ScheduledCloseTask>,
843 pub is_year_end: bool,
845}
846
847impl CloseSchedule {
848 pub fn standard_monthly(company_code: &str) -> Self {
850 Self {
851 schedule_id: format!("MONTHLY-{}", company_code),
852 company_code: company_code.to_string(),
853 period_type: FiscalPeriodType::Monthly,
854 tasks: vec![
855 ScheduledCloseTask::new(CloseTask::RunDepreciation, 1),
856 ScheduledCloseTask::new(CloseTask::PostInventoryRevaluation, 2),
857 ScheduledCloseTask::new(CloseTask::PostAccruedExpenses, 3),
858 ScheduledCloseTask::new(CloseTask::PostAccruedRevenue, 4),
859 ScheduledCloseTask::new(CloseTask::PostPrepaidAmortization, 5),
860 ScheduledCloseTask::new(CloseTask::RevalueForeignCurrency, 6),
861 ScheduledCloseTask::new(CloseTask::ReconcileArToGl, 7),
862 ScheduledCloseTask::new(CloseTask::ReconcileApToGl, 8),
863 ScheduledCloseTask::new(CloseTask::ReconcileFaToGl, 9),
864 ScheduledCloseTask::new(CloseTask::ReconcileInventoryToGl, 10),
865 ScheduledCloseTask::new(CloseTask::PostIntercompanySettlements, 11),
866 ScheduledCloseTask::new(CloseTask::AllocateCorporateOverhead, 12),
867 ScheduledCloseTask::new(CloseTask::TranslateForeignSubsidiaries, 13),
868 ScheduledCloseTask::new(CloseTask::EliminateIntercompany, 14),
869 ScheduledCloseTask::new(CloseTask::GenerateTrialBalance, 15),
870 ],
871 is_year_end: false,
872 }
873 }
874
875 pub fn year_end(company_code: &str) -> Self {
877 let mut schedule = Self::standard_monthly(company_code);
878 schedule.schedule_id = format!("YEAREND-{}", company_code);
879 schedule.is_year_end = true;
880
881 let next_seq = schedule.tasks.len() as u32 + 1;
883 schedule.tasks.push(ScheduledCloseTask::new(
884 CloseTask::CalculateTaxProvision,
885 next_seq,
886 ));
887 schedule.tasks.push(ScheduledCloseTask::new(
888 CloseTask::CloseIncomeStatement,
889 next_seq + 1,
890 ));
891 schedule.tasks.push(ScheduledCloseTask::new(
892 CloseTask::PostRetainedEarningsRollforward,
893 next_seq + 2,
894 ));
895 schedule.tasks.push(ScheduledCloseTask::new(
896 CloseTask::GenerateFinancialStatements,
897 next_seq + 3,
898 ));
899
900 schedule
901 }
902}
903
904#[derive(Debug, Clone)]
906pub struct ScheduledCloseTask {
907 pub task: CloseTask,
909 pub sequence: u32,
911 pub depends_on: Vec<CloseTask>,
913 pub is_mandatory: bool,
915 pub can_parallelize: bool,
917}
918
919impl ScheduledCloseTask {
920 pub fn new(task: CloseTask, sequence: u32) -> Self {
922 Self {
923 task,
924 sequence,
925 depends_on: Vec::new(),
926 is_mandatory: true,
927 can_parallelize: false,
928 }
929 }
930
931 pub fn depends_on(mut self, task: CloseTask) -> Self {
933 self.depends_on.push(task);
934 self
935 }
936
937 pub fn optional(mut self) -> Self {
939 self.is_mandatory = false;
940 self
941 }
942
943 pub fn parallelizable(mut self) -> Self {
945 self.can_parallelize = true;
946 self
947 }
948}
949
950#[derive(Debug, Clone)]
952pub struct YearEndClosingSpec {
953 pub company_code: String,
955 pub fiscal_year: i32,
957 pub revenue_accounts: Vec<String>,
959 pub expense_accounts: Vec<String>,
961 pub income_summary_account: String,
963 pub retained_earnings_account: String,
965 pub dividend_account: Option<String>,
967}
968
969impl Default for YearEndClosingSpec {
970 fn default() -> Self {
971 Self {
972 company_code: String::new(),
973 fiscal_year: 0,
974 revenue_accounts: vec!["4".to_string()], expense_accounts: vec!["5".to_string(), "6".to_string()], income_summary_account: "3500".to_string(),
977 retained_earnings_account: "3300".to_string(),
978 dividend_account: Some("3400".to_string()),
979 }
980 }
981}
982
983#[derive(Debug, Clone)]
985pub struct TaxProvisionInput {
986 pub company_code: String,
988 pub fiscal_year: i32,
990 pub pretax_income: Decimal,
992 pub permanent_differences: Vec<TaxAdjustment>,
994 pub temporary_differences: Vec<TaxAdjustment>,
996 pub statutory_rate: Decimal,
998 pub tax_credits: Decimal,
1000 pub prior_year_adjustment: Decimal,
1002}
1003
1004#[derive(Debug, Clone)]
1006pub struct TaxAdjustment {
1007 pub description: String,
1009 pub amount: Decimal,
1011 pub is_addition: bool,
1013}
1014
1015#[derive(Debug, Clone)]
1017pub struct TaxProvisionResult {
1018 pub company_code: String,
1020 pub fiscal_year: i32,
1022 pub pretax_income: Decimal,
1024 pub permanent_differences: Decimal,
1026 pub taxable_income: Decimal,
1028 pub current_tax_expense: Decimal,
1030 pub deferred_tax_expense: Decimal,
1032 pub total_tax_expense: Decimal,
1034 pub effective_rate: Decimal,
1036}
1037
1038impl TaxProvisionResult {
1039 pub fn calculate(input: &TaxProvisionInput) -> Self {
1041 let permanent_diff: Decimal = input
1042 .permanent_differences
1043 .iter()
1044 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1045 .sum();
1046
1047 let temporary_diff: Decimal = input
1048 .temporary_differences
1049 .iter()
1050 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1051 .sum();
1052
1053 let taxable_income = input.pretax_income + permanent_diff;
1054 let current_tax = (taxable_income * input.statutory_rate / dec!(100)).round_dp(2);
1055 let deferred_tax = (temporary_diff * input.statutory_rate / dec!(100)).round_dp(2);
1056
1057 let total_tax =
1058 current_tax + deferred_tax - input.tax_credits + input.prior_year_adjustment;
1059
1060 let effective_rate = if input.pretax_income != Decimal::ZERO {
1061 (total_tax / input.pretax_income * dec!(100)).round_dp(2)
1062 } else {
1063 Decimal::ZERO
1064 };
1065
1066 Self {
1067 company_code: input.company_code.clone(),
1068 fiscal_year: input.fiscal_year,
1069 pretax_income: input.pretax_income,
1070 permanent_differences: permanent_diff,
1071 taxable_income,
1072 current_tax_expense: current_tax,
1073 deferred_tax_expense: deferred_tax,
1074 total_tax_expense: total_tax,
1075 effective_rate,
1076 }
1077 }
1078}
1079
1080#[derive(Debug, Clone)]
1082pub struct PeriodCloseRun {
1083 pub run_id: String,
1085 pub company_code: String,
1087 pub fiscal_period: FiscalPeriod,
1089 pub status: PeriodCloseStatus,
1091 pub task_results: Vec<CloseTaskResult>,
1093 pub started_at: Option<NaiveDate>,
1095 pub completed_at: Option<NaiveDate>,
1097 pub total_journal_entries: u32,
1099 pub errors: Vec<String>,
1101}
1102
1103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1105pub enum PeriodCloseStatus {
1106 NotStarted,
1108 InProgress,
1110 Completed,
1112 CompletedWithErrors,
1114 Failed,
1116}
1117
1118impl PeriodCloseRun {
1119 pub fn new(run_id: String, company_code: String, fiscal_period: FiscalPeriod) -> Self {
1121 Self {
1122 run_id,
1123 company_code,
1124 fiscal_period,
1125 status: PeriodCloseStatus::NotStarted,
1126 task_results: Vec::new(),
1127 started_at: None,
1128 completed_at: None,
1129 total_journal_entries: 0,
1130 errors: Vec::new(),
1131 }
1132 }
1133
1134 pub fn is_success(&self) -> bool {
1136 self.status == PeriodCloseStatus::Completed
1137 }
1138
1139 pub fn failed_task_count(&self) -> usize {
1141 self.task_results
1142 .iter()
1143 .filter(|r| matches!(r.status, CloseTaskStatus::Failed(_)))
1144 .count()
1145 }
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150 use super::*;
1151
1152 #[test]
1153 fn test_fiscal_period_monthly() {
1154 let period = FiscalPeriod::monthly(2024, 1);
1155 assert_eq!(
1156 period.start_date,
1157 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()
1158 );
1159 assert_eq!(
1160 period.end_date,
1161 NaiveDate::from_ymd_opt(2024, 1, 31).unwrap()
1162 );
1163 assert_eq!(period.days(), 31);
1164 assert!(!period.is_year_end);
1165
1166 let dec_period = FiscalPeriod::monthly(2024, 12);
1167 assert!(dec_period.is_year_end);
1168 }
1169
1170 #[test]
1171 fn test_fiscal_period_quarterly() {
1172 let q1 = FiscalPeriod::quarterly(2024, 1);
1173 assert_eq!(q1.start_date, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1174 assert_eq!(q1.end_date, NaiveDate::from_ymd_opt(2024, 3, 31).unwrap());
1175
1176 let q4 = FiscalPeriod::quarterly(2024, 4);
1177 assert!(q4.is_year_end);
1178 }
1179
1180 #[test]
1181 fn test_close_schedule() {
1182 let schedule = CloseSchedule::standard_monthly("1000");
1183 assert!(!schedule.is_year_end);
1184 assert!(!schedule.tasks.is_empty());
1185
1186 let year_end = CloseSchedule::year_end("1000");
1187 assert!(year_end.is_year_end);
1188 assert!(year_end.tasks.len() > schedule.tasks.len());
1189 }
1190
1191 #[test]
1192 fn test_tax_provision() {
1193 let input = TaxProvisionInput {
1194 company_code: "1000".to_string(),
1195 fiscal_year: 2024,
1196 pretax_income: dec!(1000000),
1197 permanent_differences: vec![TaxAdjustment {
1198 description: "Meals & Entertainment".to_string(),
1199 amount: dec!(10000),
1200 is_addition: true,
1201 }],
1202 temporary_differences: vec![TaxAdjustment {
1203 description: "Depreciation Timing".to_string(),
1204 amount: dec!(50000),
1205 is_addition: false,
1206 }],
1207 statutory_rate: dec!(21),
1208 tax_credits: dec!(5000),
1209 prior_year_adjustment: Decimal::ZERO,
1210 };
1211
1212 let result = TaxProvisionResult::calculate(&input);
1213 assert_eq!(result.taxable_income, dec!(1010000)); assert!(result.current_tax_expense > Decimal::ZERO);
1215 }
1216}