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
impl PosWriteService
pub fn new(db_pool: PgPool) -> Self
pub fn with_sink(db_pool: PgPool, sink: Arc<dyn PosEventSink>) -> Self
Sourcepub fn with_outbox(self, schema: impl Into<String>) -> Self
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).
Sourcepub fn with_pin_policy(self, policy: PinPolicy) -> Self
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.
pub async fn open_session(&self, s: NewSession) -> Result<Uuid, PosError>
Source§impl PosWriteService
impl PosWriteService
Sourcepub async fn ring_sale(
&self,
sale: NewSale,
tax: &dyn PosTaxComputePort,
) -> Result<Uuid, PosError>
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.
Sourcepub async fn ring_sale_priced(
&self,
o: NewCartSale,
pricing: &dyn CartPricingPort,
tax: &dyn PosTaxComputePort,
) -> Result<Uuid, PosError>
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
impl PosWriteService
Sourcepub async fn add_tender(
&self,
pos_invoice_id: Uuid,
method: &str,
amount: Decimal,
reference_no: Option<String>,
) -> Result<TenderOutcome, PosError>
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
impl PosWriteService
Sourcepub async fn recognize_sale(
&self,
pos_invoice_id: Uuid,
billing: &dyn BillingPort,
payment: &dyn PaymentPort,
inventory: Option<&dyn InventoryPort>,
) -> Result<RecognizeOutcome, PosError>
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'.
Sourcepub async fn return_sale(
&self,
original_pos_invoice_id: Uuid,
billing: &dyn BillingPort,
payment: &dyn PaymentPort,
partial: Option<PartialCredit>,
) -> Result<ReturnOutcome, PosError>
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
impl PosWriteService
Sourcepub async fn record_cash_movement(
&self,
m: NewCashMovement,
) -> Result<Uuid, PosError>
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.
Sourcepub async fn x_report(
&self,
opening_entry_id: Uuid,
) -> Result<XReport, PosError>
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.
Sourcepub async fn close_session(
&self,
c: NewClose,
variance: &dyn PosCashVariancePort,
) -> Result<CloseOutcome, PosError>
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):
- 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 asPosError::SessionAlreadyOpen. - 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. - Variance. A non-zero net difference books through the
PosCashVariancePortto 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. - Persist. The closing entry (per-method breakdown + grand total + difference) and the
session’s flip to
closedcommit as ONE unit, thenPosSessionClosedis emitted.
Source§impl PosWriteService
impl PosWriteService
Sourcepub async fn receipt(&self, pos_invoice_id: Uuid) -> Result<Receipt, PosError>
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
impl PosWriteService
Sourcepub async fn sync_from_ui(
&self,
s: NewSyncSale,
tax: &dyn PosTaxComputePort,
billing: &dyn BillingPort,
payment: &dyn PaymentPort,
) -> Result<SyncOutcome, PosError>
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
impl PosWriteService
Sourcepub async fn set_pin(&self, s: SetPin) -> Result<(), PosError>
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).
Sourcepub async fn verify_pin(
&self,
employee_party_id: Uuid,
pin: &str,
source_ip: Option<&str>,
) -> Result<(), PosError>
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
impl PosWriteService
Sourcepub async fn alert_old_sessions(
&self,
older_than: Duration,
) -> Result<Vec<PosStaleSessionAlerted>, PosError>
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§
Auto Trait Implementations§
impl !RefUnwindSafe for PosWriteService
impl !UnwindSafe for PosWriteService
impl Freeze for PosWriteService
impl Send for PosWriteService
impl Sync for PosWriteService
impl Unpin for PosWriteService
impl UnsafeUnpin for PosWriteService
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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