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).unwrap()
183                        });
184                if date >= year_start {
185                    date.year()
186                } else {
187                    date.year() - 1
188                }
189            }
190            FiscalCalendarType::FourFourFive(_) | FiscalCalendarType::ThirteenPeriod(_) => {
191                // Simplified - would need more complex calculation
192                date.year()
193            }
194        }
195    }
196
197    /// Returns the fiscal period number for a given date.
198    pub fn fiscal_period(&self, date: NaiveDate) -> u8 {
199        match &self.calendar_type {
200            FiscalCalendarType::CalendarYear => date.month() as u8,
201            FiscalCalendarType::CustomYearStart {
202                start_month,
203                start_day: _,
204            } => {
205                let month = date.month() as u8;
206                if month >= *start_month {
207                    month - start_month + 1
208                } else {
209                    12 - start_month + month + 1
210                }
211            }
212            FiscalCalendarType::ThirteenPeriod(_) => {
213                // Simplified: 28 days per period
214                let day_of_year = date.ordinal();
215                ((day_of_year - 1) / 28 + 1).min(13) as u8
216            }
217            FiscalCalendarType::FourFourFive(config) => {
218                // Simplified 4-4-5 period calculation
219                let weeks = config.pattern.weeks_per_period();
220                let week_of_year = (date.ordinal() as u8 - 1) / 7 + 1;
221                let mut cumulative = 0u8;
222                for (quarter, _) in (0..4).enumerate() {
223                    for (period_in_q, &period_weeks) in weeks.iter().enumerate() {
224                        cumulative += period_weeks;
225                        if week_of_year <= cumulative {
226                            return (quarter * 3 + period_in_q + 1) as u8;
227                        }
228                    }
229                }
230                12 // Default to period 12
231            }
232        }
233    }
234}
235
236/// Helper function to get month name.
237fn month_name(month: u8) -> &'static str {
238    match month {
239        1 => "January",
240        2 => "February",
241        3 => "March",
242        4 => "April",
243        5 => "May",
244        6 => "June",
245        7 => "July",
246        8 => "August",
247        9 => "September",
248        10 => "October",
249        11 => "November",
250        12 => "December",
251        _ => "Unknown",
252    }
253}
254
255/// Fiscal period representation.
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257pub struct FiscalPeriod {
258    /// Fiscal year.
259    pub year: i32,
260    /// Period number (1-12 for monthly, 1-4 for quarterly).
261    pub period: u8,
262    /// Period start date.
263    pub start_date: NaiveDate,
264    /// Period end date.
265    pub end_date: NaiveDate,
266    /// Period type.
267    pub period_type: FiscalPeriodType,
268    /// Is this the year-end period?
269    pub is_year_end: bool,
270    /// Period status.
271    pub status: PeriodStatus,
272}
273
274impl FiscalPeriod {
275    /// Creates a monthly fiscal period.
276    pub fn monthly(year: i32, month: u8) -> Self {
277        let start_date = NaiveDate::from_ymd_opt(year, month as u32, 1).unwrap();
278        let end_date = if month == 12 {
279            NaiveDate::from_ymd_opt(year + 1, 1, 1)
280                .unwrap()
281                .pred_opt()
282                .unwrap()
283        } else {
284            NaiveDate::from_ymd_opt(year, month as u32 + 1, 1)
285                .unwrap()
286                .pred_opt()
287                .unwrap()
288        };
289
290        Self {
291            year,
292            period: month,
293            start_date,
294            end_date,
295            period_type: FiscalPeriodType::Monthly,
296            is_year_end: month == 12,
297            status: PeriodStatus::Open,
298        }
299    }
300
301    /// Creates a quarterly fiscal period.
302    pub fn quarterly(year: i32, quarter: u8) -> Self {
303        let start_month = (quarter - 1) * 3 + 1;
304        let end_month = quarter * 3;
305
306        let start_date = NaiveDate::from_ymd_opt(year, start_month as u32, 1).unwrap();
307        let end_date = if end_month == 12 {
308            NaiveDate::from_ymd_opt(year + 1, 1, 1)
309                .unwrap()
310                .pred_opt()
311                .unwrap()
312        } else {
313            NaiveDate::from_ymd_opt(year, end_month as u32 + 1, 1)
314                .unwrap()
315                .pred_opt()
316                .unwrap()
317        };
318
319        Self {
320            year,
321            period: quarter,
322            start_date,
323            end_date,
324            period_type: FiscalPeriodType::Quarterly,
325            is_year_end: quarter == 4,
326            status: PeriodStatus::Open,
327        }
328    }
329
330    /// Returns the number of days in the period.
331    pub fn days(&self) -> i64 {
332        (self.end_date - self.start_date).num_days() + 1
333    }
334
335    /// Returns the period key (e.g., "2024-01" for monthly).
336    pub fn key(&self) -> String {
337        format!("{}-{:02}", self.year, self.period)
338    }
339
340    /// Checks if a date falls within this period.
341    pub fn contains(&self, date: NaiveDate) -> bool {
342        date >= self.start_date && date <= self.end_date
343    }
344
345    /// Creates a fiscal period from a calendar and date.
346    ///
347    /// This method determines the correct fiscal period for a given date
348    /// based on the fiscal calendar configuration.
349    pub fn from_calendar(calendar: &FiscalCalendar, date: NaiveDate) -> Self {
350        let fiscal_year = calendar.fiscal_year(date);
351        let period_num = calendar.fiscal_period(date);
352
353        match &calendar.calendar_type {
354            FiscalCalendarType::CalendarYear => Self::monthly(fiscal_year, period_num),
355            FiscalCalendarType::CustomYearStart {
356                start_month,
357                start_day,
358            } => {
359                // Calculate the actual start and end dates for the period
360                let period_start_month = if *start_month + period_num - 1 > 12 {
361                    start_month + period_num - 1 - 12
362                } else {
363                    start_month + period_num - 1
364                };
365                let period_year = if *start_month + period_num - 1 > 12 {
366                    fiscal_year + 1
367                } else {
368                    fiscal_year
369                };
370
371                let start_date = if period_num == 1 {
372                    NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, *start_day as u32)
373                        .unwrap_or_else(|| {
374                            NaiveDate::from_ymd_opt(fiscal_year, *start_month as u32, 1).unwrap()
375                        })
376                } else {
377                    NaiveDate::from_ymd_opt(period_year, period_start_month as u32, 1).unwrap()
378                };
379
380                let end_date = if period_num == 12 {
381                    // Last period ends day before fiscal year start
382                    NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, *start_day as u32)
383                        .unwrap_or_else(|| {
384                            NaiveDate::from_ymd_opt(fiscal_year + 1, *start_month as u32, 1)
385                                .unwrap()
386                        })
387                        .pred_opt()
388                        .unwrap()
389                } else {
390                    let next_month = if period_start_month == 12 {
391                        1
392                    } else {
393                        period_start_month + 1
394                    };
395                    let next_year = if period_start_month == 12 {
396                        period_year + 1
397                    } else {
398                        period_year
399                    };
400                    NaiveDate::from_ymd_opt(next_year, next_month as u32, 1)
401                        .unwrap()
402                        .pred_opt()
403                        .unwrap()
404                };
405
406                Self {
407                    year: fiscal_year,
408                    period: period_num,
409                    start_date,
410                    end_date,
411                    period_type: FiscalPeriodType::Monthly,
412                    is_year_end: period_num == 12,
413                    status: PeriodStatus::Open,
414                }
415            }
416            FiscalCalendarType::FourFourFive(config) => {
417                // 4-4-5 calendar: 12 periods of 4 or 5 weeks each
418                let weeks = config.pattern.weeks_per_period();
419                let quarter = (period_num - 1) / 3;
420                let period_in_quarter = (period_num - 1) % 3;
421                let period_weeks = weeks[period_in_quarter as usize];
422
423                // Calculate start of fiscal year (simplified)
424                let year_start = NaiveDate::from_ymd_opt(fiscal_year, 1, 1).unwrap();
425
426                // Calculate period start by summing previous period weeks
427                let mut weeks_before = 0u32;
428                for _ in 0..quarter {
429                    for &w in &weeks {
430                        weeks_before += w as u32;
431                    }
432                }
433                for p in 0..period_in_quarter {
434                    weeks_before += weeks[p as usize] as u32;
435                }
436
437                let start_date = year_start + chrono::Duration::weeks(weeks_before as i64);
438                let end_date = start_date + chrono::Duration::weeks(period_weeks as i64)
439                    - chrono::Duration::days(1);
440
441                Self {
442                    year: fiscal_year,
443                    period: period_num,
444                    start_date,
445                    end_date,
446                    period_type: FiscalPeriodType::FourWeek,
447                    is_year_end: period_num == 12,
448                    status: PeriodStatus::Open,
449                }
450            }
451            FiscalCalendarType::ThirteenPeriod(_) => {
452                // 13 periods of 28 days each (4 weeks)
453                let year_start = NaiveDate::from_ymd_opt(fiscal_year, 1, 1).unwrap();
454                let start_date = year_start + chrono::Duration::days((period_num as i64 - 1) * 28);
455                let end_date = start_date + chrono::Duration::days(27);
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 == 13,
464                    status: PeriodStatus::Open,
465                }
466            }
467        }
468    }
469}
470
471/// Type of fiscal period.
472#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
473pub enum FiscalPeriodType {
474    /// Monthly period.
475    Monthly,
476    /// Quarterly period.
477    Quarterly,
478    /// Four-week period (used in 4-4-5 and 13-period calendars).
479    FourWeek,
480    /// Special period (13th period, adjustments).
481    Special,
482}
483
484/// Status of a fiscal period.
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
486pub enum PeriodStatus {
487    /// Period is open for posting.
488    Open,
489    /// Soft close - limited posting allowed.
490    SoftClosed,
491    /// Hard close - no posting allowed.
492    Closed,
493    /// Period is locked for audit.
494    Locked,
495}
496
497/// Close task types for period-end processing.
498#[derive(Debug, Clone, PartialEq, Eq, Hash)]
499pub enum CloseTask {
500    /// Run depreciation for fixed assets.
501    RunDepreciation,
502    /// Post inventory revaluation adjustments.
503    PostInventoryRevaluation,
504    /// Reconcile AR subledger to GL.
505    ReconcileArToGl,
506    /// Reconcile AP subledger to GL.
507    ReconcileApToGl,
508    /// Reconcile FA subledger to GL.
509    ReconcileFaToGl,
510    /// Reconcile Inventory to GL.
511    ReconcileInventoryToGl,
512    /// Post accrued expenses.
513    PostAccruedExpenses,
514    /// Post accrued revenue.
515    PostAccruedRevenue,
516    /// Post prepaid expense amortization.
517    PostPrepaidAmortization,
518    /// Allocate corporate overhead.
519    AllocateCorporateOverhead,
520    /// Post intercompany settlements.
521    PostIntercompanySettlements,
522    /// Revalue foreign currency balances.
523    RevalueForeignCurrency,
524    /// Calculate and post tax provision.
525    CalculateTaxProvision,
526    /// Translate foreign subsidiary trial balances.
527    TranslateForeignSubsidiaries,
528    /// Eliminate intercompany balances.
529    EliminateIntercompany,
530    /// Generate trial balance.
531    GenerateTrialBalance,
532    /// Generate financial statements.
533    GenerateFinancialStatements,
534    /// Close income statement accounts (year-end).
535    CloseIncomeStatement,
536    /// Post retained earnings rollforward (year-end).
537    PostRetainedEarningsRollforward,
538    /// Custom task.
539    Custom(String),
540}
541
542impl CloseTask {
543    /// Returns true if this is a year-end only task.
544    pub fn is_year_end_only(&self) -> bool {
545        matches!(
546            self,
547            CloseTask::CloseIncomeStatement | CloseTask::PostRetainedEarningsRollforward
548        )
549    }
550
551    /// Returns the task name.
552    pub fn name(&self) -> &str {
553        match self {
554            CloseTask::RunDepreciation => "Run Depreciation",
555            CloseTask::PostInventoryRevaluation => "Post Inventory Revaluation",
556            CloseTask::ReconcileArToGl => "Reconcile AR to GL",
557            CloseTask::ReconcileApToGl => "Reconcile AP to GL",
558            CloseTask::ReconcileFaToGl => "Reconcile FA to GL",
559            CloseTask::ReconcileInventoryToGl => "Reconcile Inventory to GL",
560            CloseTask::PostAccruedExpenses => "Post Accrued Expenses",
561            CloseTask::PostAccruedRevenue => "Post Accrued Revenue",
562            CloseTask::PostPrepaidAmortization => "Post Prepaid Amortization",
563            CloseTask::AllocateCorporateOverhead => "Allocate Corporate Overhead",
564            CloseTask::PostIntercompanySettlements => "Post IC Settlements",
565            CloseTask::RevalueForeignCurrency => "Revalue Foreign Currency",
566            CloseTask::CalculateTaxProvision => "Calculate Tax Provision",
567            CloseTask::TranslateForeignSubsidiaries => "Translate Foreign Subs",
568            CloseTask::EliminateIntercompany => "Eliminate Intercompany",
569            CloseTask::GenerateTrialBalance => "Generate Trial Balance",
570            CloseTask::GenerateFinancialStatements => "Generate Financials",
571            CloseTask::CloseIncomeStatement => "Close Income Statement",
572            CloseTask::PostRetainedEarningsRollforward => "Post RE Rollforward",
573            CloseTask::Custom(name) => name,
574        }
575    }
576}
577
578/// Status of a close task execution.
579#[derive(Debug, Clone, PartialEq, Eq)]
580pub enum CloseTaskStatus {
581    /// Not started.
582    Pending,
583    /// In progress.
584    InProgress,
585    /// Completed successfully.
586    Completed,
587    /// Completed with warnings.
588    CompletedWithWarnings(Vec<String>),
589    /// Failed.
590    Failed(String),
591    /// Skipped.
592    Skipped(String),
593}
594
595/// Result of executing a close task.
596#[derive(Debug, Clone)]
597pub struct CloseTaskResult {
598    /// Task that was executed.
599    pub task: CloseTask,
600    /// Company code.
601    pub company_code: String,
602    /// Fiscal period.
603    pub fiscal_period: FiscalPeriod,
604    /// Status.
605    pub status: CloseTaskStatus,
606    /// Start time.
607    pub started_at: Option<NaiveDate>,
608    /// End time.
609    pub completed_at: Option<NaiveDate>,
610    /// Journal entries created.
611    pub journal_entries_created: u32,
612    /// Total amount posted.
613    pub total_amount: Decimal,
614    /// Execution notes.
615    pub notes: Vec<String>,
616}
617
618impl CloseTaskResult {
619    /// Creates a new task result.
620    pub fn new(task: CloseTask, company_code: String, fiscal_period: FiscalPeriod) -> Self {
621        Self {
622            task,
623            company_code,
624            fiscal_period,
625            status: CloseTaskStatus::Pending,
626            started_at: None,
627            completed_at: None,
628            journal_entries_created: 0,
629            total_amount: Decimal::ZERO,
630            notes: Vec::new(),
631        }
632    }
633
634    /// Returns true if the task completed successfully.
635    pub fn is_success(&self) -> bool {
636        matches!(
637            self.status,
638            CloseTaskStatus::Completed | CloseTaskStatus::CompletedWithWarnings(_)
639        )
640    }
641}
642
643/// Accrual definition for recurring period-end entries.
644#[derive(Debug, Clone)]
645pub struct AccrualDefinition {
646    /// Accrual ID.
647    pub accrual_id: String,
648    /// Company code.
649    pub company_code: String,
650    /// Description.
651    pub description: String,
652    /// Accrual type.
653    pub accrual_type: AccrualType,
654    /// Expense/Revenue account to debit/credit.
655    pub expense_revenue_account: String,
656    /// Accrual liability/asset account.
657    pub accrual_account: String,
658    /// Calculation method.
659    pub calculation_method: AccrualCalculationMethod,
660    /// Fixed amount (if applicable).
661    pub fixed_amount: Option<Decimal>,
662    /// Percentage rate (if applicable).
663    pub percentage_rate: Option<Decimal>,
664    /// Base account for percentage calculation.
665    pub base_account: Option<String>,
666    /// Frequency.
667    pub frequency: AccrualFrequency,
668    /// Auto-reverse on first day of next period.
669    pub auto_reverse: bool,
670    /// Cost center.
671    pub cost_center: Option<String>,
672    /// Active flag.
673    pub is_active: bool,
674    /// Start date.
675    pub effective_from: NaiveDate,
676    /// End date (if defined).
677    pub effective_to: Option<NaiveDate>,
678}
679
680impl AccrualDefinition {
681    /// Creates a new accrual definition.
682    pub fn new(
683        accrual_id: String,
684        company_code: String,
685        description: String,
686        accrual_type: AccrualType,
687        expense_revenue_account: String,
688        accrual_account: String,
689    ) -> Self {
690        Self {
691            accrual_id,
692            company_code,
693            description,
694            accrual_type,
695            expense_revenue_account,
696            accrual_account,
697            calculation_method: AccrualCalculationMethod::FixedAmount,
698            fixed_amount: None,
699            percentage_rate: None,
700            base_account: None,
701            frequency: AccrualFrequency::Monthly,
702            auto_reverse: true,
703            cost_center: None,
704            is_active: true,
705            effective_from: NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
706            effective_to: None,
707        }
708    }
709
710    /// Sets the fixed amount.
711    pub fn with_fixed_amount(mut self, amount: Decimal) -> Self {
712        self.calculation_method = AccrualCalculationMethod::FixedAmount;
713        self.fixed_amount = Some(amount);
714        self
715    }
716
717    /// Sets percentage-based calculation.
718    pub fn with_percentage(mut self, rate: Decimal, base_account: &str) -> Self {
719        self.calculation_method = AccrualCalculationMethod::PercentageOfBase;
720        self.percentage_rate = Some(rate);
721        self.base_account = Some(base_account.to_string());
722        self
723    }
724
725    /// Checks if the accrual is effective for a given date.
726    pub fn is_effective_on(&self, date: NaiveDate) -> bool {
727        if !self.is_active {
728            return false;
729        }
730        if date < self.effective_from {
731            return false;
732        }
733        if let Some(end) = self.effective_to {
734            if date > end {
735                return false;
736            }
737        }
738        true
739    }
740}
741
742/// Type of accrual.
743#[derive(Debug, Clone, Copy, PartialEq, Eq)]
744pub enum AccrualType {
745    /// Accrued expense (debit expense, credit liability).
746    AccruedExpense,
747    /// Accrued revenue (debit asset, credit revenue).
748    AccruedRevenue,
749    /// Prepaid expense (debit expense, credit asset).
750    PrepaidExpense,
751    /// Deferred revenue (debit liability, credit revenue).
752    DeferredRevenue,
753}
754
755/// Calculation method for accruals.
756#[derive(Debug, Clone, Copy, PartialEq, Eq)]
757pub enum AccrualCalculationMethod {
758    /// Fixed amount each period.
759    FixedAmount,
760    /// Percentage of a base account balance.
761    PercentageOfBase,
762    /// Days-based proration.
763    DaysBased,
764    /// Calculated externally (manual entry).
765    Manual,
766}
767
768/// Frequency for accrual posting.
769#[derive(Debug, Clone, Copy, PartialEq, Eq)]
770pub enum AccrualFrequency {
771    /// Every month.
772    Monthly,
773    /// Every quarter.
774    Quarterly,
775    /// Every year.
776    Annually,
777}
778
779/// Corporate overhead allocation definition.
780#[derive(Debug, Clone)]
781pub struct OverheadAllocation {
782    /// Allocation ID.
783    pub allocation_id: String,
784    /// Source company code (corporate).
785    pub source_company: String,
786    /// Source cost center.
787    pub source_cost_center: String,
788    /// Source account.
789    pub source_account: String,
790    /// Allocation basis.
791    pub allocation_basis: AllocationBasis,
792    /// Target allocations.
793    pub targets: Vec<AllocationTarget>,
794    /// Description.
795    pub description: String,
796    /// Active flag.
797    pub is_active: bool,
798}
799
800/// Basis for overhead allocation.
801#[derive(Debug, Clone, PartialEq, Eq)]
802pub enum AllocationBasis {
803    /// Based on revenue.
804    Revenue,
805    /// Based on headcount.
806    Headcount,
807    /// Based on direct costs.
808    DirectCosts,
809    /// Based on square footage.
810    SquareFootage,
811    /// Fixed percentages.
812    FixedPercentage,
813    /// Custom formula.
814    Custom(String),
815}
816
817/// Target for overhead allocation.
818#[derive(Debug, Clone)]
819pub struct AllocationTarget {
820    /// Target company code.
821    pub company_code: String,
822    /// Target cost center.
823    pub cost_center: String,
824    /// Target account.
825    pub account: String,
826    /// Allocation percentage (for fixed percentage basis).
827    pub percentage: Option<Decimal>,
828    /// Allocation driver value (for calculated basis).
829    pub driver_value: Option<Decimal>,
830}
831
832/// Period close schedule defining the order of tasks.
833#[derive(Debug, Clone)]
834pub struct CloseSchedule {
835    /// Schedule ID.
836    pub schedule_id: String,
837    /// Company code (or "ALL" for all companies).
838    pub company_code: String,
839    /// Period type this schedule applies to.
840    pub period_type: FiscalPeriodType,
841    /// Ordered list of tasks.
842    pub tasks: Vec<ScheduledCloseTask>,
843    /// Whether this is for year-end.
844    pub is_year_end: bool,
845}
846
847impl CloseSchedule {
848    /// Creates a standard monthly close schedule.
849    pub fn standard_monthly(company_code: &str) -> Self {
850        Self {
851            schedule_id: format!("MONTHLY-{}", company_code),
852            company_code: company_code.to_string(),
853            period_type: FiscalPeriodType::Monthly,
854            tasks: vec![
855                ScheduledCloseTask::new(CloseTask::RunDepreciation, 1),
856                ScheduledCloseTask::new(CloseTask::PostInventoryRevaluation, 2),
857                ScheduledCloseTask::new(CloseTask::PostAccruedExpenses, 3),
858                ScheduledCloseTask::new(CloseTask::PostAccruedRevenue, 4),
859                ScheduledCloseTask::new(CloseTask::PostPrepaidAmortization, 5),
860                ScheduledCloseTask::new(CloseTask::RevalueForeignCurrency, 6),
861                ScheduledCloseTask::new(CloseTask::ReconcileArToGl, 7),
862                ScheduledCloseTask::new(CloseTask::ReconcileApToGl, 8),
863                ScheduledCloseTask::new(CloseTask::ReconcileFaToGl, 9),
864                ScheduledCloseTask::new(CloseTask::ReconcileInventoryToGl, 10),
865                ScheduledCloseTask::new(CloseTask::PostIntercompanySettlements, 11),
866                ScheduledCloseTask::new(CloseTask::AllocateCorporateOverhead, 12),
867                ScheduledCloseTask::new(CloseTask::TranslateForeignSubsidiaries, 13),
868                ScheduledCloseTask::new(CloseTask::EliminateIntercompany, 14),
869                ScheduledCloseTask::new(CloseTask::GenerateTrialBalance, 15),
870            ],
871            is_year_end: false,
872        }
873    }
874
875    /// Creates a year-end close schedule.
876    pub fn year_end(company_code: &str) -> Self {
877        let mut schedule = Self::standard_monthly(company_code);
878        schedule.schedule_id = format!("YEAREND-{}", company_code);
879        schedule.is_year_end = true;
880
881        // Add year-end specific tasks
882        let next_seq = schedule.tasks.len() as u32 + 1;
883        schedule.tasks.push(ScheduledCloseTask::new(
884            CloseTask::CalculateTaxProvision,
885            next_seq,
886        ));
887        schedule.tasks.push(ScheduledCloseTask::new(
888            CloseTask::CloseIncomeStatement,
889            next_seq + 1,
890        ));
891        schedule.tasks.push(ScheduledCloseTask::new(
892            CloseTask::PostRetainedEarningsRollforward,
893            next_seq + 2,
894        ));
895        schedule.tasks.push(ScheduledCloseTask::new(
896            CloseTask::GenerateFinancialStatements,
897            next_seq + 3,
898        ));
899
900        schedule
901    }
902}
903
904/// A scheduled close task with sequence and dependencies.
905#[derive(Debug, Clone)]
906pub struct ScheduledCloseTask {
907    /// The task to execute.
908    pub task: CloseTask,
909    /// Sequence number (execution order).
910    pub sequence: u32,
911    /// Tasks that must complete before this one.
912    pub depends_on: Vec<CloseTask>,
913    /// Is this task mandatory?
914    pub is_mandatory: bool,
915    /// Can this task run in parallel with others at same sequence?
916    pub can_parallelize: bool,
917}
918
919impl ScheduledCloseTask {
920    /// Creates a new scheduled task.
921    pub fn new(task: CloseTask, sequence: u32) -> Self {
922        Self {
923            task,
924            sequence,
925            depends_on: Vec::new(),
926            is_mandatory: true,
927            can_parallelize: false,
928        }
929    }
930
931    /// Adds a dependency.
932    pub fn depends_on(mut self, task: CloseTask) -> Self {
933        self.depends_on.push(task);
934        self
935    }
936
937    /// Marks as optional.
938    pub fn optional(mut self) -> Self {
939        self.is_mandatory = false;
940        self
941    }
942
943    /// Allows parallel execution.
944    pub fn parallelizable(mut self) -> Self {
945        self.can_parallelize = true;
946        self
947    }
948}
949
950/// Year-end closing entry specification.
951#[derive(Debug, Clone)]
952pub struct YearEndClosingSpec {
953    /// Company code.
954    pub company_code: String,
955    /// Fiscal year being closed.
956    pub fiscal_year: i32,
957    /// Revenue accounts to close.
958    pub revenue_accounts: Vec<String>,
959    /// Expense accounts to close.
960    pub expense_accounts: Vec<String>,
961    /// Income summary account (temporary).
962    pub income_summary_account: String,
963    /// Retained earnings account.
964    pub retained_earnings_account: String,
965    /// Dividend account (if applicable).
966    pub dividend_account: Option<String>,
967}
968
969impl Default for YearEndClosingSpec {
970    fn default() -> Self {
971        Self {
972            company_code: String::new(),
973            fiscal_year: 0,
974            revenue_accounts: vec!["4".to_string()], // All accounts starting with 4
975            expense_accounts: vec!["5".to_string(), "6".to_string()], // Accounts starting with 5, 6
976            income_summary_account: "3500".to_string(),
977            retained_earnings_account: "3300".to_string(),
978            dividend_account: Some("3400".to_string()),
979        }
980    }
981}
982
983/// Tax provision calculation inputs.
984#[derive(Debug, Clone)]
985pub struct TaxProvisionInput {
986    /// Company code.
987    pub company_code: String,
988    /// Fiscal year.
989    pub fiscal_year: i32,
990    /// Pre-tax book income.
991    pub pretax_income: Decimal,
992    /// Permanent differences (add back).
993    pub permanent_differences: Vec<TaxAdjustment>,
994    /// Temporary differences (timing).
995    pub temporary_differences: Vec<TaxAdjustment>,
996    /// Statutory tax rate.
997    pub statutory_rate: Decimal,
998    /// Tax credits available.
999    pub tax_credits: Decimal,
1000    /// Prior year over/under provision.
1001    pub prior_year_adjustment: Decimal,
1002}
1003
1004/// Tax adjustment item.
1005#[derive(Debug, Clone)]
1006pub struct TaxAdjustment {
1007    /// Description.
1008    pub description: String,
1009    /// Amount.
1010    pub amount: Decimal,
1011    /// Is this a deduction (negative) or addition (positive)?
1012    pub is_addition: bool,
1013}
1014
1015/// Tax provision result.
1016#[derive(Debug, Clone)]
1017pub struct TaxProvisionResult {
1018    /// Company code.
1019    pub company_code: String,
1020    /// Fiscal year.
1021    pub fiscal_year: i32,
1022    /// Pre-tax book income.
1023    pub pretax_income: Decimal,
1024    /// Total permanent differences.
1025    pub permanent_differences: Decimal,
1026    /// Taxable income.
1027    pub taxable_income: Decimal,
1028    /// Current tax expense.
1029    pub current_tax_expense: Decimal,
1030    /// Deferred tax expense (benefit).
1031    pub deferred_tax_expense: Decimal,
1032    /// Total tax expense.
1033    pub total_tax_expense: Decimal,
1034    /// Effective tax rate.
1035    pub effective_rate: Decimal,
1036}
1037
1038impl TaxProvisionResult {
1039    /// Calculates the tax provision from inputs.
1040    pub fn calculate(input: &TaxProvisionInput) -> Self {
1041        let permanent_diff: Decimal = input
1042            .permanent_differences
1043            .iter()
1044            .map(|d| if d.is_addition { d.amount } else { -d.amount })
1045            .sum();
1046
1047        let temporary_diff: Decimal = input
1048            .temporary_differences
1049            .iter()
1050            .map(|d| if d.is_addition { d.amount } else { -d.amount })
1051            .sum();
1052
1053        let taxable_income = input.pretax_income + permanent_diff;
1054        let current_tax = (taxable_income * input.statutory_rate / dec!(100)).round_dp(2);
1055        let deferred_tax = (temporary_diff * input.statutory_rate / dec!(100)).round_dp(2);
1056
1057        let total_tax =
1058            current_tax + deferred_tax - input.tax_credits + input.prior_year_adjustment;
1059
1060        let effective_rate = if input.pretax_income != Decimal::ZERO {
1061            (total_tax / input.pretax_income * dec!(100)).round_dp(2)
1062        } else {
1063            Decimal::ZERO
1064        };
1065
1066        Self {
1067            company_code: input.company_code.clone(),
1068            fiscal_year: input.fiscal_year,
1069            pretax_income: input.pretax_income,
1070            permanent_differences: permanent_diff,
1071            taxable_income,
1072            current_tax_expense: current_tax,
1073            deferred_tax_expense: deferred_tax,
1074            total_tax_expense: total_tax,
1075            effective_rate,
1076        }
1077    }
1078}
1079
1080/// Period close run status.
1081#[derive(Debug, Clone)]
1082pub struct PeriodCloseRun {
1083    /// Run ID.
1084    pub run_id: String,
1085    /// Company code.
1086    pub company_code: String,
1087    /// Fiscal period.
1088    pub fiscal_period: FiscalPeriod,
1089    /// Status.
1090    pub status: PeriodCloseStatus,
1091    /// Task results.
1092    pub task_results: Vec<CloseTaskResult>,
1093    /// Started at.
1094    pub started_at: Option<NaiveDate>,
1095    /// Completed at.
1096    pub completed_at: Option<NaiveDate>,
1097    /// Total journal entries created.
1098    pub total_journal_entries: u32,
1099    /// Errors encountered.
1100    pub errors: Vec<String>,
1101}
1102
1103/// Status of a period close run.
1104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1105pub enum PeriodCloseStatus {
1106    /// Not started.
1107    NotStarted,
1108    /// In progress.
1109    InProgress,
1110    /// Completed successfully.
1111    Completed,
1112    /// Completed with errors.
1113    CompletedWithErrors,
1114    /// Failed.
1115    Failed,
1116}
1117
1118impl PeriodCloseRun {
1119    /// Creates a new period close run.
1120    pub fn new(run_id: String, company_code: String, fiscal_period: FiscalPeriod) -> Self {
1121        Self {
1122            run_id,
1123            company_code,
1124            fiscal_period,
1125            status: PeriodCloseStatus::NotStarted,
1126            task_results: Vec::new(),
1127            started_at: None,
1128            completed_at: None,
1129            total_journal_entries: 0,
1130            errors: Vec::new(),
1131        }
1132    }
1133
1134    /// Returns true if all tasks completed successfully.
1135    pub fn is_success(&self) -> bool {
1136        self.status == PeriodCloseStatus::Completed
1137    }
1138
1139    /// Returns the number of failed tasks.
1140    pub fn failed_task_count(&self) -> usize {
1141        self.task_results
1142            .iter()
1143            .filter(|r| matches!(r.status, CloseTaskStatus::Failed(_)))
1144            .count()
1145    }
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150    use super::*;
1151
1152    #[test]
1153    fn test_fiscal_period_monthly() {
1154        let period = FiscalPeriod::monthly(2024, 1);
1155        assert_eq!(
1156            period.start_date,
1157            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()
1158        );
1159        assert_eq!(
1160            period.end_date,
1161            NaiveDate::from_ymd_opt(2024, 1, 31).unwrap()
1162        );
1163        assert_eq!(period.days(), 31);
1164        assert!(!period.is_year_end);
1165
1166        let dec_period = FiscalPeriod::monthly(2024, 12);
1167        assert!(dec_period.is_year_end);
1168    }
1169
1170    #[test]
1171    fn test_fiscal_period_quarterly() {
1172        let q1 = FiscalPeriod::quarterly(2024, 1);
1173        assert_eq!(q1.start_date, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1174        assert_eq!(q1.end_date, NaiveDate::from_ymd_opt(2024, 3, 31).unwrap());
1175
1176        let q4 = FiscalPeriod::quarterly(2024, 4);
1177        assert!(q4.is_year_end);
1178    }
1179
1180    #[test]
1181    fn test_close_schedule() {
1182        let schedule = CloseSchedule::standard_monthly("1000");
1183        assert!(!schedule.is_year_end);
1184        assert!(!schedule.tasks.is_empty());
1185
1186        let year_end = CloseSchedule::year_end("1000");
1187        assert!(year_end.is_year_end);
1188        assert!(year_end.tasks.len() > schedule.tasks.len());
1189    }
1190
1191    #[test]
1192    fn test_tax_provision() {
1193        let input = TaxProvisionInput {
1194            company_code: "1000".to_string(),
1195            fiscal_year: 2024,
1196            pretax_income: dec!(1000000),
1197            permanent_differences: vec![TaxAdjustment {
1198                description: "Meals & Entertainment".to_string(),
1199                amount: dec!(10000),
1200                is_addition: true,
1201            }],
1202            temporary_differences: vec![TaxAdjustment {
1203                description: "Depreciation Timing".to_string(),
1204                amount: dec!(50000),
1205                is_addition: false,
1206            }],
1207            statutory_rate: dec!(21),
1208            tax_credits: dec!(5000),
1209            prior_year_adjustment: Decimal::ZERO,
1210        };
1211
1212        let result = TaxProvisionResult::calculate(&input);
1213        assert_eq!(result.taxable_income, dec!(1010000)); // 1M + 10K permanent
1214        assert!(result.current_tax_expense > Decimal::ZERO);
1215    }
1216}