Skip to main content

datasynth_core/
iso21378.rs

1//! ISO 21378:2019 — Audit Data Collection (ADC) account classification.
2//!
3//! ISO 21378 standardises a 3-level account-classification hierarchy for
4//! audit data exchange:
5//!
6//! - **Level 1: Account Type** — five categories (`A` Assets, `L`
7//!   Liabilities, `E` Equity, `R` Revenue, `X` Expenses).
8//! - **Level 2: Account Class** — ~20 broad classes within each type
9//!   (e.g. `A.A` Cash, `A.B` Trade Receivables, `A.F` PPE).
10//! - **Level 3: Account Sub-Class** — finer-grained categories (e.g.
11//!   `A.A.A` Operating Cash, `A.B.A` Trade AR Domestic).
12//!
13//! This module exposes the ADC hierarchy as Rust enums plus a
14//! deterministic mapping from the existing
15//! [`crate::models::AccountSubType`] enum to `(AdcClass, AdcSubClass)`,
16//! so any chart of accounts produced by DataSynth can be projected onto
17//! the ADC surface for audit-exchange exports.
18//!
19//! The ISO standard does not formally enumerate every Level-3 sub-class
20//! — it provides the structure and lets implementers define sub-classes
21//! relevant to their own business. The sub-classes here cover every
22//! `AccountSubType` variant currently used by the generators, with codes
23//! that follow ISO conventions (`<TYPE>.<CLASS>.<SUBCLASS>`).
24//!
25//! # Stability
26//!
27//! ADC codes published in this module are part of the public schema:
28//! once an `AccountSubType` is mapped to `(AdcClass, AdcSubClass)`, the
29//! code remains stable across releases. New codes are appended; existing
30//! codes are not renamed.
31
32use serde::{Deserialize, Serialize};
33
34use crate::models::AccountSubType;
35
36// ──────────────────────────────────────────────────────────────────────
37// Level 1 — Account Type
38// ──────────────────────────────────────────────────────────────────────
39
40/// ISO 21378 Level-1 account type (5 categories).
41///
42/// One-letter ISO code: `A` Assets, `L` Liabilities, `E` Equity,
43/// `R` Revenue, `X` Expenses.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum AdcType {
47    /// `A` — Assets
48    Asset,
49    /// `L` — Liabilities
50    Liability,
51    /// `E` — Equity
52    Equity,
53    /// `R` — Revenue
54    Revenue,
55    /// `X` — Expenses (includes COGS)
56    Expense,
57}
58
59impl AdcType {
60    /// One-letter ISO code (`"A"`, `"L"`, `"E"`, `"R"`, `"X"`).
61    pub fn code(&self) -> &'static str {
62        match self {
63            Self::Asset => "A",
64            Self::Liability => "L",
65            Self::Equity => "E",
66            Self::Revenue => "R",
67            Self::Expense => "X",
68        }
69    }
70
71    /// Human-readable name (`"Assets"`, `"Liabilities"`, …).
72    pub fn name(&self) -> &'static str {
73        match self {
74            Self::Asset => "Assets",
75            Self::Liability => "Liabilities",
76            Self::Equity => "Equity",
77            Self::Revenue => "Revenue",
78            Self::Expense => "Expenses",
79        }
80    }
81}
82
83// ──────────────────────────────────────────────────────────────────────
84// Level 2 — Account Class
85// ──────────────────────────────────────────────────────────────────────
86
87/// ISO 21378 Level-2 account class (~24 categories).
88///
89/// Two-segment ISO code (`<type>.<class>`, e.g. `A.B`).
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum AdcClass {
93    // Assets
94    /// `A.A` — Cash & Cash Equivalents
95    AssetCash,
96    /// `A.B` — Trade Receivables
97    AssetReceivables,
98    /// `A.C` — Inventory
99    AssetInventory,
100    /// `A.D` — Prepaid Expenses & Other Current Assets
101    AssetPrepaid,
102    /// `A.E` — Long-term Investments
103    AssetInvestments,
104    /// `A.F` — Property, Plant & Equipment
105    AssetPpe,
106    /// `A.G` — Intangible Assets
107    AssetIntangible,
108    /// `A.H` — Other Long-term Assets
109    AssetOtherLongTerm,
110    /// `A.X` — Suspense / Clearing (non-standard but conventional)
111    AssetSuspense,
112
113    // Liabilities
114    /// `L.A` — Trade Payables
115    LiabilityPayables,
116    /// `L.B` — Accrued Liabilities
117    LiabilityAccrued,
118    /// `L.C` — Short-term Debt
119    LiabilityShortTermDebt,
120    /// `L.D` — Tax Liabilities
121    LiabilityTax,
122    /// `L.E` — Long-term Debt
123    LiabilityLongTermDebt,
124    /// `L.F` — Other Long-term Liabilities (incl. pension)
125    LiabilityOther,
126
127    // Equity
128    /// `E.A` — Contributed Capital
129    EquityContributed,
130    /// `E.B` — Retained Earnings
131    EquityRetained,
132    /// `E.C` — Other Comprehensive Income
133    EquityOci,
134    /// `E.D` — Treasury Stock
135    EquityTreasury,
136
137    // Revenue
138    /// `R.A` — Operating Revenue
139    RevenueOperating,
140    /// `R.B` — Other Income
141    RevenueOther,
142
143    // Expenses
144    /// `X.A` — Cost of Goods Sold / Cost of Revenue
145    ExpenseCogs,
146    /// `X.B` — Selling, General & Administrative
147    ExpenseSga,
148    /// `X.D` — Depreciation & Amortisation
149    ExpenseDepAmort,
150    /// `X.E` — Interest Expense
151    ExpenseInterest,
152    /// `X.F` — Tax Expense
153    ExpenseTax,
154    /// `X.G` — Other Expenses
155    ExpenseOther,
156}
157
158impl AdcClass {
159    /// Two-segment ISO code (`"A.B"`).
160    pub fn code(&self) -> &'static str {
161        match self {
162            Self::AssetCash => "A.A",
163            Self::AssetReceivables => "A.B",
164            Self::AssetInventory => "A.C",
165            Self::AssetPrepaid => "A.D",
166            Self::AssetInvestments => "A.E",
167            Self::AssetPpe => "A.F",
168            Self::AssetIntangible => "A.G",
169            Self::AssetOtherLongTerm => "A.H",
170            Self::AssetSuspense => "A.X",
171            Self::LiabilityPayables => "L.A",
172            Self::LiabilityAccrued => "L.B",
173            Self::LiabilityShortTermDebt => "L.C",
174            Self::LiabilityTax => "L.D",
175            Self::LiabilityLongTermDebt => "L.E",
176            Self::LiabilityOther => "L.F",
177            Self::EquityContributed => "E.A",
178            Self::EquityRetained => "E.B",
179            Self::EquityOci => "E.C",
180            Self::EquityTreasury => "E.D",
181            Self::RevenueOperating => "R.A",
182            Self::RevenueOther => "R.B",
183            Self::ExpenseCogs => "X.A",
184            Self::ExpenseSga => "X.B",
185            Self::ExpenseDepAmort => "X.D",
186            Self::ExpenseInterest => "X.E",
187            Self::ExpenseTax => "X.F",
188            Self::ExpenseOther => "X.G",
189        }
190    }
191
192    /// Human-readable class name.
193    pub fn name(&self) -> &'static str {
194        match self {
195            Self::AssetCash => "Cash & Cash Equivalents",
196            Self::AssetReceivables => "Trade Receivables",
197            Self::AssetInventory => "Inventory",
198            Self::AssetPrepaid => "Prepaid Expenses & Other Current Assets",
199            Self::AssetInvestments => "Long-term Investments",
200            Self::AssetPpe => "Property, Plant & Equipment",
201            Self::AssetIntangible => "Intangible Assets",
202            Self::AssetOtherLongTerm => "Other Long-term Assets",
203            Self::AssetSuspense => "Suspense & Clearing (Asset side)",
204            Self::LiabilityPayables => "Trade Payables",
205            Self::LiabilityAccrued => "Accrued Liabilities",
206            Self::LiabilityShortTermDebt => "Short-term Debt",
207            Self::LiabilityTax => "Tax Liabilities",
208            Self::LiabilityLongTermDebt => "Long-term Debt",
209            Self::LiabilityOther => "Other Long-term Liabilities",
210            Self::EquityContributed => "Contributed Capital",
211            Self::EquityRetained => "Retained Earnings",
212            Self::EquityOci => "Other Comprehensive Income",
213            Self::EquityTreasury => "Treasury Stock",
214            Self::RevenueOperating => "Operating Revenue",
215            Self::RevenueOther => "Other Income",
216            Self::ExpenseCogs => "Cost of Goods Sold",
217            Self::ExpenseSga => "Selling, General & Administrative",
218            Self::ExpenseDepAmort => "Depreciation & Amortisation",
219            Self::ExpenseInterest => "Interest Expense",
220            Self::ExpenseTax => "Tax Expense",
221            Self::ExpenseOther => "Other Expenses",
222        }
223    }
224
225    /// Parent ADC type.
226    pub fn adc_type(&self) -> AdcType {
227        match self {
228            Self::AssetCash
229            | Self::AssetReceivables
230            | Self::AssetInventory
231            | Self::AssetPrepaid
232            | Self::AssetInvestments
233            | Self::AssetPpe
234            | Self::AssetIntangible
235            | Self::AssetOtherLongTerm
236            | Self::AssetSuspense => AdcType::Asset,
237            Self::LiabilityPayables
238            | Self::LiabilityAccrued
239            | Self::LiabilityShortTermDebt
240            | Self::LiabilityTax
241            | Self::LiabilityLongTermDebt
242            | Self::LiabilityOther => AdcType::Liability,
243            Self::EquityContributed
244            | Self::EquityRetained
245            | Self::EquityOci
246            | Self::EquityTreasury => AdcType::Equity,
247            Self::RevenueOperating | Self::RevenueOther => AdcType::Revenue,
248            Self::ExpenseCogs
249            | Self::ExpenseSga
250            | Self::ExpenseDepAmort
251            | Self::ExpenseInterest
252            | Self::ExpenseTax
253            | Self::ExpenseOther => AdcType::Expense,
254        }
255    }
256}
257
258// ──────────────────────────────────────────────────────────────────────
259// Level 3 — Account Sub-Class
260// ──────────────────────────────────────────────────────────────────────
261
262/// ISO 21378 Level-3 account sub-class.
263///
264/// Three-segment ISO code (`<type>.<class>.<subclass>`, e.g. `A.B.A`).
265/// Codes are stable; new variants are appended, existing variants are
266/// not renamed.
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
268#[serde(rename_all = "snake_case")]
269pub enum AdcSubClass {
270    // Assets
271    /// `A.A.A` — Operating cash
272    AssetCashOperating,
273    /// `A.A.B` — Bank clearing accounts
274    AssetCashBankClearing,
275    /// `A.B.A` — Trade Accounts Receivable
276    AssetTradeAr,
277    /// `A.B.B` — Other Receivables
278    AssetOtherReceivables,
279    /// `A.C.A` — Inventory (general)
280    AssetInventoryGeneral,
281    /// `A.D.A` — Prepaid Expenses
282    AssetPrepaidExpenses,
283    /// `A.E.A` — Long-term Investments
284    AssetInvestmentsLongTerm,
285    /// `A.F.A` — Fixed Assets (gross)
286    AssetFixedAssets,
287    /// `A.F.B` — Accumulated Depreciation (contra)
288    AssetAccumulatedDepreciation,
289    /// `A.G.A` — Intangible Assets
290    AssetIntangibleAssets,
291    /// `A.H.A` — Other Assets
292    AssetOtherAssets,
293    /// `A.X.A` — Suspense Clearing (Asset side)
294    AssetSuspenseClearing,
295    /// `A.X.B` — Intercompany Clearing (Asset side)
296    AssetIntercompanyClearing,
297
298    // Liabilities
299    /// `L.A.A` — Trade Accounts Payable
300    LiabilityTradeAp,
301    /// `L.B.A` — Accrued Expenses
302    LiabilityAccruedExpenses,
303    /// `L.B.B` — Deferred Revenue
304    LiabilityDeferredRevenue,
305    /// `L.B.C` — Goods Received / Invoice Received Clearing
306    LiabilityGoodsReceivedClearing,
307    /// `L.C.A` — Short-term Debt
308    LiabilityShortTermDebt,
309    /// `L.D.A` — Tax Liabilities
310    LiabilityTaxLiabilities,
311    /// `L.E.A` — Long-term Debt
312    LiabilityLongTermDebt,
313    /// `L.F.A` — Pension Liabilities
314    LiabilityPensionLiabilities,
315    /// `L.F.B` — Other Liabilities
316    LiabilityOtherLiabilities,
317
318    // Equity
319    /// `E.A.A` — Common Stock
320    EquityCommonStock,
321    /// `E.A.B` — Additional Paid-In Capital
322    EquityAdditionalPaidIn,
323    /// `E.B.A` — Retained Earnings
324    EquityRetainedEarnings,
325    /// `E.B.B` — Net Income (current period)
326    EquityNetIncome,
327    /// `E.C.A` — Other Comprehensive Income
328    EquityOtherComprehensiveIncome,
329    /// `E.D.A` — Treasury Stock
330    EquityTreasuryStock,
331
332    // Revenue
333    /// `R.A.A` — Product Revenue
334    RevenueProduct,
335    /// `R.A.B` — Service Revenue
336    RevenueService,
337    /// `R.B.A` — Interest Income
338    RevenueInterestIncome,
339    /// `R.B.B` — Dividend Income
340    RevenueDividendIncome,
341    /// `R.B.C` — Gain on Sale of Assets
342    RevenueGainOnSale,
343    /// `R.B.D` — Other Income
344    RevenueOtherIncome,
345
346    // Expenses
347    /// `X.A.A` — Cost of Goods Sold
348    ExpenseCostOfGoodsSold,
349    /// `X.B.A` — Operating Expenses
350    ExpenseOperatingExpenses,
351    /// `X.B.B` — Selling Expenses
352    ExpenseSellingExpenses,
353    /// `X.B.C` — Administrative Expenses
354    ExpenseAdministrativeExpenses,
355    /// `X.D.A` — Depreciation Expense
356    ExpenseDepreciationExpense,
357    /// `X.D.B` — Amortisation Expense
358    ExpenseAmortisationExpense,
359    /// `X.E.A` — Interest Expense
360    ExpenseInterestExpense,
361    /// `X.F.A` — Tax Expense
362    ExpenseTaxExpense,
363    /// `X.G.A` — Foreign-Exchange Loss
364    ExpenseForeignExchangeLoss,
365    /// `X.G.B` — Loss on Sale of Assets
366    ExpenseLossOnSale,
367    /// `X.G.C` — Other Expenses
368    ExpenseOtherExpenses,
369}
370
371impl AdcSubClass {
372    /// Three-segment ISO code (`"A.B.A"`).
373    pub fn code(&self) -> &'static str {
374        match self {
375            Self::AssetCashOperating => "A.A.A",
376            Self::AssetCashBankClearing => "A.A.B",
377            Self::AssetTradeAr => "A.B.A",
378            Self::AssetOtherReceivables => "A.B.B",
379            Self::AssetInventoryGeneral => "A.C.A",
380            Self::AssetPrepaidExpenses => "A.D.A",
381            Self::AssetInvestmentsLongTerm => "A.E.A",
382            Self::AssetFixedAssets => "A.F.A",
383            Self::AssetAccumulatedDepreciation => "A.F.B",
384            Self::AssetIntangibleAssets => "A.G.A",
385            Self::AssetOtherAssets => "A.H.A",
386            Self::AssetSuspenseClearing => "A.X.A",
387            Self::AssetIntercompanyClearing => "A.X.B",
388            Self::LiabilityTradeAp => "L.A.A",
389            Self::LiabilityAccruedExpenses => "L.B.A",
390            Self::LiabilityDeferredRevenue => "L.B.B",
391            Self::LiabilityGoodsReceivedClearing => "L.B.C",
392            Self::LiabilityShortTermDebt => "L.C.A",
393            Self::LiabilityTaxLiabilities => "L.D.A",
394            Self::LiabilityLongTermDebt => "L.E.A",
395            Self::LiabilityPensionLiabilities => "L.F.A",
396            Self::LiabilityOtherLiabilities => "L.F.B",
397            Self::EquityCommonStock => "E.A.A",
398            Self::EquityAdditionalPaidIn => "E.A.B",
399            Self::EquityRetainedEarnings => "E.B.A",
400            Self::EquityNetIncome => "E.B.B",
401            Self::EquityOtherComprehensiveIncome => "E.C.A",
402            Self::EquityTreasuryStock => "E.D.A",
403            Self::RevenueProduct => "R.A.A",
404            Self::RevenueService => "R.A.B",
405            Self::RevenueInterestIncome => "R.B.A",
406            Self::RevenueDividendIncome => "R.B.B",
407            Self::RevenueGainOnSale => "R.B.C",
408            Self::RevenueOtherIncome => "R.B.D",
409            Self::ExpenseCostOfGoodsSold => "X.A.A",
410            Self::ExpenseOperatingExpenses => "X.B.A",
411            Self::ExpenseSellingExpenses => "X.B.B",
412            Self::ExpenseAdministrativeExpenses => "X.B.C",
413            Self::ExpenseDepreciationExpense => "X.D.A",
414            Self::ExpenseAmortisationExpense => "X.D.B",
415            Self::ExpenseInterestExpense => "X.E.A",
416            Self::ExpenseTaxExpense => "X.F.A",
417            Self::ExpenseForeignExchangeLoss => "X.G.A",
418            Self::ExpenseLossOnSale => "X.G.B",
419            Self::ExpenseOtherExpenses => "X.G.C",
420        }
421    }
422
423    /// Human-readable sub-class name.
424    pub fn name(&self) -> &'static str {
425        match self {
426            Self::AssetCashOperating => "Operating Cash",
427            Self::AssetCashBankClearing => "Bank Clearing",
428            Self::AssetTradeAr => "Trade Accounts Receivable",
429            Self::AssetOtherReceivables => "Other Receivables",
430            Self::AssetInventoryGeneral => "Inventory",
431            Self::AssetPrepaidExpenses => "Prepaid Expenses",
432            Self::AssetInvestmentsLongTerm => "Long-term Investments",
433            Self::AssetFixedAssets => "Fixed Assets",
434            Self::AssetAccumulatedDepreciation => "Accumulated Depreciation",
435            Self::AssetIntangibleAssets => "Intangible Assets",
436            Self::AssetOtherAssets => "Other Assets",
437            Self::AssetSuspenseClearing => "Suspense Clearing",
438            Self::AssetIntercompanyClearing => "Intercompany Clearing",
439            Self::LiabilityTradeAp => "Trade Accounts Payable",
440            Self::LiabilityAccruedExpenses => "Accrued Expenses",
441            Self::LiabilityDeferredRevenue => "Deferred Revenue",
442            Self::LiabilityGoodsReceivedClearing => "GR/IR Clearing",
443            Self::LiabilityShortTermDebt => "Short-term Debt",
444            Self::LiabilityTaxLiabilities => "Tax Liabilities",
445            Self::LiabilityLongTermDebt => "Long-term Debt",
446            Self::LiabilityPensionLiabilities => "Pension Liabilities",
447            Self::LiabilityOtherLiabilities => "Other Liabilities",
448            Self::EquityCommonStock => "Common Stock",
449            Self::EquityAdditionalPaidIn => "Additional Paid-In Capital",
450            Self::EquityRetainedEarnings => "Retained Earnings",
451            Self::EquityNetIncome => "Net Income",
452            Self::EquityOtherComprehensiveIncome => "Other Comprehensive Income",
453            Self::EquityTreasuryStock => "Treasury Stock",
454            Self::RevenueProduct => "Product Revenue",
455            Self::RevenueService => "Service Revenue",
456            Self::RevenueInterestIncome => "Interest Income",
457            Self::RevenueDividendIncome => "Dividend Income",
458            Self::RevenueGainOnSale => "Gain on Sale of Assets",
459            Self::RevenueOtherIncome => "Other Income",
460            Self::ExpenseCostOfGoodsSold => "Cost of Goods Sold",
461            Self::ExpenseOperatingExpenses => "Operating Expenses",
462            Self::ExpenseSellingExpenses => "Selling Expenses",
463            Self::ExpenseAdministrativeExpenses => "Administrative Expenses",
464            Self::ExpenseDepreciationExpense => "Depreciation Expense",
465            Self::ExpenseAmortisationExpense => "Amortisation Expense",
466            Self::ExpenseInterestExpense => "Interest Expense",
467            Self::ExpenseTaxExpense => "Tax Expense",
468            Self::ExpenseForeignExchangeLoss => "Foreign-Exchange Loss",
469            Self::ExpenseLossOnSale => "Loss on Sale of Assets",
470            Self::ExpenseOtherExpenses => "Other Expenses",
471        }
472    }
473
474    /// Parent ADC class (Level 2).
475    pub fn adc_class(&self) -> AdcClass {
476        match self {
477            Self::AssetCashOperating | Self::AssetCashBankClearing => AdcClass::AssetCash,
478            Self::AssetTradeAr | Self::AssetOtherReceivables => AdcClass::AssetReceivables,
479            Self::AssetInventoryGeneral => AdcClass::AssetInventory,
480            Self::AssetPrepaidExpenses => AdcClass::AssetPrepaid,
481            Self::AssetInvestmentsLongTerm => AdcClass::AssetInvestments,
482            Self::AssetFixedAssets | Self::AssetAccumulatedDepreciation => AdcClass::AssetPpe,
483            Self::AssetIntangibleAssets => AdcClass::AssetIntangible,
484            Self::AssetOtherAssets => AdcClass::AssetOtherLongTerm,
485            Self::AssetSuspenseClearing | Self::AssetIntercompanyClearing => {
486                AdcClass::AssetSuspense
487            }
488            Self::LiabilityTradeAp => AdcClass::LiabilityPayables,
489            Self::LiabilityAccruedExpenses
490            | Self::LiabilityDeferredRevenue
491            | Self::LiabilityGoodsReceivedClearing => AdcClass::LiabilityAccrued,
492            Self::LiabilityShortTermDebt => AdcClass::LiabilityShortTermDebt,
493            Self::LiabilityTaxLiabilities => AdcClass::LiabilityTax,
494            Self::LiabilityLongTermDebt => AdcClass::LiabilityLongTermDebt,
495            Self::LiabilityPensionLiabilities | Self::LiabilityOtherLiabilities => {
496                AdcClass::LiabilityOther
497            }
498            Self::EquityCommonStock | Self::EquityAdditionalPaidIn => AdcClass::EquityContributed,
499            Self::EquityRetainedEarnings | Self::EquityNetIncome => AdcClass::EquityRetained,
500            Self::EquityOtherComprehensiveIncome => AdcClass::EquityOci,
501            Self::EquityTreasuryStock => AdcClass::EquityTreasury,
502            Self::RevenueProduct | Self::RevenueService => AdcClass::RevenueOperating,
503            Self::RevenueInterestIncome
504            | Self::RevenueDividendIncome
505            | Self::RevenueGainOnSale
506            | Self::RevenueOtherIncome => AdcClass::RevenueOther,
507            Self::ExpenseCostOfGoodsSold => AdcClass::ExpenseCogs,
508            Self::ExpenseOperatingExpenses
509            | Self::ExpenseSellingExpenses
510            | Self::ExpenseAdministrativeExpenses => AdcClass::ExpenseSga,
511            Self::ExpenseDepreciationExpense | Self::ExpenseAmortisationExpense => {
512                AdcClass::ExpenseDepAmort
513            }
514            Self::ExpenseInterestExpense => AdcClass::ExpenseInterest,
515            Self::ExpenseTaxExpense => AdcClass::ExpenseTax,
516            Self::ExpenseForeignExchangeLoss
517            | Self::ExpenseLossOnSale
518            | Self::ExpenseOtherExpenses => AdcClass::ExpenseOther,
519        }
520    }
521}
522
523// ──────────────────────────────────────────────────────────────────────
524// Mapping from `AccountSubType` to `(AdcClass, AdcSubClass)`
525// ──────────────────────────────────────────────────────────────────────
526
527/// Map a [`AccountSubType`] to the equivalent ISO 21378 Level-3
528/// sub-class (and, transitively, the Level-2 class via
529/// [`AdcSubClass::adc_class`]).
530///
531/// Total: every variant of `AccountSubType` is mapped — the function
532/// is total, never panics, and never returns a default. New
533/// `AccountSubType` variants must be added to the match arm or the
534/// crate will fail to compile.
535pub fn from_account_sub_type(sub_type: AccountSubType) -> AdcSubClass {
536    use AccountSubType as S;
537    match sub_type {
538        S::Cash => AdcSubClass::AssetCashOperating,
539        S::AccountsReceivable => AdcSubClass::AssetTradeAr,
540        S::OtherReceivables => AdcSubClass::AssetOtherReceivables,
541        S::Inventory => AdcSubClass::AssetInventoryGeneral,
542        S::PrepaidExpenses => AdcSubClass::AssetPrepaidExpenses,
543        S::FixedAssets => AdcSubClass::AssetFixedAssets,
544        S::AccumulatedDepreciation => AdcSubClass::AssetAccumulatedDepreciation,
545        S::Investments => AdcSubClass::AssetInvestmentsLongTerm,
546        S::IntangibleAssets => AdcSubClass::AssetIntangibleAssets,
547        S::OtherAssets => AdcSubClass::AssetOtherAssets,
548        S::AccountsPayable => AdcSubClass::LiabilityTradeAp,
549        S::AccruedLiabilities => AdcSubClass::LiabilityAccruedExpenses,
550        S::ShortTermDebt => AdcSubClass::LiabilityShortTermDebt,
551        S::LongTermDebt => AdcSubClass::LiabilityLongTermDebt,
552        S::DeferredRevenue => AdcSubClass::LiabilityDeferredRevenue,
553        S::TaxLiabilities => AdcSubClass::LiabilityTaxLiabilities,
554        S::PensionLiabilities => AdcSubClass::LiabilityPensionLiabilities,
555        S::OtherLiabilities => AdcSubClass::LiabilityOtherLiabilities,
556        S::CommonStock => AdcSubClass::EquityCommonStock,
557        S::RetainedEarnings => AdcSubClass::EquityRetainedEarnings,
558        S::AdditionalPaidInCapital => AdcSubClass::EquityAdditionalPaidIn,
559        S::TreasuryStock => AdcSubClass::EquityTreasuryStock,
560        S::OtherComprehensiveIncome => AdcSubClass::EquityOtherComprehensiveIncome,
561        S::NetIncome => AdcSubClass::EquityNetIncome,
562        S::ProductRevenue => AdcSubClass::RevenueProduct,
563        S::ServiceRevenue => AdcSubClass::RevenueService,
564        S::InterestIncome => AdcSubClass::RevenueInterestIncome,
565        S::DividendIncome => AdcSubClass::RevenueDividendIncome,
566        S::GainOnSale => AdcSubClass::RevenueGainOnSale,
567        S::OtherIncome => AdcSubClass::RevenueOtherIncome,
568        S::CostOfGoodsSold => AdcSubClass::ExpenseCostOfGoodsSold,
569        S::OperatingExpenses => AdcSubClass::ExpenseOperatingExpenses,
570        S::SellingExpenses => AdcSubClass::ExpenseSellingExpenses,
571        S::AdministrativeExpenses => AdcSubClass::ExpenseAdministrativeExpenses,
572        S::DepreciationExpense => AdcSubClass::ExpenseDepreciationExpense,
573        S::AmortizationExpense => AdcSubClass::ExpenseAmortisationExpense,
574        S::InterestExpense => AdcSubClass::ExpenseInterestExpense,
575        S::TaxExpense => AdcSubClass::ExpenseTaxExpense,
576        S::ForeignExchangeLoss => AdcSubClass::ExpenseForeignExchangeLoss,
577        S::LossOnSale => AdcSubClass::ExpenseLossOnSale,
578        S::OtherExpenses => AdcSubClass::ExpenseOtherExpenses,
579        S::SuspenseClearing => AdcSubClass::AssetSuspenseClearing,
580        S::GoodsReceivedClearing => AdcSubClass::LiabilityGoodsReceivedClearing,
581        S::BankClearing => AdcSubClass::AssetCashBankClearing,
582        S::IntercompanyClearing => AdcSubClass::AssetIntercompanyClearing,
583    }
584}
585
586// ──────────────────────────────────────────────────────────────────────
587// Tests
588// ──────────────────────────────────────────────────────────────────────
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593    use crate::models::AccountSubType;
594
595    /// Every AccountSubType variant must map to a valid ISO sub-class.
596    /// This is a compile-time check (the match in `from_account_sub_type`
597    /// is exhaustive) plus a runtime sanity check that the produced
598    /// codes are well-formed.
599    #[test]
600    fn every_account_sub_type_maps_to_valid_iso_codes() {
601        for &sub in &[
602            AccountSubType::Cash,
603            AccountSubType::AccountsReceivable,
604            AccountSubType::OtherReceivables,
605            AccountSubType::Inventory,
606            AccountSubType::PrepaidExpenses,
607            AccountSubType::FixedAssets,
608            AccountSubType::AccumulatedDepreciation,
609            AccountSubType::Investments,
610            AccountSubType::IntangibleAssets,
611            AccountSubType::OtherAssets,
612            AccountSubType::AccountsPayable,
613            AccountSubType::AccruedLiabilities,
614            AccountSubType::ShortTermDebt,
615            AccountSubType::LongTermDebt,
616            AccountSubType::DeferredRevenue,
617            AccountSubType::TaxLiabilities,
618            AccountSubType::PensionLiabilities,
619            AccountSubType::OtherLiabilities,
620            AccountSubType::CommonStock,
621            AccountSubType::RetainedEarnings,
622            AccountSubType::AdditionalPaidInCapital,
623            AccountSubType::TreasuryStock,
624            AccountSubType::OtherComprehensiveIncome,
625            AccountSubType::NetIncome,
626            AccountSubType::ProductRevenue,
627            AccountSubType::ServiceRevenue,
628            AccountSubType::InterestIncome,
629            AccountSubType::DividendIncome,
630            AccountSubType::GainOnSale,
631            AccountSubType::OtherIncome,
632            AccountSubType::CostOfGoodsSold,
633            AccountSubType::OperatingExpenses,
634            AccountSubType::SellingExpenses,
635            AccountSubType::AdministrativeExpenses,
636            AccountSubType::DepreciationExpense,
637            AccountSubType::AmortizationExpense,
638            AccountSubType::InterestExpense,
639            AccountSubType::TaxExpense,
640            AccountSubType::ForeignExchangeLoss,
641            AccountSubType::LossOnSale,
642            AccountSubType::OtherExpenses,
643            AccountSubType::SuspenseClearing,
644            AccountSubType::GoodsReceivedClearing,
645            AccountSubType::BankClearing,
646            AccountSubType::IntercompanyClearing,
647        ] {
648            let sub_class = from_account_sub_type(sub);
649            let class = sub_class.adc_class();
650            let ty = class.adc_type();
651
652            // Sub-class code must be `<type>.<class>.<x>` (5 chars,
653            // dotted) and consistent with class / type codes.
654            let sub_code = sub_class.code();
655            let class_code = class.code();
656            let type_code = ty.code();
657            assert_eq!(
658                sub_code.len(),
659                5,
660                "sub-class code should be 5 chars: {sub_code}"
661            );
662            assert!(
663                sub_code.starts_with(class_code),
664                "sub-class code {sub_code} should start with class {class_code}"
665            );
666            assert!(
667                class_code.starts_with(type_code),
668                "class code {class_code} should start with type {type_code}"
669            );
670            assert!(
671                !sub_class.name().is_empty(),
672                "sub-class name should not be empty for {sub:?}"
673            );
674        }
675    }
676
677    #[test]
678    fn isocode_examples_are_correct() {
679        assert_eq!(AdcType::Asset.code(), "A");
680        assert_eq!(AdcClass::AssetReceivables.code(), "A.B");
681        assert_eq!(AdcSubClass::AssetTradeAr.code(), "A.B.A");
682        assert_eq!(
683            from_account_sub_type(AccountSubType::AccountsReceivable).code(),
684            "A.B.A"
685        );
686        assert_eq!(
687            from_account_sub_type(AccountSubType::CostOfGoodsSold).code(),
688            "X.A.A"
689        );
690        assert_eq!(
691            from_account_sub_type(AccountSubType::CommonStock).code(),
692            "E.A.A"
693        );
694    }
695
696    #[test]
697    fn parent_relationships_are_consistent() {
698        // Every sub-class's parent class must report the same type code.
699        for sub_class in [
700            AdcSubClass::AssetTradeAr,
701            AdcSubClass::LiabilityTradeAp,
702            AdcSubClass::EquityRetainedEarnings,
703            AdcSubClass::RevenueProduct,
704            AdcSubClass::ExpenseCostOfGoodsSold,
705        ] {
706            let class = sub_class.adc_class();
707            let ty = class.adc_type();
708            // Sub-class code starts with class code; class code starts with type code.
709            assert!(sub_class.code().starts_with(class.code()));
710            assert!(class.code().starts_with(ty.code()));
711        }
712    }
713}