Skip to main content

backbone_inventory/infrastructure/persistence/
delivery_note_item_repository.rs

1//! Repository for DeliveryNoteItem 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 DeliveryNoteItem SQL
6//! (4-layer rule: services orchestrate, repositories hold the SQL).
7//!
8//! Thin newtype over `backbone_orm::GenericCrudRepository<DeliveryNoteItem, 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::org_scope;
17
18use crate::domain::entity::DeliveryNoteItem;
19
20/// Table name for DeliveryNoteItem entities
21pub const TABLE_NAME: &str = "inventory.delivery_note_items";
22
23/// Repository for DeliveryNoteItem entities.
24///
25/// All standard CRUD, soft-delete, pagination, and bulk methods are
26/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
27pub struct DeliveryNoteItemRepository(
28    backbone_orm::GenericCrudRepository<DeliveryNoteItem, backbone_orm::SoftDelete>,
29);
30
31impl std::ops::Deref for DeliveryNoteItemRepository {
32    type Target = backbone_orm::GenericCrudRepository<DeliveryNoteItem, backbone_orm::SoftDelete>;
33    fn deref(&self) -> &Self::Target { &self.0 }
34}
35
36impl DeliveryNoteItemRepository {
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 delivery line insert writes. `valuation_rate`/`cogs_amount` start as literal 0s —
44/// both are only known once the movement consumes the moving average at submit.
45pub struct NewDeliveryItemRow {
46    pub id: Uuid,
47    pub delivery_id: Uuid,
48    pub item_id: Uuid,
49    pub quantity: Decimal,
50}
51
52/// A delivery line as the submit path reads it. `id` is carried because the movement writes the
53/// consumed rate + COGS back onto this exact line.
54pub struct DeliveryItemRow {
55    pub id: Uuid,
56    pub item_id: Uuid,
57    pub quantity: Decimal,
58}
59
60/// A delivery line as the CANCEL path reads it: the stored `cogs_amount` is the exact value the
61/// outflow consumed (for the drain-to-zero line, the whole remaining value) — the cancellation
62/// restores that exact amount, so the bin reblends to its pre-delivery state.
63pub struct DeliveryCancelItemRow {
64    pub item_id: Uuid,
65    pub quantity: Decimal,
66    pub cogs_amount: Decimal,
67}
68
69/// Hand-written DeliveryNoteItem SQL. Lives here per the module's 4-layer rule.
70impl DeliveryNoteItemRepository {
71    /// Insert one delivery line. Takes the CALLER'S connection so it commits with its header; the
72    /// caller has already relayed the ambient org scope onto it — don't re-bind here.
73    pub async fn insert_item(
74        &self,
75        conn: &mut sqlx::PgConnection,
76        l: &NewDeliveryItemRow,
77    ) -> Result<(), sqlx::Error> {
78        sqlx::query(
79            r#"INSERT INTO inventory.delivery_note_items (id, delivery_id, item_id, quantity, valuation_rate, cogs_amount)
80               VALUES ($1,$2,$3,$4,0,0)"#,
81        )
82        .bind(l.id).bind(l.delivery_id).bind(l.item_id).bind(l.quantity)
83        .execute(conn)
84        .await?;
85        Ok(())
86    }
87
88    /// Read the live lines in `id` order — the order the movement applies them (and therefore the
89    /// order `sle_no` is assigned in), so it must not change. The scoped read helper makes it ride
90    /// the ambient org scope's request-dedicated connection, so the composing decorator's fence
91    /// (ADR-0029) bounds what the read can see.
92    pub async fn fetch_items(
93        &self,
94        pool: &PgPool,
95        delivery_id: Uuid,
96    ) -> Result<Vec<DeliveryItemRow>, sqlx::Error> {
97        let rows = org_scope::fetch_all_rows_scoped(
98            pool,
99            sqlx::query(
100                r#"SELECT id, item_id, quantity FROM inventory.delivery_note_items
101                   WHERE delivery_id=$1 AND (metadata->>'deleted_at') IS NULL ORDER BY id"#,
102            )
103            .bind(delivery_id),
104        )
105        .await?;
106        Ok(rows.iter().map(|it| DeliveryItemRow {
107            id: it.get("id"), item_id: it.get("item_id"), quantity: it.get("quantity"),
108        }).collect())
109    }
110
111    /// Record what the outflow actually cost: the rate the moving average was at, and the COGS taken.
112    /// Must ride the movement's transaction (the bin's `FOR UPDATE` is held on it).
113    pub async fn update_valuation(
114        &self,
115        conn: &mut sqlx::PgConnection,
116        line_id: Uuid,
117        valuation_rate: Decimal,
118        cogs_amount: Decimal,
119    ) -> Result<(), sqlx::Error> {
120        sqlx::query("UPDATE inventory.delivery_note_items SET valuation_rate=$2, cogs_amount=$3 WHERE id=$1")
121            .bind(line_id).bind(valuation_rate).bind(cogs_amount)
122            .execute(conn)
123            .await?;
124        Ok(())
125    }
126
127    /// Read the live lines with their stored COGS — what the cancel path reverses. Same scope/order
128    /// guarantees as [`Self::fetch_items`].
129    pub async fn fetch_cancel_items(
130        &self,
131        pool: &PgPool,
132        delivery_id: Uuid,
133    ) -> Result<Vec<DeliveryCancelItemRow>, sqlx::Error> {
134        let rows = org_scope::fetch_all_rows_scoped(
135            pool,
136            sqlx::query(
137                r#"SELECT item_id, quantity, cogs_amount FROM inventory.delivery_note_items
138                   WHERE delivery_id=$1 AND (metadata->>'deleted_at') IS NULL ORDER BY id"#,
139            )
140            .bind(delivery_id),
141        )
142        .await?;
143        Ok(rows.iter().map(|it| DeliveryCancelItemRow {
144            item_id: it.get("item_id"), quantity: it.get("quantity"), cogs_amount: it.get("cogs_amount"),
145        }).collect())
146    }
147}
148
149backbone_core::impl_crud_repository!(DeliveryNoteItemRepository, DeliveryNoteItem, soft_delete);