backbone_banking/infrastructure/persistence/bank_reconciliation_repository.rs
1//! Repository for BankReconciliation entities
2//!
3//! Originally generated by metaphor-schema; now **user-owned** — this exact path is declared under
4//! `user_owned` in `metaphor.codegen.yaml`, so the generator skips it wholesale. The custom methods
5//! below hold the hand-written BankReconciliation SQL (4-layer rule: services orchestrate, repos hold
6//! SQL).
7//!
8//! Thin newtype over `backbone_orm::GenericCrudRepository<BankReconciliation, backbone_orm::SoftDelete>`.
9//! All standard CRUD methods are available via `Deref`.
10
11use anyhow::Result;
12use rust_decimal::Decimal;
13use sqlx::PgPool;
14use uuid::Uuid;
15
16use backbone_orm::org_scope;
17
18use crate::domain::entity::BankReconciliation;
19
20/// Table name for BankReconciliation entities
21pub const TABLE_NAME: &str = "banking.bank_reconciliations";
22
23/// Repository for BankReconciliation entities.
24///
25/// All standard CRUD, soft-delete, pagination, and bulk methods are
26/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
27pub struct BankReconciliationRepository(
28 backbone_orm::GenericCrudRepository<BankReconciliation, backbone_orm::SoftDelete>,
29);
30
31impl std::ops::Deref for BankReconciliationRepository {
32 type Target = backbone_orm::GenericCrudRepository<BankReconciliation, backbone_orm::SoftDelete>;
33 fn deref(&self) -> &Self::Target {
34 &self.0
35 }
36}
37
38impl BankReconciliationRepository {
39 /// Create a new repository instance.
40 pub fn new(pool: PgPool) -> Self {
41 Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
42 }
43}
44
45/// The exact row a reconciliation session writes.
46///
47/// Mirrors the raw column shape rather than the `BankReconciliation` entity: `status` binds as `&str`
48/// and is cast at the DB (`$10::recon_status`), so a bad value fails as a DB error rather than a
49/// deserialize panic. `unreconciled_count` is the EXCEPTION SNAPSHOT at sign-off — persisted so the
50/// close decision stays audit-reconstructable.
51pub struct NewReconciliationRow<'a> {
52 pub id: Uuid,
53 pub bank_account_id: Uuid,
54 pub from_date: chrono::NaiveDate,
55 pub to_date: chrono::NaiveDate,
56 pub statement_closing_balance: Decimal,
57 pub ledger_balance: Decimal,
58 pub computed_difference: Decimal,
59 pub unreconciled_count: i32,
60 /// `open` | `balanced` | `closed`.
61 pub status: &'a str,
62}
63
64/// Hand-written BankReconciliation SQL. Lives here (not in the write service) per the module's
65/// 4-layer rule. The close DECISION (open / balanced / closed) stays in the service.
66impl BankReconciliationRepository {
67 /// Record a reconciliation session in its decided state.
68 ///
69 /// A write outside any transaction: takes the pool and runs `org_scope::execute_scoped` so the
70 /// statement rides the request-dedicated connection carrying the composing service's org scope
71 /// — the same ambient-scope discipline as the exception count that decided `status`
72 /// (ADR-0029). Undecorated deployments execute plainly on the pool.
73 pub async fn insert_reconciliation(
74 &self,
75 pool: &PgPool,
76 r: &NewReconciliationRow<'_>,
77 ) -> Result<(), sqlx::Error> {
78 org_scope::execute_scoped(
79 pool,
80 sqlx::query(
81 r#"INSERT INTO banking.bank_reconciliations
82 (id, bank_account_id, from_date, to_date, statement_closing_balance,
83 ledger_balance, computed_difference, unreconciled_count, status)
84 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::recon_status)"#,
85 )
86 .bind(r.id)
87 .bind(r.bank_account_id)
88 .bind(r.from_date)
89 .bind(r.to_date)
90 .bind(r.statement_closing_balance)
91 .bind(r.ledger_balance)
92 .bind(r.computed_difference)
93 .bind(r.unreconciled_count)
94 .bind(r.status),
95 )
96 .await?;
97 Ok(())
98 }
99}
100
101backbone_core::impl_crud_repository!(
102 BankReconciliationRepository,
103 BankReconciliation,
104 soft_delete
105);