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