backbone-payroll 0.3.47

Payroll: salary structures, payroll runs and computed salary slips over effective-dated statutory tables, plus compensation changes
Documentation
//! Repository for SalaryComponent entities
//!
//! Originally generated by metaphor-schema; now **user-owned** — this exact path is declared under
//! `user_owned` in `metaphor.codegen.yaml`, so the generator skips it wholesale. The custom methods
//! below hold the hand-written SalaryComponent SQL (4-layer rule: services orchestrate, repos hold SQL).
//!
//! Thin newtype over `backbone_orm::GenericCrudRepository<SalaryComponent, backbone_orm::SoftDelete>`.
//! All standard CRUD methods are available via `Deref`.

use anyhow::Result;
use rust_decimal::Decimal;
use sqlx::{PgPool, Row};
use uuid::Uuid;

// The multi-row read twin lives only in the legacy `company_scope` module. Its connection discipline
// is what this adapter needs — request-dedicated connection when the composing service bound one,
// plain pool otherwise. The helper's legacy task-local branch is never taken: this module sets no
// legacy scope of its own (ADR-0029).
use backbone_orm::company_scope::fetch_all_rows_scoped;

use crate::domain::entity::SalaryComponent;

/// Table name for SalaryComponent entities
pub const TABLE_NAME: &str = "payroll.salary_components";

/// Repository for SalaryComponent entities.
///
/// All standard CRUD, soft-delete, pagination, and bulk methods are
/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
pub struct SalaryComponentRepository(
    backbone_orm::GenericCrudRepository<SalaryComponent, backbone_orm::SoftDelete>,
);

impl std::ops::Deref for SalaryComponentRepository {
    type Target = backbone_orm::GenericCrudRepository<SalaryComponent, backbone_orm::SoftDelete>;
    fn deref(&self) -> &Self::Target { &self.0 }
}

impl SalaryComponentRepository {
    /// Create a new repository instance.
    pub fn new(pool: PgPool) -> Self {
        Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
    }
}

/// The exact row one structure component writes.
///
/// Mirrors the raw column shape rather than the `SalaryComponent` entity: `component_type` is cast at
/// the DB (`$4::component_type`), which is what lets a bad variant fail as a DB error instead of a
/// deserialize panic. `amount` is the service's already money-rounded value.
pub struct NewComponentRow<'a> {
    pub id: Uuid,
    pub structure_id: Uuid,
    pub name: &'a str,
    pub component_type: &'a str,
    pub amount: Decimal,
    pub gl_account_id: Uuid,
}

/// One structure component, as the slip builder reads it: `component_type` comes back as text so the
/// service can branch earning-vs-deduction without a domain enum in the way.
pub struct ComponentRow {
    pub name: String,
    pub component_type: String,
    pub amount: Decimal,
    pub gl_account_id: Uuid,
}

/// Hand-written SalaryComponent SQL. Lives here (not in the write service) per the module's 4-layer
/// rule: services orchestrate and own the unit of work, repositories hold the SQL.
impl SalaryComponentRepository {
    /// Insert one earning/deduction component of a structure.
    ///
    /// Takes the CALLER'S connection so the structure and all its components commit as ONE unit. The
    /// caller has already relayed the ambient org request scope onto it — don't re-bind here. The
    /// component's own org scoping rides its parent structure row (ADR-0029); the module carries no
    /// denormalized tenancy column.
    pub async fn insert_component(
        &self,
        conn: &mut sqlx::PgConnection,
        c: &NewComponentRow<'_>,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"INSERT INTO payroll.salary_components
                 (id, structure_id, name, component_type, amount, gl_account_id)
               VALUES ($1,$2,$3,$4::component_type,$5,$6)"#,
        )
        .bind(c.id).bind(c.structure_id).bind(c.name).bind(c.component_type)
        .bind(c.amount).bind(c.gl_account_id)
        .execute(conn)
        .await?;
        Ok(())
    }

    /// List a structure's components — the earnings and fixed deductions a slip is assembled from.
    ///
    /// A read outside any transaction: takes the pool and runs `fetch_all_rows_scoped` so the
    /// composing service's tenancy RLS fence applies (ADR-0029). The caller relays the ambient org
    /// request scope onto its own transaction first, or runs under HTTP where the
    /// request-dedicated connection already carries it.
    pub async fn list_by_structure(
        &self,
        pool: &PgPool,
        structure_id: Uuid,
    ) -> Result<Vec<ComponentRow>, sqlx::Error> {
        let rows = fetch_all_rows_scoped(
            pool,
            sqlx::query(
                "SELECT name, component_type::text AS ct, amount, gl_account_id FROM payroll.salary_components WHERE structure_id=$1")
                .bind(structure_id),
        )
        .await?;
        Ok(rows.iter().map(|r| ComponentRow {
            name: r.get("name"), component_type: r.get("ct"), amount: r.get("amount"),
            gl_account_id: r.get("gl_account_id"),
        }).collect())
    }
}

backbone_core::impl_crud_repository!(SalaryComponentRepository, SalaryComponent, soft_delete);