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//! - Close tasks and workflows
7//! - Accrual definitions and schedules
8//! - Year-end closing entries
9
10use chrono::NaiveDate;
11use rust_decimal::Decimal;
12use rust_decimal_macros::dec;
13
14/// Fiscal period representation.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct FiscalPeriod {
17    /// Fiscal year.
18    pub year: i32,
19    /// Period number (1-12 for monthly, 1-4 for quarterly).
20    pub period: u8,
21    /// Period start date.
22    pub start_date: NaiveDate,
23    /// Period end date.
24    pub end_date: NaiveDate,
25    /// Period type.
26    pub period_type: FiscalPeriodType,
27    /// Is this the year-end period?
28    pub is_year_end: bool,
29    /// Period status.
30    pub status: PeriodStatus,
31}
32
33impl FiscalPeriod {
34    /// Creates a monthly fiscal period.
35    pub fn monthly(year: i32, month: u8) -> Self {
36        let start_date = NaiveDate::from_ymd_opt(year, month as u32, 1).unwrap();
37        let end_date = if month == 12 {
38            NaiveDate::from_ymd_opt(year + 1, 1, 1)
39                .unwrap()
40                .pred_opt()
41                .unwrap()
42        } else {
43            NaiveDate::from_ymd_opt(year, month as u32 + 1, 1)
44                .unwrap()
45                .pred_opt()
46                .unwrap()
47        };
48
49        Self {
50            year,
51            period: month,
52            start_date,
53            end_date,
54            period_type: FiscalPeriodType::Monthly,
55            is_year_end: month == 12,
56            status: PeriodStatus::Open,
57        }
58    }
59
60    /// Creates a quarterly fiscal period.
61    pub fn quarterly(year: i32, quarter: u8) -> Self {
62        let start_month = (quarter - 1) * 3 + 1;
63        let end_month = quarter * 3;
64
65        let start_date = NaiveDate::from_ymd_opt(year, start_month as u32, 1).unwrap();
66        let end_date = if end_month == 12 {
67            NaiveDate::from_ymd_opt(year + 1, 1, 1)
68                .unwrap()
69                .pred_opt()
70                .unwrap()
71        } else {
72            NaiveDate::from_ymd_opt(year, end_month as u32 + 1, 1)
73                .unwrap()
74                .pred_opt()
75                .unwrap()
76        };
77
78        Self {
79            year,
80            period: quarter,
81            start_date,
82            end_date,
83            period_type: FiscalPeriodType::Quarterly,
84            is_year_end: quarter == 4,
85            status: PeriodStatus::Open,
86        }
87    }
88
89    /// Returns the number of days in the period.
90    pub fn days(&self) -> i64 {
91        (self.end_date - self.start_date).num_days() + 1
92    }
93
94    /// Returns the period key (e.g., "2024-01" for monthly).
95    pub fn key(&self) -> String {
96        format!("{}-{:02}", self.year, self.period)
97    }
98
99    /// Checks if a date falls within this period.
100    pub fn contains(&self, date: NaiveDate) -> bool {
101        date >= self.start_date && date <= self.end_date
102    }
103}
104
105/// Type of fiscal period.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum FiscalPeriodType {
108    /// Monthly period.
109    Monthly,
110    /// Quarterly period.
111    Quarterly,
112    /// Special period (13th period, adjustments).
113    Special,
114}
115
116/// Status of a fiscal period.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub enum PeriodStatus {
119    /// Period is open for posting.
120    Open,
121    /// Soft close - limited posting allowed.
122    SoftClosed,
123    /// Hard close - no posting allowed.
124    Closed,
125    /// Period is locked for audit.
126    Locked,
127}
128
129/// Close task types for period-end processing.
130#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131pub enum CloseTask {
132    /// Run depreciation for fixed assets.
133    RunDepreciation,
134    /// Post inventory revaluation adjustments.
135    PostInventoryRevaluation,
136    /// Reconcile AR subledger to GL.
137    ReconcileArToGl,
138    /// Reconcile AP subledger to GL.
139    ReconcileApToGl,
140    /// Reconcile FA subledger to GL.
141    ReconcileFaToGl,
142    /// Reconcile Inventory to GL.
143    ReconcileInventoryToGl,
144    /// Post accrued expenses.
145    PostAccruedExpenses,
146    /// Post accrued revenue.
147    PostAccruedRevenue,
148    /// Post prepaid expense amortization.
149    PostPrepaidAmortization,
150    /// Allocate corporate overhead.
151    AllocateCorporateOverhead,
152    /// Post intercompany settlements.
153    PostIntercompanySettlements,
154    /// Revalue foreign currency balances.
155    RevalueForeignCurrency,
156    /// Calculate and post tax provision.
157    CalculateTaxProvision,
158    /// Translate foreign subsidiary trial balances.
159    TranslateForeignSubsidiaries,
160    /// Eliminate intercompany balances.
161    EliminateIntercompany,
162    /// Generate trial balance.
163    GenerateTrialBalance,
164    /// Generate financial statements.
165    GenerateFinancialStatements,
166    /// Close income statement accounts (year-end).
167    CloseIncomeStatement,
168    /// Post retained earnings rollforward (year-end).
169    PostRetainedEarningsRollforward,
170    /// Custom task.
171    Custom(String),
172}
173
174impl CloseTask {
175    /// Returns true if this is a year-end only task.
176    pub fn is_year_end_only(&self) -> bool {
177        matches!(
178            self,
179            CloseTask::CloseIncomeStatement | CloseTask::PostRetainedEarningsRollforward
180        )
181    }
182
183    /// Returns the task name.
184    pub fn name(&self) -> &str {
185        match self {
186            CloseTask::RunDepreciation => "Run Depreciation",
187            CloseTask::PostInventoryRevaluation => "Post Inventory Revaluation",
188            CloseTask::ReconcileArToGl => "Reconcile AR to GL",
189            CloseTask::ReconcileApToGl => "Reconcile AP to GL",
190            CloseTask::ReconcileFaToGl => "Reconcile FA to GL",
191            CloseTask::ReconcileInventoryToGl => "Reconcile Inventory to GL",
192            CloseTask::PostAccruedExpenses => "Post Accrued Expenses",
193            CloseTask::PostAccruedRevenue => "Post Accrued Revenue",
194            CloseTask::PostPrepaidAmortization => "Post Prepaid Amortization",
195            CloseTask::AllocateCorporateOverhead => "Allocate Corporate Overhead",
196            CloseTask::PostIntercompanySettlements => "Post IC Settlements",
197            CloseTask::RevalueForeignCurrency => "Revalue Foreign Currency",
198            CloseTask::CalculateTaxProvision => "Calculate Tax Provision",
199            CloseTask::TranslateForeignSubsidiaries => "Translate Foreign Subs",
200            CloseTask::EliminateIntercompany => "Eliminate Intercompany",
201            CloseTask::GenerateTrialBalance => "Generate Trial Balance",
202            CloseTask::GenerateFinancialStatements => "Generate Financials",
203            CloseTask::CloseIncomeStatement => "Close Income Statement",
204            CloseTask::PostRetainedEarningsRollforward => "Post RE Rollforward",
205            CloseTask::Custom(name) => name,
206        }
207    }
208}
209
210/// Status of a close task execution.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub enum CloseTaskStatus {
213    /// Not started.
214    Pending,
215    /// In progress.
216    InProgress,
217    /// Completed successfully.
218    Completed,
219    /// Completed with warnings.
220    CompletedWithWarnings(Vec<String>),
221    /// Failed.
222    Failed(String),
223    /// Skipped.
224    Skipped(String),
225}
226
227/// Result of executing a close task.
228#[derive(Debug, Clone)]
229pub struct CloseTaskResult {
230    /// Task that was executed.
231    pub task: CloseTask,
232    /// Company code.
233    pub company_code: String,
234    /// Fiscal period.
235    pub fiscal_period: FiscalPeriod,
236    /// Status.
237    pub status: CloseTaskStatus,
238    /// Start time.
239    pub started_at: Option<NaiveDate>,
240    /// End time.
241    pub completed_at: Option<NaiveDate>,
242    /// Journal entries created.
243    pub journal_entries_created: u32,
244    /// Total amount posted.
245    pub total_amount: Decimal,
246    /// Execution notes.
247    pub notes: Vec<String>,
248}
249
250impl CloseTaskResult {
251    /// Creates a new task result.
252    pub fn new(task: CloseTask, company_code: String, fiscal_period: FiscalPeriod) -> Self {
253        Self {
254            task,
255            company_code,
256            fiscal_period,
257            status: CloseTaskStatus::Pending,
258            started_at: None,
259            completed_at: None,
260            journal_entries_created: 0,
261            total_amount: Decimal::ZERO,
262            notes: Vec::new(),
263        }
264    }
265
266    /// Returns true if the task completed successfully.
267    pub fn is_success(&self) -> bool {
268        matches!(
269            self.status,
270            CloseTaskStatus::Completed | CloseTaskStatus::CompletedWithWarnings(_)
271        )
272    }
273}
274
275/// Accrual definition for recurring period-end entries.
276#[derive(Debug, Clone)]
277pub struct AccrualDefinition {
278    /// Accrual ID.
279    pub accrual_id: String,
280    /// Company code.
281    pub company_code: String,
282    /// Description.
283    pub description: String,
284    /// Accrual type.
285    pub accrual_type: AccrualType,
286    /// Expense/Revenue account to debit/credit.
287    pub expense_revenue_account: String,
288    /// Accrual liability/asset account.
289    pub accrual_account: String,
290    /// Calculation method.
291    pub calculation_method: AccrualCalculationMethod,
292    /// Fixed amount (if applicable).
293    pub fixed_amount: Option<Decimal>,
294    /// Percentage rate (if applicable).
295    pub percentage_rate: Option<Decimal>,
296    /// Base account for percentage calculation.
297    pub base_account: Option<String>,
298    /// Frequency.
299    pub frequency: AccrualFrequency,
300    /// Auto-reverse on first day of next period.
301    pub auto_reverse: bool,
302    /// Cost center.
303    pub cost_center: Option<String>,
304    /// Active flag.
305    pub is_active: bool,
306    /// Start date.
307    pub effective_from: NaiveDate,
308    /// End date (if defined).
309    pub effective_to: Option<NaiveDate>,
310}
311
312impl AccrualDefinition {
313    /// Creates a new accrual definition.
314    pub fn new(
315        accrual_id: String,
316        company_code: String,
317        description: String,
318        accrual_type: AccrualType,
319        expense_revenue_account: String,
320        accrual_account: String,
321    ) -> Self {
322        Self {
323            accrual_id,
324            company_code,
325            description,
326            accrual_type,
327            expense_revenue_account,
328            accrual_account,
329            calculation_method: AccrualCalculationMethod::FixedAmount,
330            fixed_amount: None,
331            percentage_rate: None,
332            base_account: None,
333            frequency: AccrualFrequency::Monthly,
334            auto_reverse: true,
335            cost_center: None,
336            is_active: true,
337            effective_from: NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
338            effective_to: None,
339        }
340    }
341
342    /// Sets the fixed amount.
343    pub fn with_fixed_amount(mut self, amount: Decimal) -> Self {
344        self.calculation_method = AccrualCalculationMethod::FixedAmount;
345        self.fixed_amount = Some(amount);
346        self
347    }
348
349    /// Sets percentage-based calculation.
350    pub fn with_percentage(mut self, rate: Decimal, base_account: &str) -> Self {
351        self.calculation_method = AccrualCalculationMethod::PercentageOfBase;
352        self.percentage_rate = Some(rate);
353        self.base_account = Some(base_account.to_string());
354        self
355    }
356
357    /// Checks if the accrual is effective for a given date.
358    pub fn is_effective_on(&self, date: NaiveDate) -> bool {
359        if !self.is_active {
360            return false;
361        }
362        if date < self.effective_from {
363            return false;
364        }
365        if let Some(end) = self.effective_to {
366            if date > end {
367                return false;
368            }
369        }
370        true
371    }
372}
373
374/// Type of accrual.
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub enum AccrualType {
377    /// Accrued expense (debit expense, credit liability).
378    AccruedExpense,
379    /// Accrued revenue (debit asset, credit revenue).
380    AccruedRevenue,
381    /// Prepaid expense (debit expense, credit asset).
382    PrepaidExpense,
383    /// Deferred revenue (debit liability, credit revenue).
384    DeferredRevenue,
385}
386
387/// Calculation method for accruals.
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub enum AccrualCalculationMethod {
390    /// Fixed amount each period.
391    FixedAmount,
392    /// Percentage of a base account balance.
393    PercentageOfBase,
394    /// Days-based proration.
395    DaysBased,
396    /// Calculated externally (manual entry).
397    Manual,
398}
399
400/// Frequency for accrual posting.
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402pub enum AccrualFrequency {
403    /// Every month.
404    Monthly,
405    /// Every quarter.
406    Quarterly,
407    /// Every year.
408    Annually,
409}
410
411/// Corporate overhead allocation definition.
412#[derive(Debug, Clone)]
413pub struct OverheadAllocation {
414    /// Allocation ID.
415    pub allocation_id: String,
416    /// Source company code (corporate).
417    pub source_company: String,
418    /// Source cost center.
419    pub source_cost_center: String,
420    /// Source account.
421    pub source_account: String,
422    /// Allocation basis.
423    pub allocation_basis: AllocationBasis,
424    /// Target allocations.
425    pub targets: Vec<AllocationTarget>,
426    /// Description.
427    pub description: String,
428    /// Active flag.
429    pub is_active: bool,
430}
431
432/// Basis for overhead allocation.
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub enum AllocationBasis {
435    /// Based on revenue.
436    Revenue,
437    /// Based on headcount.
438    Headcount,
439    /// Based on direct costs.
440    DirectCosts,
441    /// Based on square footage.
442    SquareFootage,
443    /// Fixed percentages.
444    FixedPercentage,
445    /// Custom formula.
446    Custom(String),
447}
448
449/// Target for overhead allocation.
450#[derive(Debug, Clone)]
451pub struct AllocationTarget {
452    /// Target company code.
453    pub company_code: String,
454    /// Target cost center.
455    pub cost_center: String,
456    /// Target account.
457    pub account: String,
458    /// Allocation percentage (for fixed percentage basis).
459    pub percentage: Option<Decimal>,
460    /// Allocation driver value (for calculated basis).
461    pub driver_value: Option<Decimal>,
462}
463
464/// Period close schedule defining the order of tasks.
465#[derive(Debug, Clone)]
466pub struct CloseSchedule {
467    /// Schedule ID.
468    pub schedule_id: String,
469    /// Company code (or "ALL" for all companies).
470    pub company_code: String,
471    /// Period type this schedule applies to.
472    pub period_type: FiscalPeriodType,
473    /// Ordered list of tasks.
474    pub tasks: Vec<ScheduledCloseTask>,
475    /// Whether this is for year-end.
476    pub is_year_end: bool,
477}
478
479impl CloseSchedule {
480    /// Creates a standard monthly close schedule.
481    pub fn standard_monthly(company_code: &str) -> Self {
482        Self {
483            schedule_id: format!("MONTHLY-{}", company_code),
484            company_code: company_code.to_string(),
485            period_type: FiscalPeriodType::Monthly,
486            tasks: vec![
487                ScheduledCloseTask::new(CloseTask::RunDepreciation, 1),
488                ScheduledCloseTask::new(CloseTask::PostInventoryRevaluation, 2),
489                ScheduledCloseTask::new(CloseTask::PostAccruedExpenses, 3),
490                ScheduledCloseTask::new(CloseTask::PostAccruedRevenue, 4),
491                ScheduledCloseTask::new(CloseTask::PostPrepaidAmortization, 5),
492                ScheduledCloseTask::new(CloseTask::RevalueForeignCurrency, 6),
493                ScheduledCloseTask::new(CloseTask::ReconcileArToGl, 7),
494                ScheduledCloseTask::new(CloseTask::ReconcileApToGl, 8),
495                ScheduledCloseTask::new(CloseTask::ReconcileFaToGl, 9),
496                ScheduledCloseTask::new(CloseTask::ReconcileInventoryToGl, 10),
497                ScheduledCloseTask::new(CloseTask::PostIntercompanySettlements, 11),
498                ScheduledCloseTask::new(CloseTask::AllocateCorporateOverhead, 12),
499                ScheduledCloseTask::new(CloseTask::TranslateForeignSubsidiaries, 13),
500                ScheduledCloseTask::new(CloseTask::EliminateIntercompany, 14),
501                ScheduledCloseTask::new(CloseTask::GenerateTrialBalance, 15),
502            ],
503            is_year_end: false,
504        }
505    }
506
507    /// Creates a year-end close schedule.
508    pub fn year_end(company_code: &str) -> Self {
509        let mut schedule = Self::standard_monthly(company_code);
510        schedule.schedule_id = format!("YEAREND-{}", company_code);
511        schedule.is_year_end = true;
512
513        // Add year-end specific tasks
514        let next_seq = schedule.tasks.len() as u32 + 1;
515        schedule.tasks.push(ScheduledCloseTask::new(
516            CloseTask::CalculateTaxProvision,
517            next_seq,
518        ));
519        schedule.tasks.push(ScheduledCloseTask::new(
520            CloseTask::CloseIncomeStatement,
521            next_seq + 1,
522        ));
523        schedule.tasks.push(ScheduledCloseTask::new(
524            CloseTask::PostRetainedEarningsRollforward,
525            next_seq + 2,
526        ));
527        schedule.tasks.push(ScheduledCloseTask::new(
528            CloseTask::GenerateFinancialStatements,
529            next_seq + 3,
530        ));
531
532        schedule
533    }
534}
535
536/// A scheduled close task with sequence and dependencies.
537#[derive(Debug, Clone)]
538pub struct ScheduledCloseTask {
539    /// The task to execute.
540    pub task: CloseTask,
541    /// Sequence number (execution order).
542    pub sequence: u32,
543    /// Tasks that must complete before this one.
544    pub depends_on: Vec<CloseTask>,
545    /// Is this task mandatory?
546    pub is_mandatory: bool,
547    /// Can this task run in parallel with others at same sequence?
548    pub can_parallelize: bool,
549}
550
551impl ScheduledCloseTask {
552    /// Creates a new scheduled task.
553    pub fn new(task: CloseTask, sequence: u32) -> Self {
554        Self {
555            task,
556            sequence,
557            depends_on: Vec::new(),
558            is_mandatory: true,
559            can_parallelize: false,
560        }
561    }
562
563    /// Adds a dependency.
564    pub fn depends_on(mut self, task: CloseTask) -> Self {
565        self.depends_on.push(task);
566        self
567    }
568
569    /// Marks as optional.
570    pub fn optional(mut self) -> Self {
571        self.is_mandatory = false;
572        self
573    }
574
575    /// Allows parallel execution.
576    pub fn parallelizable(mut self) -> Self {
577        self.can_parallelize = true;
578        self
579    }
580}
581
582/// Year-end closing entry specification.
583#[derive(Debug, Clone)]
584pub struct YearEndClosingSpec {
585    /// Company code.
586    pub company_code: String,
587    /// Fiscal year being closed.
588    pub fiscal_year: i32,
589    /// Revenue accounts to close.
590    pub revenue_accounts: Vec<String>,
591    /// Expense accounts to close.
592    pub expense_accounts: Vec<String>,
593    /// Income summary account (temporary).
594    pub income_summary_account: String,
595    /// Retained earnings account.
596    pub retained_earnings_account: String,
597    /// Dividend account (if applicable).
598    pub dividend_account: Option<String>,
599}
600
601impl Default for YearEndClosingSpec {
602    fn default() -> Self {
603        Self {
604            company_code: String::new(),
605            fiscal_year: 0,
606            revenue_accounts: vec!["4".to_string()], // All accounts starting with 4
607            expense_accounts: vec!["5".to_string(), "6".to_string()], // Accounts starting with 5, 6
608            income_summary_account: "3500".to_string(),
609            retained_earnings_account: "3300".to_string(),
610            dividend_account: Some("3400".to_string()),
611        }
612    }
613}
614
615/// Tax provision calculation inputs.
616#[derive(Debug, Clone)]
617pub struct TaxProvisionInput {
618    /// Company code.
619    pub company_code: String,
620    /// Fiscal year.
621    pub fiscal_year: i32,
622    /// Pre-tax book income.
623    pub pretax_income: Decimal,
624    /// Permanent differences (add back).
625    pub permanent_differences: Vec<TaxAdjustment>,
626    /// Temporary differences (timing).
627    pub temporary_differences: Vec<TaxAdjustment>,
628    /// Statutory tax rate.
629    pub statutory_rate: Decimal,
630    /// Tax credits available.
631    pub tax_credits: Decimal,
632    /// Prior year over/under provision.
633    pub prior_year_adjustment: Decimal,
634}
635
636/// Tax adjustment item.
637#[derive(Debug, Clone)]
638pub struct TaxAdjustment {
639    /// Description.
640    pub description: String,
641    /// Amount.
642    pub amount: Decimal,
643    /// Is this a deduction (negative) or addition (positive)?
644    pub is_addition: bool,
645}
646
647/// Tax provision result.
648#[derive(Debug, Clone)]
649pub struct TaxProvisionResult {
650    /// Company code.
651    pub company_code: String,
652    /// Fiscal year.
653    pub fiscal_year: i32,
654    /// Pre-tax book income.
655    pub pretax_income: Decimal,
656    /// Total permanent differences.
657    pub permanent_differences: Decimal,
658    /// Taxable income.
659    pub taxable_income: Decimal,
660    /// Current tax expense.
661    pub current_tax_expense: Decimal,
662    /// Deferred tax expense (benefit).
663    pub deferred_tax_expense: Decimal,
664    /// Total tax expense.
665    pub total_tax_expense: Decimal,
666    /// Effective tax rate.
667    pub effective_rate: Decimal,
668}
669
670impl TaxProvisionResult {
671    /// Calculates the tax provision from inputs.
672    pub fn calculate(input: &TaxProvisionInput) -> Self {
673        let permanent_diff: Decimal = input
674            .permanent_differences
675            .iter()
676            .map(|d| if d.is_addition { d.amount } else { -d.amount })
677            .sum();
678
679        let temporary_diff: Decimal = input
680            .temporary_differences
681            .iter()
682            .map(|d| if d.is_addition { d.amount } else { -d.amount })
683            .sum();
684
685        let taxable_income = input.pretax_income + permanent_diff;
686        let current_tax = (taxable_income * input.statutory_rate / dec!(100)).round_dp(2);
687        let deferred_tax = (temporary_diff * input.statutory_rate / dec!(100)).round_dp(2);
688
689        let total_tax =
690            current_tax + deferred_tax - input.tax_credits + input.prior_year_adjustment;
691
692        let effective_rate = if input.pretax_income != Decimal::ZERO {
693            (total_tax / input.pretax_income * dec!(100)).round_dp(2)
694        } else {
695            Decimal::ZERO
696        };
697
698        Self {
699            company_code: input.company_code.clone(),
700            fiscal_year: input.fiscal_year,
701            pretax_income: input.pretax_income,
702            permanent_differences: permanent_diff,
703            taxable_income,
704            current_tax_expense: current_tax,
705            deferred_tax_expense: deferred_tax,
706            total_tax_expense: total_tax,
707            effective_rate,
708        }
709    }
710}
711
712/// Period close run status.
713#[derive(Debug, Clone)]
714pub struct PeriodCloseRun {
715    /// Run ID.
716    pub run_id: String,
717    /// Company code.
718    pub company_code: String,
719    /// Fiscal period.
720    pub fiscal_period: FiscalPeriod,
721    /// Status.
722    pub status: PeriodCloseStatus,
723    /// Task results.
724    pub task_results: Vec<CloseTaskResult>,
725    /// Started at.
726    pub started_at: Option<NaiveDate>,
727    /// Completed at.
728    pub completed_at: Option<NaiveDate>,
729    /// Total journal entries created.
730    pub total_journal_entries: u32,
731    /// Errors encountered.
732    pub errors: Vec<String>,
733}
734
735/// Status of a period close run.
736#[derive(Debug, Clone, Copy, PartialEq, Eq)]
737pub enum PeriodCloseStatus {
738    /// Not started.
739    NotStarted,
740    /// In progress.
741    InProgress,
742    /// Completed successfully.
743    Completed,
744    /// Completed with errors.
745    CompletedWithErrors,
746    /// Failed.
747    Failed,
748}
749
750impl PeriodCloseRun {
751    /// Creates a new period close run.
752    pub fn new(run_id: String, company_code: String, fiscal_period: FiscalPeriod) -> Self {
753        Self {
754            run_id,
755            company_code,
756            fiscal_period,
757            status: PeriodCloseStatus::NotStarted,
758            task_results: Vec::new(),
759            started_at: None,
760            completed_at: None,
761            total_journal_entries: 0,
762            errors: Vec::new(),
763        }
764    }
765
766    /// Returns true if all tasks completed successfully.
767    pub fn is_success(&self) -> bool {
768        self.status == PeriodCloseStatus::Completed
769    }
770
771    /// Returns the number of failed tasks.
772    pub fn failed_task_count(&self) -> usize {
773        self.task_results
774            .iter()
775            .filter(|r| matches!(r.status, CloseTaskStatus::Failed(_)))
776            .count()
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783
784    #[test]
785    fn test_fiscal_period_monthly() {
786        let period = FiscalPeriod::monthly(2024, 1);
787        assert_eq!(
788            period.start_date,
789            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()
790        );
791        assert_eq!(
792            period.end_date,
793            NaiveDate::from_ymd_opt(2024, 1, 31).unwrap()
794        );
795        assert_eq!(period.days(), 31);
796        assert!(!period.is_year_end);
797
798        let dec_period = FiscalPeriod::monthly(2024, 12);
799        assert!(dec_period.is_year_end);
800    }
801
802    #[test]
803    fn test_fiscal_period_quarterly() {
804        let q1 = FiscalPeriod::quarterly(2024, 1);
805        assert_eq!(q1.start_date, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
806        assert_eq!(q1.end_date, NaiveDate::from_ymd_opt(2024, 3, 31).unwrap());
807
808        let q4 = FiscalPeriod::quarterly(2024, 4);
809        assert!(q4.is_year_end);
810    }
811
812    #[test]
813    fn test_close_schedule() {
814        let schedule = CloseSchedule::standard_monthly("1000");
815        assert!(!schedule.is_year_end);
816        assert!(!schedule.tasks.is_empty());
817
818        let year_end = CloseSchedule::year_end("1000");
819        assert!(year_end.is_year_end);
820        assert!(year_end.tasks.len() > schedule.tasks.len());
821    }
822
823    #[test]
824    fn test_tax_provision() {
825        let input = TaxProvisionInput {
826            company_code: "1000".to_string(),
827            fiscal_year: 2024,
828            pretax_income: dec!(1000000),
829            permanent_differences: vec![TaxAdjustment {
830                description: "Meals & Entertainment".to_string(),
831                amount: dec!(10000),
832                is_addition: true,
833            }],
834            temporary_differences: vec![TaxAdjustment {
835                description: "Depreciation Timing".to_string(),
836                amount: dec!(50000),
837                is_addition: false,
838            }],
839            statutory_rate: dec!(21),
840            tax_credits: dec!(5000),
841            prior_year_adjustment: Decimal::ZERO,
842        };
843
844        let result = TaxProvisionResult::calculate(&input);
845        assert_eq!(result.taxable_income, dec!(1010000)); // 1M + 10K permanent
846        assert!(result.current_tax_expense > Decimal::ZERO);
847    }
848}