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 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);