Skip to main content

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