Skip to main content

PosWriteService

Struct PosWriteService 

Source
pub struct PosWriteService { /* private fields */ }
Expand description

The repositories are held behind Arc only so this service stays Clone (its HTTP surface clones it per request) — GenericCrudRepository is not itself Clone. They are stateless handles over the same pool; the Arc carries no shared mutable state. The PIN attempt ring is the one piece of genuinely shared mutable state — Mutex, never held across an .await.

Implementations§

Source§

impl PosWriteService

Source

pub fn new(db_pool: PgPool) -> Self

Source

pub fn with_sink(db_pool: PgPool, sink: Arc<dyn PosEventSink>) -> Self

Source

pub fn with_outbox(self, schema: impl Into<String>) -> Self

Opt into durable, outbox-backed PosTenderCompleted staging: add_tender will insert the event into {schema}.outbox_events inside the tender transaction (so it commits atomically with the tender). A composing service runs backbone_outbox::runner to drain the queue and recognise the sale — surviving a crash between commit and the in-process sink spawn. None/unset keeps the fire-and-forget behaviour. The schema must already exist (the service runs backbone_outbox::outbox::migrate(pool, schema) at startup).

Source

pub fn with_pin_policy(self, policy: PinPolicy) -> Self

Override the manager-PIN windows (lockout, throttling, strength). Callers that want the environment to decide can pass PinPolicy::from_env.

Source

pub async fn open_session(&self, s: NewSession) -> Result<Uuid, PosError>

Source§

impl PosWriteService

Source

pub async fn ring_sale( &self, sale: NewSale, tax: &dyn PosTaxComputePort, ) -> Result<Uuid, PosError>

Ring a ticket. The money is SERVER-DERIVED end to end: the caller supplies the basket (items/qty/unit price/discount) and the register; the compute core resolves tax through the register’s configured templates (tax: &dyn PosTaxComputePort) and rounds per the register’s cash-rounding config. Nothing the caller might claim about totals is read.

Source

pub async fn ring_sale_priced( &self, o: NewCartSale, pricing: &dyn CartPricingPort, tax: &dyn PosTaxComputePort, ) -> Result<Uuid, PosError>

Ring a ticket whose prices are resolved by promo’s CART pricer (the cart seam, ADR-002). POS hands the whole basket (list prices + item dimensions + optional coupon) to the CartPricingPort; promo returns per-line nets folding in line rules, order-total discounts, and bundles. POS maps each net back to a unit_price/discount_amount pair so the compute core reproduces the conserved cart total. Zero normal Cargo edge to promo.

Source§

impl PosWriteService

Source

pub async fn add_tender( &self, pos_invoice_id: Uuid, method: &str, amount: Decimal, reference_no: Option<String>, ) -> Result<TenderOutcome, PosError>

Add a tender line; recompute paid_total + change_due (overpayment). Ticket must be draft.

Source§

impl PosWriteService

Source

pub async fn recognize_sale( &self, pos_invoice_id: Uuid, billing: &dyn BillingPort, payment: &dyn PaymentPort, inventory: Option<&dyn InventoryPort>, ) -> Result<RecognizeOutcome, PosError>

Recognise a fully-tendered sale: drive billing (raise + post the Sales Invoice → revenue) then payment (settle the rounded_total against it → Dr Cash · Cr A/R), link the results, mark the ticket paid, and emit PosInvoicePaid. Idempotent on status='paid'.

Source

pub async fn return_sale( &self, original_pos_invoice_id: Uuid, billing: &dyn BillingPort, payment: &dyn PaymentPort, partial: Option<PartialCredit>, ) -> Result<ReturnOutcome, PosError>

Return a recognised sale (council 2026-07-05, the KEEP returns flow). Drives billing (credit note — Dr Revenue · Cr A/R, invoice → cancelled) and payment (refund — Dr A/R · Cr Cash), so revenue, cash, and A/R all net back to zero. Records an is_return ticket linked via return_against, flips the original → returned, emits PosInvoiceReturned. Idempotent: both downstream reversals are idempotent, and the ticket/event are gated on the original’s paid→returned transition — a repeat return refunds/credits at most once. POS posts no GL.

Source§

impl PosWriteService

Source

pub async fn record_cash_movement( &self, m: NewCashMovement, ) -> Result<Uuid, PosError>

