Skip to main content

backbone_accounting/
lib.ext.rs

1//! The module's extension (hand-written; ADR-0031): what the module adds beside its
2//! generated services. The generated module and builder carry `ModuleExt` and
3//! `ModuleBuilderExt` and dereference to them, so their fields read as `module.field`.
4
5#[allow(unused_imports)]
6use super::*;
7
8/// State the module adds; read through the generated module's `Deref`.
9pub struct ModuleExt {
10    pub(crate) db_pool: PgPool,
11    pub(crate) reconcile_write_service:
12        Arc<crate::application::service::reconcile_write_service::ReconcileWriteService>,
13    /// The check printing surface: per-bank-journal check-number sequences and
14    /// the printed-check registry.
15    pub(crate) check_printing_service:
16        Arc<crate::application::service::check_printing_service::CheckPrintingService>,
17    /// The EMV(QRCPS)/QRIS display surface: merchant QR configuration and the
18    /// invoice payload render.
19    pub(crate) emv_qr_service:
20        Arc<crate::application::service::emv_qr_service::EmvQrService>,
21    /// The tax-tag legal-change repair verb and its audit ledger read.
22    pub(crate) tax_tag_repair_service:
23        Arc<crate::application::service::tax_tag_repair_service::TaxTagRepairService>,
24    /// Host-implemented cash-basis tax deferral lookup, kept so post-build
25    /// service variants (e.g. the exchange-account one) keep the flip wired.
26    pub(crate) deferred_tax: Option<Arc<dyn crate::domain::repositories::DeferredTaxLookup>>,
27    /// Host-implemented budget control, kept so every posting path the host
28    /// builds from this module (the HTTP posting verb, the journal workflow,
29    /// the period close) can arm the consult from one registration.
30    pub(crate) budget_control: Option<Arc<dyn crate::domain::repositories::BudgetControlPort>>,
31    /// Chart install engine over the datasets registered via `with_chart_dataset`.
32    pub(crate) chart_install_service:
33        Arc<crate::application::service::chart_install_service::ChartInstallService>,
34    /// On-the-fly financial reports (TB tree, BS, P&L, GL, partner ledger, aged AR/AP).
35    pub(crate) reporting_service:
36        Arc<crate::application::service::reporting_service::ReportingService>,
37}
38
39/// State the builder adds.
40pub struct ModuleBuilderExt {
41    deferred_tax: Option<Arc<dyn crate::domain::repositories::DeferredTaxLookup>>,
42    budget_control: Option<Arc<dyn crate::domain::repositories::BudgetControlPort>>,
43    chart_datasets: Vec<Arc<crate::domain::chart_dataset::ChartDataset>>,
44}
45
46impl Default for ModuleBuilderExt {
47    fn default() -> Self {
48        Self {
49            deferred_tax: None,
50            budget_control: None,
51            chart_datasets: Vec::new(),
52        }
53    }
54}
55
56impl ModuleBuilderExt {
57    /// Build the extension's state from what the generated build made.
58    #[allow(unused_variables, clippy::redundant_clone)]
59    pub(crate) fn build(self, parts: &ModuleParts<'_>) -> anyhow::Result<ModuleExt> {
60        let db_pool = parts.db_pool.clone();
61        // Reconciliation write service: graph port + posting port (reversals of
62        // generated moves) over the shared pool; no FX account by default (fail-closed).
63        let reconcile_graph_repository = std::sync::Arc::new(
64            crate::infrastructure::persistence::reconcile_graph_repository::SqlxReconcileGraphRepository::new(),
65        );
66        let reconcile_posting_repository = std::sync::Arc::new(
67            crate::infrastructure::persistence::posting_repository::SqlxPostingRepository::new(
68                db_pool.clone(),
69            ),
70        );
71        let reconcile_write_service = std::sync::Arc::new(
72            crate::application::service::reconcile_write_service::ReconcileWriteService::new(
73                reconcile_graph_repository,
74                reconcile_posting_repository,
75                db_pool.clone(),
76                None,
77            )
78            .with_deferred_tax_if_set(self.deferred_tax.clone()),
79        );
80        // Chart install engine: datasets registered by the host drive idempotent
81        // chart installs onto fresh companies (provenance columns identify own rows).
82        let chart_install_service = std::sync::Arc::new(
83            crate::application::service::chart_install_service::ChartInstallService::new(
84                std::sync::Arc::new(
85                    crate::infrastructure::persistence::chart_install_repository::SqlxChartInstallRepository::new(),
86                ),
87                db_pool.clone(),
88                self.chart_datasets,
89            ),
90        );
91        // Financial reports: computed on the fly over the scoped ledger reads.
92        let reporting_service = std::sync::Arc::new(
93            crate::application::service::reporting_service::ReportingService::new(
94                std::sync::Arc::new(
95                    crate::infrastructure::persistence::reporting_repository::SqlxReportingRepository::new(
96                        db_pool.clone(),
97                    ),
98                ),
99            ),
100        );
101        // Check printing: per-bank-journal number sequences + the printed-check
102        // registry, all writes through the guarded verb set.
103        let check_printing_service = std::sync::Arc::new(
104            crate::application::service::check_printing_service::CheckPrintingService::new(
105                db_pool.clone(),
106            ),
107        );
108        // EMV(QRCPS)/QRIS display: merchant QR config + invoice payload render.
109        let emv_qr_service = std::sync::Arc::new(
110            crate::application::service::emv_qr_service::EmvQrService::new(db_pool.clone()),
111        );
112        // Tax-tag legal-change repair verb + its audit ledger read.
113        let tax_tag_repair_service = std::sync::Arc::new(
114            crate::application::service::tax_tag_repair_service::TaxTagRepairService::new(
115                db_pool.clone(),
116            ),
117        );
118        Ok(ModuleExt {
119            db_pool,
120            reconcile_write_service,
121            deferred_tax: self.deferred_tax,
122            budget_control: self.budget_control,
123            chart_install_service,
124            reporting_service,
125            check_printing_service,
126            emv_qr_service,
127            tax_tag_repair_service,
128        })
129    }
130}
131
132impl crate::ThisModule {
133    /// The registered budget-control port, if any — the single registration a
134    /// host threads into every `PostingService` it builds from this module
135    /// (`.with_budget_control_if_set(m.budget_control())`), so the HTTP
136    /// posting verb, the journal workflow, and the period close all consult
137    /// the same control.
138    pub fn budget_control(
139        &self,
140    ) -> Option<Arc<dyn crate::domain::repositories::BudgetControlPort>> {
141        self.budget_control.clone()
142    }
143    /// The on-the-fly financial reports (Trial Balance tree, Balance Sheet, Income
144    /// Statement, General Ledger, Partner Ledger, Aged AR/AP). Read-only; scoped reads.
145    pub fn reporting_service(
146        &self,
147    ) -> std::sync::Arc<crate::application::service::reporting_service::ReportingService> {
148        self.reporting_service.clone()
149    }
150    /// The reconciliation write service — the ONLY sanctioned writer of the
151    /// reconciliation graph (guards + CLAMP + side-effecting unlink). The graph tables
152    /// carry no CRUD surface anywhere; `guarded_routes` mounts its verb routes.
153    pub fn reconcile_write_service(
154        &self,
155    ) -> std::sync::Arc<crate::application::service::reconcile_write_service::ReconcileWriteService>
156    {
157        self.reconcile_write_service.clone()
158    }
159    /// Build a reconcile write service with the FX (exchange-difference) account
160    /// configured. Without one, any rate divergence that demands an exchange move fails
161    /// closed (`exchange_account_unconfigured`) — normal single-currency posts never
162    /// generate one, so the default service is complete for them.
163    pub fn reconcile_write_service_with_exchange_account(
164        &self,
165        exchange_account_id: uuid::Uuid,
166    ) -> std::sync::Arc<crate::application::service::reconcile_write_service::ReconcileWriteService>
167    {
168        std::sync::Arc::new(
169            crate::application::service::reconcile_write_service::ReconcileWriteService::new(
170                std::sync::Arc::new(
171                    crate::infrastructure::persistence::reconcile_graph_repository::SqlxReconcileGraphRepository::new(),
172                ),
173                std::sync::Arc::new(
174                    crate::infrastructure::persistence::posting_repository::SqlxPostingRepository::new(
175                        self.db_pool.clone(),
176                    ),
177                ),
178                self.db_pool.clone(),
179                Some(exchange_account_id),
180            )
181            .with_deferred_tax_if_set(self.deferred_tax.clone()),
182        )
183    }
184    /// The chart install engine — validates, gates, and writes a registered chart
185    /// dataset into the accounts, idempotently. The install VERB itself is
186    /// host-mounted (it orchestrates beyond this module); this handle is the engine
187    /// the host verb drives.
188    pub fn chart_install_service(
189        &self,
190    ) -> std::sync::Arc<crate::application::service::chart_install_service::ChartInstallService>
191    {
192        self.chart_install_service.clone()
193    }
194    /// The check printing surface — sequence registration, atomic number
195    /// allocation, the printed-check registry, and voids. `guarded_routes`
196    /// mounts its verb routes; there is no CRUD path to these tables.
197    pub fn check_printing_service(
198        &self,
199    ) -> std::sync::Arc<crate::application::service::check_printing_service::CheckPrintingService>
200    {
201        self.check_printing_service.clone()
202    }
203    /// The EMV(QRCPS)/QRIS display surface — merchant QR configuration and the
204    /// invoice payload render (payloads are built on demand, never stored).
205    pub fn emv_qr_service(
206        &self,
207    ) -> std::sync::Arc<crate::application::service::emv_qr_service::EmvQrService> {
208        self.emv_qr_service.clone()
209    }
210    /// The tax-tag legal-change repair verb (reason mandatory, locked periods
211    /// refused, closed periods overridden explicitly, every run audit-stamped)
212    /// plus its audit-ledger read.
213    pub fn tax_tag_repair_service(
214        &self,
215    ) -> std::sync::Arc<crate::application::service::tax_tag_repair_service::TaxTagRepairService>
216    {
217        self.tax_tag_repair_service.clone()
218    }
219}
220
221impl crate::ThisModuleBuilder {
222    /// Register a chart-of-accounts dataset installable through the chart install
223    /// engine. Datasets are data (commonly shipped by an l10n module); registering
224    /// one makes it listable via `GET /accounting/charts` and installable by the
225    /// host-mounted install verb.
226    pub fn with_chart_dataset(
227        mut self,
228        dataset: Arc<crate::domain::chart_dataset::ChartDataset>,
229    ) -> Self {
230        self.chart_datasets.push(dataset);
231        self
232    }
233    /// Wire the host's cash-basis tax deferral lookup: partial reconciliations
234    /// landing on receivable/payable lines flip deferred (on_payment) tax
235    /// pro-rata from the transition account onto the real tax account. Hosts
236    /// without a tax module leave it unset — no flips, everything else works.
237    pub fn with_deferred_tax(
238        mut self,
239        port: Arc<dyn crate::domain::repositories::DeferredTaxLookup>,
240    ) -> Self {
241        self.deferred_tax = Some(port);
242        self
243    }
244    /// Wire the host's budget control: postings through the GL-posting
245    /// chokepoint are checked against confirmed budget positions (account x
246    /// cost center x fiscal period) after the pure double-entry rules pass.
247    /// Warn-enforcement breaches log and proceed; block-enforcement breaches
248    /// refuse the posting. Hosts without a budget module leave it unset — no
249    /// check, everything else works (fail-open).
250    pub fn with_budget_control(
251        mut self,
252        port: Arc<dyn crate::domain::repositories::BudgetControlPort>,
253    ) -> Self {
254        self.budget_control = Some(port);
255        self
256    }
257}