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