backbone-inventory 0.17.2

Inventory: stock ledger of record (SLE) + moving-average valuation; emits COGS/asset AccountingPost (Delivery/Receipt -> GL)
Documentation
//! Repository for DeliveryNoteItem entities
//!
//! Originally generated by metaphor-schema; now **user-owned** — this exact path is declared under
//! `user_owned` in `metaphor.codegen.yaml`, so the generator skips it wholesale. The custom methods
//! below hold the hand-written DeliveryNoteItem SQL
//! (4-layer rule: services orchestrate, repositories hold the SQL).
//!
//! Thin newtype over `backbone_orm::GenericCrudRepository<DeliveryNoteItem, backbone_orm::SoftDelete>`.
//! All standard CRUD methods are available via `Deref`.

use anyhow::Result;
use rust_decimal::Decimal;
use sqlx::{PgPool, Row};
use uuid::Uuid;

use backbone_orm::org_scope;

use crate::domain::entity::DeliveryNoteItem;

/// Table name for DeliveryNoteItem entities
pub const TABLE_NAME: &str = "inventory.delivery_note_items";

/// Repository for DeliveryNoteItem entities.
///
/// All standard CRUD, soft-delete, pagination, and bulk methods are
/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
pub struct DeliveryNoteItemRepository(
    backbone_orm::GenericCrudRepository<DeliveryNoteItem, backbone_orm::SoftDelete>,
);

impl std::ops::Deref for DeliveryNoteItemRepository {
    type Target = backbone_orm::GenericCrudRepository<DeliveryNoteItem, backbone_orm::SoftDelete>;
    fn deref(&self) -> &Self::Target { &self.0 }
}

impl DeliveryNoteItemRepository {
    /// Create a new repository instance.
    pub fn new(pool: PgPool) -> Self {
        Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
    }
}

/// The exact row a delivery line insert writes. `valuation_rate`/`cogs_amount` start as literal 0s —
/// both are only known once the movement consumes the moving average at submit.
pub struct NewDeliveryItemRow {
    pub id: Uuid,
    pub delivery_id: Uuid,
    pub item_id: Uuid,
    pub quantity: Decimal,
}

/// A delivery line as the submit path reads it. `id` is carried because the movement writes the
/// consumed rate + COGS back onto this exact line.
pub struct DeliveryItemRow {
    pub id: Uuid,
    pub item_id: Uuid,
    pub quantity: Decimal,
}

/// A delivery line as the CANCEL path reads it: the stored `cogs_amount` is the exact value the
/// outflow consumed (for the drain-to-zero line, the whole remaining value) — the cancellation
/// restores that exact amount, so the bin reblends to its pre-delivery state.
pub struct DeliveryCancelItemRow {
    pub item_id: Uuid,
    pub quantity: Decimal,
    pub cogs_amount: Decimal,
}

/// Hand-written DeliveryNoteItem SQL. Lives here per the module's 4-layer rule.
impl DeliveryNoteItemRepository {
    /// Insert one delivery line. Takes the CALLER'S connection so it commits with its header; the
    /// caller has already relayed the ambient org scope onto it — don't re-bind here.
    pub async fn insert_item(
        &self,
        conn: &mut sqlx::PgConnection,
        l: &NewDeliveryItemRow,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"INSERT INTO inventory.delivery_note_items (id, delivery_id, item_id, quantity, valuation_rate, cogs_amount)
               VALUES ($1,$2,$3,$4,0,0)"#,
        )
        .bind(l.id).bind(l.delivery_id).bind(l.item_id).bind(l.quantity)
        .execute(conn)
        .await?;
        Ok(())
    }

    /// Read the live lines in `id` order — the order the movement applies them (and therefore the
    /// order `sle_no` is assigned in), so it must not change. The scoped read helper makes it ride
    /// the ambient org scope's request-dedicated connection, so the composing decorator's fence
    /// (ADR-0029) bounds what the read can see.
    pub async fn fetch_items(
        &self,
        pool: &PgPool,
        delivery_id: Uuid,
    ) -> Result<Vec<DeliveryItemRow>, sqlx::Error> {
        let rows = org_scope::fetch_all_rows_scoped(
            pool,
            sqlx::query(
                r#"SELECT id, item_id, quantity FROM inventory.delivery_note_items
                   WHERE delivery_id=$1 AND (metadata->>'deleted_at') IS NULL ORDER BY id"#,
            )
            .bind(delivery_id),
        )
        .await?;
        Ok(rows.iter().map(|it| DeliveryItemRow {
            id: it.get("id"), item_id: it.get("item_id"), quantity: it.get("quantity"),
        }).collect())
    }

    /// Record what the outflow actually cost: the rate the moving average was at, and the COGS taken.
    /// Must ride the movement's transaction (the bin's `FOR UPDATE` is held on it).
    pub async fn update_valuation(
        &self,
        conn: &mut sqlx::PgConnection,
        line_id: Uuid,
        valuation_rate: Decimal,
        cogs_amount: Decimal,
    ) -> Result<(), sqlx::Error> {
        sqlx::query("UPDATE inventory.delivery_note_items SET valuation_rate=$2, cogs_amount=$3 WHERE id=$1")
            .bind(line_id).bind(valuation_rate).bind(cogs_amount)
            .execute(conn)
            .await?;
        Ok(())
    }

    /// Read the live lines with their stored COGS — what the cancel path reverses. Same scope/order
    /// guarantees as [`Self::fetch_items`].
    pub async fn fetch_cancel_items(
        &self,
        pool: &PgPool,
        delivery_id: Uuid,
    ) -> Result<Vec<DeliveryCancelItemRow>, sqlx::Error> {
        let rows = org_scope::fetch_all_rows_scoped(
            pool,
            sqlx::query(
                r#"SELECT item_id, quantity, cogs_amount FROM inventory.delivery_note_items
                   WHERE delivery_id=$1 AND (metadata->>'deleted_at') IS NULL ORDER BY id"#,
            )
            .bind(delivery_id),
        )
        .await?;
        Ok(rows.iter().map(|it| DeliveryCancelItemRow {
            item_id: it.get("item_id"), quantity: it.get("quantity"), cogs_amount: it.get("cogs_amount"),
        }).collect())
    }
}

backbone_core::impl_crud_repository!(DeliveryNoteItemRepository, DeliveryNoteItem, soft_delete);