backbone-pos 0.7.2

Point of sale — cashier session, ticket + multi-tender; orchestrates billing (revenue) + payment (settlement), posts no GL directly
Documentation
//! POS domain events (hand-authored, user-owned) — the public extension surface.
//!
//! POS posts NO GL itself; `PosInvoicePaid` (carrying the billing invoice + payment it drove) is the
//! hook a loyalty/analytics consumer subscribes to, and `PosSessionClosed` carries the drawer
//! reconciliation result. The retail path reuses billing (revenue) + payment (settlement) as the GL
//! emitters — POS is the aggregator over them.
//!
//! **Tenancy twin (ADR-0029):** every event struct keeps its `company_id` field as an outbound
//! wire shape, but the value it carries is the acting org unit — the ambient org scope's legacy
//! company echo (the spine mirrored company ids into org units verbatim, so during the transition
//! the two names meet on the same uuid). Nothing in this module keys a statement on it; consumers
//! that still bind a company scope read it as their unit id, and it retires with the legacy twin.

use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// A cashier session opened.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PosSessionOpened {
    pub opening_entry_id: Uuid,
    pub pos_profile_id: Uuid,
    pub company_id: Uuid,
}

/// A sale was fully tendered + recognised — POS drove billing (a real Sales Invoice + revenue post)
/// and payment (tender settlement). Carries both so a consumer needn't call back into POS.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PosInvoicePaid {
    pub pos_invoice_id: Uuid,
    pub company_id: Uuid,
    pub grand_total: Decimal,
    pub rounded_total: Decimal,
    pub billing_invoice_id: Uuid,
    pub payment_id: Uuid,
}

/// A cashier session was closed + reconciled (drawer counted vs expected per method).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PosSessionClosed {
    pub closing_entry_id: Uuid,
    pub opening_entry_id: Uuid,
    pub company_id: Uuid,
    pub difference_total: Decimal,
}

/// A sale was returned — POS drove billing (credit note) + payment (refund), so revenue, cash, and
/// A/R all net back to zero. Carries the original + the return ticket for downstream (loyalty claw-back,
/// analytics).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PosInvoiceReturned {
    pub pos_invoice_id: Uuid,
    pub return_ticket_id: Uuid,
    pub company_id: Uuid,
    pub billing_invoice_id: Uuid,
    pub amount: Decimal,
}

/// A ticket just became fully tendered — it is ready to RECOGNISE (drive billing + payment + inventory).
/// A composing service subscribes to this and calls `recognize_sale`, turning the counter into an
/// event-driven flow instead of an inline call. Emitted once, on the tender that crosses the threshold.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PosTenderCompleted {
    pub pos_invoice_id: Uuid,
    pub company_id: Uuid,
}

/// An open cashier session crossed the stale threshold (open past the configured age, 7 days by
/// default). Emitted by the old-session alert scheduler — a LOGICAL LINK to the mail-activity
/// choreography, with NO Cargo edge to the mail module: the host subscribes and schedules the
/// "Session open too long" activity on the cashier (party ref). Once-only per session: the claim
/// stamps a marker before this fires, so a scheduler rerun does not re-alert.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PosStaleSessionAlerted {
    pub opening_entry_id: Uuid,
    pub pos_profile_id: Uuid,
    pub company_id: Uuid,
    /// The cashier the alert is for — a party.Party logical ref the host resolves to its activity owner.
    pub cashier_party_id: Uuid,
    pub opened_at: chrono::NaiveDateTime,
}

/// The POS domain-event union.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum PosEvent {
    PosSessionOpened(PosSessionOpened),
    PosTenderCompleted(PosTenderCompleted),
    PosInvoicePaid(PosInvoicePaid),
    PosSessionClosed(PosSessionClosed),
    PosInvoiceReturned(PosInvoiceReturned),
    PosStaleSessionAlerted(PosStaleSessionAlerted),
}

/// Sink for POS domain events. Fire-and-forget; a real adapter wires a bus, tests record.
pub trait PosEventSink: Send + Sync {
    fn publish(&self, event: PosEvent);
}

/// Default sink — emits structured tracing events.
pub struct LoggingSink;

impl PosEventSink for LoggingSink {
    fn publish(&self, event: PosEvent) {
        tracing::info!(target: "pos.events", ?event, "pos domain event");
    }
}