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