Skip to main content

backbone_inventory/infrastructure/persistence/
purchase_receipt_item_repository.rs

1//! Repository for PurchaseReceiptItem 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 PurchaseReceiptItem SQL
6//! (4-layer rule: services orchestrate, repositories hold the SQL).
7//!
8//! Thin newtype over `backbone_orm::GenericCrudRepository<PurchaseReceiptItem, 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::PurchaseReceiptItem;
19
20/// Table name for PurchaseReceiptItem entities
21pub const TABLE_NAME: &str = "inventory.purchase_receipt_items";
22
23/// Repository for PurchaseReceiptItem entities.
24///
25/// All standard CRUD, soft-delete, pagination, and bulk methods are
26/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
27pub struct PurchaseReceiptItemRepository(
28    backbone_orm::GenericCrudRepository<PurchaseReceiptItem, backbone_orm::SoftDelete>,
29);
30
31impl std::ops::Deref for PurchaseReceiptItemRepository {
32    type Target = backbone_orm::GenericCrudRepository<PurchaseReceiptItem, backbone_orm::SoftDelete>;
33    fn deref(&self) -> &Self::Target { &self.0 }
34}
35
36impl PurchaseReceiptItemRepository {
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 receipt line insert writes. `amount` arrives already rounded.
44pub struct NewReceiptItemRow {
45    pub id: Uuid,
46    pub receipt_id: Uuid,
47    pub item_id: Uuid,
48    pub quantity: Decimal,
49    pub rate: Decimal,
50    pub amount: Decimal,
51    /// Landed-cost service line (the seam owned by inventory): the door skips move-minting
52    /// for flagged lines — they carry cost into a LandedCost document, not stock.
53    pub is_landed_costs_line: bool,
54}
55
56/// A receipt line as the submit path reads it: what arrived, and at what cost.
57pub struct ReceiptItemRow {
58    pub item_id: Uuid,
59    pub quantity: Decimal,
60    pub rate: Decimal,
61    /// Landed-cost service line — the submit/cancel doors skip it (no stock behind it).
62    pub is_landed_costs_line: bool,
63}
64
65/// Hand-written PurchaseReceiptItem SQL. Lives here per the module's 4-layer rule.
66impl PurchaseReceiptItemRepository {
67    /// Insert one receipt line. Takes the CALLER'S connection so it commits with its header; the
68    /// caller has already relayed the ambient org scope onto it — don't re-bind here.
69    pub async fn insert_item(
70        &self,
71        conn: &mut sqlx::PgConnection,
72        l: &NewReceiptItemRow,
73    ) -> Result<(), sqlx::Error> {
74        sqlx::query(
75            r#"INSERT INTO inventory.purchase_receipt_items
76                   (id, receipt_id, item_id, quantity, rate, amount, is_landed_costs_line)
77               VALUES ($1,$2,$3,$4,$5,$6,$7)"#,
78        )
79        .bind(l.id).bind(l.receipt_id).bind(l.item_id).bind(l.quantity).bind(l.rate).bind(l.amount)
80        .bind(l.is_landed_costs_line)
81        .execute(conn)
82        .await?;
83        Ok(())
84    }
85
86    /// Read the live lines in `id` order — the order the movement applies them (and therefore the
87    /// order `sle_no` is assigned in), so it must not change. The scoped read helper makes it ride
88    /// the ambient org scope's request-dedicated connection, so the composing decorator's fence
89    /// (ADR-0029) bounds what the read can see.
90    pub async fn fetch_items(
91        &self,
92        pool: &PgPool,
93        receipt_id: Uuid,
94    ) -> Result<Vec<ReceiptItemRow>, sqlx::Error> {
95        let rows = org_scope::fetch_all_rows_scoped(
96            pool,
97            sqlx::query(
98                r#"SELECT item_id, quantity, rate, is_landed_costs_line FROM inventory.purchase_receipt_items
99                   WHERE receipt_id=$1 AND (metadata->>'deleted_at') IS NULL ORDER BY id"#,
100            )
101            .bind(receipt_id),
102        )
103        .await?;
104        Ok(rows.iter().map(|it| ReceiptItemRow {
105            item_id: it.get("item_id"), quantity: it.get("quantity"), rate: it.get("rate"),
106            is_landed_costs_line: it.get("is_landed_costs_line"),
107        }).collect())
108    }
109}
110
111backbone_core::impl_crud_repository!(PurchaseReceiptItemRepository, PurchaseReceiptItem, soft_delete);