backbone_payroll/infrastructure/persistence/salary_component_repository.rs
1//! Repository for SalaryComponent 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 SalaryComponent SQL (4-layer rule: services orchestrate, repos hold SQL).
6//!
7//! Thin newtype over `backbone_orm::GenericCrudRepository<SalaryComponent, backbone_orm::SoftDelete>`.
8//! All standard CRUD methods are available via `Deref`.
9
10use anyhow::Result;
11use rust_decimal::Decimal;
12use sqlx::{PgPool, Row};
13use uuid::Uuid;
14
15// The multi-row read twin rides the `org_scope` module: request-dedicated connection when
16// the composer bound one, plain pool otherwise, no scope invented. This module sets no
17// scope of its own (ADR-0029), so nothing reaches for a legacy company bind.
18use backbone_orm::org_scope::fetch_all_rows_scoped;
19
20use crate::domain::entity::SalaryComponent;
21
22/// Table name for SalaryComponent entities
23pub const TABLE_NAME: &str = "payroll.salary_components";
24
25/// Repository for SalaryComponent entities.
26///
27/// All standard CRUD, soft-delete, pagination, and bulk methods are
28/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
29pub struct SalaryComponentRepository(
30 backbone_orm::GenericCrudRepository<SalaryComponent, backbone_orm::SoftDelete>,
31);
32
33impl std::ops::Deref for SalaryComponentRepository {
34 type Target = backbone_orm::GenericCrudRepository<SalaryComponent, backbone_orm::SoftDelete>;
35 fn deref(&self) -> &Self::Target { &self.0 }
36}
37
38impl SalaryComponentRepository {
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 one structure component writes.
46///
47/// Mirrors the raw column shape rather than the `SalaryComponent` entity: `component_type` is cast at
48/// the DB (`$4::component_type`), which is what lets a bad variant fail as a DB error instead of a
49/// deserialize panic. `amount` is the service's already money-rounded value.
50pub struct NewComponentRow<'a> {
51 pub id: Uuid,
52 pub structure_id: Uuid,
53 pub name: &'a str,
54 pub component_type: &'a str,
55 pub amount: Decimal,
56 pub gl_account_id: Uuid,
57}
58
59/// One structure component, as the slip builder reads it: `component_type` comes back as text so the
60/// service can branch earning-vs-deduction without a domain enum in the way.
61pub struct ComponentRow {
62 pub name: String,
63 pub component_type: String,
64 pub amount: Decimal,
65 pub gl_account_id: Uuid,
66}
67
68/// Hand-written SalaryComponent SQL. Lives here (not in the write service) per the module's 4-layer
69/// rule: services orchestrate and own the unit of work, repositories hold the SQL.
70impl SalaryComponentRepository {
71 /// Insert one earning/deduction component of a structure.
72 ///
73 /// Takes the CALLER'S connection so the structure and all its components commit as ONE unit. The
74 /// caller has already relayed the ambient org request scope onto it — don't re-bind here. The
75 /// component's own org scoping rides its parent structure row (ADR-0029); the module carries no
76 /// denormalized tenancy column.
77 pub async fn insert_component(
78 &self,
79 conn: &mut sqlx::PgConnection,
80 c: &NewComponentRow<'_>,
81 ) -> Result<(), sqlx::Error> {
82 sqlx::query(
83 r#"INSERT INTO payroll.salary_components
84 (id, structure_id, name, component_type, amount, gl_account_id)
85 VALUES ($1,$2,$3,$4::component_type,$5,$6)"#,
86 )
87 .bind(c.id).bind(c.structure_id).bind(c.name).bind(c.component_type)
88 .bind(c.amount).bind(c.gl_account_id)
89 .execute(conn)
90 .await?;
91 Ok(())
92 }
93
94 /// List a structure's components — the earnings and fixed deductions a slip is assembled from.
95 ///
96 /// A read outside any transaction: takes the pool and runs `fetch_all_rows_scoped` so the
97 /// composing service's tenancy RLS fence applies (ADR-0029). The caller relays the ambient org
98 /// request scope onto its own transaction first, or runs under HTTP where the
99 /// request-dedicated connection already carries it.
100 pub async fn list_by_structure(
101 &self,
102 pool: &PgPool,
103 structure_id: Uuid,
104 ) -> Result<Vec<ComponentRow>, sqlx::Error> {
105 let rows = fetch_all_rows_scoped(
106 pool,
107 sqlx::query(
108 "SELECT name, component_type::text AS ct, amount, gl_account_id FROM payroll.salary_components WHERE structure_id=$1")
109 .bind(structure_id),
110 )
111 .await?;
112 Ok(rows.iter().map(|r| ComponentRow {
113 name: r.get("name"), component_type: r.get("ct"), amount: r.get("amount"),
114 gl_account_id: r.get("gl_account_id"),
115 }).collect())
116 }
117}
118
119backbone_core::impl_crud_repository!(SalaryComponentRepository, SalaryComponent, soft_delete);