backbone_maintenance/infrastructure/persistence/
maintenance_visit_repository.rs1use anyhow::Result;
12use chrono::NaiveDate;
13use rust_decimal::Decimal;
14use sqlx::{PgPool, Row};
15use uuid::Uuid;
16
17use backbone_orm::{company_scope, org_scope};
18
19use crate::domain::entity::MaintenanceVisit;
20
21pub const TABLE_NAME: &str = "maintenance.maintenance_visits";
23
24pub struct MaintenanceVisitRepository(
29 backbone_orm::GenericCrudRepository<MaintenanceVisit, backbone_orm::SoftDelete>,
30);
31
32impl std::ops::Deref for MaintenanceVisitRepository {
33 type Target = backbone_orm::GenericCrudRepository<MaintenanceVisit, backbone_orm::SoftDelete>;
34 fn deref(&self) -> &Self::Target { &self.0 }
35}
36
37impl MaintenanceVisitRepository {
38 pub fn new(pool: PgPool) -> Self {
40 Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
41 }
42}
43
44pub struct NewVisitRow<'a> {
50 pub id: Uuid,
51 pub asset_id: Uuid,
52 pub schedule_id: Option<Uuid>,
53 pub maintenance_type: &'a str,
54 pub warehouse_id: Option<Uuid>,
55 pub warranty_claim_id: Option<Uuid>,
56 pub scheduled_date: NaiveDate,
57 pub labor_cost: Decimal,
58 pub maintenance_expense_account_id: Uuid,
59 pub parts_inventory_account_id: Uuid,
60 pub labor_payable_account_id: Uuid,
61}
62
63pub struct VisitCompletionRow {
66 pub asset_id: Uuid,
67 pub status: String,
68 pub warehouse_id: Option<Uuid>,
69 pub labor_cost: Decimal,
70 pub maintenance_expense_account_id: Option<Uuid>,
71 pub parts_inventory_account_id: Option<Uuid>,
72 pub labor_payable_account_id: Option<Uuid>,
73 pub journal_id: Option<Uuid>,
74 pub total_cost: Decimal,
75 pub schedule_id: Option<Uuid>,
76 pub maintenance_type: String,
77}
78
79pub struct VisitCompletion {
81 pub performed_date: NaiveDate,
82 pub parts_cost: Decimal,
83 pub total_cost: Decimal,
84 pub journal_id: Option<Uuid>,
85 pub accounting_post_id: Option<Uuid>,
86}
87
88impl MaintenanceVisitRepository {
91 pub async fn insert_visit(&self, pool: &PgPool, v: &NewVisitRow<'_>) -> Result<(), sqlx::Error> {
98 org_scope::execute_scoped(
99 pool,
100 sqlx::query(
101 r#"INSERT INTO maintenance.maintenance_visits
102 (id, asset_id, schedule_id, maintenance_type, status, warehouse_id,
103 warranty_claim_id, scheduled_date, labor_cost, parts_cost, total_cost,
104 maintenance_expense_account_id, parts_inventory_account_id, labor_payable_account_id)
105 VALUES ($1,$2,$3,$4::maintenance_type,'planned'::visit_status,$5,$6,$7,$8,0,0,$9,$10,$11)"#,
106 )
107 .bind(v.id).bind(v.asset_id).bind(v.schedule_id).bind(v.maintenance_type)
108 .bind(v.warehouse_id).bind(v.warranty_claim_id).bind(v.scheduled_date).bind(v.labor_cost)
109 .bind(v.maintenance_expense_account_id).bind(v.parts_inventory_account_id)
110 .bind(v.labor_payable_account_id),
111 )
112 .await?;
113 Ok(())
114 }
115
116 pub async fn fetch_status(
121 &self,
122 pool: &PgPool,
123 visit_id: Uuid,
124 ) -> Result<Option<String>, sqlx::Error> {
125 company_scope::fetch_optional_scalar_scoped(
126 pool,
127 sqlx::query_scalar(
128 "SELECT status::text FROM maintenance.maintenance_visits WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
129 )
130 .bind(visit_id),
131 )
132 .await
133 }
134
135 pub async fn fetch_for_completion(
138 &self,
139 pool: &PgPool,
140 visit_id: Uuid,
141 ) -> Result<Option<VisitCompletionRow>, sqlx::Error> {
142 let row = company_scope::fetch_optional_row_scoped(
143 pool,
144 sqlx::query(
145 r#"SELECT asset_id, status::text AS status, warehouse_id, labor_cost,
146 maintenance_expense_account_id, parts_inventory_account_id, labor_payable_account_id,
147 journal_id, total_cost, schedule_id, maintenance_type::text AS maintenance_type
148 FROM maintenance.maintenance_visits WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
149 )
150 .bind(visit_id),
151 )
152 .await?;
153 Ok(row.map(|r| VisitCompletionRow {
154 asset_id: r.get("asset_id"),
155 status: r.get("status"),
156 warehouse_id: r.get("warehouse_id"),
157 labor_cost: r.get("labor_cost"),
158 maintenance_expense_account_id: r.get("maintenance_expense_account_id"),
159 parts_inventory_account_id: r.get("parts_inventory_account_id"),
160 labor_payable_account_id: r.get("labor_payable_account_id"),
161 journal_id: r.get("journal_id"),
162 total_cost: r.get("total_cost"),
163 schedule_id: r.get("schedule_id"),
164 maintenance_type: r.get("maintenance_type"),
165 }))
166 }
167
168 pub async fn claim_in_progress(
175 &self,
176 pool: &PgPool,
177 visit_id: Uuid,
178 ) -> Result<u64, sqlx::Error> {
179 let r = org_scope::execute_scoped(
180 pool,
181 sqlx::query(
182 r#"UPDATE maintenance.maintenance_visits SET status='in_progress'::visit_status
183 WHERE id=$1 AND status='planned'::visit_status"#,
184 )
185 .bind(visit_id),
186 )
187 .await?;
188 Ok(r.rows_affected())
189 }
190
191 pub async fn complete(
198 &self,
199 conn: &mut sqlx::PgConnection,
200 visit_id: Uuid,
201 c: &VisitCompletion,
202 ) -> Result<u64, sqlx::Error> {
203 let r = sqlx::query(
204 r#"UPDATE maintenance.maintenance_visits
205 SET status='completed'::visit_status, performed_date=$2, parts_cost=$3, total_cost=$4,
206 journal_id=$5, accounting_post_id=$6
207 WHERE id=$1 AND status IN ('planned'::visit_status,'in_progress'::visit_status)"#,
208 )
209 .bind(visit_id).bind(c.performed_date).bind(c.parts_cost).bind(c.total_cost)
210 .bind(c.journal_id).bind(c.accounting_post_id)
211 .execute(conn)
212 .await?;
213 Ok(r.rows_affected())
214 }
215
216 pub async fn fetch_journal_id(
218 &self,
219 pool: &PgPool,
220 visit_id: Uuid,
221 ) -> Result<Option<Uuid>, sqlx::Error> {
222 company_scope::fetch_one_scalar_scoped(
223 pool,
224 sqlx::query_scalar("SELECT journal_id FROM maintenance.maintenance_visits WHERE id=$1")
225 .bind(visit_id),
226 )
227 .await
228 }
229
230 pub async fn cancel(&self, pool: &PgPool, visit_id: Uuid) -> Result<u64, sqlx::Error> {
233 let r = org_scope::execute_scoped(
234 pool,
235 sqlx::query(
236 r#"UPDATE maintenance.maintenance_visits SET status='cancelled'::visit_status
237 WHERE id=$1 AND status IN ('planned'::visit_status,'in_progress'::visit_status)"#,
238 )
239 .bind(visit_id),
240 )
241 .await?;
242 Ok(r.rows_affected())
243 }
244}
245
246backbone_core::impl_crud_repository!(MaintenanceVisitRepository, MaintenanceVisit, soft_delete);