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();
233 ((day_of_year - 1) / 28 + 1).min(13) as u8
234 }
235 FiscalCalendarType::FourFourFive(config) => {
236 let weeks = config.pattern.weeks_per_period();
254 let week_of_year = (date.ordinal() as u8 - 1) / 7 + 1;
255 let mut cumulative = 0u8;
256 for (quarter, _) in (0..4).enumerate() {
257 for (period_in_q, &period_weeks) in weeks.iter().enumerate() {
258 cumulative += period_weeks;
259 if week_of_year <= cumulative {
260 return (quarter * 3 + period_in_q + 1) as u8;
261 }
262 }
263 }
264 12 }
266 }
267 }
268}
269
270fn month_name(month: u8) -> &'static str {
272 match month {
273 1 => "January",
274 2 => "February",
275 3 => "March",
276 4 => "April",
277 5 => "May",
278 6 => "June",
279 7 => "July",
280 8 => "August",
281 9 => "September",
282 10 => "October",
283 11 => "November",
284 12 => "December",
285 _ => "Unknown",
286 }
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Hash)]
291pub struct FiscalPeriod {
292 pub year: i32,
294 pub period: u8,
296 pub start_date: NaiveDate,
298 pub end_date: NaiveDate,
300 pub period_type: FiscalPeriodType,
302 pub is_year_end: bool,
304 pub status: PeriodStatus,
306}
307
308impl FiscalPeriod {
309 pub fn monthly(year: i32, month: u8) -> Self {
311 let start_date =
312 NaiveDate::from_ymd_opt(year, month as u32, 1).expect("valid date components");
313 let end_date = if month == 12 {
314 NaiveDate::from_ymd_opt(year + 1, 1, 1)
315 .expect("valid date components")
316 .pred_opt()
317 .expect("valid date components")
318 } else {
319 NaiveDate::from_ymd_opt(year, month as u32 + 1, 1)
320 .expect("valid date components")
321 .pred_opt()
322 .expect("valid date components")
323 };
324
325 Self {
326 year,
327 period: month,
328 start_date,
329 end_date,
330 period_type: FiscalPeriodType::Monthly,
331 is_year_end: month == 12,
332 status: PeriodStatus::Open,
333 }
334 }
335
336 pub fn quarterly(year: i32, quarter: u8) -> Self {
338 let start_month = (quarter - 1) * 3 + 1;
339 let end_month = quarter * 3;
340
341 let start_date =
342 NaiveDate::from_ymd_opt(year, start_month as u32, 1).expect("valid date components");
343 let end_date = if end_month == 12 {
344 NaiveDate::from_ymd_opt(year + 1, 1, 1)
345 .expect("valid date components")
346 .pred_opt()
347 .expect("valid date components")
348 } else {
349 NaiveDate::from_ymd_opt(year, end_month as u32 + 1, 1)
350 .expect("valid date components")
351 .pred_opt()
352 .expect("valid date components")
353 };
354
355 Self {
356 year,
357 period: quarter,
358 start_date,
359 end_date,
360 period_type: FiscalPeriodType::Quarterly,
361 is_year_end: quarter == 4,
362 status: PeriodStatus::Open,
363 }
364 }
365
366 pub fn days(&self) -> i64 {
368 (self.end_date - self.start_date).num_days() + 1
369 }
370
371 pub fn key(&self) -> String {
373 format!("{}-{:02}", self.year, self.period)
374 }
375
376 pub fn contains(&self, date: NaiveDate) -> bool {
378 date >= self.start_date && date <= self.end_date
379 }
380
381 pub fn from_calendar(calendar: &FiscalCalendar, date: NaiveDate) -> Self {
386 let fiscal_year = calendar.fiscal_year(date);
387 let period_num = calendar.fiscal_period(date);
388
389 match &calendar.calendar_type {
390 FiscalCalendarType::CalendarYear => Self::monthly(fiscal_year, period_num),
391 FiscalCalendarType::CustomYearStart {
392 start_month,
393 start_day,
394 } => {
395 let period_start_month = if *start_month + period_num - 1 > 12 {
397 start_month + period_num - 1 - 12
398 } else {
399 start_month + period_num - 1
400 };
401 let period_year = if *start_month + period_num - 1 > 12 {
402 fiscal_year + 1
403 } else {
404 fiscal_year
405 };
406
407 let start_date = if period_num == 1 {
408 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, *start_day as u32)
409 .unwrap_or_else(|| {
410 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, 1)
411 .expect("valid date components")
412 })
413 } else {
414 NaiveDate::from_ymd_opt(period_year, period_start_month as u32, 1)
415 .expect("valid date components")
416 };
417
418 let end_date = if period_num == 12 {
419 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, *start_day as u32)
421 .unwrap_or_else(|| {
422 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, 1)
423 .expect("valid date components")
424 })
425 .pred_opt()
426 .expect("valid date components")
427 } else {
428 let next_month = if period_start_month == 12 {
429 1
430 } else {
431 period_start_month + 1
432 };
433 let next_year = if period_start_month == 12 {
434 period_year + 1
435 } else {
436 period_year
437 };
438 NaiveDate::from_ymd_opt(next_year, next_month as u32, 1)
439 .expect("valid date components")
440 .pred_opt()
441 .expect("valid date components")
442 };
443
444 Self {
445 year: fiscal_year,
446 period: period_num,
447 start_date,
448 end_date,
449 period_type: FiscalPeriodType::Monthly,
450 is_year_end: period_num == 12,
451 status: PeriodStatus::Open,
452 }
453 }
454 FiscalCalendarType::FourFourFive(config) => {
455 let weeks = config.pattern.weeks_per_period();
457 let quarter = (period_num - 1) / 3;
458 let period_in_quarter = (period_num - 1) % 3;
459 let period_weeks = weeks[period_in_quarter as usize];
460
461 let year_start =
463 NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
464
465 let mut weeks_before = 0u32;
467 for _ in 0..quarter {
468 for &w in &weeks {
469 weeks_before += w as u32;
470 }
471 }
472 for p in 0..period_in_quarter {
473 weeks_before += weeks[p as usize] as u32;
474 }
475
476 let start_date = year_start + chrono::Duration::weeks(weeks_before as i64);
477 let end_date = start_date + chrono::Duration::weeks(period_weeks as i64)
478 - chrono::Duration::days(1);
479
480 Self {
481 year: fiscal_year,
482 period: period_num,
483 start_date,
484 end_date,
485 period_type: FiscalPeriodType::FourWeek,
486 is_year_end: period_num == 12,
487 status: PeriodStatus::Open,
488 }
489 }
490 FiscalCalendarType::ThirteenPeriod(_) => {
491 let year_start =
493 NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
494 let start_date = year_start + chrono::Duration::days((period_num as i64 - 1) * 28);
495 let end_date = start_date + chrono::Duration::days(27);
496
497 Self {
498 year: fiscal_year,
499 period: period_num,
500 start_date,
501 end_date,
502 period_type: FiscalPeriodType::FourWeek,
503 is_year_end: period_num == 13,
504 status: PeriodStatus::Open,
505 }
506 }
507 }
508 }
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
513pub enum FiscalPeriodType {
514 Monthly,
516 Quarterly,
518 FourWeek,
520 Special,
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
526pub enum PeriodStatus {
527 Open,
529 SoftClosed,
531 Closed,
533 Locked,
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Hash)]
539pub enum CloseTask {
540 RunDepreciation,
542 PostInventoryRevaluation,
544 ReconcileArToGl,
546 ReconcileApToGl,
548 ReconcileFaToGl,
550 ReconcileInventoryToGl,
552 PostAccruedExpenses,
554 PostAccruedRevenue,
556 PostPrepaidAmortization,
558 AllocateCorporateOverhead,
560 PostIntercompanySettlements,
562 RevalueForeignCurrency,
564 CalculateTaxProvision,
566 TranslateForeignSubsidiaries,
568 EliminateIntercompany,
570 GenerateTrialBalance,
572 GenerateFinancialStatements,
574 CloseIncomeStatement,
576 PostRetainedEarningsRollforward,
578 Custom(String),
580}
581
582impl CloseTask {
583 pub fn is_year_end_only(&self) -> bool {
585 matches!(
586 self,
587 CloseTask::CloseIncomeStatement | CloseTask::PostRetainedEarningsRollforward
588 )
589 }
590
591 pub fn name(&self) -> &str {
593 match self {
594 CloseTask::RunDepreciation => "Run Depreciation",
595 CloseTask::PostInventoryRevaluation => "Post Inventory Revaluation",
596 CloseTask::ReconcileArToGl => "Reconcile AR to GL",
597 CloseTask::ReconcileApToGl => "Reconcile AP to GL",
598 CloseTask::ReconcileFaToGl => "Reconcile FA to GL",
599 CloseTask::ReconcileInventoryToGl => "Reconcile Inventory to GL",
600 CloseTask::PostAccruedExpenses => "Post Accrued Expenses",
601 CloseTask::PostAccruedRevenue => "Post Accrued Revenue",
602 CloseTask::PostPrepaidAmortization => "Post Prepaid Amortization",
603 CloseTask::AllocateCorporateOverhead => "Allocate Corporate Overhead",
604 CloseTask::PostIntercompanySettlements => "Post IC Settlements",
605 CloseTask::RevalueForeignCurrency => "Revalue Foreign Currency",
606 CloseTask::CalculateTaxProvision => "Calculate Tax Provision",
607 CloseTask::TranslateForeignSubsidiaries => "Translate Foreign Subs",
608 CloseTask::EliminateIntercompany => "Eliminate Intercompany",
609 CloseTask::GenerateTrialBalance => "Generate Trial Balance",
610 CloseTask::GenerateFinancialStatements => "Generate Financials",
611 CloseTask::CloseIncomeStatement => "Close Income Statement",
612 CloseTask::PostRetainedEarningsRollforward => "Post RE Rollforward",
613 CloseTask::Custom(name) => name,
614 }
615 }
616}
617
618#[derive(Debug, Clone, PartialEq, Eq)]
620pub enum CloseTaskStatus {
621 Pending,
623 InProgress,
625 Completed,
627 CompletedWithWarnings(Vec<String>),
629 Failed(String),
631 Skipped(String),
633}
634
635#[derive(Debug, Clone)]
637pub struct CloseTaskResult {
638 pub task: CloseTask,
640 pub company_code: String,
642 pub fiscal_period: FiscalPeriod,
644 pub status: CloseTaskStatus,
646 pub started_at: Option<NaiveDate>,
648 pub completed_at: Option<NaiveDate>,
650 pub journal_entries_created: u32,
652 pub total_amount: Decimal,
654 pub notes: Vec<String>,
656}
657
658impl CloseTaskResult {
659 pub fn new(task: CloseTask, company_code: String, fiscal_period: FiscalPeriod) -> Self {
661 Self {
662 task,
663 company_code,
664 fiscal_period,
665 status: CloseTaskStatus::Pending,
666 started_at: None,
667 completed_at: None,
668 journal_entries_created: 0,
669 total_amount: Decimal::ZERO,
670 notes: Vec::new(),
671 }
672 }
673
674 pub fn is_success(&self) -> bool {
676 matches!(
677 self.status,
678 CloseTaskStatus::Completed | CloseTaskStatus::CompletedWithWarnings(_)
679 )
680 }
681}
682
683#[derive(Debug, Clone)]
685pub struct AccrualDefinition {
686 pub accrual_id: String,
688 pub company_code: String,
690 pub description: String,
692 pub accrual_type: AccrualType,
694 pub expense_revenue_account: String,
696 pub accrual_account: String,
698 pub calculation_method: AccrualCalculationMethod,
700 pub fixed_amount: Option<Decimal>,
702 pub percentage_rate: Option<Decimal>,
704 pub base_account: Option<String>,
706 pub frequency: AccrualFrequency,
708 pub auto_reverse: bool,
710 pub cost_center: Option<String>,
712 pub is_active: bool,
714 pub effective_from: NaiveDate,
716 pub effective_to: Option<NaiveDate>,
718}
719
720impl AccrualDefinition {
721 pub fn new(
723 accrual_id: String,
724 company_code: String,
725 description: String,
726 accrual_type: AccrualType,
727 expense_revenue_account: String,
728 accrual_account: String,
729 ) -> Self {
730 Self {
731 accrual_id,
732 company_code,
733 description,
734 accrual_type,
735 expense_revenue_account,
736 accrual_account,
737 calculation_method: AccrualCalculationMethod::FixedAmount,
738 fixed_amount: None,
739 percentage_rate: None,
740 base_account: None,
741 frequency: AccrualFrequency::Monthly,
742 auto_reverse: true,
743 cost_center: None,
744 is_active: true,
745 effective_from: NaiveDate::from_ymd_opt(2020, 1, 1).expect("valid date components"),
746 effective_to: None,
747 }
748 }
749
750 pub fn with_fixed_amount(mut self, amount: Decimal) -> Self {
752 self.calculation_method = AccrualCalculationMethod::FixedAmount;
753 self.fixed_amount = Some(amount);
754 self
755 }
756
757 pub fn with_percentage(mut self, rate: Decimal, base_account: &str) -> Self {
759 self.calculation_method = AccrualCalculationMethod::PercentageOfBase;
760 self.percentage_rate = Some(rate);
761 self.base_account = Some(base_account.to_string());
762 self
763 }
764
765 pub fn is_effective_on(&self, date: NaiveDate) -> bool {
767 if !self.is_active {
768 return false;
769 }
770 if date < self.effective_from {
771 return false;
772 }
773 if let Some(end) = self.effective_to {
774 if date > end {
775 return false;
776 }
777 }
778 true
779 }
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq)]
784pub enum AccrualType {
785 AccruedExpense,
787 AccruedRevenue,
789 PrepaidExpense,
791 DeferredRevenue,
793}
794
795#[derive(Debug, Clone, Copy, PartialEq, Eq)]
797pub enum AccrualCalculationMethod {
798 FixedAmount,
800 PercentageOfBase,
802 DaysBased,
804 Manual,
806}
807
808#[derive(Debug, Clone, Copy, PartialEq, Eq)]
810pub enum AccrualFrequency {
811 Monthly,
813 Quarterly,
815 Annually,
817}
818
819#[derive(Debug, Clone)]
821pub struct OverheadAllocation {
822 pub allocation_id: String,
824 pub source_company: String,
826 pub source_cost_center: String,
828 pub source_account: String,
830 pub allocation_basis: AllocationBasis,
832 pub targets: Vec<AllocationTarget>,
834 pub description: String,
836 pub is_active: bool,
838}
839
840#[derive(Debug, Clone, PartialEq, Eq)]
842pub enum AllocationBasis {
843 Revenue,
845 Headcount,
847 DirectCosts,
849 SquareFootage,
851 FixedPercentage,
853 Custom(String),
855}
856
857#[derive(Debug, Clone)]
859pub struct AllocationTarget {
860 pub company_code: String,
862 pub cost_center: String,
864 pub account: String,
866 pub percentage: Option<Decimal>,
868 pub driver_value: Option<Decimal>,
870}
871
872#[derive(Debug, Clone)]
874pub struct CloseSchedule {
875 pub schedule_id: String,
877 pub company_code: String,
879 pub period_type: FiscalPeriodType,
881 pub tasks: Vec<ScheduledCloseTask>,
883 pub is_year_end: bool,
885}
886
887impl CloseSchedule {
888 pub fn standard_monthly(company_code: &str) -> Self {
890 Self {
891 schedule_id: format!("MONTHLY-{company_code}"),
892 company_code: company_code.to_string(),
893 period_type: FiscalPeriodType::Monthly,
894 tasks: vec![
895 ScheduledCloseTask::new(CloseTask::RunDepreciation, 1),
896 ScheduledCloseTask::new(CloseTask::PostInventoryRevaluation, 2),
897 ScheduledCloseTask::new(CloseTask::PostAccruedExpenses, 3),
898 ScheduledCloseTask::new(CloseTask::PostAccruedRevenue, 4),
899 ScheduledCloseTask::new(CloseTask::PostPrepaidAmortization, 5),
900 ScheduledCloseTask::new(CloseTask::RevalueForeignCurrency, 6),
901 ScheduledCloseTask::new(CloseTask::ReconcileArToGl, 7),
902 ScheduledCloseTask::new(CloseTask::ReconcileApToGl, 8),
903 ScheduledCloseTask::new(CloseTask::ReconcileFaToGl, 9),
904 ScheduledCloseTask::new(CloseTask::ReconcileInventoryToGl, 10),
905 ScheduledCloseTask::new(CloseTask::PostIntercompanySettlements, 11),
906 ScheduledCloseTask::new(CloseTask::AllocateCorporateOverhead, 12),
907 ScheduledCloseTask::new(CloseTask::TranslateForeignSubsidiaries, 13),
908 ScheduledCloseTask::new(CloseTask::EliminateIntercompany, 14),
909 ScheduledCloseTask::new(CloseTask::GenerateTrialBalance, 15),
910 ],
911 is_year_end: false,
912 }
913 }
914
915 pub fn year_end(company_code: &str) -> Self {
917 let mut schedule = Self::standard_monthly(company_code);
918 schedule.schedule_id = format!("YEAREND-{company_code}");
919 schedule.is_year_end = true;
920
921 let next_seq = schedule.tasks.len() as u32 + 1;
923 schedule.tasks.push(ScheduledCloseTask::new(
924 CloseTask::CalculateTaxProvision,
925 next_seq,
926 ));
927 schedule.tasks.push(ScheduledCloseTask::new(
928 CloseTask::CloseIncomeStatement,
929 next_seq + 1,
930 ));
931 schedule.tasks.push(ScheduledCloseTask::new(
932 CloseTask::PostRetainedEarningsRollforward,
933 next_seq + 2,
934 ));
935 schedule.tasks.push(ScheduledCloseTask::new(
936 CloseTask::GenerateFinancialStatements,
937 next_seq + 3,
938 ));
939
940 schedule
941 }
942}
943
944#[derive(Debug, Clone)]
946pub struct ScheduledCloseTask {
947 pub task: CloseTask,
949 pub sequence: u32,
951 pub depends_on: Vec<CloseTask>,
953 pub is_mandatory: bool,
955 pub can_parallelize: bool,
957}
958
959impl ScheduledCloseTask {
960 pub fn new(task: CloseTask, sequence: u32) -> Self {
962 Self {
963 task,
964 sequence,
965 depends_on: Vec::new(),
966 is_mandatory: true,
967 can_parallelize: false,
968 }
969 }
970
971 pub fn depends_on(mut self, task: CloseTask) -> Self {
973 self.depends_on.push(task);
974 self
975 }
976
977 pub fn optional(mut self) -> Self {
979 self.is_mandatory = false;
980 self
981 }
982
983 pub fn parallelizable(mut self) -> Self {
985 self.can_parallelize = true;
986 self
987 }
988}
989
990#[derive(Debug, Clone)]
992pub struct YearEndClosingSpec {
993 pub company_code: String,
995 pub fiscal_year: i32,
997 pub revenue_accounts: Vec<String>,
999 pub expense_accounts: Vec<String>,
1001 pub income_summary_account: String,
1003 pub retained_earnings_account: String,
1005 pub dividend_account: Option<String>,
1007}
1008
1009impl Default for YearEndClosingSpec {
1010 fn default() -> Self {
1011 Self {
1012 company_code: String::new(),
1013 fiscal_year: 0,
1014 revenue_accounts: vec!["4".to_string()], expense_accounts: vec!["5".to_string(), "6".to_string()], income_summary_account: "3500".to_string(),
1017 retained_earnings_account: "3300".to_string(),
1018 dividend_account: Some("3400".to_string()),
1019 }
1020 }
1021}
1022
1023#[derive(Debug, Clone)]
1025pub struct TaxProvisionInput {
1026 pub company_code: String,
1028 pub fiscal_year: i32,
1030 pub pretax_income: Decimal,
1032 pub permanent_differences: Vec<TaxAdjustment>,
1034 pub temporary_differences: Vec<TaxAdjustment>,
1036 pub statutory_rate: Decimal,
1038 pub tax_credits: Decimal,
1040 pub prior_year_adjustment: Decimal,
1042}
1043
1044#[derive(Debug, Clone)]
1046pub struct TaxAdjustment {
1047 pub description: String,
1049 pub amount: Decimal,
1051 pub is_addition: bool,
1053}
1054
1055#[derive(Debug, Clone)]
1057pub struct TaxProvisionResult {
1058 pub company_code: String,
1060 pub fiscal_year: i32,
1062 pub pretax_income: Decimal,
1064 pub permanent_differences: Decimal,
1066 pub taxable_income: Decimal,
1068 pub current_tax_expense: Decimal,
1070 pub deferred_tax_expense: Decimal,
1072 pub total_tax_expense: Decimal,
1074 pub effective_rate: Decimal,
1076}
1077
1078impl TaxProvisionResult {
1079 pub fn calculate(input: &TaxProvisionInput) -> Self {
1081 let permanent_diff: Decimal = input
1082 .permanent_differences
1083 .iter()
1084 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1085 .sum();
1086
1087 let temporary_diff: Decimal = input
1088 .temporary_differences
1089 .iter()
1090 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1091 .sum();
1092
1093 let taxable_income = input.pretax_income + permanent_diff;
1094 let current_tax = (taxable_income * input.statutory_rate / dec!(100)).round_dp(2);
1095 let deferred_tax = (temporary_diff * input.statutory_rate / dec!(100)).round_dp(2);
1096
1097 let total_tax =
1098 current_tax + deferred_tax - input.tax_credits + input.prior_year_adjustment;
1099
1100 let effective_rate = if input.pretax_income != Decimal::ZERO {
1101 (total_tax / input.pretax_income * dec!(100)).round_dp(2)
1102 } else {
1103 Decimal::ZERO
1104 };
1105
1106 Self {
1107 company_code: input.company_code.clone(),
1108 fiscal_year: input.fiscal_year,
1109 pretax_income: input.pretax_income,
1110 permanent_differences: permanent_diff,
1111 taxable_income,
1112 current_tax_expense: current_tax,
1113 deferred_tax_expense: deferred_tax,
1114 total_tax_expense: total_tax,
1115 effective_rate,
1116 }
1117 }
1118}
1119
1120#[derive(Debug, Clone)]
1122pub struct PeriodCloseRun {
1123 pub run_id: String,
1125 pub company_code: String,
1127 pub fiscal_period: FiscalPeriod,
1129 pub status: PeriodCloseStatus,
1131 pub task_results: Vec<CloseTaskResult>,
1133 pub started_at: Option<NaiveDate>,
1135 pub completed_at: Option<NaiveDate>,
1137 pub total_journal_entries: u32,
1139 pub errors: Vec<String>,
1141}
1142
1143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1145pub enum PeriodCloseStatus {
1146 NotStarted,
1148 InProgress,
1150 Completed,
1152 CompletedWithErrors,
1154 Failed,
1156}
1157
1158impl PeriodCloseRun {
1159 pub fn new(run_id: String, company_code: String, fiscal_period: FiscalPeriod) -> Self {
1161 Self {
1162 run_id,
1163 company_code,
1164 fiscal_period,
1165 status: PeriodCloseStatus::NotStarted,
1166 task_results: Vec::new(),
1167 started_at: None,
1168 completed_at: None,
1169 total_journal_entries: 0,
1170 errors: Vec::new(),
1171 }
1172 }
1173
1174 pub fn is_success(&self) -> bool {
1176 self.status == PeriodCloseStatus::Completed
1177 }
1178
1179 pub fn failed_task_count(&self) -> usize {
1181 self.task_results
1182 .iter()
1183 .filter(|r| matches!(r.status, CloseTaskStatus::Failed(_)))
1184 .count()
1185 }
1186}
1187
1188#[cfg(test)]
1189#[allow(clippy::unwrap_used)]
1190mod tests {
1191 use super::*;
1192
1193 #[test]
1194 fn test_fiscal_period_monthly() {
1195 let period = FiscalPeriod::monthly(2024, 1);
1196 assert_eq!(
1197 period.start_date,
1198 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()
1199 );
1200 assert_eq!(
1201 period.end_date,
1202 NaiveDate::from_ymd_opt(2024, 1, 31).unwrap()
1203 );
1204 assert_eq!(period.days(), 31);
1205 assert!(!period.is_year_end);
1206
1207 let dec_period = FiscalPeriod::monthly(2024, 12);
1208 assert!(dec_period.is_year_end);
1209 }
1210
1211 #[test]
1212 fn test_fiscal_period_quarterly() {
1213 let q1 = FiscalPeriod::quarterly(2024, 1);
1214 assert_eq!(q1.start_date, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1215 assert_eq!(q1.end_date, NaiveDate::from_ymd_opt(2024, 3, 31).unwrap());
1216
1217 let q4 = FiscalPeriod::quarterly(2024, 4);
1218 assert!(q4.is_year_end);
1219 }
1220
1221 #[test]
1222 fn test_close_schedule() {
1223 let schedule = CloseSchedule::standard_monthly("1000");
1224 assert!(!schedule.is_year_end);
1225 assert!(!schedule.tasks.is_empty());
1226
1227 let year_end = CloseSchedule::year_end("1000");
1228 assert!(year_end.is_year_end);
1229 assert!(year_end.tasks.len() > schedule.tasks.len());
1230 }
1231
1232 #[test]
1233 fn test_tax_provision() {
1234 let input = TaxProvisionInput {
1235 company_code: "1000".to_string(),
1236 fiscal_year: 2024,
1237 pretax_income: dec!(1000000),
1238 permanent_differences: vec![TaxAdjustment {
1239 description: "Meals & Entertainment".to_string(),
1240 amount: dec!(10000),
1241 is_addition: true,
1242 }],
1243 temporary_differences: vec![TaxAdjustment {
1244 description: "Depreciation Timing".to_string(),
1245 amount: dec!(50000),
1246 is_addition: false,
1247 }],
1248 statutory_rate: dec!(21),
1249 tax_credits: dec!(5000),
1250 prior_year_adjustment: Decimal::ZERO,
1251 };
1252
1253 let result = TaxProvisionResult::calculate(&input);
1254 assert_eq!(result.taxable_income, dec!(1010000)); assert!(result.current_tax_expense > Decimal::ZERO);
1256 }
1257}