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                // Simplified - would need more complex calculation
193                date.year()
194            }
195        }
196    }
197
198    /// Returns the fiscal period number for a given date.
199    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                // Simplified: 28 days per period
215                let day_of_year = date.ordinal();
216                ((day_of_year - 1) / 28 + 1).min(13) as u8
217            }
218            FiscalCalendarType::FourFourFive(config) => {
219                // Simplified 4-4-5 period calculation
220                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 // Default to period 12
232            }
233        }
234    }
235}
236
237/// Helper function to get month name.
238fn 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/// Fiscal period representation.
257#[derive(Debug, Clone, PartialEq, Eq, Hash)]
258pub struct FiscalPeriod {
259    /// Fiscal year.
260    pub year: i32,
261    /// Period number (1-12 for monthly, 1-4 for quarterly).
262    pub period: u8,
263    /// Period start date.
264    pub start_date: NaiveDate,
265    /// Period end date.
266    pub end_date: NaiveDate,
267    /// Period type.
268    pub period_type: FiscalPeriodType,
269    /// Is this the year-end period?
270    pub is_year_end: bool,
271    /// Period status.
272    pub status: PeriodStatus,
273}
274
275impl FiscalPeriod {
276    /// Creates a monthly fiscal period.
277    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    /// Creates a quarterly fiscal period.
304    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    /// Returns the number of days in the period.
334    pub fn days(&self) -> i64 {
335        (self.end_date - self.start_date).num_days() + 1
336    }
337
338    /// Returns the period key (e.g., "2024-01" for monthly).
339    pub fn key(&self) -> String {
340        format!("{}-{:02}", self.year, self.period)
341    }
342
343    /// Checks if a date falls within this period.
344    pub fn contains(&self, date: NaiveDate) -> bool {
345        date >= self.start_date && date <= self.end_date
346    }
347
348    /// Creates a fiscal period from a calendar and date.
349    ///
350    /// This method determines the correct fiscal period for a given date
351    /// based on the fiscal calendar configuration.
352    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                // Calculate the actual start and end dates for the period
363                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                    // Last period ends day before fiscal year start
387                    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                // 4-4-5 calendar: 12 periods of 4 or 5 weeks each
423                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                // Calculate start of fiscal year (simplified)
429                let year_start =
430                    NaiveDate::from_ymd_opt(fiscal_year, 1, 1).expect("valid date components");
431
432                // Calculate period start by summing previous period weeks
433                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                // 13 periods of 28 days each (4 weeks)
459                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/// Type of fiscal period.
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
480pub enum FiscalPeriodType {
481    /// Monthly period.
482    Monthly,
483    /// Quarterly period.
484    Quarterly,
485    /// Four-week period (used in 4-4-5 and 13-period calendars).
486    FourWeek,
487    /// Special period (13th period, adjustments).
488    Special,
489}
490
491/// Status of a fiscal period.
492#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
493pub enum PeriodStatus {
494    /// Period is open for posting.
495    Open,
496    /// Soft close - limited posting allowed.
497    SoftClosed,
498    /// Hard close - no posting allowed.
499    Closed,
500    /// Period is locked for audit.
501    Locked,
502}
503
504/// Close task types for period-end processing.
505#[derive(Debug, Clone, PartialEq, Eq, Hash)]
506pub enum CloseTask {
507    /// Run depreciation for fixed assets.
508    RunDepreciation,
509    /// Post inventory revaluation adjustments.
510    PostInventoryRevaluation,
511    /// Reconcile AR subledger to GL.
512    ReconcileArToGl,
513    /// Reconcile AP subledger to GL.
514    ReconcileApToGl,
515    /// Reconcile FA subledger to GL.
516    ReconcileFaToGl,
517    /// Reconcile Inventory to GL.
518    ReconcileInventoryToGl,
519    /// Post accrued expenses.
520    PostAccruedExpenses,
521    /// Post accrued revenue.
522    PostAccruedRevenue,
523    /// Post prepaid expense amortization.
524    PostPrepaidAmortization,
525    /// Allocate corporate overhead.
526    AllocateCorporateOverhead,
527    /// Post intercompany settlements.
528    PostIntercompanySettlements,
529    /// Revalue foreign currency balances.
530    RevalueForeignCurrency,
531    /// Calculate and post tax provision.
532    CalculateTaxProvision,
533    /// Translate foreign subsidiary trial balances.
534    TranslateForeignSubsidiaries,
535    /// Eliminate intercompany balances.
536    EliminateIntercompany,
537    /// Generate trial balance.
538    GenerateTrialBalance,
539    /// Generate financial statements.
540    GenerateFinancialStatements,
541    /// Close income statement accounts (year-end).
542    CloseIncomeStatement,
543    /// Post retained earnings rollforward (year-end).
544    PostRetainedEarningsRollforward,
545    /// Custom task.
546    Custom(String),
547}
548
549impl CloseTask {
550    /// Returns true if this is a year-end only task.
551    pub fn is_year_end_only(&self) -> bool {
552        matches!(
553            self,
554            CloseTask::CloseIncomeStatement | CloseTask::PostRetainedEarningsRollforward
555        )
556    }
557
558    /// Returns the task name.
559    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/// Status of a close task execution.
586#[derive(Debug, Clone, PartialEq, Eq)]
587pub enum CloseTaskStatus {
588    /// Not started.
589    Pending,
590    /// In progress.
591    InProgress,
592    /// Completed successfully.
593    Completed,
594    /// Completed with warnings.
595    CompletedWithWarnings(Vec<String>),
596    /// Failed.
597    Failed(String),
598    /// Skipped.
599    Skipped(String),
600}
601
602/// Result of executing a close task.
603#[derive(Debug, Clone)]
604pub struct CloseTaskResult {
605    /// Task that was executed.
606    pub task: CloseTask,
607    /// Company code.
608    pub company_code: String,
609    /// Fiscal period.
610    pub fiscal_period: FiscalPeriod,
611    /// Status.
612    pub status: CloseTaskStatus,
613    /// Start time.
614    pub started_at: Option<NaiveDate>,
615    /// End time.
616    pub completed_at: Option<NaiveDate>,
617    /// Journal entries created.
618    pub journal_entries_created: u32,
619    /// Total amount posted.
620    pub total_amount: Decimal,
621    /// Execution notes.
622    pub notes: Vec<String>,
623}
624
625impl CloseTaskResult {
626    /// Creates a new task result.
627    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    /// Returns true if the task completed successfully.
642    pub fn is_success(&self) -> bool {
643        matches!(
644            self.status,
645            CloseTaskStatus::Completed | CloseTaskStatus::CompletedWithWarnings(_)
646        )
647    }
648}
649
650/// Accrual definition for recurring period-end entries.
651#[derive(Debug, Clone)]
652pub struct AccrualDefinition {
653    /// Accrual ID.
654    pub accrual_id: String,
655    /// Company code.
656    pub company_code: String,
657    /// Description.
658    pub description: String,
659    /// Accrual type.
660    pub accrual_type: AccrualType,
661    /// Expense/Revenue account to debit/credit.
662    pub expense_revenue_account: String,
663    /// Accrual liability/asset account.
664    pub accrual_account: String,
665    /// Calculation method.
666    pub calculation_method: AccrualCalculationMethod,
667    /// Fixed amount (if applicable).
668    pub fixed_amount: Option<Decimal>,
669    /// Percentage rate (if applicable).
670    pub percentage_rate: Option<Decimal>,
671    /// Base account for percentage calculation.
672    pub base_account: Option<String>,
673    /// Frequency.
674    pub frequency: AccrualFrequency,
675    /// Auto-reverse on first day of next period.
676    pub auto_reverse: bool,
677    /// Cost center.
678    pub cost_center: Option<String>,
679    /// Active flag.
680    pub is_active: bool,
681    /// Start date.
682    pub effective_from: NaiveDate,
683    /// End date (if defined).
684    pub effective_to: Option<NaiveDate>,
685}
686
687impl AccrualDefinition {
688    /// Creates a new accrual definition.
689    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    /// Sets the fixed amount.
718    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    /// Sets percentage-based calculation.
725    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    /// Checks if the accrual is effective for a given date.
733    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/// Type of accrual.
750#[derive(Debug, Clone, Copy, PartialEq, Eq)]
751pub enum AccrualType {
752    /// Accrued expense (debit expense, credit liability).
753    AccruedExpense,
754    /// Accrued revenue (debit asset, credit revenue).
755    AccruedRevenue,
756    /// Prepaid expense (debit expense, credit asset).
757    PrepaidExpense,
758    /// Deferred revenue (debit liability, credit revenue).
759    DeferredRevenue,
760}
761
762/// Calculation method for accruals.
763#[derive(Debug, Clone, Copy, PartialEq, Eq)]
764pub enum AccrualCalculationMethod {
765    /// Fixed amount each period.
766    FixedAmount,
767    /// Percentage of a base account balance.
768    PercentageOfBase,
769    /// Days-based proration.
770    DaysBased,
771    /// Calculated externally (manual entry).
772    Manual,
773}
774
775/// Frequency for accrual posting.
776#[derive(Debug, Clone, Copy, PartialEq, Eq)]
777pub enum AccrualFrequency {
778    /// Every month.
779    Monthly,
780    /// Every quarter.
781    Quarterly,
782    /// Every year.
783    Annually,
784}
785
786/// Corporate overhead allocation definition.
787#[derive(Debug, Clone)]
788pub struct OverheadAllocation {
789    /// Allocation ID.
790    pub allocation_id: String,
791    /// Source company code (corporate).
792    pub source_company: String,
793    /// Source cost center.
794    pub source_cost_center: String,
795    /// Source account.
796    pub source_account: String,
797    /// Allocation basis.
798    pub allocation_basis: AllocationBasis,
799    /// Target allocations.
800    pub targets: Vec<AllocationTarget>,
801    /// Description.
802    pub description: String,
803    /// Active flag.
804    pub is_active: bool,
805}
806
807/// Basis for overhead allocation.
808#[derive(Debug, Clone, PartialEq, Eq)]
809pub enum AllocationBasis {
810    /// Based on revenue.
811    Revenue,
812    /// Based on headcount.
813    Headcount,
814    /// Based on direct costs.
815    DirectCosts,
816    /// Based on square footage.
817    SquareFootage,
818    /// Fixed percentages.
819    FixedPercentage,
820    /// Custom formula.
821    Custom(String),
822}
823
824/// Target for overhead allocation.
825#[derive(Debug, Clone)]
826pub struct AllocationTarget {
827    /// Target company code.
828    pub company_code: String,
829    /// Target cost center.
830    pub cost_center: String,
831    /// Target account.
832    pub account: String,
833    /// Allocation percentage (for fixed percentage basis).
834    pub percentage: Option<Decimal>,
835    /// Allocation driver value (for calculated basis).
836    pub driver_value: Option<Decimal>,
837}
838
839/// Period close schedule defining the order of tasks.
840#[derive(Debug, Clone)]
841pub struct CloseSchedule {
842    /// Schedule ID.
843    pub schedule_id: String,
844    /// Company code (or "ALL" for all companies).
845    pub company_code: String,
846    /// Period type this schedule applies to.
847    pub period_type: FiscalPeriodType,
848    /// Ordered list of tasks.
849    pub tasks: Vec<ScheduledCloseTask>,
850    /// Whether this is for year-end.
851    pub is_year_end: bool,
852}
853
854impl CloseSchedule {
855    /// Creates a standard monthly close schedule.
856    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    /// Creates a year-end close schedule.
883    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        // Add year-end specific tasks
889        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/// A scheduled close task with sequence and dependencies.
912#[derive(Debug, Clone)]
913pub struct ScheduledCloseTask {
914    /// The task to execute.
915    pub task: CloseTask,
916    /// Sequence number (execution order).
917    pub sequence: u32,
918    /// Tasks that must complete before this one.
919    pub depends_on: Vec<CloseTask>,
920    /// Is this task mandatory?
921    pub is_mandatory: bool,
922    /// Can this task run in parallel with others at same sequence?
923    pub can_parallelize: bool,
924}
925
926impl ScheduledCloseTask {
927    /// Creates a new scheduled task.
928    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    /// Adds a dependency.
939    pub fn depends_on(mut self, task: CloseTask) -> Self {
940        self.depends_on.push(task);
941        self
942    }
943
944    /// Marks as optional.
945    pub fn optional(mut self) -> Self {
946        self.is_mandatory = false;
947        self
948    }
949
950    /// Allows parallel execution.
951    pub fn parallelizable(mut self) -> Self {
952        self.can_parallelize = true;
953        self
954    }
955}
956
957/// Year-end closing entry specification.
958#[derive(Debug, Clone)]
959pub struct YearEndClosingSpec {
960    /// Company code.
961    pub company_code: String,
962    /// Fiscal year being closed.
963    pub fiscal_year: i32,
964    /// Revenue accounts to close.
965    pub revenue_accounts: Vec<String>,
966    /// Expense accounts to close.
967    pub expense_accounts: Vec<String>,
968    /// Income summary account (temporary).
969    pub income_summary_account: String,
970    /// Retained earnings account.
971    pub retained_earnings_account: String,
972    /// Dividend account (if applicable).
973    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()], // All accounts starting with 4
982            expense_accounts: vec!["5".to_string(), "6".to_string()], // Accounts starting with 5, 6
983            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/// Tax provision calculation inputs.
991#[derive(Debug, Clone)]
992pub struct TaxProvisionInput {
993    /// Company code.
994    pub company_code: String,
995    /// Fiscal year.
996    pub fiscal_year: i32,
997    /// Pre-tax book income.
998    pub pretax_income: Decimal,
999    /// Permanent differences (add back).
1000    pub permanent_differences: Vec<TaxAdjustment>,
1001    /// Temporary differences (timing).
1002    pub temporary_differences: Vec<TaxAdjustment>,
1003    /// Statutory tax rate.
1004    pub statutory_rate: Decimal,
1005    /// Tax credits available.
1006    pub tax_credits: Decimal,
1007    /// Prior year over/under provision.
1008    pub prior_year_adjustment: Decimal,
1009}
1010
1011/// Tax adjustment item.
1012#[derive(Debug, Clone)]
1013pub struct TaxAdjustment {
1014    /// Description.
1015    pub description: String,
1016    /// Amount.
1017    pub amount: Decimal,
1018    /// Is this a deduction (negative) or addition (positive)?
1019    pub is_addition: bool,
1020}
1021
1022/// Tax provision result.
1023#[derive(Debug, Clone)]
1024pub struct TaxProvisionResult {
1025    /// Company code.
1026    pub company_code: String,
1027    /// Fiscal year.
1028    pub fiscal_year: i32,
1029    /// Pre-tax book income.
1030    pub pretax_income: Decimal,
1031    /// Total permanent differences.
1032    pub permanent_differences: Decimal,
1033    /// Taxable income.
1034    pub taxable_income: Decimal,
1035    /// Current tax expense.
1036    pub current_tax_expense: Decimal,
1037    /// Deferred tax expense (benefit).
1038    pub deferred_tax_expense: Decimal,
1039    /// Total tax expense.
1040    pub total_tax_expense: Decimal,
1041    /// Effective tax rate.
1042    pub effective_rate: Decimal,
1043}
1044
1045impl TaxProvisionResult {
1046    /// Calculates the tax provision from inputs.
1047    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/// Period close run status.
1088#[derive(Debug, Clone)]
1089pub struct PeriodCloseRun {
1090    /// Run ID.
1091    pub run_id: String,
1092    /// Company code.
1093    pub company_code: String,
1094    /// Fiscal period.
1095    pub fiscal_period: FiscalPeriod,
1096    /// Status.
1097    pub status: PeriodCloseStatus,
1098    /// Task results.
1099    pub task_results: Vec<CloseTaskResult>,
1100    /// Started at.
1101    pub started_at: Option<NaiveDate>,
1102    /// Completed at.
1103    pub completed_at: Option<NaiveDate>,
1104    /// Total journal entries created.
1105    pub total_journal_entries: u32,
1106    /// Errors encountered.
1107    pub errors: Vec<String>,
1108}
1109
1110/// Status of a period close run.
1111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1112pub enum PeriodCloseStatus {
1113    /// Not started.
1114    NotStarted,
1115    /// In progress.
1116    InProgress,
1117    /// Completed successfully.
1118    Completed,
1119    /// Completed with errors.
1120    CompletedWithErrors,
1121    /// Failed.
1122    Failed,
1123}
1124
1125impl PeriodCloseRun {
1126    /// Creates a new period close run.
1127    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    /// Returns true if all tasks completed successfully.
1142    pub fn is_success(&self) -> bool {
1143        self.status == PeriodCloseStatus::Completed
1144    }
1145
1146    /// Returns the number of failed tasks.
1147    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)); // 1M + 10K permanent
1222        assert!(result.current_tax_expense > Decimal::ZERO);
1223    }
1224}