Current-price contract
Checkout reads uncached period metadata andGET /api/v1/workspaces/{wsId}/inventory/sales-periods/{periodId}/prices.
The response contains price rows and a server as_of instant. Resolution uses the
selected period, product, unit, warehouse and explicit wallet currency. Effective
intervals are half-open [valid_from, valid_to); duplicate applicable rows,
malformed responses and missing prices block the sale. Product allow/block lists
apply separately. Period dates are inclusive in the period’s IANA timezone,
checked against server time, with final eligibility checked atomically by the
server. No device-timezone date or catalog fallback decides scheduled pricing.
The compact checkout shows season, currency, UTC quote time and period timezone.
Quotes expire after 15 seconds of local receipt age and are refreshed while the
checkout is open. Automatic failures retry at most once every ten seconds;
manual refresh remains immediate. Failed refreshes clear visible quotes. Missing lines display an
unavailable amount rather than a zero-priced sale or a mixed-currency total.
Atomic creation and retry
Scheduled sales require online access and call the existing authenticatedPOST /api/v1/workspaces/{wsId}/finance/invoices directly, with
price_mode: custom, inventory_period_id, inventory_request_id and immutable
price_id on each line. They never enter the offline mutation queue and never
create an invoice followed by a separate period assignment. The backend derives
configured workspace currency, checks wallet currency and precision, validates
current prices/stock, and persists historical snapshots plus period assignment in
one transaction. The UI currency is a quote-selection constraint, not authority
to change workspace currency. Zero prices are legitimate only when explicitly
returned by the server.
Before any scheduled POST, checkout writes and verifies an encrypted operation
journal using the existing secure-storage adapter. It retains the exact request
UUID/body, original currency, quote provenance and frozen product display labels
in an actor/workspace scope. Cache eviction, logout, scope switches and controller
recreation do not erase unresolved operations. Read, corruption, version and write
failures block creation; they never become an empty cart. One app-wide scope lock
prevents simultaneous checkouts preparing distinct operations. Separate business
drafts on another device are outside this local journal contract.
An unresolved operation opens a dedicated recovery view. Metadata Retry and cart
editing are unavailable there; current catalog, wallet, category and period
metadata cannot strand recovery or route it into legacy creation. Operators can
leave and resume in the owning account/workspace. No automatic/offline replay
occurs.
An actor/workspace switch during a POST or receipt lookup clears the mounted
checkout immediately and queues the current scope’s catalog reload until the
active save finishes. Every switch advances a scope revision, including A→B→A;
a late response cannot acknowledge, navigate or display an error on the reset
form merely because its actor/workspace pair matches again. The original scoped
journal retains its UUID and immutable body. Returning to its owning scope opens
recovery (or the recorded confirmed result); switching away loads the new scope.
An explicit Check sale result action calls the authenticated uncached
GET /api/v1/workspaces/{wsId}/inventory/sale-requests/{requestId}. The server
verifies current canonical membership and create-sales action permission, derives
the actor from authentication and selects only that actor’s workspace/request
receipt. It returns only state, request ID and committed invoice ID. The existing
private receipt/tombstone stays authoritative after invoice deletion. Lost
permissions and unavailable rollout schema are errors, never an empty receipt.
not_observed is not proof an earlier transaction cannot still commit. This
explicit action may resend only the original UUID and immutable payload. Every
HTTP rejection retains uncertainty, including 400/401/403/409/422/503. Changed
DEFAULT_CURRENCY can reject resubmission while the original is in flight; future
receipt lookup resolves a committed outcome independently of current pricing
configuration. No replacement request/key or revised cart is allowed. If the
original never commits and continues to be rejected, the operation stays blocked
for manual reconciliation. This slice does not add a terminal server rejection or
cancellation protocol or permit unsafe journal reset.
A valid invoice response or matching receipt makes checkout terminal. Cache
invalidation and remembered-category preferences are best-effort maintenance and
cannot turn confirmed creation into a resubmittable cart. Confirmation is stored
before its journal can be acknowledged. A confirmation-write failure retains the
prepared record for next-entry receipt reconciliation while keeping known success
terminal in the current view. Acknowledgement removes only a matching confirmed
record. Acknowledgement does not reopen the terminal controller/cart: another sale
requires a new checkout instance. Corrupt or unknown-version journals intentionally
fail closed for manual reconciliation; deletion without a verified receipt could
produce a duplicate invoice. Expired quotes stay non-actionable and hidden while
refreshing, and stale-state notifications occur only on freshness transitions.
Sale-edit scope changes show an explicit instruction to close and reopen in the
owning workspace. Scheduled history stays read only and ordinary historical line prices
remain their recorded prices.
Permissions and rollout limits
Canonical actor/workspace membership remains server-owned. Hidden-module preferences do not grant or revoke authorization. Period discovery currently requires view-sales permission while quote GET also permits create-sales. A create-only actor may therefore be unable to discover a period; mobile fails closed rather than broadening access. Missing pricing schema/RPC (503), denial, invalid timezone, missing metadata or wallet-price currency mismatch block scheduled submission. Source presence on main does not prove production migration availability. Legacy selected-period assignment retains its existing separate creation/assignment semantics; mode changes racing that legacy path still need server rollout/integration review. Revenue/profit remain withheld: the analytics aggregate still needs verified currency, linked checkout/invoice deduplication, refund/cancellation and reporting timezone semantics. This change does not modify any financial aggregation.Regression evidence
apps/mobile/test/features/inventory/season_pricing/ covers interval boundaries,
currency separation, malformed quotes, timezone eligibility, product scope,
expiry/offline/denial, actor/workspace races, exact-payload idempotent retry, direct
atomic repository transport and mounted checkout/historical guards. The existing
partial-option checkout regression remains in the focused suite.
Recovery regressions cover persistent-store round trips, write-before-send,
restart/scope isolation, late completion, concurrent checkouts, rejection identity
retention, denied lookup and confirmed receipt handling. Mounted tests exercise
preference/cache failures after success, metadata Retry removal while uncertain,
and recovery after metadata denial. Twelve mounted scope-switch cases cover
actor and workspace A→B/A→B→A during delayed POST success, timeout and receipt
responses; they assert current-scope readiness and preserved original identity.
Route tests verify permissions, canonical actor
filters, minimal output and unavailable versus not-observed semantics.
Native keychain/keystore durability and exact-head mobile/API CI remain release
gates. Mocked storage does not prove native persistence or deployed endpoint
availability. This read-only satellite API uses the existing receipt schema;
no database migration or application is part of the recovery follow-up.
Ordinary catalog rows and legacy history keep their VND denomination even when
a selected wallet has another currency; no FX conversion is implied. Scheduled
rows retain their scheduled/recorded currency. Recovered confirmed sales remember
the income category with the same best-effort policy as initial submissions.