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()
194 }
195 }
196 }
197
198 pub fn fiscal_period(&self, date: NaiveDate) -> u8 {
200 match &self.calendar_type {
201 FiscalCalendarType::CalendarYear => date.month() as u8,
202 FiscalCalendarType::CustomYearStart {
203 start_month,
204 start_day: _,
205 } => {
206 let month = date.month() as u8;
207 if month >= *start_month {
208 month - start_month + 1
209 } else {
210 12 - start_month + month + 1
211 }
212 }
213 FiscalCalendarType::ThirteenPeriod(_) => {
214 let day_of_year = date.ordinal();
216 ((day_of_year - 1) / 28 + 1).min(13) as u8
217 }
218 FiscalCalendarType::FourFourFive(config) => {
219 let weeks = config.pattern.weeks_per_period();
221 let week_of_year = (date.ordinal() as u8 - 1) / 7 + 1;
222 let mut cumulative = 0u8;
223 for (quarter, _) in (0..4).enumerate() {
224 for (period_in_q, &period_weeks) in weeks.iter().enumerate() {
225 cumulative += period_weeks;
226 if week_of_year <= cumulative {
227 return (quarter * 3 + period_in_q + 1) as u8;
228 }
229 }
230 }
231 12 }
233 }
234 }
235}
236
237fn month_name(month: u8) -> &'static str {
239 match month {
240 1 => "January",
241 2 => "February",
242 3 => "March",
243 4 => "April",
244 5 => "May",
245 6 => "June",
246 7 => "July",
247 8 => "August",
248 9 => "September",
249 10 => "October",
250 11 => "November",
251 12 => "December",
252 _ => "Unknown",
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
258pub struct FiscalPeriod {
259 pub year: i32,
261 pub period: u8,
263 pub start_date: NaiveDate,
265 pub end_date: NaiveDate,
267 pub period_type: FiscalPeriodType,
269 pub is_year_end: bool,
271 pub status: PeriodStatus,
273}
274
275impl FiscalPeriod {
276 pub fn monthly(year: i32, month: u8) -> Self {
278 let start_date =
279 NaiveDate::from_ymd_opt(year, month as u32, 1).expect("valid date components");
280 let end_date = if month == 12 {
281 NaiveDate::from_ymd_opt(year + 1, 1, 1)
282 .expect("valid date components")
283 .pred_opt()
284 .expect("valid date components")
285 } else {
286 NaiveDate::from_ymd_opt(year, month as u32 + 1, 1)
287 .expect("valid date components")
288 .pred_opt()
289 .expect("valid date components")
290 };
291
292 Self {
293 year,
294 period: month,
295 start_date,
296 end_date,
297 period_type: FiscalPeriodType::Monthly,
298 is_year_end: month == 12,
299 status: PeriodStatus::Open,
300 }
301 }
302
303 pub fn quarterly(year: i32, quarter: u8) -> Self {
305 let start_month = (quarter - 1) * 3 + 1;
306 let end_month = quarter * 3;
307
308 let start_date =
309 NaiveDate::from_ymd_opt(year, start_month as u32, 1).expect("valid date components");
310 let end_date = if end_month == 12 {
311 NaiveDate::from_ymd_opt(year + 1, 1, 1)
312 .expect("valid date components")
313 .pred_opt()
314 .expect("valid date components")
315 } else {
316 NaiveDate::from_ymd_opt(year, end_month as u32 + 1, 1)
317 .expect("valid date components")
318 .pred_opt()
319 .expect("valid date components")
320 };
321
322 Self {
323 year,
324 period: quarter,
325 start_date,
326 end_date,
327 period_type: FiscalPeriodType::Quarterly,
328 is_year_end: quarter == 4,
329 status: PeriodStatus::Open,
330 }
331 }
332
333 pub fn days(&self) -> i64 {
335 (self.end_date - self.start_date).num_days() + 1
336 }
337
338 pub fn key(&self) -> String {
340 format!("{}-{:02}", self.year, self.period)
341 }
342
343 pub fn contains(&self, date: NaiveDate) -> bool {
345 date >= self.start_date && date <= self.end_date
346 }
347
348 pub fn from_calendar(calendar: &FiscalCalendar, date: NaiveDate) -> Self {
353 let fiscal_year = calendar.fiscal_year(date);
354 let period_num = calendar.fiscal_period(date);
355
356 match &calendar.calendar_type {
357 FiscalCalendarType::CalendarYear => Self::monthly(fiscal_year, period_num),
358 FiscalCalendarType::CustomYearStart {
359 start_month,
360 start_day,
361 } => {
362 let period_start_month = if *start_month + period_num - 1 > 12 {
364 start_month + period_num - 1 - 12
365 } else {
366 start_month + period_num - 1
367 };
368 let period_year = if *start_month + period_num - 1 > 12 {
369 fiscal_year + 1
370 } else {
371 fiscal_year
372 };
373
374 let start_date = if period_num == 1 {
375 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, *start_day as u32)
376 .unwrap_or_else(|| {
377 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, 1)
378 .expect("valid date components")
379 })
380 } else {
381 NaiveDate::from_ymd_opt(period_year, period_start_month as u32, 1)
382 .expect("valid date components")
383 };
384
385 let end_date = if period_num == 12 {
386 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, *start_day as u32)
388 .unwrap_or_else(|| {
389 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, 1)
390 .expect("valid date components")
391 })
392 .pred_opt()
393 .expect("valid date components")
394 } else {
395 let next_month = if period_start_month == 12 {
396 1
397 } else {
398 period_start_month + 1
399 };
400 let next_year = if period_start_month == 12 {
401 period_year + 1
402 } else {
403 period_year
404 };
405 NaiveDate::from_ymd_opt(next_year, next_month as u32, 1)
406 .expect("valid date components")
407 .pred_opt()
408 .expect("valid date components")
409 };
410
411 Self {
412 year: fiscal_year,
413 period: period_num,
414 start_date,
415 end_date,
416 period_type: FiscalPeriodType::Monthly,
417 is_year_end: period_num == 12,
418 status: PeriodStatus::Open,
419 }
420 }
421 FiscalCalendarType::FourFourFive(config) => {
422 let weeks = config.pattern.weeks_per_period();
424 let quarter = (period_num - 1) / 3;
425 let period_in_quarter = (period_num - 1) % 3;
426 let period_weeks = weeks[period_in_quarter as usize];
427
428 let year_start =
430 NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
431
432 let mut weeks_before = 0u32;
434 for _ in 0..quarter {
435 for &w in &weeks {
436 weeks_before += w as u32;
437 }
438 }
439 for p in 0..period_in_quarter {
440 weeks_before += weeks[p as usize] as u32;
441 }
442
443 let start_date = year_start + chrono::Duration::weeks(weeks_before as i64);
444 let end_date = start_date + chrono::Duration::weeks(period_weeks as i64)
445 - chrono::Duration::days(1);
446
447 Self {
448 year: fiscal_year,
449 period: period_num,
450 start_date,
451 end_date,
452 period_type: FiscalPeriodType::FourWeek,
453 is_year_end: period_num == 12,
454 status: PeriodStatus::Open,
455 }
456 }
457 FiscalCalendarType::ThirteenPeriod(_) => {
458 let year_start =
460 NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
461 let start_date = year_start + chrono::Duration::days((period_num as i64 - 1) * 28);
462 let end_date = start_date + chrono::Duration::days(27);
463
464 Self {
465 year: fiscal_year,
466 period: period_num,
467 start_date,
468 end_date,
469 period_type: FiscalPeriodType::FourWeek,
470 is_year_end: period_num == 13,
471 status: PeriodStatus::Open,
472 }
473 }
474 }
475 }
476}
477
478#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
480pub enum FiscalPeriodType {
481 Monthly,
483 Quarterly,
485 FourWeek,
487 Special,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
493pub enum PeriodStatus {
494 Open,
496 SoftClosed,
498 Closed,
500 Locked,
502}
503
504#[derive(Debug, Clone, PartialEq, Eq, Hash)]
506pub enum CloseTask {
507 RunDepreciation,
509 PostInventoryRevaluation,
511 ReconcileArToGl,
513 ReconcileApToGl,
515 ReconcileFaToGl,
517 ReconcileInventoryToGl,
519 PostAccruedExpenses,
521 PostAccruedRevenue,
523 PostPrepaidAmortization,
525 AllocateCorporateOverhead,
527 PostIntercompanySettlements,
529 RevalueForeignCurrency,
531 CalculateTaxProvision,
533 TranslateForeignSubsidiaries,
535 EliminateIntercompany,
537 GenerateTrialBalance,
539 GenerateFinancialStatements,
541 CloseIncomeStatement,
543 PostRetainedEarningsRollforward,
545 Custom(String),
547}
548
549impl CloseTask {
550 pub fn is_year_end_only(&self) -> bool {
552 matches!(
553 self,
554 CloseTask::CloseIncomeStatement | CloseTask::PostRetainedEarningsRollforward
555 )
556 }
557
558 pub fn name(&self) -> &str {
560 match self {
561 CloseTask::RunDepreciation => "Run Depreciation",
562 CloseTask::PostInventoryRevaluation => "Post Inventory Revaluation",
563 CloseTask::ReconcileArToGl => "Reconcile AR to GL",
564 CloseTask::ReconcileApToGl => "Reconcile AP to GL",
565 CloseTask::ReconcileFaToGl => "Reconcile FA to GL",
566 CloseTask::ReconcileInventoryToGl => "Reconcile Inventory to GL",
567 CloseTask::PostAccruedExpenses => "Post Accrued Expenses",
568 CloseTask::PostAccruedRevenue => "Post Accrued Revenue",
569 CloseTask::PostPrepaidAmortization => "Post Prepaid Amortization",
570 CloseTask::AllocateCorporateOverhead => "Allocate Corporate Overhead",
571 CloseTask::PostIntercompanySettlements => "Post IC Settlements",
572 CloseTask::RevalueForeignCurrency => "Revalue Foreign Currency",
573 CloseTask::CalculateTaxProvision => "Calculate Tax Provision",
574 CloseTask::TranslateForeignSubsidiaries => "Translate Foreign Subs",
575 CloseTask::EliminateIntercompany => "Eliminate Intercompany",
576 CloseTask::GenerateTrialBalance => "Generate Trial Balance",
577 CloseTask::GenerateFinancialStatements => "Generate Financials",
578 CloseTask::CloseIncomeStatement => "Close Income Statement",
579 CloseTask::PostRetainedEarningsRollforward => "Post RE Rollforward",
580 CloseTask::Custom(name) => name,
581 }
582 }
583}
584
585#[derive(Debug, Clone, PartialEq, Eq)]
587pub enum CloseTaskStatus {
588 Pending,
590 InProgress,
592 Completed,
594 CompletedWithWarnings(Vec<String>),
596 Failed(String),
598 Skipped(String),
600}
601
602#[derive(Debug, Clone)]
604pub struct CloseTaskResult {
605 pub task: CloseTask,
607 pub company_code: String,
609 pub fiscal_period: FiscalPeriod,
611 pub status: CloseTaskStatus,
613 pub started_at: Option<NaiveDate>,
615 pub completed_at: Option<NaiveDate>,
617 pub journal_entries_created: u32,
619 pub total_amount: Decimal,
621 pub notes: Vec<String>,
623}
624
625impl CloseTaskResult {
626 pub fn new(task: CloseTask, company_code: String, fiscal_period: FiscalPeriod) -> Self {
628 Self {
629 task,
630 company_code,
631 fiscal_period,
632 status: CloseTaskStatus::Pending,
633 started_at: None,
634 completed_at: None,
635 journal_entries_created: 0,
636 total_amount: Decimal::ZERO,
637 notes: Vec::new(),
638 }
639 }
640
641 pub fn is_success(&self) -> bool {
643 matches!(
644 self.status,
645 CloseTaskStatus::Completed | CloseTaskStatus::CompletedWithWarnings(_)
646 )
647 }
648}
649
650#[derive(Debug, Clone)]
652pub struct AccrualDefinition {
653 pub accrual_id: String,
655 pub company_code: String,
657 pub description: String,
659 pub accrual_type: AccrualType,
661 pub expense_revenue_account: String,
663 pub accrual_account: String,
665 pub calculation_method: AccrualCalculationMethod,
667 pub fixed_amount: Option<Decimal>,
669 pub percentage_rate: Option<Decimal>,
671 pub base_account: Option<String>,
673 pub frequency: AccrualFrequency,
675 pub auto_reverse: bool,
677 pub cost_center: Option<String>,
679 pub is_active: bool,
681 pub effective_from: NaiveDate,
683 pub effective_to: Option<NaiveDate>,
685}
686
687impl AccrualDefinition {
688 pub fn new(
690 accrual_id: String,
691 company_code: String,
692 description: String,
693 accrual_type: AccrualType,
694 expense_revenue_account: String,
695 accrual_account: String,
696 ) -> Self {
697 Self {
698 accrual_id,
699 company_code,
700 description,
701 accrual_type,
702 expense_revenue_account,
703 accrual_account,
704 calculation_method: AccrualCalculationMethod::FixedAmount,
705 fixed_amount: None,
706 percentage_rate: None,
707 base_account: None,
708 frequency: AccrualFrequency::Monthly,
709 auto_reverse: true,
710 cost_center: None,
711 is_active: true,
712 effective_from: NaiveDate::from_ymd_opt(2020, 1, 1).expect("valid date components"),
713 effective_to: None,
714 }
715 }
716
717 pub fn with_fixed_amount(mut self, amount: Decimal) -> Self {
719 self.calculation_method = AccrualCalculationMethod::FixedAmount;
720 self.fixed_amount = Some(amount);
721 self
722 }
723
724 pub fn with_percentage(mut self, rate: Decimal, base_account: &str) -> Self {
726 self.calculation_method = AccrualCalculationMethod::PercentageOfBase;
727 self.percentage_rate = Some(rate);
728 self.base_account = Some(base_account.to_string());
729 self
730 }
731
732 pub fn is_effective_on(&self, date: NaiveDate) -> bool {
734 if !self.is_active {
735 return false;
736 }
737 if date < self.effective_from {
738 return false;
739 }
740 if let Some(end) = self.effective_to {
741 if date > end {
742 return false;
743 }
744 }
745 true
746 }
747}
748
749#[derive(Debug, Clone, Copy, PartialEq, Eq)]
751pub enum AccrualType {
752 AccruedExpense,
754 AccruedRevenue,
756 PrepaidExpense,
758 DeferredRevenue,
760}
761
762#[derive(Debug, Clone, Copy, PartialEq, Eq)]
764pub enum AccrualCalculationMethod {
765 FixedAmount,
767 PercentageOfBase,
769 DaysBased,
771 Manual,
773}
774
775#[derive(Debug, Clone, Copy, PartialEq, Eq)]
777pub enum AccrualFrequency {
778 Monthly,
780 Quarterly,
782 Annually,
784}
785
786#[derive(Debug, Clone)]
788pub struct OverheadAllocation {
789 pub allocation_id: String,
791 pub source_company: String,
793 pub source_cost_center: String,
795 pub source_account: String,
797 pub allocation_basis: AllocationBasis,
799 pub targets: Vec<AllocationTarget>,
801 pub description: String,
803 pub is_active: bool,
805}
806
807#[derive(Debug, Clone, PartialEq, Eq)]
809pub enum AllocationBasis {
810 Revenue,
812 Headcount,
814 DirectCosts,
816 SquareFootage,
818 FixedPercentage,
820 Custom(String),
822}
823
824#[derive(Debug, Clone)]
826pub struct AllocationTarget {
827 pub company_code: String,
829 pub cost_center: String,
831 pub account: String,
833 pub percentage: Option<Decimal>,
835 pub driver_value: Option<Decimal>,
837}
838
839#[derive(Debug, Clone)]
841pub struct CloseSchedule {
842 pub schedule_id: String,
844 pub company_code: String,
846 pub period_type: FiscalPeriodType,
848 pub tasks: Vec<ScheduledCloseTask>,
850 pub is_year_end: bool,
852}
853
854impl CloseSchedule {
855 pub fn standard_monthly(company_code: &str) -> Self {
857 Self {
858 schedule_id: format!("MONTHLY-{}", company_code),
859 company_code: company_code.to_string(),
860 period_type: FiscalPeriodType::Monthly,
861 tasks: vec![
862 ScheduledCloseTask::new(CloseTask::RunDepreciation, 1),
863 ScheduledCloseTask::new(CloseTask::PostInventoryRevaluation, 2),
864 ScheduledCloseTask::new(CloseTask::PostAccruedExpenses, 3),
865 ScheduledCloseTask::new(CloseTask::PostAccruedRevenue, 4),
866 ScheduledCloseTask::new(CloseTask::PostPrepaidAmortization, 5),
867 ScheduledCloseTask::new(CloseTask::RevalueForeignCurrency, 6),
868 ScheduledCloseTask::new(CloseTask::ReconcileArToGl, 7),
869 ScheduledCloseTask::new(CloseTask::ReconcileApToGl, 8),
870 ScheduledCloseTask::new(CloseTask::ReconcileFaToGl, 9),
871 ScheduledCloseTask::new(CloseTask::ReconcileInventoryToGl, 10),
872 ScheduledCloseTask::new(CloseTask::PostIntercompanySettlements, 11),
873 ScheduledCloseTask::new(CloseTask::AllocateCorporateOverhead, 12),
874 ScheduledCloseTask::new(CloseTask::TranslateForeignSubsidiaries, 13),
875 ScheduledCloseTask::new(CloseTask::EliminateIntercompany, 14),
876 ScheduledCloseTask::new(CloseTask::GenerateTrialBalance, 15),
877 ],
878 is_year_end: false,
879 }
880 }
881
882 pub fn year_end(company_code: &str) -> Self {
884 let mut schedule = Self::standard_monthly(company_code);
885 schedule.schedule_id = format!("YEAREND-{}", company_code);
886 schedule.is_year_end = true;
887
888 let next_seq = schedule.tasks.len() as u32 + 1;
890 schedule.tasks.push(ScheduledCloseTask::new(
891 CloseTask::CalculateTaxProvision,
892 next_seq,
893 ));
894 schedule.tasks.push(ScheduledCloseTask::new(
895 CloseTask::CloseIncomeStatement,
896 next_seq + 1,
897 ));
898 schedule.tasks.push(ScheduledCloseTask::new(
899 CloseTask::PostRetainedEarningsRollforward,
900 next_seq + 2,
901 ));
902 schedule.tasks.push(ScheduledCloseTask::new(
903 CloseTask::GenerateFinancialStatements,
904 next_seq + 3,
905 ));
906
907 schedule
908 }
909}
910
911#[derive(Debug, Clone)]
913pub struct ScheduledCloseTask {
914 pub task: CloseTask,
916 pub sequence: u32,
918 pub depends_on: Vec<CloseTask>,
920 pub is_mandatory: bool,
922 pub can_parallelize: bool,
924}
925
926impl ScheduledCloseTask {
927 pub fn new(task: CloseTask, sequence: u32) -> Self {
929 Self {
930 task,
931 sequence,
932 depends_on: Vec::new(),
933 is_mandatory: true,
934 can_parallelize: false,
935 }
936 }
937
938 pub fn depends_on(mut self, task: CloseTask) -> Self {
940 self.depends_on.push(task);
941 self
942 }
943
944 pub fn optional(mut self) -> Self {
946 self.is_mandatory = false;
947 self
948 }
949
950 pub fn parallelizable(mut self) -> Self {
952 self.can_parallelize = true;
953 self
954 }
955}
956
957#[derive(Debug, Clone)]
959pub struct YearEndClosingSpec {
960 pub company_code: String,
962 pub fiscal_year: i32,
964 pub revenue_accounts: Vec<String>,
966 pub expense_accounts: Vec<String>,
968 pub income_summary_account: String,
970 pub retained_earnings_account: String,
972 pub dividend_account: Option<String>,
974}
975
976impl Default for YearEndClosingSpec {
977 fn default() -> Self {
978 Self {
979 company_code: String::new(),
980 fiscal_year: 0,
981 revenue_accounts: vec!["4".to_string()], expense_accounts: vec!["5".to_string(), "6".to_string()], income_summary_account: "3500".to_string(),
984 retained_earnings_account: "3300".to_string(),
985 dividend_account: Some("3400".to_string()),
986 }
987 }
988}
989
990#[derive(Debug, Clone)]
992pub struct TaxProvisionInput {
993 pub company_code: String,
995 pub fiscal_year: i32,
997 pub pretax_income: Decimal,
999 pub permanent_differences: Vec<TaxAdjustment>,
1001 pub temporary_differences: Vec<TaxAdjustment>,
1003 pub statutory_rate: Decimal,
1005 pub tax_credits: Decimal,
1007 pub prior_year_adjustment: Decimal,
1009}
1010
1011#[derive(Debug, Clone)]
1013pub struct TaxAdjustment {
1014 pub description: String,
1016 pub amount: Decimal,
1018 pub is_addition: bool,
1020}
1021
1022#[derive(Debug, Clone)]
1024pub struct TaxProvisionResult {
1025 pub company_code: String,
1027 pub fiscal_year: i32,
1029 pub pretax_income: Decimal,
1031 pub permanent_differences: Decimal,
1033 pub taxable_income: Decimal,
1035 pub current_tax_expense: Decimal,
1037 pub deferred_tax_expense: Decimal,
1039 pub total_tax_expense: Decimal,
1041 pub effective_rate: Decimal,
1043}
1044
1045impl TaxProvisionResult {
1046 pub fn calculate(input: &TaxProvisionInput) -> Self {
1048 let permanent_diff: Decimal = input
1049 .permanent_differences
1050 .iter()
1051 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1052 .sum();
1053
1054 let temporary_diff: Decimal = input
1055 .temporary_differences
1056 .iter()
1057 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1058 .sum();
1059
1060 let taxable_income = input.pretax_income + permanent_diff;
1061 let current_tax = (taxable_income * input.statutory_rate / dec!(100)).round_dp(2);
1062 let deferred_tax = (temporary_diff * input.statutory_rate / dec!(100)).round_dp(2);
1063
1064 let total_tax =
1065 current_tax + deferred_tax - input.tax_credits + input.prior_year_adjustment;
1066
1067 let effective_rate = if input.pretax_income != Decimal::ZERO {
1068 (total_tax / input.pretax_income * dec!(100)).round_dp(2)
1069 } else {
1070 Decimal::ZERO
1071 };
1072
1073 Self {
1074 company_code: input.company_code.clone(),
1075 fiscal_year: input.fiscal_year,
1076 pretax_income: input.pretax_income,
1077 permanent_differences: permanent_diff,
1078 taxable_income,
1079 current_tax_expense: current_tax,
1080 deferred_tax_expense: deferred_tax,
1081 total_tax_expense: total_tax,
1082 effective_rate,
1083 }
1084 }
1085}
1086
1087#[derive(Debug, Clone)]
1089pub struct PeriodCloseRun {
1090 pub run_id: String,
1092 pub company_code: String,
1094 pub fiscal_period: FiscalPeriod,
1096 pub status: PeriodCloseStatus,
1098 pub task_results: Vec<CloseTaskResult>,
1100 pub started_at: Option<NaiveDate>,
1102 pub completed_at: Option<NaiveDate>,
1104 pub total_journal_entries: u32,
1106 pub errors: Vec<String>,
1108}
1109
1110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1112pub enum PeriodCloseStatus {
1113 NotStarted,
1115 InProgress,
1117 Completed,
1119 CompletedWithErrors,
1121 Failed,
1123}
1124
1125impl PeriodCloseRun {
1126 pub fn new(run_id: String, company_code: String, fiscal_period: FiscalPeriod) -> Self {
1128 Self {
1129 run_id,
1130 company_code,
1131 fiscal_period,
1132 status: PeriodCloseStatus::NotStarted,
1133 task_results: Vec::new(),
1134 started_at: None,
1135 completed_at: None,
1136 total_journal_entries: 0,
1137 errors: Vec::new(),
1138 }
1139 }
1140
1141 pub fn is_success(&self) -> bool {
1143 self.status == PeriodCloseStatus::Completed
1144 }
1145
1146 pub fn failed_task_count(&self) -> usize {
1148 self.task_results
1149 .iter()
1150 .filter(|r| matches!(r.status, CloseTaskStatus::Failed(_)))
1151 .count()
1152 }
1153}
1154
1155#[cfg(test)]
1156#[allow(clippy::unwrap_used)]
1157mod tests {
1158 use super::*;
1159
1160 #[test]
1161 fn test_fiscal_period_monthly() {
1162 let period = FiscalPeriod::monthly(2024, 1);
1163 assert_eq!(
1164 period.start_date,
1165 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()
1166 );
1167 assert_eq!(
1168 period.end_date,
1169 NaiveDate::from_ymd_opt(2024, 1, 31).unwrap()
1170 );
1171 assert_eq!(period.days(), 31);
1172 assert!(!period.is_year_end);
1173
1174 let dec_period = FiscalPeriod::monthly(2024, 12);
1175 assert!(dec_period.is_year_end);
1176 }
1177
1178 #[test]
1179 fn test_fiscal_period_quarterly() {
1180 let q1 = FiscalPeriod::quarterly(2024, 1);
1181 assert_eq!(q1.start_date, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1182 assert_eq!(q1.end_date, NaiveDate::from_ymd_opt(2024, 3, 31).unwrap());
1183
1184 let q4 = FiscalPeriod::quarterly(2024, 4);
1185 assert!(q4.is_year_end);
1186 }
1187
1188 #[test]
1189 fn test_close_schedule() {
1190 let schedule = CloseSchedule::standard_monthly("1000");
1191 assert!(!schedule.is_year_end);
1192 assert!(!schedule.tasks.is_empty());
1193
1194 let year_end = CloseSchedule::year_end("1000");
1195 assert!(year_end.is_year_end);
1196 assert!(year_end.tasks.len() > schedule.tasks.len());
1197 }
1198
1199 #[test]
1200 fn test_tax_provision() {
1201 let input = TaxProvisionInput {
1202 company_code: "1000".to_string(),
1203 fiscal_year: 2024,
1204 pretax_income: dec!(1000000),
1205 permanent_differences: vec![TaxAdjustment {
1206 description: "Meals & Entertainment".to_string(),
1207 amount: dec!(10000),
1208 is_addition: true,
1209 }],
1210 temporary_differences: vec![TaxAdjustment {
1211 description: "Depreciation Timing".to_string(),
1212 amount: dec!(50000),
1213 is_addition: false,
1214 }],
1215 statutory_rate: dec!(21),
1216 tax_credits: dec!(5000),
1217 prior_year_adjustment: Decimal::ZERO,
1218 };
1219
1220 let result = TaxProvisionResult::calculate(&input);
1221 assert_eq!(result.taxable_income, dec!(1010000)); assert!(result.current_tax_expense > Decimal::ZERO);
1223 }
1224}