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()
199 }
200 }
201 }
202
203 pub fn fiscal_period(&self, date: NaiveDate) -> u8 {
205 match &self.calendar_type {
206 FiscalCalendarType::CalendarYear => date.month() as u8,
207 FiscalCalendarType::CustomYearStart {
208 start_month,
209 start_day: _,
210 } => {
211 let month = date.month() as u8;
212 if month >= *start_month {
213 month - start_month + 1
214 } else {
215 12 - start_month + month + 1
216 }
217 }
218 FiscalCalendarType::ThirteenPeriod(_) => {
219 let day_of_year = date.ordinal();
223 ((day_of_year - 1) / 28 + 1).min(13) as u8
224 }
225 FiscalCalendarType::FourFourFive(config) => {
226 let weeks = config.pattern.weeks_per_period();
231 let week_of_year = (date.ordinal() as u8 - 1) / 7 + 1;
232 let mut cumulative = 0u8;
233 for (quarter, _) in (0..4).enumerate() {
234 for (period_in_q, &period_weeks) in weeks.iter().enumerate() {
235 cumulative += period_weeks;
236 if week_of_year <= cumulative {
237 return (quarter * 3 + period_in_q + 1) as u8;
238 }
239 }
240 }
241 12 }
243 }
244 }
245}
246
247fn month_name(month: u8) -> &'static str {
249 match month {
250 1 => "January",
251 2 => "February",
252 3 => "March",
253 4 => "April",
254 5 => "May",
255 6 => "June",
256 7 => "July",
257 8 => "August",
258 9 => "September",
259 10 => "October",
260 11 => "November",
261 12 => "December",
262 _ => "Unknown",
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Hash)]
268pub struct FiscalPeriod {
269 pub year: i32,
271 pub period: u8,
273 pub start_date: NaiveDate,
275 pub end_date: NaiveDate,
277 pub period_type: FiscalPeriodType,
279 pub is_year_end: bool,
281 pub status: PeriodStatus,
283}
284
285impl FiscalPeriod {
286 pub fn monthly(year: i32, month: u8) -> Self {
288 let start_date =
289 NaiveDate::from_ymd_opt(year, month as u32, 1).expect("valid date components");
290 let end_date = if month == 12 {
291 NaiveDate::from_ymd_opt(year + 1, 1, 1)
292 .expect("valid date components")
293 .pred_opt()
294 .expect("valid date components")
295 } else {
296 NaiveDate::from_ymd_opt(year, month as u32 + 1, 1)
297 .expect("valid date components")
298 .pred_opt()
299 .expect("valid date components")
300 };
301
302 Self {
303 year,
304 period: month,
305 start_date,
306 end_date,
307 period_type: FiscalPeriodType::Monthly,
308 is_year_end: month == 12,
309 status: PeriodStatus::Open,
310 }
311 }
312
313 pub fn quarterly(year: i32, quarter: u8) -> Self {
315 let start_month = (quarter - 1) * 3 + 1;
316 let end_month = quarter * 3;
317
318 let start_date =
319 NaiveDate::from_ymd_opt(year, start_month as u32, 1).expect("valid date components");
320 let end_date = if end_month == 12 {
321 NaiveDate::from_ymd_opt(year + 1, 1, 1)
322 .expect("valid date components")
323 .pred_opt()
324 .expect("valid date components")
325 } else {
326 NaiveDate::from_ymd_opt(year, end_month as u32 + 1, 1)
327 .expect("valid date components")
328 .pred_opt()
329 .expect("valid date components")
330 };
331
332 Self {
333 year,
334 period: quarter,
335 start_date,
336 end_date,
337 period_type: FiscalPeriodType::Quarterly,
338 is_year_end: quarter == 4,
339 status: PeriodStatus::Open,
340 }
341 }
342
343 pub fn days(&self) -> i64 {
345 (self.end_date - self.start_date).num_days() + 1
346 }
347
348 pub fn key(&self) -> String {
350 format!("{}-{:02}", self.year, self.period)
351 }
352
353 pub fn contains(&self, date: NaiveDate) -> bool {
355 date >= self.start_date && date <= self.end_date
356 }
357
358 pub fn from_calendar(calendar: &FiscalCalendar, date: NaiveDate) -> Self {
363 let fiscal_year = calendar.fiscal_year(date);
364 let period_num = calendar.fiscal_period(date);
365
366 match &calendar.calendar_type {
367 FiscalCalendarType::CalendarYear => Self::monthly(fiscal_year, period_num),
368 FiscalCalendarType::CustomYearStart {
369 start_month,
370 start_day,
371 } => {
372 let period_start_month = if *start_month + period_num - 1 > 12 {
374 start_month + period_num - 1 - 12
375 } else {
376 start_month + period_num - 1
377 };
378 let period_year = if *start_month + period_num - 1 > 12 {
379 fiscal_year + 1
380 } else {
381 fiscal_year
382 };
383
384 let start_date = if period_num == 1 {
385 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, *start_day as u32)
386 .unwrap_or_else(|| {
387 NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, 1)
388 .expect("valid date components")
389 })
390 } else {
391 NaiveDate::from_ymd_opt(period_year, period_start_month as u32, 1)
392 .expect("valid date components")
393 };
394
395 let end_date = if period_num == 12 {
396 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, *start_day as u32)
398 .unwrap_or_else(|| {
399 NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, 1)
400 .expect("valid date components")
401 })
402 .pred_opt()
403 .expect("valid date components")
404 } else {
405 let next_month = if period_start_month == 12 {
406 1
407 } else {
408 period_start_month + 1
409 };
410 let next_year = if period_start_month == 12 {
411 period_year + 1
412 } else {
413 period_year
414 };
415 NaiveDate::from_ymd_opt(next_year, next_month as u32, 1)
416 .expect("valid date components")
417 .pred_opt()
418 .expect("valid date components")
419 };
420
421 Self {
422 year: fiscal_year,
423 period: period_num,
424 start_date,
425 end_date,
426 period_type: FiscalPeriodType::Monthly,
427 is_year_end: period_num == 12,
428 status: PeriodStatus::Open,
429 }
430 }
431 FiscalCalendarType::FourFourFive(config) => {
432 let weeks = config.pattern.weeks_per_period();
434 let quarter = (period_num - 1) / 3;
435 let period_in_quarter = (period_num - 1) % 3;
436 let period_weeks = weeks[period_in_quarter as usize];
437
438 let year_start =
440 NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
441
442 let mut weeks_before = 0u32;
444 for _ in 0..quarter {
445 for &w in &weeks {
446 weeks_before += w as u32;
447 }
448 }
449 for p in 0..period_in_quarter {
450 weeks_before += weeks[p as usize] as u32;
451 }
452
453 let start_date = year_start + chrono::Duration::weeks(weeks_before as i64);
454 let end_date = start_date + chrono::Duration::weeks(period_weeks as i64)
455 - chrono::Duration::days(1);
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 == 12,
464 status: PeriodStatus::Open,
465 }
466 }
467 FiscalCalendarType::ThirteenPeriod(_) => {
468 let year_start =
470 NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
471 let start_date = year_start + chrono::Duration::days((period_num as i64 - 1) * 28);
472 let end_date = start_date + chrono::Duration::days(27);
473
474 Self {
475 year: fiscal_year,
476 period: period_num,
477 start_date,
478 end_date,
479 period_type: FiscalPeriodType::FourWeek,
480 is_year_end: period_num == 13,
481 status: PeriodStatus::Open,
482 }
483 }
484 }
485 }
486}
487
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
490pub enum FiscalPeriodType {
491 Monthly,
493 Quarterly,
495 FourWeek,
497 Special,
499}
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
503pub enum PeriodStatus {
504 Open,
506 SoftClosed,
508 Closed,
510 Locked,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Hash)]
516pub enum CloseTask {
517 RunDepreciation,
519 PostInventoryRevaluation,
521 ReconcileArToGl,
523 ReconcileApToGl,
525 ReconcileFaToGl,
527 ReconcileInventoryToGl,
529 PostAccruedExpenses,
531 PostAccruedRevenue,
533 PostPrepaidAmortization,
535 AllocateCorporateOverhead,
537 PostIntercompanySettlements,
539 RevalueForeignCurrency,
541 CalculateTaxProvision,
543 TranslateForeignSubsidiaries,
545 EliminateIntercompany,
547 GenerateTrialBalance,
549 GenerateFinancialStatements,
551 CloseIncomeStatement,
553 PostRetainedEarningsRollforward,
555 Custom(String),
557}
558
559impl CloseTask {
560 pub fn is_year_end_only(&self) -> bool {
562 matches!(
563 self,
564 CloseTask::CloseIncomeStatement | CloseTask::PostRetainedEarningsRollforward
565 )
566 }
567
568 pub fn name(&self) -> &str {
570 match self {
571 CloseTask::RunDepreciation => "Run Depreciation",
572 CloseTask::PostInventoryRevaluation => "Post Inventory Revaluation",
573 CloseTask::ReconcileArToGl => "Reconcile AR to GL",
574 CloseTask::ReconcileApToGl => "Reconcile AP to GL",
575 CloseTask::ReconcileFaToGl => "Reconcile FA to GL",
576 CloseTask::ReconcileInventoryToGl => "Reconcile Inventory to GL",
577 CloseTask::PostAccruedExpenses => "Post Accrued Expenses",
578 CloseTask::PostAccruedRevenue => "Post Accrued Revenue",
579 CloseTask::PostPrepaidAmortization => "Post Prepaid Amortization",
580 CloseTask::AllocateCorporateOverhead => "Allocate Corporate Overhead",
581 CloseTask::PostIntercompanySettlements => "Post IC Settlements",
582 CloseTask::RevalueForeignCurrency => "Revalue Foreign Currency",
583 CloseTask::CalculateTaxProvision => "Calculate Tax Provision",
584 CloseTask::TranslateForeignSubsidiaries => "Translate Foreign Subs",
585 CloseTask::EliminateIntercompany => "Eliminate Intercompany",
586 CloseTask::GenerateTrialBalance => "Generate Trial Balance",
587 CloseTask::GenerateFinancialStatements => "Generate Financials",
588 CloseTask::CloseIncomeStatement => "Close Income Statement",
589 CloseTask::PostRetainedEarningsRollforward => "Post RE Rollforward",
590 CloseTask::Custom(name) => name,
591 }
592 }
593}
594
595#[derive(Debug, Clone, PartialEq, Eq)]
597pub enum CloseTaskStatus {
598 Pending,
600 InProgress,
602 Completed,
604 CompletedWithWarnings(Vec<String>),
606 Failed(String),
608 Skipped(String),
610}
611
612#[derive(Debug, Clone)]
614pub struct CloseTaskResult {
615 pub task: CloseTask,
617 pub company_code: String,
619 pub fiscal_period: FiscalPeriod,
621 pub status: CloseTaskStatus,
623 pub started_at: Option<NaiveDate>,
625 pub completed_at: Option<NaiveDate>,
627 pub journal_entries_created: u32,
629 pub total_amount: Decimal,
631 pub notes: Vec<String>,
633}
634
635impl CloseTaskResult {
636 pub fn new(task: CloseTask, company_code: String, fiscal_period: FiscalPeriod) -> Self {
638 Self {
639 task,
640 company_code,
641 fiscal_period,
642 status: CloseTaskStatus::Pending,
643 started_at: None,
644 completed_at: None,
645 journal_entries_created: 0,
646 total_amount: Decimal::ZERO,
647 notes: Vec::new(),
648 }
649 }
650
651 pub fn is_success(&self) -> bool {
653 matches!(
654 self.status,
655 CloseTaskStatus::Completed | CloseTaskStatus::CompletedWithWarnings(_)
656 )
657 }
658}
659
660#[derive(Debug, Clone)]
662pub struct AccrualDefinition {
663 pub accrual_id: String,
665 pub company_code: String,
667 pub description: String,
669 pub accrual_type: AccrualType,
671 pub expense_revenue_account: String,
673 pub accrual_account: String,
675 pub calculation_method: AccrualCalculationMethod,
677 pub fixed_amount: Option<Decimal>,
679 pub percentage_rate: Option<Decimal>,
681 pub base_account: Option<String>,
683 pub frequency: AccrualFrequency,
685 pub auto_reverse: bool,
687 pub cost_center: Option<String>,
689 pub is_active: bool,
691 pub effective_from: NaiveDate,
693 pub effective_to: Option<NaiveDate>,
695}
696
697impl AccrualDefinition {
698 pub fn new(
700 accrual_id: String,
701 company_code: String,
702 description: String,
703 accrual_type: AccrualType,
704 expense_revenue_account: String,
705 accrual_account: String,
706 ) -> Self {
707 Self {
708 accrual_id,
709 company_code,
710 description,
711 accrual_type,
712 expense_revenue_account,
713 accrual_account,
714 calculation_method: AccrualCalculationMethod::FixedAmount,
715 fixed_amount: None,
716 percentage_rate: None,
717 base_account: None,
718 frequency: AccrualFrequency::Monthly,
719 auto_reverse: true,
720 cost_center: None,
721 is_active: true,
722 effective_from: NaiveDate::from_ymd_opt(2020, 1, 1).expect("valid date components"),
723 effective_to: None,
724 }
725 }
726
727 pub fn with_fixed_amount(mut self, amount: Decimal) -> Self {
729 self.calculation_method = AccrualCalculationMethod::FixedAmount;
730 self.fixed_amount = Some(amount);
731 self
732 }
733
734 pub fn with_percentage(mut self, rate: Decimal, base_account: &str) -> Self {
736 self.calculation_method = AccrualCalculationMethod::PercentageOfBase;
737 self.percentage_rate = Some(rate);
738 self.base_account = Some(base_account.to_string());
739 self
740 }
741
742 pub fn is_effective_on(&self, date: NaiveDate) -> bool {
744 if !self.is_active {
745 return false;
746 }
747 if date < self.effective_from {
748 return false;
749 }
750 if let Some(end) = self.effective_to {
751 if date > end {
752 return false;
753 }
754 }
755 true
756 }
757}
758
759#[derive(Debug, Clone, Copy, PartialEq, Eq)]
761pub enum AccrualType {
762 AccruedExpense,
764 AccruedRevenue,
766 PrepaidExpense,
768 DeferredRevenue,
770}
771
772#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774pub enum AccrualCalculationMethod {
775 FixedAmount,
777 PercentageOfBase,
779 DaysBased,
781 Manual,
783}
784
785#[derive(Debug, Clone, Copy, PartialEq, Eq)]
787pub enum AccrualFrequency {
788 Monthly,
790 Quarterly,
792 Annually,
794}
795
796#[derive(Debug, Clone)]
798pub struct OverheadAllocation {
799 pub allocation_id: String,
801 pub source_company: String,
803 pub source_cost_center: String,
805 pub source_account: String,
807 pub allocation_basis: AllocationBasis,
809 pub targets: Vec<AllocationTarget>,
811 pub description: String,
813 pub is_active: bool,
815}
816
817#[derive(Debug, Clone, PartialEq, Eq)]
819pub enum AllocationBasis {
820 Revenue,
822 Headcount,
824 DirectCosts,
826 SquareFootage,
828 FixedPercentage,
830 Custom(String),
832}
833
834#[derive(Debug, Clone)]
836pub struct AllocationTarget {
837 pub company_code: String,
839 pub cost_center: String,
841 pub account: String,
843 pub percentage: Option<Decimal>,
845 pub driver_value: Option<Decimal>,
847}
848
849#[derive(Debug, Clone)]
851pub struct CloseSchedule {
852 pub schedule_id: String,
854 pub company_code: String,
856 pub period_type: FiscalPeriodType,
858 pub tasks: Vec<ScheduledCloseTask>,
860 pub is_year_end: bool,
862}
863
864impl CloseSchedule {
865 pub fn standard_monthly(company_code: &str) -> Self {
867 Self {
868 schedule_id: format!("MONTHLY-{company_code}"),
869 company_code: company_code.to_string(),
870 period_type: FiscalPeriodType::Monthly,
871 tasks: vec![
872 ScheduledCloseTask::new(CloseTask::RunDepreciation, 1),
873 ScheduledCloseTask::new(CloseTask::PostInventoryRevaluation, 2),
874 ScheduledCloseTask::new(CloseTask::PostAccruedExpenses, 3),
875 ScheduledCloseTask::new(CloseTask::PostAccruedRevenue, 4),
876 ScheduledCloseTask::new(CloseTask::PostPrepaidAmortization, 5),
877 ScheduledCloseTask::new(CloseTask::RevalueForeignCurrency, 6),
878 ScheduledCloseTask::new(CloseTask::ReconcileArToGl, 7),
879 ScheduledCloseTask::new(CloseTask::ReconcileApToGl, 8),
880 ScheduledCloseTask::new(CloseTask::ReconcileFaToGl, 9),
881 ScheduledCloseTask::new(CloseTask::ReconcileInventoryToGl, 10),
882 ScheduledCloseTask::new(CloseTask::PostIntercompanySettlements, 11),
883 ScheduledCloseTask::new(CloseTask::AllocateCorporateOverhead, 12),
884 ScheduledCloseTask::new(CloseTask::TranslateForeignSubsidiaries, 13),
885 ScheduledCloseTask::new(CloseTask::EliminateIntercompany, 14),
886 ScheduledCloseTask::new(CloseTask::GenerateTrialBalance, 15),
887 ],
888 is_year_end: false,
889 }
890 }
891
892 pub fn year_end(company_code: &str) -> Self {
894 let mut schedule = Self::standard_monthly(company_code);
895 schedule.schedule_id = format!("YEAREND-{company_code}");
896 schedule.is_year_end = true;
897
898 let next_seq = schedule.tasks.len() as u32 + 1;
900 schedule.tasks.push(ScheduledCloseTask::new(
901 CloseTask::CalculateTaxProvision,
902 next_seq,
903 ));
904 schedule.tasks.push(ScheduledCloseTask::new(
905 CloseTask::CloseIncomeStatement,
906 next_seq + 1,
907 ));
908 schedule.tasks.push(ScheduledCloseTask::new(
909 CloseTask::PostRetainedEarningsRollforward,
910 next_seq + 2,
911 ));
912 schedule.tasks.push(ScheduledCloseTask::new(
913 CloseTask::GenerateFinancialStatements,
914 next_seq + 3,
915 ));
916
917 schedule
918 }
919}
920
921#[derive(Debug, Clone)]
923pub struct ScheduledCloseTask {
924 pub task: CloseTask,
926 pub sequence: u32,
928 pub depends_on: Vec<CloseTask>,
930 pub is_mandatory: bool,
932 pub can_parallelize: bool,
934}
935
936impl ScheduledCloseTask {
937 pub fn new(task: CloseTask, sequence: u32) -> Self {
939 Self {
940 task,
941 sequence,
942 depends_on: Vec::new(),
943 is_mandatory: true,
944 can_parallelize: false,
945 }
946 }
947
948 pub fn depends_on(mut self, task: CloseTask) -> Self {
950 self.depends_on.push(task);
951 self
952 }
953
954 pub fn optional(mut self) -> Self {
956 self.is_mandatory = false;
957 self
958 }
959
960 pub fn parallelizable(mut self) -> Self {
962 self.can_parallelize = true;
963 self
964 }
965}
966
967#[derive(Debug, Clone)]
969pub struct YearEndClosingSpec {
970 pub company_code: String,
972 pub fiscal_year: i32,
974 pub revenue_accounts: Vec<String>,
976 pub expense_accounts: Vec<String>,
978 pub income_summary_account: String,
980 pub retained_earnings_account: String,
982 pub dividend_account: Option<String>,
984}
985
986impl Default for YearEndClosingSpec {
987 fn default() -> Self {
988 Self {
989 company_code: String::new(),
990 fiscal_year: 0,
991 revenue_accounts: vec!["4".to_string()], expense_accounts: vec!["5".to_string(), "6".to_string()], income_summary_account: "3500".to_string(),
994 retained_earnings_account: "3300".to_string(),
995 dividend_account: Some("3400".to_string()),
996 }
997 }
998}
999
1000#[derive(Debug, Clone)]
1002pub struct TaxProvisionInput {
1003 pub company_code: String,
1005 pub fiscal_year: i32,
1007 pub pretax_income: Decimal,
1009 pub permanent_differences: Vec<TaxAdjustment>,
1011 pub temporary_differences: Vec<TaxAdjustment>,
1013 pub statutory_rate: Decimal,
1015 pub tax_credits: Decimal,
1017 pub prior_year_adjustment: Decimal,
1019}
1020
1021#[derive(Debug, Clone)]
1023pub struct TaxAdjustment {
1024 pub description: String,
1026 pub amount: Decimal,
1028 pub is_addition: bool,
1030}
1031
1032#[derive(Debug, Clone)]
1034pub struct TaxProvisionResult {
1035 pub company_code: String,
1037 pub fiscal_year: i32,
1039 pub pretax_income: Decimal,
1041 pub permanent_differences: Decimal,
1043 pub taxable_income: Decimal,
1045 pub current_tax_expense: Decimal,
1047 pub deferred_tax_expense: Decimal,
1049 pub total_tax_expense: Decimal,
1051 pub effective_rate: Decimal,
1053}
1054
1055impl TaxProvisionResult {
1056 pub fn calculate(input: &TaxProvisionInput) -> Self {
1058 let permanent_diff: Decimal = input
1059 .permanent_differences
1060 .iter()
1061 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1062 .sum();
1063
1064 let temporary_diff: Decimal = input
1065 .temporary_differences
1066 .iter()
1067 .map(|d| if d.is_addition { d.amount } else { -d.amount })
1068 .sum();
1069
1070 let taxable_income = input.pretax_income + permanent_diff;
1071 let current_tax = (taxable_income * input.statutory_rate / dec!(100)).round_dp(2);
1072 let deferred_tax = (temporary_diff * input.statutory_rate / dec!(100)).round_dp(2);
1073
1074 let total_tax =
1075 current_tax + deferred_tax - input.tax_credits + input.prior_year_adjustment;
1076
1077 let effective_rate = if input.pretax_income != Decimal::ZERO {
1078 (total_tax / input.pretax_income * dec!(100)).round_dp(2)
1079 } else {
1080 Decimal::ZERO
1081 };
1082
1083 Self {
1084 company_code: input.company_code.clone(),
1085 fiscal_year: input.fiscal_year,
1086 pretax_income: input.pretax_income,
1087 permanent_differences: permanent_diff,
1088 taxable_income,
1089 current_tax_expense: current_tax,
1090 deferred_tax_expense: deferred_tax,
1091 total_tax_expense: total_tax,
1092 effective_rate,
1093 }
1094 }
1095}
1096
1097#[derive(Debug, Clone)]
1099pub struct PeriodCloseRun {
1100 pub run_id: String,
1102 pub company_code: String,
1104 pub fiscal_period: FiscalPeriod,
1106 pub status: PeriodCloseStatus,
1108 pub task_results: Vec<CloseTaskResult>,
1110 pub started_at: Option<NaiveDate>,
1112 pub completed_at: Option<NaiveDate>,
1114 pub total_journal_entries: u32,
1116 pub errors: Vec<String>,
1118}
1119
1120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1122pub enum PeriodCloseStatus {
1123 NotStarted,
1125 InProgress,
1127 Completed,
1129 CompletedWithErrors,
1131 Failed,
1133}
1134
1135impl PeriodCloseRun {
1136 pub fn new(run_id: String, company_code: String, fiscal_period: FiscalPeriod) -> Self {
1138 Self {
1139 run_id,
1140 company_code,
1141 fiscal_period,
1142 status: PeriodCloseStatus::NotStarted,
1143 task_results: Vec::new(),
1144 started_at: None,
1145 completed_at: None,
1146 total_journal_entries: 0,
1147 errors: Vec::new(),
1148 }
1149 }
1150
1151 pub fn is_success(&self) -> bool {
1153 self.status == PeriodCloseStatus::Completed
1154 }
1155
1156 pub fn failed_task_count(&self) -> usize {
1158 self.task_results
1159 .iter()
1160 .filter(|r| matches!(r.status, CloseTaskStatus::Failed(_)))
1161 .count()
1162 }
1163}
1164
1165#[cfg(test)]
1166#[allow(clippy::unwrap_used)]
1167mod tests {
1168 use super::*;
1169
1170 #[test]
1171 fn test_fiscal_period_monthly() {
1172 let period = FiscalPeriod::monthly(2024, 1);
1173 assert_eq!(
1174 period.start_date,
1175 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()
1176 );
1177 assert_eq!(
1178 period.end_date,
1179 NaiveDate::from_ymd_opt(2024, 1, 31).unwrap()
1180 );
1181 assert_eq!(period.days(), 31);
1182 assert!(!period.is_year_end);
1183
1184 let dec_period = FiscalPeriod::monthly(2024, 12);
1185 assert!(dec_period.is_year_end);
1186 }
1187
1188 #[test]
1189 fn test_fiscal_period_quarterly() {
1190 let q1 = FiscalPeriod::quarterly(2024, 1);
1191 assert_eq!(q1.start_date, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1192 assert_eq!(q1.end_date, NaiveDate::from_ymd_opt(2024, 3, 31).unwrap());
1193
1194 let q4 = FiscalPeriod::quarterly(2024, 4);
1195 assert!(q4.is_year_end);
1196 }
1197
1198 #[test]
1199 fn test_close_schedule() {
1200 let schedule = CloseSchedule::standard_monthly("1000");
1201 assert!(!schedule.is_year_end);
1202 assert!(!schedule.tasks.is_empty());
1203
1204 let year_end = CloseSchedule::year_end("1000");
1205 assert!(year_end.is_year_end);
1206 assert!(year_end.tasks.len() > schedule.tasks.len());
1207 }
1208
1209 #[test]
1210 fn test_tax_provision() {
1211 let input = TaxProvisionInput {
1212 company_code: "1000".to_string(),
1213 fiscal_year: 2024,
1214 pretax_income: dec!(1000000),
1215 permanent_differences: vec![TaxAdjustment {
1216 description: "Meals & Entertainment".to_string(),
1217 amount: dec!(10000),
1218 is_addition: true,
1219 }],
1220 temporary_differences: vec![TaxAdjustment {
1221 description: "Depreciation Timing".to_string(),
1222 amount: dec!(50000),
1223 is_addition: false,
1224 }],
1225 statutory_rate: dec!(21),
1226 tax_credits: dec!(5000),
1227 prior_year_adjustment: Decimal::ZERO,
1228 };
1229
1230 let result = TaxProvisionResult::calculate(&input);
1231 assert_eq!(result.taxable_income, dec!(1010000)); assert!(result.current_tax_expense > Decimal::ZERO);
1233 }
1234}