Record a non-sale cash drawer movement (pay_in / pay_out / drop / no_sale) against an OPEN session in the caller’s scope. close_session folds pay-ins into and pay-outs/drops out of the expected cash drawer, so a mid-shift movement no longer surfaces as an unexplained variance.

Source

pub async fn x_report( &self, opening_entry_id: Uuid, ) -> Result<XReport, PosError>

Mid-shift drawer read (X-report): the SAME expected drawer + running totals as the Z-report, but without counting, closing the session, or writing anything. Read-only + idempotent — a cashier can pull it any number of times during the shift. Requires an OPEN session in the caller’s scope.

Source

pub async fn close_session( &self, c: NewClose, variance: &dyn PosCashVariancePort, ) -> Result<CloseOutcome, PosError>

Close a session — the Z-report. A PRIVILEGED mutation: the manager PIN on NewClose::manager is verified server-side before anything is read or written, because a close is the one POS verb that books a GL correction on its own account.

Order of operations (every guard runs before any write — a failed close leaves NOTHING):

  1. Guards. The session must exist and be open; it must hold no DRAFT sale tickets (PosError::SessionHasDraftOrders — post or void them first); and no half-recognised tickets (billing linked but the draft→paid flip never landed — PosError::SessionHasUnpostedInvoices; retry recognition first). The manager PIN is verified. A second open session on the register is impossible by the one-open-session constraint, and opening a new one while this close is in flight collides there and surfaces as PosError::SessionAlreadyOpen.
  2. Count. For each tender method expected = opening_float + Σ recognised tenders (cash also − Σ change_due + Σ cash-movement net) — the statement legs (what the journals say) against the counted legs (what the drawer holds). difference = counted − expected.
  3. Variance. A non-zero net difference books through the PosCashVariancePort to the register’s cash account against its difference/write-off account — the ONE new GL surface a close produces (per-ticket posting stays THE posting path). Booking happens BEFORE the closing-entry transaction: if the seam refuses, the close rolls back entirely (nothing has been written); the seam’s idempotency key is the session, so a crash between booking and commit re-books nothing on retry.
  4. Persist. The closing entry (per-method breakdown + grand total + difference) and the session’s flip to closed commit as ONE unit, then PosSessionClosed is emitted.
Source§

impl PosWriteService

Source

pub async fn receipt(&self, pos_invoice_id: Uuid) -> Result<Receipt, PosError>

Assemble the receipt for a ticket: register + money breakdown (incl. server-computed PPN) + lines + tenders + change. Ambient-scope read (ADR-0029) — any ticket in the caller’s scope can be (re)printed; another unit’s reads as plain absence. The GL is billing’s; this is the customer-facing slip.

Source§

impl PosWriteService

Source

pub async fn sync_from_ui( &self, s: NewSyncSale, tax: &dyn PosTaxComputePort, billing: &dyn BillingPort, payment: &dyn PaymentPort, ) -> Result<SyncOutcome, PosError>

Replay one offline ticket. See the module doc for the identity/trust contract. tax resolves the document-grade compute (as on ring_sale); billing/payment are only touched when the replay is a REFUND (they drive the same reversal pair return_sale does).

Source§

impl PosWriteService

Source

pub async fn set_pin(&self, s: SetPin) -> Result<(), PosError>

Set (or replace) a manager’s PIN. Hashes with argon2 server-side and persists ONLY the hash; setting a new PIN clears any lockout and failure history (the administrative unlock).

Source

pub async fn verify_pin( &self, employee_party_id: Uuid, pin: &str, source_ip: Option<&str>, ) -> Result<(), PosError>

Verify a manager’s PIN — the gate every privileged mutation calls. Fails CLOSED on every abuse signal: unknown manager (PinNotFound), wrong PIN (PinInvalid), locked (PinLocked), source over its throttle budget (PinThrottled). Succeeds by clearing the failure counter. State changes ride the persisted counters, so a process restart never resets an in-progress attack.

Source§

impl PosWriteService

Source

pub async fn alert_old_sessions( &self, older_than: Duration, ) -> Result<Vec<PosStaleSessionAlerted>, PosError>

Alert the caller’s scope’s stale open sessions. Returns the alerts that fired (empty = nothing stale or everything already latched). Idempotent per session: a rerun re-alerts nothing.

Trait Implementations§

Source§

impl Clone for PosWriteService

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more