backbone-pos 0.7.0

Point of sale — cashier session, ticket + multi-tender; orchestrates billing (revenue) + payment (settlement), posts no GL directly
Documentation
//! Repository for PosCashMovement 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 non-sale drawer-movement SQL (4-layer rule: services orchestrate, repos hold SQL).
//!
//! Thin newtype over `backbone_orm::GenericCrudRepository<PosCashMovement, 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;

// The scalar read twin lives only in the legacy `company_scope` module; what this repository needs
// from it is the connection discipline — request-dedicated connection when the composing service
// bound one, plain pool otherwise. Its legacy task-local branch never fires: this module sets no
// legacy scope of its own (ADR-0029).
use backbone_orm::company_scope::fetch_one_scalar_scoped;

use crate::domain::entity::PosCashMovement;

/// Table name for PosCashMovement entities
pub const TABLE_NAME: &str = "pos.pos_cash_movements";

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

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

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

/// The exact row a non-sale drawer movement writes. Mirrors the raw column shape rather than the
/// `PosCashMovement` entity: `movement_type` binds as `&str` with a DB-side cast
/// (`$6::pos_cash_movement_type`), so an unknown kind fails as a DB error, not a deserialize panic.
pub struct NewCashMovementRow<'a> {
    pub id: Uuid,
    pub pos_profile_id: Uuid,
    pub opening_entry_id: Uuid,
    pub cashier_party_id: Uuid,
    pub movement_type: &'a str,
    pub amount: Decimal,
    pub reason: Option<&'a str>,
    pub moved_at: chrono::NaiveDateTime,
}

/// Hand-written non-sale drawer-movement SQL. Lives here (not in the write service) per the module's
/// 4-layer rule: services orchestrate and own the unit of work, repositories hold the SQL.
impl PosCashMovementRepository {
    /// Record a non-sale drawer movement (`pay_in` / `pay_out` / `drop` / `no_sale`).
    ///
    /// Takes the CALLER'S connection so the open-session guard and the movement commit as one unit;
    /// the caller has already relayed the ambient org scope onto it (ADR-0029), which is what
    /// satisfies the fence's WITH CHECK under a composed host.
    pub async fn insert_movement(
        &self,
        conn: &mut sqlx::PgConnection,
        m: &NewCashMovementRow<'_>,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"INSERT INTO pos.pos_cash_movements
                (id, pos_profile_id, opening_entry_id, cashier_party_id, movement_type, amount, reason, moved_at)
               VALUES ($1,$2,$3,$4,$5::pos_cash_movement_type,$6,$7,$8)"#,
        )
        .bind(m.id).bind(m.pos_profile_id).bind(m.opening_entry_id).bind(m.cashier_party_id)
        .bind(m.movement_type).bind(m.amount).bind(m.reason).bind(m.moved_at)
        .execute(conn)
        .await?;
        Ok(())
    }

    /// Net cash effect of a session's non-sale movements: pay-ins add to the drawer, pay-outs and drops
    /// remove from it, `no_sale` has no cash effect. Folding this into the expected drawer is what stops
    /// a mid-shift movement reading as an unexplained variance.
    ///
    /// ID-only: rides the ambient org scope (ADR-0029) — the fence bounds the sum to the caller's
    /// entitled units.
    pub async fn net_for_session(
        &self,
        pool: &PgPool,
        opening_entry_id: Uuid,
    ) -> Result<Decimal, sqlx::Error> {
        fetch_one_scalar_scoped(
            pool,
            sqlx::query_scalar(
                r#"SELECT COALESCE(SUM(CASE movement_type
                        WHEN 'pay_in' THEN amount
                        WHEN 'pay_out' THEN -amount
                        WHEN 'drop' THEN -amount
                        ELSE 0 END), 0)
                   FROM pos.pos_cash_movements
                   WHERE opening_entry_id=$1 AND (metadata->>'deleted_at') IS NULL"#,
            )
            .bind(opening_entry_id),
        )
        .await
    }
}

backbone_core::impl_crud_repository!(PosCashMovementRepository, PosCashMovement, soft_delete);