Skip to main content

backbone_inventory/infrastructure/persistence/
warehouse_repository.rs

1//! Repository for Warehouse 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 Warehouse registration SQL
6//! (4-layer rule: services orchestrate, repositories hold the SQL).
7//!
8//! Thin newtype over `backbone_orm::GenericCrudRepository<Warehouse, backbone_orm::SoftDelete>`.
9//! All standard CRUD methods are available via `Deref`.
10
11use anyhow::Result;
12use sqlx::{PgPool, Row};
13use uuid::Uuid;
14
15use backbone_orm::org_scope;
16
17use crate::domain::entity::Warehouse;
18
19/// Table name for Warehouse entities
20pub const TABLE_NAME: &str = "inventory.warehouses";
21
22/// Repository for Warehouse entities.
23///
24/// All standard CRUD, soft-delete, pagination, and bulk methods are
25/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
26pub struct WarehouseRepository(
27    backbone_orm::GenericCrudRepository<Warehouse, backbone_orm::SoftDelete>,
28);
29
30impl std::ops::Deref for WarehouseRepository {
31    type Target = backbone_orm::GenericCrudRepository<Warehouse, backbone_orm::SoftDelete>;
32    fn deref(&self) -> &Self::Target { &self.0 }
33}
34
35impl WarehouseRepository {
36    /// Create a new repository instance.
37    pub fn new(pool: PgPool) -> Self {
38        Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
39    }
40}
41
42/// The exact row a warehouse registration writes.
43///
44/// Mirrors the raw column shape rather than the `Warehouse` entity: `warehouse_type` is cast at the
45/// DB (`$5::warehouse_type`) so a bad value fails as a DB error, not a deserialize panic.
46pub struct NewWarehouseRow<'a> {
47    pub id: Uuid,
48    pub code: &'a str,
49    pub name: &'a str,
50    pub warehouse_type: &'a str,
51    pub parent_warehouse_id: Option<Uuid>,
52    pub is_group: bool,
53}
54
55/// Hand-written Warehouse SQL. Lives here per the module's 4-layer rule.
56impl WarehouseRepository {
57    /// Register a warehouse.
58    ///
59    /// A write outside any transaction: takes the pool and runs `org_scope::execute_scoped` so
60    /// the write rides the ambient org scope's request-dedicated connection — the composing
61    /// service sets it per request, and it satisfies the decorator's WITH CHECK fence
62    /// (ADR-0029). Undecorated (module tests) the insert runs plain.
63    ///
64    /// Returns the raw `sqlx::Error` deliberately: the caller inspects it for a unique violation
65    /// (the composing decorator's org-scoped code arbiter) to turn a duplicate warehouse code
66    /// into a domain error.
67    pub async fn insert_warehouse(
68        &self,
69        pool: &PgPool,
70        w: &NewWarehouseRow<'_>,
71    ) -> Result<(), sqlx::Error> {
72        org_scope::execute_scoped(
73            pool,
74            sqlx::query(
75                r#"INSERT INTO inventory.warehouses
76                    (id, code, name, warehouse_type, parent_warehouse_id, is_group)
77                   VALUES ($1,$2,$3,$4::warehouse_type,$5,$6)"#,
78            )
79            .bind(w.id).bind(w.code).bind(w.name).bind(w.warehouse_type)
80            .bind(w.parent_warehouse_id).bind(w.is_group),
81        )
82        .await?;
83        Ok(())
84    }
85
86    /// Resolve a warehouse-pivot candidate: does this warehouse exist and is it a concrete stock
87    /// warehouse (not a grouping node)? Backs the availability scope read's
88    /// fail-loud pivot validation — a typo'd warehouse id must refuse typed, never
89    /// project a silently-empty (all-sold-out) storefront. `Ok(None)` = no such live warehouse;
90    /// `Some(is_group)` = found, with the grouping flag for the caller to refuse.
91    ///
92    /// Pool-based and org-scoped: the read rides the ambient org scope the composing service set
93    /// per request (ADR-0029).
94    pub async fn fetch_pivot_warehouse(
95        &self,
96        pool: &PgPool,
97        warehouse_id: Uuid,
98    ) -> Result<Option<bool>, sqlx::Error> {
99        let row = org_scope::fetch_optional_row_scoped(
100            pool,
101            sqlx::query(
102                r#"SELECT is_group FROM inventory.warehouses
103                   WHERE id = $1
104                     AND (metadata->>'deleted_at') IS NULL"#,
105            )
106            .bind(warehouse_id),
107        )
108        .await?;
109        Ok(row.map(|r| r.get("is_group")))
110    }
111}
112
113backbone_core::impl_crud_repository!(WarehouseRepository, Warehouse, soft_delete);