Skip to main content

backbone_maintenance/infrastructure/persistence/
maintenance_visit_part_repository.rs

1//! Repository for MaintenanceVisitPart 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 MaintenanceVisitPart SQL (4-layer rule: services orchestrate, repos
6//! hold SQL).
7//!
8//! Thin newtype over `backbone_orm::GenericCrudRepository<MaintenanceVisitPart, backbone_orm::SoftDelete>`.
9//! All standard CRUD methods are available via `Deref`.
10
11use anyhow::Result;
12use rust_decimal::Decimal;
13use sqlx::{PgPool, Row};
14use uuid::Uuid;
15
16use backbone_orm::{company_scope, org_scope};
17
18use crate::domain::entity::MaintenanceVisitPart;
19
20/// Table name for MaintenanceVisitPart entities
21pub const TABLE_NAME: &str = "maintenance.maintenance_visit_parts";
22
23/// Repository for MaintenanceVisitPart entities.
24///
25/// All standard CRUD, soft-delete, pagination, and bulk methods are
26/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
27pub struct MaintenanceVisitPartRepository(
28    backbone_orm::GenericCrudRepository<MaintenanceVisitPart, backbone_orm::SoftDelete>,
29);
30
31impl std::ops::Deref for MaintenanceVisitPartRepository {
32    type Target = backbone_orm::GenericCrudRepository<MaintenanceVisitPart, backbone_orm::SoftDelete>;
33    fn deref(&self) -> &Self::Target { &self.0 }
34}
35
36impl MaintenanceVisitPartRepository {
37    /// Create a new repository instance.
38    pub fn new(pool: PgPool) -> Self {
39        Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
40    }
41}
42
43/// A part line as the completion path reads it — quantity only; it is valued on completion.
44pub struct VisitPartRow {
45    pub id: Uuid,
46    pub item_id: Uuid,
47    pub quantity: Decimal,
48}
49
50/// Hand-written MaintenanceVisitPart SQL. Lives here (not in the write service) per the module's
51/// 4-layer rule: services orchestrate and own the unit of work, repositories hold the SQL.
52impl MaintenanceVisitPartRepository {
53    /// Add a part line to a visit (quantity only; `unit_cost`/`amount` are valued on completion).
54    ///
55    /// Runs on the pool via `org_scope::execute_scoped`, which binds the ambient org scope (the
56    /// composing service sets it per request) so the INSERT passes the decorator's WITH CHECK
57    /// fence; the decorator's fill trigger stamps the acting unit (ADR-0029). The visit-status
58    /// check the caller makes first already fences the line to a visit it may see. Undecorated
59    /// (module tests) the insert runs plain.
60    pub async fn insert_part(
61        &self,
62        pool: &PgPool,
63        id: Uuid,
64        visit_id: Uuid,
65        item_id: Uuid,
66        quantity: Decimal,
67    ) -> Result<(), sqlx::Error> {
68        org_scope::execute_scoped(
69            pool,
70            sqlx::query(
71                r#"INSERT INTO maintenance.maintenance_visit_parts
72                       (id, visit_id, item_id, quantity, unit_cost, amount)
73                   VALUES ($1,$2,$3,$4,0,0)"#,
74            )
75            .bind(id).bind(visit_id).bind(item_id).bind(quantity),
76        )
77        .await?;
78        Ok(())
79    }
80
81    /// The visit's frozen part set (caller-scoped — rides the org-bound request connection).
82    pub async fn list_for_visit(
83        &self,
84        pool: &PgPool,
85        visit_id: Uuid,
86    ) -> Result<Vec<VisitPartRow>, sqlx::Error> {
87        let rows = company_scope::fetch_all_rows_scoped(
88            pool,
89            sqlx::query(
90                "SELECT id, item_id, quantity FROM maintenance.maintenance_visit_parts WHERE visit_id=$1",
91            )
92            .bind(visit_id),
93        )
94        .await?;
95        Ok(rows
96            .iter()
97            .map(|r| VisitPartRow {
98                id: r.get("id"), item_id: r.get("item_id"), quantity: r.get("quantity"),
99            })
100            .collect())
101    }
102
103    /// Write back the valuation inventory returned for one line (moving-average rate and extended
104    /// value). Runs via `org_scope::execute_scoped` under the ambient org scope; both values are
105    /// expected already money-rounded.
106    pub async fn set_valuation(
107        &self,
108        pool: &PgPool,
109        visit_id: Uuid,
110        item_id: Uuid,
111        unit_cost: Decimal,
112        amount: Decimal,
113    ) -> Result<(), sqlx::Error> {
114        org_scope::execute_scoped(
115            pool,
116            sqlx::query(
117                r#"UPDATE maintenance.maintenance_visit_parts SET unit_cost=$3, amount=$4
118                   WHERE visit_id=$1 AND item_id=$2"#,
119            )
120            .bind(visit_id).bind(item_id).bind(unit_cost).bind(amount),
121        )
122        .await?;
123        Ok(())
124    }
125}
126
127backbone_core::impl_crud_repository!(MaintenanceVisitPartRepository, MaintenanceVisitPart, soft_delete);