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)
183 .expect("valid date components")
184 });
185 if date >= year_start {
186 date.year()
187 } else {
188 date.year() - 1
189 }
190 }
191 FiscalCalendarType::FourFourFive(_) | FiscalCalendarType::ThirteenPeriod(_) => {
192 date.year()
209 }
210 }
211 }
212
213 pub fn fiscal_period(&self, date: NaiveDate) -> u8 {
215 match &self.calendar_type {
216 FiscalCalendarType::CalendarYear => date.month() as u8,
217 FiscalCalendarType::CustomYearStart {
218 start_month,
219 start_day: _,
220 } => {
221 let month = date.month() as u8;
222 if month >= *start_month {
223 month - start_month + 1
224 } else {
225 12 - start_month + month + 1
226 }
227 }
228 FiscalCalendarType::ThirteenPeriod(_) => {
229 let day_of_year = date.ordinal();
236 ((day_of_year - 1) / 28 + 1).min(13) as u8
237 }
238 FiscalCalendarType::FourFourFive(config) => {
239 let weeks = config.pattern.weeks_per_period();
257 let week_of_year = (date.ordinal() as u8 - 1) / 7 + 1;
258 let mut cumulative = 0u8;
259 for (quarter, _) in (0..4).enumerate() {
260 for (period_in_q, &period_weeks) in weeks.iter().enumerate() {
261 cumulative += period_weeks;
262 if week_of_year <= cumulative {
263 return (quarter * 3 + period_in_q + 1) as u8;
264 }
265 }
266 }
267 12 }
269 }
270 }
271}
272
273fn month_name(month: u8) -> &'static str {
275 match month {
276 1 => "January",
277 2 => "February",
278 3 => "March",
279 4 => "April",
280 5 => "May",
281 6 => "June",
282 7 => "July",
283 8 => "August",
284 9 => "September",
285 10 => "October",
286 11 => "November",
287 12 => "December",
288 _ => "Unknown",
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Hash)]
294pub struct FiscalPeriod {
295 pub year: i32,
297 pub period: u8,
299 pub start_date: NaiveDate,
301 pub end_date: NaiveDate,
303 pub period_type: FiscalPeriodType,
305 pub is_year_end: bool,
307 pub status: PeriodStatus,
309}
310
311impl FiscalPeriod {
312 pub fn monthly(year: i32, month: u8) -> Self {
314 let start_date =
315 NaiveDate::from_ymd_opt(year, month as u32, 1).expect("valid date components");
316 let end_date = if month == 12 {
317 NaiveDate::from_ymd_opt(year + 1, 1, 1)
318 .expect("valid date components")
319 .pred_opt()
320 .expect("valid date components")
321 } else {
322 NaiveDate::from_ymd_opt(year, month as u32 + 1, 1)
323 .expect("valid date components")
324 .pred_opt()
325 .expect("valid date components")
326 };
327
328 Self {
329 year,
330 period: month,
331 start_date,
332 end_date,
333 period_type: FiscalPeriodType::Monthly,
334 is_year_end: month == 12,
335 status: PeriodStatus::Open,
336 }
337 }
338
339 pub fn quarterly(year: i32, quarter: u8) -> Self {
341 let start_month = (quarter - 1) * 3 + 1;
342 let end_month = quarter * 3;
343
344 let start_date =
345 NaiveDate::from_ymd_opt(year, start_month as u32, 1).expect("valid date components");
346 let end_date = if end_month == 12 {
347 NaiveDate::from_ymd_opt(year + 1, 1, 1)
348 .expect("valid date components")
349 .pred_opt()
350 .expect("valid date components")
351 } else {
352 NaiveDate::from_ymd_opt(year, end_month as u32 + 1, 1)
353 .expect("valid date components")
354 .pred_opt()
355 .expect("valid date components")
356 };
357
358 Self {
359 year,
360 period: quarter,
361 start_date,
362 end_date,
363 period_type: FiscalPeriodType::Quarterly,
364 is_year_end: quarter == 4,
365 status: PeriodStatus::Open,
366 }
367 }
368
369 pub fn days(&self) -> i64 {
371 (self.end_date - self.start_date).num_days() + 1
372 }
373
374 pub fn key(&self) -> String {
376 format!("{}-{:02}", self.year, self.period)
377 }
378
379 pub fn contains(&self, date: NaiveDate) -> bool {
381 date >= self.start_date && date <= self.end_date
382 }
383
384 pub fn from_calendar(calendar: &FiscalCalendar, date: NaiveDate) -> Self {
389 let fiscal_year = calendar.fiscal_year(date);
390 let period_num = calendar.fiscal_period(date);
391
392 match &calendar.calendar_type {
393 FiscalCalendarType::CalendarYear => Self::monthly(fiscal_year, period_num),
394 FiscalCalendarType::CustomYearStart {
395 start_month,
396 start_day,
397 } => {
398 let period_start_month = if *start_month + period_num - 1 > 12 {
400 start_month + period_num - 1 - 12
401 } else {
402 start_month + period_num - 1
403 };
404 let period_year = if *start_month + period_num - 1 > 12 {
405 fiscal_year + 1
406 } else {
407 fiscal_year
408 };
409
410 let start_date = if period_num == 1 {
411 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, *start_day as u32)
412 .unwrap_or_else(|| {
413 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, 1)
414 .expect("valid date components")
415 })
416 } else {
417 NaiveDate::from_ymd_opt(period_year, period_start_month as u32, 1)
418 .expect("valid date components")
419 };
420
421 let end_date = if period_num == 12 {
422 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, *start_day as u32)
424 .unwrap_or_else(|| {
425 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, 1)
426 .expect("valid date components")
427 })
428 .pred_opt()
429 .expect("valid date components")
430 } else {
431 let next_month = if period_start_month == 12 {
432 1
433 } else {
434 period_start_month + 1
435 };
436 let next_year = if period_start_month == 12 {
437 period_year + 1
438 } else {
439 period_year
440 };
441 NaiveDate::from_ymd_opt(next_year, next_month as u32, 1)
442 .expect("valid date components")
443 .pred_opt()
444 .expect("valid date components")
445 };
446
447 Self {
448 year: fiscal_year,
449 period: period_num,
450 start_date,
451 end_date,
452 period_type: FiscalPeriodType::Monthly,
453 is_year_end: period_num == 12,
454 status: PeriodStatus::Open,
455 }
456 }
457 FiscalCalendarType::FourFourFive(config) => {
458 let weeks = config.pattern.weeks_per_period();
460 let quarter = (period_num - 1) / 3;
461 let period_in_quarter = (period_num - 1) % 3;
462 let period_weeks = weeks[period_in_quarter as usize];
463
464 let year_start =
466 NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
467
468 let mut weeks_before = 0u32;
470 for _ in 0..quarter {
471 for &w in &weeks {
472 weeks_before += w as u32;
473 }
474 }
475 for p in 0..period_in_quarter {
476 weeks_before += weeks[p as usize] as u32;
477 }
478
479 let start_date = year_start + chrono::Duration::weeks(weeks_before as i64);
480 let end_date = start_date + chrono::Duration::weeks(period_weeks as i64)
481 - chrono::Duration::days(1);
482
483 Self {
484 year: fiscal_year,
485 period: period_num,
486 start_date,
487 end_date,
488 period_type: FiscalPeriodType::FourWeek,
489 is_year_end: period_num == 12,
490 status: PeriodStatus::Open,
491 }
492 }
493 FiscalCalendarType::ThirteenPeriod(_) => {
494 let year_start =
496 NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
497 let start_date = year_start + chrono::Duration::days((period_num as i64 - 1) * 28);
498 let end_date = start_date + chrono::Duration::days(27);
499
500 Self {
501 year: fiscal_year,
502 period: period_num,
503 start_date,
504 end_date,
505 period_type: FiscalPeriodType::FourWeek,
506 is_year_end: period_num == 13,
507 status: PeriodStatus::Open,
508 }
509 }
510 }
511 }
512}
513
514#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
516pub enum FiscalPeriodType {
517 Monthly,
519 Quarterly,
521 FourWeek,
523 Special,
525}
526
527#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
529pub enum PeriodStatus {
530 Open,
532 SoftClosed,
534 Closed,
536 Locked,
538}
539
540#[derive(Debug, Clone, PartialEq, Eq, Hash)]
542pub enum CloseTask {
543 RunDepreciation,
545 PostInventoryRevaluation,
547 ReconcileArToGl,
549 ReconcileApToGl,
551 ReconcileFaToGl,
553 ReconcileInventoryToGl,
555 PostAccruedExpenses,
557 PostAccruedRevenue,
559 PostPrepaidAmortization,
561 AllocateCorporateOverhead,
563 PostIntercompanySettlements,
565 RevalueForeignCurrency,
567 CalculateTaxProvision,
569 TranslateForeignSubsidiaries,
571 EliminateIntercompany,
573 GenerateTrialBalance,
575 GenerateFinancialStatements,
577 CloseIncomeStatement,
579 PostRetainedEarningsRollforward,
581 Custom(String),
583}
584
585impl CloseTask {
586 pub fn is_year_end_only(&self) -> bool {
588 matches!(
589 self,
590 CloseTask::CloseIncomeStatement | CloseTask::PostRetainedEarningsRollforward
591 )
592 }
593
594 pub fn name(&self) -> &str {
596 match self {
597 CloseTask::RunDepreciation => "Run Depreciation",
598 CloseTask::PostInventoryRevaluation => "Post Inventory Revaluation",
599 CloseTask::ReconcileArToGl => "Reconcile AR to GL",
600 CloseTask::ReconcileApToGl => "Reconcile AP to GL",
601 CloseTask::ReconcileFaToGl => "Reconcile FA to GL",
602 CloseTask::ReconcileInventoryToGl => "Reconcile Inventory to GL",
603 CloseTask::PostAccruedExpenses => "Post Accrued Expenses",
604 CloseTask::PostAccruedRevenue => "Post Accrued Revenue",
605 CloseTask::PostPrepaidAmortization => "Post Prepaid Amortization",
606 CloseTask::AllocateCorporateOverhead => "Allocate Corporate Overhead",
607 CloseTask::PostIntercompanySettlements => "Post IC Settlements",
608 CloseTask::RevalueForeignCurrency => "Revalue Foreign Currency",
609 CloseTask::CalculateTaxProvision => "Calculate Tax Provision",
610 CloseTask::TranslateForeignSubsidiaries => "Translate Foreign Subs",
611 CloseTask::EliminateIntercompany => "Eliminate Intercompany",
612 CloseTask::GenerateTrialBalance => "Generate Trial Balance",
613 CloseTask::GenerateFinancialStatements => "Generate Financials",
614 CloseTask::CloseIncomeStatement => "Close Income Statement",
615 CloseTask::PostRetainedEarningsRollforward => "Post RE Rollforward",
616 CloseTask::Custom(name) => name,
617 }
618 }
619}
620
621#[derive(Debug, Clone, PartialEq, Eq)]
623pub enum CloseTaskStatus {
624 Pending,
626 InProgress,
628 Completed,
630 CompletedWithWarnings(Vec<String>),
632 Failed(String),
634 Skipped(String),
636}
637
638#[derive(Debug, Clone)]
640pub struct CloseTaskResult {
641 pub task: CloseTask,
643 pub company_code: String,
645 pub fiscal_period: FiscalPeriod,
647 pub status: CloseTaskStatus,
649 pub started_at: Option<NaiveDate>,
651 pub completed_at: Option<NaiveDate>,
653 pub journal_entries_created: u32,
655 pub total_amount: Decimal,
657 pub notes: Vec<String>,
659}
660
661impl CloseTaskResult {
662 pub fn new(task: CloseTask, company_code: String, fiscal_period: FiscalPeriod) -> Self {
664 Self {
665 task,
666 company_code,
667 fiscal_period,
668 status: CloseTaskStatus::Pending,
669 started_at: None,
670 completed_at: None,
671 journal_entries_created: 0,
672 total_amount: Decimal::ZERO,
673 notes: Vec::new(),
674 }
675 }
676
677 pub fn is_success(&self) -> bool {
679 matches!(
680 self.status,
681 CloseTaskStatus::Completed | CloseTaskStatus::CompletedWithWarnings(_)
682 )
683 }
684}
685
686#[derive(Debug, Clone)]
688pub struct AccrualDefinition {
689 pub accrual_id: String,
691 pub company_code: String,
693 pub description: String,
695 pub accrual_type: AccrualType,
697 pub expense_revenue_account: String,
699 pub accrual_account: String,
701 pub calculation_method: AccrualCalculationMethod,
703 pub fixed_amount: Option<Decimal>,
705 pub percentage_rate: Option<Decimal>,
707 pub base_account: Option<String>,
709 pub frequency: AccrualFrequency,
711 pub auto_reverse: bool,
713 pub cost_center: Option<String>,
715 pub is_active: bool,
717 pub effective_from: NaiveDate,
719 pub effective_to: Option<NaiveDate>,
721}
722
723impl AccrualDefinition {
724 pub fn new(
726 accrual_id: String,
727 company_code: String,
728 description: String,
729 accrual_type: AccrualType,
730 expense_revenue_account: String,
731 accrual_account: String,
732 ) -> Self {
733 Self {
734 accrual_id,
735 company_code,
736 description,
737 accrual_type,
738 expense_revenue_account,
739 accrual_account,
740 calculation_method: AccrualCalculationMethod::FixedAmount,
741 fixed_amount: None,
742 percentage_rate: None,
743 base_account: None,
744 frequency: AccrualFrequency::Monthly,
745 auto_reverse: true,
746 cost_center: None,
747 is_active: true,
748 effective_from: NaiveDate::from_ymd_opt(2020, 1, 1).expect("valid date components"),
749 effective_to: None,
750 }
751 }
752
753 pub fn with_fixed_amount(mut self, amount: Decimal) -> Self {
755 self.calculation_method = AccrualCalculationMethod::FixedAmount;
756 self.fixed_amount = Some(amount);
757 self
758 }
759
760 pub fn with_percentage(mut self, rate: Decimal, base_account: &str) -> Self {
762 self.calculation_method = AccrualCalculationMethod::PercentageOfBase;
763 self.percentage_rate = Some(rate);
764 self.base_account = Some(base_account.to_string());
765 self
766 }
767
768 pub fn is_effective_on(&self, date: NaiveDate) -> bool {
770 if !self.is_active {
771 return false;
772 }
773 if date < self.effective_from {
774 return false;
775 }
776 if let Some(end) = self.effective_to {
777 if date > end {
778 return false;
779 }
780 }
781 true
782 }
783}
784
785#[derive(Debug, Clone, Copy, PartialEq, Eq)]
787pub enum AccrualType {
788 AccruedExpense,
790 AccruedRevenue,
792 PrepaidExpense,
794 DeferredRevenue,
796}
797
798#[derive(Debug, Clone, Copy, PartialEq, Eq)]
800pub enum AccrualCalculationMethod {
801 FixedAmount,
803 PercentageOfBase,
805 DaysBased,
807 Manual,
809}
810
811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
813pub enum AccrualFrequency {
814 Monthly,
816 Quarterly,
818 Annually,
820}
821
822#[derive(Debug, Clone)]
824pub struct OverheadAllocation {
825 pub allocation_id: String,
827 pub source_company: String,
829 pub source_cost_center: String,
831 pub source_account: String,
833 pub allocation_basis: AllocationBasis,
835 pub targets: Vec<AllocationTarget>,
837 pub description: String,
839 pub is_active: bool,
841}
842
843#[derive(Debug, Clone, PartialEq, Eq)]
845pub enum AllocationBasis {
846 Revenue,
848 Headcount,
850 DirectCosts,
852 SquareFootage,
854 FixedPercentage,
856 Custom(String),
858}
859
860#[derive(Debug, Clone)]
862pub struct AllocationTarget {
863 pub company_code: String,
865 pub cost_center: String,
867 pub account: String,
869 pub percentage: Option<Decimal>,
871 pub driver_value: Option<Decimal>,
873}
874
875#[derive(Debug, Clone)]
877pub struct CloseSchedule {
878 pub schedule_id: String,
880 pub company_code: String,
882 pub period_type: FiscalPeriodType,
884 pub tasks: Vec<ScheduledCloseTask>,
886 pub is_year_end: bool,
888}
889
890impl CloseSchedule {
891 pub fn standard_monthly(company_code: &str) -> Self {
893 Self {
894 schedule_id: format!("MONTHLY-{company_code}"),
895 company_code: company_code.to_string(),
896 period_type: FiscalPeriodType::Monthly,
897 tasks: vec![
898 ScheduledCloseTask::new(CloseTask::RunDepreciation, 1),
899 ScheduledCloseTask::new(CloseTask::PostInventoryRevaluation, 2),
900 ScheduledCloseTask::new(CloseTask::PostAccruedExpenses, 3),
901 ScheduledCloseTask::new(CloseTask::PostAccruedRevenue, 4),
902 ScheduledCloseTask::new(CloseTask::PostPrepaidAmortization, 5),
903 ScheduledCloseTask::new(CloseTask::RevalueForeignCurrency, 6),
904 ScheduledCloseTask::new(CloseTask::ReconcileArToGl, 7),
905 ScheduledCloseTask::new(CloseTask::ReconcileApToGl, 8),
906 ScheduledCloseTask::new(CloseTask::ReconcileFaToGl, 9),
907 ScheduledCloseTask::new(CloseTask::ReconcileInventoryToGl, 10),
908 ScheduledCloseTask::new(CloseTask::PostIntercompanySettlements, 11),
909 ScheduledCloseTask::new(CloseTask::AllocateCorporateOverhead, 12),
910 ScheduledCloseTask::new(CloseTask::TranslateForeignSubsidiaries, 13),
911 ScheduledCloseTask::new(CloseTask::EliminateIntercompany, 14),
912 ScheduledCloseTask::new(CloseTask::GenerateTrialBalance, 15),
913 ],
914 is_year_end: false,
915 }
916 }
917
918 pub fn year_end(company_code: &str) -> Self {
920 let mut schedule = Self::standard_monthly(company_code);
921 schedule.schedule_id = format!("YEAREND-{company_code}");
922 schedule.is_year_end = true;
923
924 let next_seq = schedule.tasks.len() as u32 + 1;
926 schedule.tasks.push(ScheduledCloseTask::new(
927 CloseTask::CalculateTaxProvision,
928 next_seq,
929 ));
930 schedule.tasks.push(ScheduledCloseTask::new(
931 CloseTask::CloseIncomeStatement,
932 next_seq + 1,
933 ));
934 schedule.tasks.push(ScheduledCloseTask::new(
935 CloseTask::PostRetainedEarningsRollforward,
936 next_seq + 2,
937 ));
938 schedule.tasks.push(ScheduledCloseTask::new(
939 CloseTask::GenerateFinancialStatements,
940 next_seq + 3,
941 ));
942
943 schedule
944 }
945}
946
947#[derive(Debug, Clone)]
949pub struct ScheduledCloseTask {
950 pub task: CloseTask,
952 pub sequence: u32,
954 pub depends_on: Vec<CloseTask>,
956 pub is_mandatory: bool,
958 pub can_parallelize: bool,
960}
961
962impl ScheduledCloseTask {
963 pub fn new(task: CloseTask, sequence: u32) -> Self {
965 Self {
966 task,
967 sequence,
968 depends_on: Vec::new(),
969 is_mandatory: true,
970 can_parallelize: false,
971 }
972 }
973
974 pub fn depends_on(mut self, task: CloseTask) -> Self {
976 self.depends_on.push(task);
977 self
978 }
979
980 pub fn optional(mut self) -> Self {
982 self.is_mandatory = false;
983 self
984 }
985
986 pub fn parallelizable(mut self) -> Self {
988 self.can_parallelize = true;
989 self
990 }
991}
992
993#[derive(Debug, Clone)]
995pub struct YearEndClosingSpec {
996 pub company_code: String,
998 pub fiscal_year: i32,
1000 pub revenue_accounts: Vec<String>,
1002 pub expense_accounts: Vec<String>,
1004 pub income_summary_account: String,
1006 pub retained_earnings_account: String,
1008 pub dividend_account: Option<String>,
1010}
1011
1012impl Default for YearEndClosingSpec {
1013 fn default() -> Self {
1014 Self {
1015 company_code: String::new(),
1016 fiscal_year: 0,
1017 revenue_accounts: vec!["4".to_string()], expense_accounts: vec!["5".to_string(), "6".to_string()], income_summary_account: "3500".to_string(),
1020 retained_earnings_account: "3300".to_string(),
1021 dividend_account: Some("3400".to_string()),
1022 }
1023 }
1024}
1025
1026#[derive(Debug, Clone)]
1028pub struct TaxProvisionInput {
1029 pub company_code: String,
1031 pub fiscal_year: i32,
1033 pub pretax_income: Decimal,
1035 pub permanent_differences: Vec<TaxAdjustment>,
1037 pub temporary_differences: Vec<TaxAdjustment>,
1039 pub statutory_rate: Decimal,
1041 pub tax_credits: Decimal,
1043 pub prior_year_adjustment: Decimal,
1045}
1046
1047#[derive(Debug, Clone)]
1049pub struct TaxAdjustment {
1050 pub description: String,
1052 pub amount: Decimal,
1054 pub is_addition: bool,
1056}
1057
1058#[derive(Debug, Clone)]
1060pub struct TaxProvisionResult {
1061 pub company_code: String,
1063 pub fiscal_year: i32,
1065 pub pretax_income: Decimal,
1067 pub permanent_differences: Decimal,
1069 pub taxable_income: Decimal,
1071 pub current_tax_expense: Decimal,
1073 pub deferred_tax_expense: Decimal,
1075 pub total_tax_expense: Decimal,
1077 pub effective_rate: Decimal,
1079}
1080
1081impl TaxProvisionResult {
1082 pub fn calculate(input: &TaxProvisionInput) -> Self {
1084 let permanent_diff: Decimal = input
1085 .permanent_differences
1086 .iter()
1087 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1088 .sum();
1089
1090 let temporary_diff: Decimal = input
1091 .temporary_differences
1092 .iter()
1093 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1094 .sum();
1095
1096 let taxable_income = input.pretax_income + permanent_diff;
1097 let current_tax = (taxable_income * input.statutory_rate / dec!(100)).round_dp(2);
1098 let deferred_tax = (temporary_diff * input.statutory_rate / dec!(100)).round_dp(2);
1099
1100 let total_tax =
1101 current_tax + deferred_tax - input.tax_credits + input.prior_year_adjustment;
1102
1103 let effective_rate = if input.pretax_income != Decimal::ZERO {
1104 (total_tax / input.pretax_income * dec!(100)).round_dp(2)
1105 } else {
1106 Decimal::ZERO
1107 };
1108
1109 Self {
1110 company_code: input.company_code.clone(),
1111 fiscal_year: input.fiscal_year,
1112 pretax_income: input.pretax_income,
1113 permanent_differences: permanent_diff,
1114 taxable_income,
1115 current_tax_expense: current_tax,
1116 deferred_tax_expense: deferred_tax,
1117 total_tax_expense: total_tax,
1118 effective_rate,
1119 }
1120 }
1121}
1122
1123#[derive(Debug, Clone)]
1125pub struct PeriodCloseRun {
1126 pub run_id: String,
1128 pub company_code: String,
1130 pub fiscal_period: FiscalPeriod,
1132 pub status: PeriodCloseStatus,
1134 pub task_results: Vec<CloseTaskResult>,
1136 pub started_at: Option<NaiveDate>,
1138 pub completed_at: Option<NaiveDate>,
1140 pub total_journal_entries: u32,
1142 pub errors: Vec<String>,
1144}
1145
1146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1148pub enum PeriodCloseStatus {
1149 NotStarted,
1151 InProgress,
1153 Completed,
1155 CompletedWithErrors,
1157 Failed,
1159}
1160
1161impl PeriodCloseRun {
1162 pub fn new(run_id: String, company_code: String, fiscal_period: FiscalPeriod) -> Self {
1164 Self {
1165 run_id,
1166 company_code,
1167 fiscal_period,
1168 status: PeriodCloseStatus::NotStarted,
1169 task_results: Vec::new(),
1170 started_at: None,
1171 completed_at: None,
1172 total_journal_entries: 0,
1173 errors: Vec::new(),
1174 }
1175 }
1176
1177 pub fn is_success(&self) -> bool {
1179 self.status == PeriodCloseStatus::Completed
1180 }
1181
1182 pub fn failed_task_count(&self) -> usize {
1184 self.task_results
1185 .iter()
1186 .filter(|r| matches!(r.status, CloseTaskStatus::Failed(_)))
1187 .count()
1188 }
1189}
1190
1191#[cfg(test)]
1192mod tests {
1193 use super::*;
1194
1195 #[test]
1196 fn test_fiscal_period_monthly() {
1197 let period = FiscalPeriod::monthly(2024, 1);
1198 assert_eq!(
1199 period.start_date,
1200 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()
1201 );
1202 assert_eq!(
1203 period.end_date,
1204 NaiveDate::from_ymd_opt(2024, 1, 31).unwrap()
1205 );
1206 assert_eq!(period.days(), 31);
1207 assert!(!period.is_year_end);
1208
1209 let dec_period = FiscalPeriod::monthly(2024, 12);
1210 assert!(dec_period.is_year_end);
1211 }
1212
1213 #[test]
1214 fn test_fiscal_period_quarterly() {
1215 let q1 = FiscalPeriod::quarterly(2024, 1);
1216 assert_eq!(q1.start_date, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1217 assert_eq!(q1.end_date, NaiveDate::from_ymd_opt(2024, 3, 31).unwrap());
1218
1219 let q4 = FiscalPeriod::quarterly(2024, 4);
1220 assert!(q4.is_year_end);
1221 }
1222
1223 #[test]
1224 fn test_close_schedule() {
1225 let schedule = CloseSchedule::standard_monthly("1000");
1226 assert!(!schedule.is_year_end);
1227 assert!(!schedule.tasks.is_empty());
1228
1229 let year_end = CloseSchedule::year_end("1000");
1230 assert!(year_end.is_year_end);
1231 assert!(year_end.tasks.len() > schedule.tasks.len());
1232 }
1233
1234 #[test]
1235 fn test_tax_provision() {
1236 let input = TaxProvisionInput {
1237 company_code: "1000".to_string(),
1238 fiscal_year: 2024,
1239 pretax_income: dec!(1000000),
1240 permanent_differences: vec![TaxAdjustment {
1241 description: "Meals & Entertainment".to_string(),
1242 amount: dec!(10000),
1243 is_addition: true,
1244 }],
1245 temporary_differences: vec![TaxAdjustment {
1246 description: "Depreciation Timing".to_string(),
1247 amount: dec!(50000),
1248 is_addition: false,
1249 }],
1250 statutory_rate: dec!(21),
1251 tax_credits: dec!(5000),
1252 prior_year_adjustment: Decimal::ZERO,
1253 };
1254
1255 let result = TaxProvisionResult::calculate(&input);
1256 assert_eq!(result.taxable_income, dec!(1010000)); assert!(result.current_tax_expense > Decimal::ZERO);
1258 }
1259}