Skip to main content

backbone_maintenance/infrastructure/persistence/
maintenance_schedule_repository.rs

1//! Repository for MaintenanceSchedule 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 MaintenanceSchedule SQL (4-layer rule: services orchestrate, repos
6//! hold SQL).
7//!
8//! Thin newtype over `backbone_orm::GenericCrudRepository<MaintenanceSchedule, backbone_orm::SoftDelete>`.
9//! All standard CRUD methods are available via `Deref`.
10
11use anyhow::Result;
12use chrono::NaiveDate;
13use sqlx::PgPool;
14use uuid::Uuid;
15
16use backbone_orm::org_scope;
17
18use crate::domain::entity::MaintenanceSchedule;
19
20/// Table name for MaintenanceSchedule entities
21pub const TABLE_NAME: &str = "maintenance.maintenance_schedules";
22
23/// Repository for MaintenanceSchedule entities.
24///
25/// All standard CRUD, soft-delete, pagination, and bulk methods are
26/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
27pub struct MaintenanceScheduleRepository(
28    backbone_orm::GenericCrudRepository<MaintenanceSchedule, backbone_orm::SoftDelete>,
29);
30
31impl std::ops::Deref for MaintenanceScheduleRepository {
32    type Target = backbone_orm::GenericCrudRepository<MaintenanceSchedule, backbone_orm::SoftDelete>;
33    fn deref(&self) -> &Self::Target { &self.0 }
34}
35
36impl MaintenanceScheduleRepository {
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/// The exact row a preventive-schedule definition writes.
44///
45/// Mirrors the raw column shape rather than the `MaintenanceSchedule` entity.
46pub struct NewScheduleRow<'a> {
47    pub id: Uuid,
48    pub asset_id: Uuid,
49    pub name: &'a str,
50    pub interval_days: i32,
51    pub next_due_date: NaiveDate,
52}
53
54/// Hand-written MaintenanceSchedule SQL. Lives here (not in the write service) per the module's
55/// 4-layer rule: services orchestrate and own the unit of work, repositories hold the SQL.
56impl MaintenanceScheduleRepository {
57    /// Define a preventive maintenance schedule for an asset.
58    ///
59    /// Runs outside a transaction on the pool via `org_scope::execute_scoped`, which binds the
60    /// ambient org scope (the composing service sets it per request) so the INSERT passes the
61    /// decorator's WITH CHECK fence; the decorator's fill trigger stamps the acting unit
62    /// (ADR-0029). Undecorated (module tests) the insert runs plain.
63    pub async fn insert_schedule(
64        &self,
65        pool: &PgPool,
66        s: &NewScheduleRow<'_>,
67    ) -> Result<(), sqlx::Error> {
68        org_scope::execute_scoped(
69            pool,
70            sqlx::query(
71                r#"INSERT INTO maintenance.maintenance_schedules
72                     (id, asset_id, name, interval_days, next_due_date, status)
73                   VALUES ($1,$2,$3,$4,$5,'active')"#,
74            )
75            .bind(s.id).bind(s.asset_id).bind(s.name)
76            .bind(s.interval_days).bind(s.next_due_date),
77        )
78        .await?;
79        Ok(())
80    }
81
82    /// Advance a preventive schedule so the next visit becomes due at `performed_date + interval_days`
83    /// — the recurrence that makes a preventive plan preventive.
84    ///
85    /// Takes the CALLER'S connection so it commits as one unit with the visit completion that gates it
86    /// (at most once, via that completion's CAS). The caller has already relayed the ambient org
87    /// scope onto it — don't re-bind here.
88    pub async fn advance_next_due(
89        &self,
90        conn: &mut sqlx::PgConnection,
91        schedule_id: Uuid,
92        performed_date: NaiveDate,
93    ) -> Result<(), sqlx::Error> {
94        sqlx::query(
95            r#"UPDATE maintenance.maintenance_schedules
96               SET next_due_date = ($2::date + make_interval(days => interval_days))::date
97               WHERE id=$1 AND status='active' AND (metadata->>'deleted_at') IS NULL"#,
98        )
99        .bind(schedule_id).bind(performed_date)
100        .execute(conn)
101        .await?;
102        Ok(())
103    }
104}
105
106backbone_core::impl_crud_repository!(MaintenanceScheduleRepository, MaintenanceSchedule, soft_delete);