Skip to main content

datasynth_core/models/
period_close.rs

1//! Period close models.
2//!
3//! This module provides models for fiscal period management and
4//! period-end close processes including:
5//! - Fiscal period definitions
6//! - Fiscal calendar types (calendar year, custom year start, 4-4-5, 13-period)
7//! - Close tasks and workflows
8//! - Accrual definitions and schedules
9//! - Year-end closing entries
10
11use chrono::{Datelike, NaiveDate};
12use rust_decimal::Decimal;
13use rust_decimal_macros::dec;
14use serde::{Deserialize, Serialize};
15
16/// Type of fiscal calendar used by the organization.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
18#[serde(tag = "type", rename_all = "snake_case")]
19pub enum FiscalCalendarType {
20    /// Standard calendar year (Jan 1 - Dec 31).
21    #[default]
22    CalendarYear,
23    /// Custom year start (e.g., July 1 for government fiscal years).
24    CustomYearStart {
25        /// Month the fiscal year starts (1-12).
26        start_month: u8,
27        /// Day the fiscal year starts (1-31).
28        start_day: u8,
29    },
30    /// 4-4-5 retail calendar (52/53 week years with 4-4-5, 4-5-4, or 5-4-4 pattern).
31    FourFourFive(FourFourFiveConfig),
32    /// 13-period calendar (13 equal 4-week periods).
33    ThirteenPeriod(ThirteenPeriodConfig),
34}
35
36/// Configuration for 4-4-5 retail calendar.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct FourFourFiveConfig {
39    /// Week pattern for each quarter.
40    pub pattern: WeekPattern,
41    /// Anchor point for determining fiscal year start.
42    pub anchor: FourFourFiveAnchor,
43    /// Where to place the leap week in 53-week years.
44    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), // Last Saturday of January
52            leap_week_placement: LeapWeekPlacement::Q4Period3,
53        }
54    }
55}
56
57/// Week pattern for 4-4-5 calendar quarters.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum WeekPattern {
61    /// 4 weeks, 4 weeks, 5 weeks per quarter.
62    FourFourFive,
63    /// 4 weeks, 5 weeks, 4 weeks per quarter.
64    FourFiveFour,
65    /// 5 weeks, 4 weeks, 4 weeks per quarter.
66    FiveFourFour,
67}
68
69impl WeekPattern {
70    /// Returns the number of weeks in each period of a quarter.
71    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/// Anchor point for 4-4-5 fiscal year start.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(tag = "type", content = "month", rename_all = "snake_case")]
83pub enum FourFourFiveAnchor {
84    /// Fiscal year starts on the first Sunday of a month.
85    FirstSundayOf(u8),
86    /// Fiscal year starts on the last Saturday of a month.
87    LastSaturdayOf(u8),
88    /// Fiscal year ends on the Saturday nearest to a month end.
89    NearestSaturdayTo(u8),
90}
91
92/// Where to place the leap week in 53-week years.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum LeapWeekPlacement {
96    /// Add leap week to Q4 Period 3 (most common).
97    Q4Period3,
98    /// Add leap week to Q1 Period 1.
99    Q1Period1,
100}
101
102/// Configuration for 13-period calendar.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct ThirteenPeriodConfig {
105    /// First day of fiscal year (day of year, 1-366).
106    pub year_start_day: u16,
107    /// Month containing year start (for display purposes).
108    pub year_start_month: u8,
109}
110
111impl Default for ThirteenPeriodConfig {
112    fn default() -> Self {
113        Self {
114            year_start_day: 1,   // January 1
115            year_start_month: 1, // January
116        }
117    }
118}
119
120/// Fiscal calendar definition.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct FiscalCalendar {
123    /// Type of fiscal calendar.
124    pub calendar_type: FiscalCalendarType,
125    /// Name of the fiscal calendar (e.g., "US Federal", "Retail 445").
126    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    /// Creates a standard calendar year fiscal calendar.
140    pub fn calendar_year() -> Self {
141        Self::default()
142    }
143
144    /// Creates a fiscal calendar with custom year start.
145    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    /// Creates a 4-4-5 retail calendar.
156    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    /// Creates a 13-period calendar.
164    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    /// Returns the fiscal year for a given date.
172    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                // APPROXIMATION: Returns the calendar year as a proxy for the fiscal year.
193                //
194                // Both the 4-4-5 and 13-period calendars are week-based: the fiscal year
195                // starts on a specific day of the week (e.g. the last Saturday of January)
196                // anchored by `FourFourFiveConfig::anchor` or a similar configuration.
197                // This means the fiscal year can begin in late December of the *prior*
198                // calendar year, causing dates in the week of Dec 25 – Jan 3 to belong
199                // to a different fiscal year than the calendar year suggests.
200                //
201                // A correct implementation would:
202                //   1. Resolve the anchor date for the current year (e.g. last Saturday ≤ Jan 31).
203                //   2. Compare the input date against that anchor.
204                //   3. Return `date.year() - 1` when the date falls before the anchor.
205                //
206                // Until that logic is implemented, synthetic data generated near
207                // fiscal-year boundaries may have off-by-one period labels.
208                date.year()
209            }
210        }
211    }
212
213    /// Returns the fiscal period number for a given date.
214    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                // LIMITATION: Simplified — assumes 28 calendar days per period
230                // (13 × 28 = 364, ignoring the 365th day). A precise implementation
231                // would use the actual week-based boundaries derived from the
232                // configured year-start anchor. Sufficient for simulation; not
233                // appropriate for production period-close where day-365 placement
234                // matters.
235                let day_of_year = date.ordinal();
236                ((day_of_year - 1) / 28 + 1).min(13) as u8
237            }
238            FiscalCalendarType::FourFourFive(config) => {
239                // APPROXIMATION: Simplified 4-4-5 period calculation.
240                //
241                // This implementation ignores the configured anchor
242                // (`FirstSundayOf` / `LastSaturdayOf`) and assumes the fiscal year starts
243                // on January 1 of the calendar year.  It then maps the date's week-of-year
244                // to a period number using the configured 4-4-5 / 4-5-4 / 5-4-4 pattern.
245                //
246                // Known limitations:
247                //   • Dates near the fiscal-year boundary (late December / early January)
248                //     may be assigned to the wrong period when the true anchor differs from
249                //     January 1.
250                //   • 53-week years (where the 52nd week falls before year-end) are not
251                //     handled; `LeapWeekPlacement` is ignored.
252                //
253                // A precise implementation would resolve the anchor date, build a week-number
254                // → period mapping for the full 52 (or 53) weeks, and look up the input
255                // date's ISO-week number in that mapping.
256                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 // Default to period 12
268            }
269        }
270    }
271}
272
273/// Helper function to get month name.
274fn 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/// Fiscal period representation.
293#[derive(Debug, Clone, PartialEq, Eq, Hash)]
294pub struct FiscalPeriod {
295    /// Fiscal year.
296    pub year: i32,
297    /// Period number (1-12 for monthly, 1-4 for quarterly).
298    pub period: u8,
299    /// Period start date.
300    pub start_date: NaiveDate,
301    /// Period end date.
302    pub end_date: NaiveDate,
303    /// Period type.
304    pub period_type: FiscalPeriodType,
305    /// Is this the year-end period?
306    pub is_year_end: bool,
307    /// Period status.
308    pub status: PeriodStatus,
309}
310
311impl FiscalPeriod {
312    /// Creates a monthly fiscal period.
313    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    /// Creates a quarterly fiscal period.
340    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    /// Returns the number of days in the period.
370    pub fn days(&self) -> i64 {
371        (self.end_date - self.start_date).num_days() + 1
372    }
373
374    /// Returns the period key (e.g., "2024-01" for monthly).
375    pub fn key(&self) -> String {
376        format!("{}-{:02}", self.year, self.period)
377    }
378
379    /// Checks if a date falls within this period.
380    pub fn contains(&self, date: NaiveDate) -> bool {
381        date >= self.start_date && date <= self.end_date
382    }
383
384    /// Creates a fiscal period from a calendar and date.
385    ///
386    /// This method determines the correct fiscal period for a given date
387    /// based on the fiscal calendar configuration.
388    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                // Calculate the actual start and end dates for the period
399                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                    // Last period ends day before fiscal year start
423                    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                // 4-4-5 calendar: 12 periods of 4 or 5 weeks each
459                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                // Calculate start of fiscal year (simplified)
465                let year_start =
466                    NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
467
468                // Calculate period start by summing previous period weeks
469                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                // 13 periods of 28 days each (4 weeks)
495                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/// Type of fiscal period.
515#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
516pub enum FiscalPeriodType {
517    /// Monthly period.
518    Monthly,
519    /// Quarterly period.
520    Quarterly,
521    /// Four-week period (used in 4-4-5 and 13-period calendars).
522    FourWeek,
523    /// Special period (13th period, adjustments).
524    Special,
525}
526
527/// Status of a fiscal period.
528#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
529pub enum PeriodStatus {
530    /// Period is open for posting.
531    Open,
532    /// Soft close - limited posting allowed.
533    SoftClosed,
534    /// Hard close - no posting allowed.
535    Closed,
536    /// Period is locked for audit.
537    Locked,
538}
539
540/// Close task types for period-end processing.
541#[derive(Debug, Clone, PartialEq, Eq, Hash)]
542pub enum CloseTask {
543    /// Run depreciation for fixed assets.
544    RunDepreciation,
545    /// Post inventory revaluation adjustments.
546    PostInventoryRevaluation,
547    /// Reconcile AR subledger to GL.
548    ReconcileArToGl,
549    /// Reconcile AP subledger to GL.
550    ReconcileApToGl,
551    /// Reconcile FA subledger to GL.
552    ReconcileFaToGl,
553    /// Reconcile Inventory to GL.
554    ReconcileInventoryToGl,
555    /// Post accrued expenses.
556    PostAccruedExpenses,
557    /// Post accrued revenue.
558    PostAccruedRevenue,
559    /// Post prepaid expense amortization.
560    PostPrepaidAmortization,
561    /// Allocate corporate overhead.
562    AllocateCorporateOverhead,
563    /// Post intercompany settlements.
564    PostIntercompanySettlements,
565    /// Revalue foreign currency balances.
566    RevalueForeignCurrency,
567    /// Calculate and post tax provision.
568    CalculateTaxProvision,
569    /// Translate foreign subsidiary trial balances.
570    TranslateForeignSubsidiaries,
571    /// Eliminate intercompany balances.
572    EliminateIntercompany,
573    /// Generate trial balance.
574    GenerateTrialBalance,
575    /// Generate financial statements.
576    GenerateFinancialStatements,
577    /// Close income statement accounts (year-end).
578    CloseIncomeStatement,
579    /// Post retained earnings rollforward (year-end).
580    PostRetainedEarningsRollforward,
581    /// Custom task.
582    Custom(String),
583}
584
585impl CloseTask {
586    /// Returns true if this is a year-end only task.
587    pub fn is_year_end_only(&self) -> bool {
588        matches!(
589            self,
590            CloseTask::CloseIncomeStatement | CloseTask::PostRetainedEarningsRollforward
591        )
592    }
593
594    /// Returns the task name.
595    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/// Status of a close task execution.
622#[derive(Debug, Clone, PartialEq, Eq)]
623pub enum CloseTaskStatus {
624    /// Not started.
625    Pending,
626    /// In progress.
627    InProgress,
628    /// Completed successfully.
629    Completed,
630    /// Completed with warnings.
631    CompletedWithWarnings(Vec<String>),
632    /// Failed.
633    Failed(String),
634    /// Skipped.
635    Skipped(String),
636}
637
638/// Result of executing a close task.
639#[derive(Debug, Clone)]
640pub struct CloseTaskResult {
641    /// Task that was executed.
642    pub task: CloseTask,
643    /// Company code.
644    pub company_code: String,
645    /// Fiscal period.
646    pub fiscal_period: FiscalPeriod,
647    /// Status.
648    pub status: CloseTaskStatus,
649    /// Start time.
650    pub started_at: Option<NaiveDate>,
651    /// End time.
652    pub completed_at: Option<NaiveDate>,
653    /// Journal entries created.
654    pub journal_entries_created: u32,
655    /// Total amount posted.
656    pub total_amount: Decimal,
657    /// Execution notes.
658    pub notes: Vec<String>,
659}
660
661impl CloseTaskResult {
662    /// Creates a new task result.
663    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    /// Returns true if the task completed successfully.
678    pub fn is_success(&self) -> bool {
679        matches!(
680            self.status,
681            CloseTaskStatus::Completed | CloseTaskStatus::CompletedWithWarnings(_)
682        )
683    }
684}
685
686/// Accrual definition for recurring period-end entries.
687#[derive(Debug, Clone)]
688pub struct AccrualDefinition {
689    /// Accrual ID.
690    pub accrual_id: String,
691    /// Company code.
692    pub company_code: String,
693    /// Description.
694    pub description: String,
695    /// Accrual type.
696    pub accrual_type: AccrualType,
697    /// Expense/Revenue account to debit/credit.
698    pub expense_revenue_account: String,
699    /// Accrual liability/asset account.
700    pub accrual_account: String,
701    /// Calculation method.
702    pub calculation_method: AccrualCalculationMethod,
703    /// Fixed amount (if applicable).
704    pub fixed_amount: Option<Decimal>,
705    /// Percentage rate (if applicable).
706    pub percentage_rate: Option<Decimal>,
707    /// Base account for percentage calculation.
708    pub base_account: Option<String>,
709    /// Frequency.
710    pub frequency: AccrualFrequency,
711    /// Auto-reverse on first day of next period.
712    pub auto_reverse: bool,
713    /// Cost center.
714    pub cost_center: Option<String>,
715    /// Active flag.
716    pub is_active: bool,
717    /// Start date.
718    pub effective_from: NaiveDate,
719    /// End date (if defined).
720    pub effective_to: Option<NaiveDate>,
721}
722
723impl AccrualDefinition {
724    /// Creates a new accrual definition.
725    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    /// Sets the fixed amount.
754    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    /// Sets percentage-based calculation.
761    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    /// Checks if the accrual is effective for a given date.
769    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/// Type of accrual.
786#[derive(Debug, Clone, Copy, PartialEq, Eq)]
787pub enum AccrualType {
788    /// Accrued expense (debit expense, credit liability).
789    AccruedExpense,
790    /// Accrued revenue (debit asset, credit revenue).
791    AccruedRevenue,
792    /// Prepaid expense (debit expense, credit asset).
793    PrepaidExpense,
794    /// Deferred revenue (debit liability, credit revenue).
795    DeferredRevenue,
796}
797
798/// Calculation method for accruals.
799#[derive(Debug, Clone, Copy, PartialEq, Eq)]
800pub enum AccrualCalculationMethod {
801    /// Fixed amount each period.
802    FixedAmount,
803    /// Percentage of a base account balance.
804    PercentageOfBase,
805    /// Days-based proration.
806    DaysBased,
807    /// Calculated externally (manual entry).
808    Manual,
809}
810
811/// Frequency for accrual posting.
812#[derive(Debug, Clone, Copy, PartialEq, Eq)]
813pub enum AccrualFrequency {
814    /// Every month.
815    Monthly,
816    /// Every quarter.
817    Quarterly,
818    /// Every year.
819    Annually,
820}
821
822/// Corporate overhead allocation definition.
823#[derive(Debug, Clone)]
824pub struct OverheadAllocation {
825    /// Allocation ID.
826    pub allocation_id: String,
827    /// Source company code (corporate).
828    pub source_company: String,
829    /// Source cost center.
830    pub source_cost_center: String,
831    /// Source account.
832    pub source_account: String,
833    /// Allocation basis.
834    pub allocation_basis: AllocationBasis,
835    /// Target allocations.
836    pub targets: Vec<AllocationTarget>,
837    /// Description.
838    pub description: String,
839    /// Active flag.
840    pub is_active: bool,
841}
842
843/// Basis for overhead allocation.
844#[derive(Debug, Clone, PartialEq, Eq)]
845pub enum AllocationBasis {
846    /// Based on revenue.
847    Revenue,
848    /// Based on headcount.
849    Headcount,
850    /// Based on direct costs.
851    DirectCosts,
852    /// Based on square footage.
853    SquareFootage,
854    /// Fixed percentages.
855    FixedPercentage,
856    /// Custom formula.
857    Custom(String),
858}
859
860/// Target for overhead allocation.
861#[derive(Debug, Clone)]
862pub struct AllocationTarget {
863    /// Target company code.
864    pub company_code: String,
865    /// Target cost center.
866    pub cost_center: String,
867    /// Target account.
868    pub account: String,
869    /// Allocation percentage (for fixed percentage basis).
870    pub percentage: Option<Decimal>,
871    /// Allocation driver value (for calculated basis).
872    pub driver_value: Option<Decimal>,
873}
874
875/// Period close schedule defining the order of tasks.
876#[derive(Debug, Clone)]
877pub struct CloseSchedule {
878    /// Schedule ID.
879    pub schedule_id: String,
880    /// Company code (or "ALL" for all companies).
881    pub company_code: String,
882    /// Period type this schedule applies to.
883    pub period_type: FiscalPeriodType,
884    /// Ordered list of tasks.
885    pub tasks: Vec<ScheduledCloseTask>,
886    /// Whether this is for year-end.
887    pub is_year_end: bool,
888}
889
890impl CloseSchedule {
891    /// Creates a standard monthly close schedule.
892    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    /// Creates a year-end close schedule.
919    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        // Add year-end specific tasks
925        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/// A scheduled close task with sequence and dependencies.
948#[derive(Debug, Clone)]
949pub struct ScheduledCloseTask {
950    /// The task to execute.
951    pub task: CloseTask,
952    /// Sequence number (execution order).
953    pub sequence: u32,
954    /// Tasks that must complete before this one.
955    pub depends_on: Vec<CloseTask>,
956    /// Is this task mandatory?
957    pub is_mandatory: bool,
958    /// Can this task run in parallel with others at same sequence?
959    pub can_parallelize: bool,
960}
961
962impl ScheduledCloseTask {
963    /// Creates a new scheduled task.
964    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    /// Adds a dependency.
975    pub fn depends_on(mut self, task: CloseTask) -> Self {
976        self.depends_on.push(task);
977        self
978    }
979
980    /// Marks as optional.
981    pub fn optional(mut self) -> Self {
982        self.is_mandatory = false;
983        self
984    }
985
986    /// Allows parallel execution.
987    pub fn parallelizable(mut self) -> Self {
988        self.can_parallelize = true;
989        self
990    }
991}
992
993/// Year-end closing entry specification.
994#[derive(Debug, Clone)]
995pub struct YearEndClosingSpec {
996    /// Company code.
997    pub company_code: String,
998    /// Fiscal year being closed.
999    pub fiscal_year: i32,
1000    /// Revenue accounts to close.
1001    pub revenue_accounts: Vec<String>,
1002    /// Expense accounts to close.
1003    pub expense_accounts: Vec<String>,
1004    /// Income summary account (temporary).
1005    pub income_summary_account: String,
1006    /// Retained earnings account.
1007    pub retained_earnings_account: String,
1008    /// Dividend account (if applicable).
1009    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()], // All accounts starting with 4
1018            expense_accounts: vec!["5".to_string(), "6".to_string()], // Accounts starting with 5, 6
1019            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/// Tax provision calculation inputs.
1027#[derive(Debug, Clone)]
1028pub struct TaxProvisionInput {
1029    /// Company code.
1030    pub company_code: String,
1031    /// Fiscal year.
1032    pub fiscal_year: i32,
1033    /// Pre-tax book income.
1034    pub pretax_income: Decimal,
1035    /// Permanent differences (add back).
1036    pub permanent_differences: Vec<TaxAdjustment>,
1037    /// Temporary differences (timing).
1038    pub temporary_differences: Vec<TaxAdjustment>,
1039    /// Statutory tax rate.
1040    pub statutory_rate: Decimal,
1041    /// Tax credits available.
1042    pub tax_credits: Decimal,
1043    /// Prior year over/under provision.
1044    pub prior_year_adjustment: Decimal,
1045}
1046
1047/// Tax adjustment item.
1048#[derive(Debug, Clone)]
1049pub struct TaxAdjustment {
1050    /// Description.
1051    pub description: String,
1052    /// Amount.
1053    pub amount: Decimal,
1054    /// Is this a deduction (negative) or addition (positive)?
1055    pub is_addition: bool,
1056}
1057
1058/// Tax provision result.
1059#[derive(Debug, Clone)]
1060pub struct TaxProvisionResult {
1061    /// Company code.
1062    pub company_code: String,
1063    /// Fiscal year.
1064    pub fiscal_year: i32,
1065    /// Pre-tax book income.
1066    pub pretax_income: Decimal,
1067    /// Total permanent differences.
1068    pub permanent_differences: Decimal,
1069    /// Taxable income.
1070    pub taxable_income: Decimal,
1071    /// Current tax expense.
1072    pub current_tax_expense: Decimal,
1073    /// Deferred tax expense (benefit).
1074    pub deferred_tax_expense: Decimal,
1075    /// Total tax expense.
1076    pub total_tax_expense: Decimal,
1077    /// Effective tax rate.
1078    pub effective_rate: Decimal,
1079}
1080
1081impl TaxProvisionResult {
1082    /// Calculates the tax provision from inputs.
1083    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/// Period close run status.
1124#[derive(Debug, Clone)]
1125pub struct PeriodCloseRun {
1126    /// Run ID.
1127    pub run_id: String,
1128    /// Company code.
1129    pub company_code: String,
1130    /// Fiscal period.
1131    pub fiscal_period: FiscalPeriod,
1132    /// Status.
1133    pub status: PeriodCloseStatus,
1134    /// Task results.
1135    pub task_results: Vec<CloseTaskResult>,
1136    /// Started at.
1137    pub started_at: Option<NaiveDate>,
1138    /// Completed at.
1139    pub completed_at: Option<NaiveDate>,
1140    /// Total journal entries created.
1141    pub total_journal_entries: u32,
1142    /// Errors encountered.
1143    pub errors: Vec<String>,
1144}
1145
1146/// Status of a period close run.
1147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1148pub enum PeriodCloseStatus {
1149    /// Not started.
1150    NotStarted,
1151    /// In progress.
1152    InProgress,
1153    /// Completed successfully.
1154    Completed,
1155    /// Completed with errors.
1156    CompletedWithErrors,
1157    /// Failed.
1158    Failed,
1159}
1160
1161impl PeriodCloseRun {
1162    /// Creates a new period close run.
1163    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    /// Returns true if all tasks completed successfully.
1178    pub fn is_success(&self) -> bool {
1179        self.status == PeriodCloseStatus::Completed
1180    }
1181
1182    /// Returns the number of failed tasks.
1183    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)); // 1M + 10K permanent
1257        assert!(result.current_tax_expense > Decimal::ZERO);
1258    }
1259}