1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//! Guarded route composition — the RECOMMENDED way to mount the accounting module.
//!
//! Hand-authored (user-owned; see `metaphor.codegen.yaml`). Closes the CRUD-bypass the council
//! flagged: the generated `routes()` exposes full mutable CRUD (POST/PATCH/upsert/bulk) on every
//! entity, including the posted GL records — which lets a caller write an unbalanced posting or
//! PATCH a posted ledger row, bypassing double-entry validation entirely.
//!
//! Here the **posted GL entities (Journal, JournalLine, Ledger, AccountingPost) are READ-ONLY**
//! over HTTP; the only sanctioned writer is `PostingService` (`POST /accounting/posts`). Master /
//! configuration entities keep full CRUD.
use axum::Router;
use crate::AccountingModule;
use super::{
create_account_routes, create_accounting_post_read_routes, create_cost_center_routes,
create_financial_statement_routes, create_fiscal_period_routes,
create_journal_line_read_routes, create_journal_read_routes, create_ledger_read_routes,
create_reconciliation_item_routes, create_reconciliation_routes,
};
/// Mount the accounting module with the posted-GL write paths locked to `PostingService`.
/// Prefer this over `AccountingModule::routes()` for any real deployment.
pub fn create_guarded_accounting_routes(m: &AccountingModule) -> Router {
Router::new()
// Master / configuration data — full CRUD is appropriate here.
.merge(create_account_routes(m.account_service.clone()))
.merge(create_cost_center_routes(m.cost_center_service.clone()))
.merge(create_fiscal_period_routes(m.fiscal_period_service.clone()))
.merge(create_financial_statement_routes(
m.financial_statement_service.clone(),
))
.merge(create_reconciliation_routes(
m.reconciliation_service.clone(),
))
.merge(create_reconciliation_item_routes(
m.reconciliation_item_service.clone(),
))
// Posted GL — READ ONLY. Writes flow only through the GL-posting contract.
.merge(create_journal_read_routes(m.journal_service.clone()))
.merge(create_journal_line_read_routes(
m.journal_line_service.clone(),
))
.merge(create_ledger_read_routes(m.ledger_service.clone()))
.merge(create_accounting_post_read_routes(
m.accounting_post_service.clone(),
))
// Financial reports — read-only, computed on the fly (no stored statements).
.merge(
crate::presentation::http::reporting_handler::create_reporting_routes(
m.reporting_service(),
),
)
// Reconciliation verbs — the ONLY write surface onto the reconciliation graph.
// The graph tables themselves mount nothing here (and are `enabled: false`).
.merge(
crate::presentation::http::reconcile_handler::create_reconcile_verb_routes(
m.reconcile_write_service(),
),
)
// Registered chart datasets — read-only listing. The install verb is
// host-mounted (it orchestrates chart + tax across modules and is gated by
// the composing service's authority middleware).
.merge(
crate::presentation::http::chart_routes::create_chart_routes(m.chart_install_service()),
)
// Check printing verbs — the ONLY write surface onto the check sequence
// and printed-check registry tables (no CRUD mount for those models).
// The registry verbs belong behind the host's accounting-officer gate.
.merge(
crate::presentation::http::check_printing_handler::create_check_printing_routes(
m.check_printing_service(),
),
)
// EMV(QRCPS)/QRIS display — merchant config verb + on-demand payload
// render. Payloads are rebuilt per request, never stored.
.merge(
crate::presentation::http::emv_qr_handler::create_emv_qr_routes(m.emv_qr_service()),
)
// Tax-tag legal-change repair — the officer verb + audit read. Never
// scheduler-invoked: reason mandatory, locked periods refused, closed
// periods overridden explicitly, every run audit-stamped.
.merge(
crate::presentation::http::tax_tag_repair_handler::create_tax_tag_repair_routes(
m.tax_tag_repair_service(),
),
)
}