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}