backbone-inventory 0.17.2

Inventory: stock ledger of record (SLE) + moving-average valuation; emits COGS/asset AccountingPost (Delivery/Receipt -> GL)
Documentation
//! Repository for StockMoveLine 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 move-line SQL — the MIRROR arm of the reservation triangle (spec
//! stock-business-logic §3): each line records the grain that touched a quant
//! (item x src x dest x lot x package x owner x quantity); `quantity` mirrors the quant's
//! authoritative `reserved_quantity`, never the reverse (T2). `state` mirrors the parent move's
//! state (projection shape, ADR-0016) — written only by [`Self::mirror_state`].
//!
//! 4-layer rule: services orchestrate, repositories hold SQL. Write methods take the CALLER'S
//! connection; the caller has already relayed the ambient org scope onto it.

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

use crate::domain::entity::StockMoveLine;

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

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

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

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

/// The exact row a move-line insert writes (the reservation mirror minted by `_action_assign`).
pub struct NewMoveLineRow<'a> {
    pub id: Uuid,
    pub quantity: Decimal,
    pub lot_id: Option<Uuid>,
    pub package_id: Option<Uuid>,
    pub result_package_id: Option<Uuid>,
    pub owner_id: Option<Uuid>,
    pub move_id: Uuid,
    pub picking_id: Option<Uuid>,
    pub location_id: Uuid,
    pub location_dest_id: Uuid,
    pub item_id: Uuid,
    /// The mirrored state at mint time (matches the parent move's post-assign state).
    pub state: &'a str,
}

/// A move-line row as the engine reads it. `state` decodes as text.
pub struct MoveLineRow {
    pub id: Uuid,
    pub quantity: Decimal,
    pub lot_id: Option<Uuid>,
    pub package_id: Option<Uuid>,
    pub result_package_id: Option<Uuid>,
    pub owner_id: Option<Uuid>,
    pub state: String,
    pub move_id: Uuid,
    pub location_id: Uuid,
    pub location_dest_id: Uuid,
    pub item_id: Uuid,
}

impl StockMoveLineRepository {
    /// Insert one move line (the per-quant reservation mirror).
    pub async fn insert_line(
        &self,
        conn: &mut sqlx::PgConnection,
        l: &NewMoveLineRow<'_>,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"INSERT INTO inventory.stock_move_lines
                 (id, quantity, lot_id, package_id, result_package_id, owner_id, state, move_id,
                  picking_id, location_id, location_dest_id, item_id)
               VALUES ($1,$2,$3,$4,$5,$6,$7::move_state,$8,$9,$10,$11,$12)"#,
        )
        .bind(l.id).bind(l.quantity).bind(l.lot_id).bind(l.package_id).bind(l.result_package_id)
        .bind(l.owner_id).bind(l.state).bind(l.move_id).bind(l.picking_id)
        .bind(l.location_id).bind(l.location_dest_id).bind(l.item_id)
        .execute(conn)
        .await?;
        Ok(())
    }

    /// Retarget every line of one move onto its grouping transfer (the picking-assignment
    /// write's line half — lines minted before the move joined a picking carry a NULL
    /// `picking_id`, and the transfer's line gather reads this column). Runs inside the
    /// caller's transaction; no-op when the move has no lines yet.
    pub async fn retarget_picking(
        &self,
        conn: &mut sqlx::PgConnection,
        move_id: Uuid,
        picking_id: Uuid,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"UPDATE inventory.stock_move_lines
               SET picking_id = $2
               WHERE move_id = $1 AND (metadata->>'deleted_at') IS NULL"#,
        )
        .bind(move_id)
        .bind(picking_id)
        .execute(&mut *conn)
        .await?;
        Ok(())
    }

    /// All live lines of one move (execution order: mint order).
    pub async fn fetch_lines_for_move(
        &self,
        conn: &mut sqlx::PgConnection,
        move_id: Uuid,
    ) -> Result<Vec<MoveLineRow>, sqlx::Error> {
        let rows = sqlx::query(
            r#"SELECT id, quantity, lot_id, package_id, result_package_id, owner_id,
                      state::text AS state, move_id, location_id, location_dest_id, item_id
               FROM inventory.stock_move_lines
               WHERE move_id=$1 AND (metadata->>'deleted_at') IS NULL
               ORDER BY id"#,
        )
        .bind(move_id)
        .fetch_all(&mut *conn)
        .await?;
        Ok(rows.into_iter().map(line_row_of).collect())
    }

    /// One move line by id, as a landed-cost repost reads it (the target-line facts: its frozen
    /// done quantity, its move, and the destination location the debit leg's account chain
    /// resolves from). `None` when the line does not exist or is soft-deleted.
    pub async fn fetch_line(
        &self,
        conn: &mut sqlx::PgConnection,
        move_line_id: Uuid,
    ) -> Result<Option<MoveLineRow>, sqlx::Error> {
        let row = sqlx::query(
            r#"SELECT id, quantity, lot_id, package_id, result_package_id, owner_id,
                      state::text AS state, move_id, location_id, location_dest_id, item_id
               FROM inventory.stock_move_lines
               WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
        )
        .bind(move_line_id)
        .fetch_optional(&mut *conn)
        .await?;
        Ok(row.map(line_row_of))
    }

    /// Sum of the mirror quantities of one move — the aggregate the move state derives from
    /// (`assigned` when it meets demand, `partially_available` when positive but short).
    pub async fn sum_mirror_qty(
        &self,
        conn: &mut sqlx::PgConnection,
        move_id: Uuid,
    ) -> Result<Decimal, sqlx::Error> {
        let qty: Option<Decimal> = sqlx::query_scalar(
            r#"SELECT SUM(quantity) FROM inventory.stock_move_lines
               WHERE move_id=$1 AND (metadata->>'deleted_at') IS NULL"#,
        )
        .bind(move_id)
        .fetch_one(&mut *conn)
        .await?;
        Ok(qty.unwrap_or(Decimal::ZERO))
    }

    /// Rewrite one line's mirror quantity (reserve/unreserve delta landing on the mirror arm).
    pub async fn set_quantity(
        &self,
        conn: &mut sqlx::PgConnection,
        line_id: Uuid,
        quantity: Decimal,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"UPDATE inventory.stock_move_lines SET quantity=$2
               WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
        )
        .bind(line_id).bind(quantity)
        .execute(conn)
        .await?;
        Ok(())
    }

    /// Mirror the parent move's state onto every line of the move (the projection arm — lines
    /// never carry a state of their own; the `_free_reservation_index` partial rides this column).
    pub async fn mirror_state(
        &self,
        conn: &mut sqlx::PgConnection,
        move_id: Uuid,
        state: &str,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"UPDATE inventory.stock_move_lines SET state=$2::move_state
               WHERE move_id=$1 AND (metadata->>'deleted_at') IS NULL"#,
        )
        .bind(move_id).bind(state)
        .execute(conn)
        .await?;
        Ok(())
    }
}

fn line_row_of(r: sqlx::postgres::PgRow) -> MoveLineRow {
    MoveLineRow {
        id: r.get("id"),
        quantity: r.get("quantity"),
        lot_id: r.get("lot_id"),
        package_id: r.get("package_id"),
        result_package_id: r.get("result_package_id"),
        owner_id: r.get("owner_id"),
        state: r.get("state"),
        move_id: r.get("move_id"),
        location_id: r.get("location_id"),
        location_dest_id: r.get("location_dest_id"),
        item_id: r.get("item_id"),
    }
}

backbone_core::impl_crud_repository!(StockMoveLineRepository, StockMoveLine, soft_delete);