> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Inventory

> How inventory.tuturuuu.com is wired as the inventory satellite app, operator console, public storefront, checkout reservation, and audit operations surface.

Inventory lives in `apps/inventory` and runs locally through Portless at
`https://inventory.tuturuuu.localhost`. Production is intended for
`https://inventory.tuturuuu.com`.

## Web sale item picker

Record sale shows one selectable row per product, unit and warehouse identity.
Overlapping loaded pages and repeated stock entries do not create duplicate rows
or add stock quantities. The later provided complete product snapshot supplies
amounts, prices and archived status; page position does not guarantee chronological
freshness, so server sale validation remains authoritative. Distinct products
with the same name and distinct unit/warehouse variants remain separate. Equal name or price
sort results use stock identity as a stable tie-breaker. Season eligibility and
scheduled pricing continue to apply after catalog normalization, and server
permissions and checkout validation remain authoritative.

The item list occupies the space remaining beneath the search/filter controls
and above the pinned sale footer. It scrolls within that space on mobile and
desktop, rather than stopping at a fixed list height. This behavior is covered
by `sale-create-items.test.ts`, `sale-product-picker.test.ts` and the mounted
`sale-product-picker-layout.test.tsx` regression; DOM checks do not establish
browser layout or customer-flow success.

Web Record sale uses two tabs: Items and Checkout. Checkout combines editable
cart, revenue details and review in compact accordions; collapsed headers retain
item counts, selected revenue targets and confirmation readiness. Missing revenue
or sale-name details open the Revenue section on entry. Going back to Items keeps
those edits and quantities. Confirmation remains an explicit action and retains
current season-price validation, required revenue fields, in-flight submission
protection, and the same request ID when retrying an uncertain response.

The Items list loads another page when its end is within 200 pixels of the
actual list viewport, including when the initial list is shorter than that
viewport. Raw page progress rearms loading even when filters/season eligibility
hide every new item. Only one page request starts at a time. A thrown or resolved
query error stops automatic loading and leaves a localized manual Load more
retry. Filters and tab/dialog closure disconnect the old observer; manual loading
also works when IntersectionObserver is unavailable. Regression evidence lives
in `sale-picker-pagination.test.tsx` and `season-sale-integration.test.tsx`;
these mounted tests do not establish live browser/provider success.

## Native Inventory presentation

The Flutter mini-app keeps Overview, Products, Sales, Manage and Storefront
destinations in the established floating navigation. Overview retains Sell and
Create product as visible workflows; it does not repeat the dock destinations.
Manage keeps every setup collection and its permitted add action visible without
a second counter dashboard. Season selection, creation, editing and archiving
remain visible on Sales.

Product summaries show metadata once and keep each quantity and price beside its
warehouse and unit. Quantities across different units are not summed. A null
quantity means unlimited stock and must never display as zero or a low-stock
warning. An absent stock row is shown as unconfigured. The Products low-stock
count covers loaded search results, not the entire catalog. Native presentation
does not change server permissions, mutation authorization, or canonical workspace
membership. Existing pending-sync indicators remain attached to their owning rows.

Native Overview analytics describe the loaded sample, not workspace-wide totals.
Receipt activity groups unique dated receipts by device-local day for the selected
7- or 30-day view and reports records without dates separately. Loaded revenue
keeps currencies separate, excludes pending receipts and duplicate IDs, and
reports receipts without an explicit currency. Stock summaries keep incompatible
units separate and preserve unlimited versus unconfigured stock. These rules are
covered by `inventory_analytics_redesign_test.dart`.

Manage opens dedicated catalog, sales-period and audit destinations. The native
product and checkout forms retain their primary actions while options refresh;
queued deletion rows retain their existing retry or manual-conflict controls.
Verified permission hints remain isolated by account and workspace and are
revalidated online; an online denial revokes them. Server authorization remains
authoritative. `inventory_access_scope_test.dart`,
`workspace_permissions_offline_test.dart` and the Inventory editor tests cover
scope changes, revocation and pending writes.

Products, Sales and searchable forms submit through native search actions. An
empty or whitespace-only submission closes search and clears the filter. A
nonempty submission keeps results visible and dismisses the keyboard. Old shell
search callbacks cannot replace the current query or update an unmounted owner.
`inventory_search_submit_test.dart` covers mounted shell and standalone form
submissions; the shared Apps search policy uses the same empty-query behavior.

The product editor preserves unlimited stock when an unrelated field is edited.
Leave quantity empty to represent unlimited stock; finite zero remains zero.
Manage clears previous rows and add controls when the authenticated account or
selected workspace changes. An open setup sheet cannot submit into a different
account/workspace scope. These UI safeguards supplement server authorization.

Native Inventory writes accept pending setup selections. The dependency queue
creates referenced owners, manufacturers, categories, units, warehouses and
Finance categories before product writes, then applies their authoritative IDs.
The earlier sync-first guard is replaced by automatic prerequisite handling.
Repository regressions in `inventory_setup_dependencies_test.dart` cover both
product writes, all six pending references, nullable optional fields and
resource-scoped mappings. Legacy already-completed setup creates whose old API
returned no ID cannot retroactively recover local-ID provenance; that historical
limitation remains tracked in [issue #5737](https://github.com/tutur3u/platform/issues/5737).

This compact presentation does not establish parity for web commerce checkout,
payment providers, stock ledgers, scoped analytics, costing or effective season
pricing. Those require their existing authorized API contracts and separate
verification; today's catalog price must not replace historical sold prices.

## Automatic offline dependency sync implementation

Native period-sale creation omits optional notes when none were entered; it does
not send JSON null to the non-nullable optional server field. Empty entered notes
remain an empty string. A confirmed invoice remains a successful sale if local
cache invalidation fails; the client attempts scoped resource-cache eviction
without repeating the financial write. Server validation and authorization
failures still propagate. The native HTTP regressions in
`inventory_api_mutation_contract_test.dart` cover omitted, empty and populated
notes, durable receipts and post-acknowledgment cache failure. Read contracts are
covered by `inventory_api_read_contract_test.dart`. These client safeguards do
not change server permissions, season pricing or stock accounting.

Native create and supported Inventory mutations persist their operation identity
before any HTTP request, including successful online creates. Online callers
receive the authoritative acknowledged resource; offline or temporarily blocked
callers keep their optimistic representation. This source behavior becomes
available after the matching mobile/API/database release has passed its gates.

A queued product write must wait for referenced Inventory owners, manufacturers,
categories, units, warehouses and Finance categories to be created, regardless
of enqueue order. Dependencies and mappings belong to the authenticated account,
workspace and resource type. Related mutations retain their per-entity order;
independent ready writes can progress. Missing prerequisites and dependency
cycles remain visible without deleting queued work.

Account/workspace purges serialize with durable pending writes. Malformed encrypted
pending records remain visible as conflicts while independent valid records can
continue; corrupt reference metadata cannot abort the entire replay pass. Online
sale-period clearing preserves the nullable server response. These boundaries are
covered by `offline_inventory_review_regression_test.dart` and the foreground
verification regressions in `offline_inventory_verification_test.dart`.

Product create permits at most 500 stock rows. Stock quantity and minimum quantity
must be integers, matching their database columns; unit prices preserve fractional
major currency amounts. `offline-create-schema.test.ts` and
`inventory-offline-create.sql` cover these limits for the offline create contract.

The dedicated setup/product/period create contract returns authoritative IDs and
deduplicates the original operation identity before the client retries an uncertain
create. The client never falls back to a legacy create route when this contract is
missing.
After acknowledgment, the queue persists the mapping and rewrites dependent
paths/payload references. An acknowledged create must not be sent again because
mapping storage or cache refresh failed. Automatic sync must preserve user data
across account switches, restart, retry and temporary network loss.

A missing endpoint or RPC retains the mutation and retries with bounded backoff,
connectivity, authentication and app-resume wakeups. Permission/domain failures,
canceled/deleted prerequisites, malformed payloads, ambiguous creators and cycles
remain stored with localized diagnostics; they cannot block unrelated ready work.
Confirmed create and write acknowledgments survive restart before mapping/dequeue.
Successful deletions persist resource-scoped tombstones, preventing stale mappings
from restoring deleted resources. Automatic replay never invents a replacement
for a canceled/deleted prerequisite. Typed references never rewrite free text,
workspace path segments or query parameters.

The dedicated online scheduled-sale journal keeps its existing receipt recovery
contract. Legacy invoices without an inventory period do not gain retry safety;
an uncertain legacy invoice remains retained for review, rather than duplicating
its financial effects. Wallets retain their existing stable client-ID contract.
Regression evidence is in `offline_inventory_replay_test.dart`,
`offline_dependency_graph_test.dart`, `offline_inventory_mutation_test.dart`,
`finance_repository_inventory_dependency_test.dart` and
`offline_changes_sheet_test.dart`; these synthetic tests do not establish native
airplane-mode or authenticated production acceptance.

The app is a registered Tuturuuu satellite app. It uses Tuturuuu app-session
auth for `targetApp: 'inventory'`, exposes local `/verify-token` and
`/api/auth/verify-app-token` handoff routes, and forwards fallback `/api/*`
traffic to centralized `apps/web` APIs. Keep protected inventory, payment,
Stripe Connect, checkout, Square Terminal, and audit mutations behind `apps/web` so provider
secrets, bot protection, request logging, and Observability stay centralized.
Workspace-scoped Inventory APIs in `apps/web` must authorize through
`authorizeInventoryWorkspace`, which accepts the Inventory app-session cookie
and then performs workspace membership and permission checks. Authenticated
dashboard and workspace API access is permission-driven; do not hide these
routes behind the `ENABLE_INVENTORY` workspace config. Keep public storefront
delivery separate, since published storefront behavior may still apply
storefront-specific rollout gates. Do not use `resolveAuthenticatedSessionUser`,
`supabase.auth.getUser()`, or request-scoped Supabase clients as the primary auth
gate for these routes, because the satellite clears local Supabase cookies and
forwards app-session cookies instead.
The operator console uses the same collapsible Tuturuuu satellite workspace
structure as the Tasks, Finance, Calendar, and CMS apps: dashboard layouts should
keep server work to auth/workspace gating, then render client-side views backed
by TanStack Query and `@tuturuuu/internal-api`. Do not reintroduce a separate
Inventory-only app shell or direct client Supabase reads for protected workspace
data.
The local token verifier must call the central Web verifier with
`verificationBaseUrl: WEB_APP_URL`; Inventory protected routes require both the
host-only Inventory app-session cookie and the Web-issued app-session cookie
used by rewritten `apps/web` API requests. Local-only token validation will
create a redirect loop back to platform login. Current-user bootstrap APIs such
as `/api/v1/users/me/default-workspace` and `/api/v1/users/me/profile` must keep
`inventory` in their app-session audience allowlist, or `/dashboard` will accept
the local handoff cookies and then bounce back to Web auth after the bootstrap
request returns `401`.

Public storefront routes live at `/store/[storeSlug]` inside `apps/inventory`
and are intentionally exempt from the operator auth proxy. They should only
read published storefront data through `apps/web` public APIs and should create
checkout reservations through the central `apps/web` RPC wrapper. Do not add
storefront CRUD, reservation writes, invoice finalization, or settlement writes
directly to `apps/inventory`.

## Local Development

Use:

```bash theme={null}
bun dev:inventory
```

The package `dev` script runs Portless. For direct port debugging, run
`apps/inventory` with `bun dev:app`, which falls back to port `7815`.

## Product Boundary

Inventory starts with workspace-scoped surfaces for:

* product catalog categorization by type, owner, manufacturer, talent,
  supplier, channel, and fulfillment policy
* stock movement and reservation ledgers
* bundle and promotion availability controls
* checkout fee visibility for processing fees, Tuturuuu platform fees,
  conversion fees, settlement estimates, and net payout
* payment and inventory audit streams
* Stripe Connect readiness for B2B2C sellers who link their own Stripe account

The commerce storefront extension adds:

* operator routes for overview, catalog, stock, bundles, storefronts,
  checkouts, sales, setup readiness, and audits
* public routes for `/store/[storeSlug]`,
  `/store/[storeSlug]/products/[listingId]`, cart, checkout, and order status
* private-schema tables for storefronts, listings, bundles, checkout sessions,
  checkout lines, reservations, Square connection state, terminal checkout
  identifiers, and settlement ledger entries
* commerce money (listing/bundle prices, checkout amounts, settlement, and
  costing) stored in integer minor units of the row currency (cents for USD,
  whole units for JPY/VND); convert with `@tuturuuu/utils/money` and enter with
  the shared `MoneyInput`. See the Polar storefront and Square Terminal
  integration runbooks.
* Square Terminal settings for workspace app credentials, OAuth/manual tokens,
  location selection, device pairing, webhook verification, and commerce actions
  that send, cancel, and reconcile terminal payments after local stock is
  reserved. The same settings surface provides guarded Square catalog and stock
  import, additive publish, and conflict-aware two-way sync. Provider deletion
  is deliberately unsupported.
* service-role-only RPCs for creating reservations, releasing or expiring
  reservations, materializing checkout TTL expiry, and linking a completed
  checkout to a `finance_invoice`
* private sales periods that group both invoice-backed and completed checkout
  sales into seasons, conventions, campaigns, or other operating windows. A
  sale has at most one period assignment, while archived periods keep their
  historical assignments and remain available for reporting.
* durable Finance source entries for every completed real-provider checkout,
  refund, Square chargeback hold/release, and audited provider-unavailable
  adjustment. Simulated checkouts are excluded. Manual Inventory sales continue
  using their existing Finance invoice path.

The Inventory Sales list shows whether a provider checkout is linked, pending,
refunded, or disputed and links to the corresponding Finance transaction or
reconciliation entry. Unlinked entries are operationally visible but do not
affect Finance ledger totals.

New commerce tables belong in the `private` schema, not `public`. Public and
satellite clients must go through Inventory-owned `apps/inventory` APIs plus
`@tuturuuu/internal-api`; direct Supabase client access to these tables is not
part of the contract. Protected workspace routes must authenticate and normalize
the workspace with the request-scoped client before any private-table read or
write, then use server-only database access or service-role RPCs behind
`apps/inventory`.

Convention selling uses an explicit sales period. The sale dialog defaults to an
in-range configured period, otherwise the sole current scheduled period. An intentionally configured current
legacy default is honored; legacy-only implicit selection requires a choice. Overlapping
or absent current periods require a choice. Operators can explicitly choose the
legacy unassigned workflow; it is never an automatic fallback.

New periods default to effective season pricing with explicit dates and an IANA
timezone. Prices are major currency units, scoped to product, unit, warehouse and
period. Price intervals start at local midnight; end dates are inclusive. A new
later interval closes an earlier interval without replacing its amount. Price
history does not alter stock quantity, legacy catalog prices or completed invoice
snapshots. Existing periods remain in legacy pricing mode until deliberately
changed; the migration performs no price backfill. Priced period dates, timezone
and pricing mode are immutable. Use another period to change those boundaries.

Scheduled selling filters eligible active products and requires a current price
in the workspace currency. Gaps and missing prices prevent checkout; another
season's prices are never borrowed. Price controls require catalog-management
permissions. Managers can preview upcoming selected-season prices in the price
history, labeled with effective dates and the season timezone. Viewing a future
price does not make it eligible for checkout at the current sale time.
Offline/stale carts require refresh and review, with no silently
changed total. Period-linked invoice creation validates the quote and eligibility
and captures line amounts, stock changes and period assignment in one private
transaction after the existing Finance authorization checks. Historical invoices,
reports and completed commerce orders continue using their saved amounts. Price
receipts survive invoice deletion as durable idempotency tombstones: deleted
requests cannot recreate a canceled sale, and restored invoices retain the same
receipt and captured prices. Legacy manual periods keep six-decimal major-unit
precision, existing currency fallback and existing finite overselling behavior.
Provider listings/promotions and their minor-unit prices are intentionally
unchanged. Scheduled manual sales keep the existing custom-price workflow's
no-promotion semantics and require a matching wallet currency.

The additive migration must be installed before scheduled controls can be used.
Missing tables/RPCs fail closed; never apply production migrations manually.

Sales-period clients use the Inventory-owned routes below. Keep these paths in
the Flutter API mapping check whenever the mobile catalog changes:

* `GET/POST /api/v1/workspaces/:wsId/inventory/sales-periods`
* `PATCH/DELETE /api/v1/workspaces/:wsId/inventory/sales-periods/:periodId`
* `GET/POST /api/v1/workspaces/:wsId/inventory/sales-periods/:periodId/prices`
* `PUT /api/v1/workspaces/:wsId/inventory/sales/:saleId/period`
* `GET /api/v1/workspaces/:wsId/inventory/sales?period_id=:periodId`

Core stock and setup data lives in private inventory tables:
`private.inventory_products`, `private.inventory_units`,
`private.inventory_warehouses`, `private.inventory_suppliers`,
`private.inventory_batches`, `private.inventory_batch_products`,
`private.inventory_owners`, `private.inventory_audit_logs`, and
`private.inventory_manufacturers`. Dashboard pages and APIs should access them
through server-owned `apps/web` routes. Prefer private-schema RPCs for product
catalog, low-stock, and other repeated join-heavy reads; call them with
`createAdminClient().schema('private').rpc(...)` from server code.

Manufacturers are a normalized workspace setup entity matching the
supplier-style management model. Products store only
`workspace_products.manufacturer_id`; API responses may still include
`manufacturer` as a display name for older clients. Legacy imports that send
package/product manufacturer text should upsert the trimmed name into
`private.inventory_manufacturers`, assign the resulting `manufacturer_id`, and
avoid writing manufacturer text back to `workspace_products`.

Bundle components are a workspace-scoped stock contract. When creating or
updating a bundle, every component's product, unit, warehouse, and stock row
must belong to the same workspace as the bundle. The database trigger on
`private.inventory_bundle_components` enforces that invariant for direct writes,
and the checkout reservation RPC re-checks the same workspace before locking
stock. Do not trust stored bundle component UUIDs as already authorized input.

Do not hardcode Stripe fee schedules as durable truth. The checkout estimator is
for quoting and operator review; production reconciliation should persist the
estimate shown to the seller and then reconcile it against actual Stripe balance
transaction fee rows after settlement.

## Deployment

Inventory has dedicated Vercel workflows:

* `.github/workflows/vercel-preview-inventory.yaml`
* `.github/workflows/vercel-production-inventory.yaml`

These workflows use `VERCEL_INVENTORY_PROJECT_ID`. Add that secret before
enabling hosted deployments for `inventory.tuturuuu.com`.

Price history references durable product/unit/warehouse identities instead of the
replaceable stock tuple. Combined product edits and dedicated stock edits for
priced products commit metadata, tuple changes and movement history together;
a failed edit leaves no partial metadata or phantom stock movement. Scheduled
period scope/rule replacement takes the same period lock as checkout in a single
transaction. Search filters never invalidate cart quotes; selecting the current
period is a no-op. Price authoring forwards the scoped catalog pagination so
products after the first page can be selected without preloading another dialog.

Atomic pricing-aware edit routing also applies before a product has its first authored price and while a legacy period is being converted. The database transaction resolves current state under its locks; routing never relies on an earlier mode or price-existence read. Checkout, stock editing and price authoring acquire stock tuples before the product row to avoid mixed legacy/scheduled lock inversion. If price support is installed but its edit transaction is unavailable, edits fail closed instead of falling back to separate writes.

On native mobile, an active user Save may open the existing verification dialog
for that record only. Background replay never opens verification dialogs or
persists verification tokens. Canceling that dialog leaves the already-requested
Save queued: the editor closes and the item keeps its existing **Waiting to sync**
indicator, rather than claiming server completion or creating another operation
on a repeated Save. The original operation identity is reused on later replay.
Explicitly discarding the queued change cancels unsent work; canceling verification
does not discard it. Account changes and real membership or permission denials
retain their original error behavior. Regression coverage lives in
`offline_inventory_verification_test.dart` and the mounted product editor tests.

### Transactional offline create validation

Automatic dependency sync uses a separate deduplicated create
contract. Its private receipt and all resource, stock, history and audit effects
commit together. Reusing an operation identity with different input is rejected.
The service authorizes the captured account, workspace and resource permission
before invoking this contract; customers cannot read receipts or forge actors.

`.github/workflows/inventory-offline-contract.yaml` validates the exact PR head
by applying the complete schema in a disposable hosted database, running strict
`inventory-offline-create.sql` pgTAP assertions, and generating
`packages/types/src/supabase.ts` as an artifact. A queued workflow is not schema
or type parity evidence. The native scheduler persists operation identities before
sending, orders prerequisites, and rewrites acknowledged references automatically.
An unavailable server contract retains queued work and retries automatically; it
never falls back to an unsafe legacy create. Release readiness still requires the
exact-source database, generated-type, native, and owning-app CI gates.

The new `/api/v1/workspaces/:wsId/inventory/offline-mutations` POST route
accepts an operation identity, an explicit create kind and a strictly validated
payload. Current workspace membership and the owning kind permission are checked
on every retry, even when a prior receipt exists; a revoked grant denies the
request. Inventory stock initialization additionally requires stock permission.
Finance categories use `create_transactions` and accept Inventory or Finance
app sessions. Receipt identities have no TTL or automatic pruning.

Recognized responses carry `X-Tuturuuu-Offline-Contract`. Missing RPC/schema
returns `OFFLINE_CONTRACT_UNAVAILABLE` with 503; an unknown route on an older
deployment has no contract header. These rollout gaps must retain the operation
and retry automatically, while a marked domain 404 or permission 403 is permanent.
The native implementation must never fall back to an undeduplicated create POST.

Offline catalog create receipts intentionally have no time-based expiry. Their
private storage lasts for the owning actor/workspace lifetime and is removed by
those owners' deletion cascades. The mobile outbox has no maximum retry age, so
pruning a receipt could replay an old successful create as a new resource.
Changing this default requires a coordinated client/server operation expiry and
server rejection of expired operation IDs before receipt deletion is enabled.
This rule covers inventory catalog/setup create receipts, including stored
request/response data; it does not introduce a financial transaction retention
policy. The private table is inaccessible to ordinary client roles. Receipt
replay and payload-identity checks in `inventory-offline-create.sql` guard the
current behavior; lifecycle pruning remains intentionally disabled.

An offline create RPC that completes with a malformed receipt or a different
resource returns sanitized HTTP 500 `OFFLINE_CONTRACT_RESPONSE_MISMATCH`.
No row IDs are exposed, and the same operation remains retained for operator
review. A missing RPC function/schema remains HTTP 503
`OFFLINE_CONTRACT_UNAVAILABLE`. These distinguish a server receipt defect from
contract availability; the client must not blindly resend a new create identity.
`offline-create-route.test.ts` covers malformed receipts and resource mismatches.

## Merge products and warehouses

The Inventory web operator exposes **Merge products** above the product table and
**Merge warehouses** in Setup. Select a source and destination, review the current
stock/reference preview, choose which metadata and overlapping price, minimum-stock
and revenue-share configuration to keep, then acknowledge the destination before
confirming. Both update/manage access, inventory deletion permission, and stock
adjustment permission are required. This is ordinary authenticated inventory work;
it is not an offline bulk download and must not require download-protection setup.

Product selectors search active products independently of catalog filters and the
currently loaded table page. Each selector supports loading more results. Warehouse
stock reviews resolve product names by identity; confirmation waits for those names
and offers a retry when they cannot load. On narrow screens the dialog fills the
viewport, keeps its actions pinned, and scrolls the review body; long record names
wrap rather than push conflict details outside the screen. Failed previews and
failed merges retain the dialog and require a fresh acknowledgment before retrying.
Regression coverage: `inventory-merge-dialog.test.tsx` and
`inventory-merge-product-options.test.tsx`.

Different unit structures are supported: separate units remain separate stock rows
and quantities are never converted between units. Matching finite stock quantities
are added exactly once. Two unlimited rows remain unlimited. Mixed unlimited and
finite rows are blocked until the operator explicitly adjusts that stock outside
the merge. Active reservations and active/upcoming scheduled source prices block the operation. Duplicate provider mappings,
storefront variants, bundle identities and other live unique identities are shown
as conflicts and must be resolved in their owning editor before merging; no provider
objects or conflicting records are silently deleted. The global price policy also
chooses the associated minimum stock and revenue-share settings together.

The merge is one database transaction with a current preview version and a durable
receipt. Concurrent stock/reference changes invalidate the preview; retries of the
same source, destination, version and policies do not add stock again. Live references
move to the destination while invoices, movement history, scheduled price quotes and
checkout snapshots keep their original IDs and values. Source products are archived;
source warehouses are removed from new selectors. Their durable rows remain available
for historical detail. Future writes using a merged source identity, including stale
web/mobile/offline/provider writes, reject atomically with a refresh-and-select-the-
destination message. Historical updates that do not change their identity remain
allowed. Authorized invoice recovery is the narrow exception for historical inserts:
it restores only exact saved invoice lines with their original product and warehouse
IDs, after invoice recovery permissions and workspace checks. A private transaction
capability restricts this exception to the restoration RPC; new sales and stale writes
cannot use it, including by setting a restoration flag. Restoring history does not
consume the combined stock again.

New merge preview and apply require the final schema readiness RPC and the active
warehouse view, restoration guard and writer guards. During deployment before gated
migrations, ordinary warehouse reads may use the legacy table only after the existing
product-form-options RPC successfully returns a valid warehouse array without an
`inventoryMergeSchema` marker. Alias-table creation atomically replaces that RPC to
return `aliases-v1`; marker presence and a missing active view fail closed. Cached
PostgREST metadata cannot substitute an older server function body, and a missing-table
API error alone cannot establish legacy schema. Merge actions remain unavailable
until readiness succeeds.

Merge transactions reserve the buckets containing the source and destination identities.
Identity coordination uses 16 stable buckets per resource kind and workspace, so
large imports and stock/reference updates acquire at most 32 identity keys within
one workspace instead of one advisory lock per record. Product/warehouse parents
supply the authoritative workspace for all writers; a child row cannot choose a
different namespace. Different workspaces have separate bucket namespaces. Two
unrelated identities in the same workspace can share a bucket: when a merge holds
it exclusively, that unrelated write safely returns retryable `55P03` rather than
waiting or bypassing guards. Retry after the merge finishes. Actual workspace
removal exempts its deletion cascades because that workspace can no longer merge;
ordinary deletes in a live workspace retain all identity and merged-source guards.
Reference inserts, updates, deletes and checkout state changes coordinate with those
identities; unrelated workspace CRUD and other buckets remain available. Contention fails
fast and requires refresh/retry. Row-lock waits are limited separately; this does not
establish a deadline for the entire RPC. New reference tables must install both the
merged-reference guard and the identity writer coordination trigger before merging
can use them.

Offline creation receipts retain their original identity rather than creating
a replacement resource; operators must refresh and select the destination for new work.

The feature does not execute automatic background merges, convert units, merge external
provider objects, or delete historical identities. New tables that reference inventory
identities must install the merged-reference guard as part of their migration; the
preview enumerates current foreign keys and fails closed for unsupported composite
relationships. Regression evidence lives in `inventory-merge.sql`, `inventory-merge-restoration.sql`,
`inventory-merge-concurrency.mjs`, `inventory-merge-readiness.sql`, the merge API route
tests, internal API transport tests, and Inventory merge-dialog tests. Deployment needs
the additive merge and repair migrations before execution becomes available; missing merge
RPCs return a temporary-unavailable response while existing warehouse lists continue
working on an older schema.

The existing `inventory-offline-contract.yaml` CI lane applies the full isolated
schema and runs offline create plus merge, alias pagination, restoration and
readiness pgTAP fixtures. Its testing phase also runs the three-client
`inventory-merge-concurrency.mjs` executor; SQL or concurrent-writer failures
fail the gate and still pass through owned-stack cleanup.

## Merge sales seasons and periods

The Inventory web operator can merge two sales periods using an explicit, paged
review. Both preview and apply require workspace membership, `update_invoices`
and `delete_invoices`, plus `manage_inventory_catalog` or legacy
`update_inventory`. The Rust migration source mirrors this contract; Web remains
the live API. The sales page derives the merge action from this complete role
contract on the server; missing or failed permission lookup hides the action.
Catalog management alone does not expose it. Regression coverage includes
`sales/page.test.tsx` and `sales-periods-panel.test.tsx`. Flutter has no
season-merge action in this change.

The destination keeps its name, dates, time zone and pricing mode. Description
and the complete product eligibility rule set each require a source/destination
choice. With live or upcoming source prices, calendar or pricing-mode differences
block merging. Without those prices, calendar differences are reviewable and the
destination calendar remains unchanged. Chosen eligibility rules must allow every
source price product; the review displays excluded-price counts before approval.

Overlapping schedules require either blocking the merge or explicitly keeping
the destination overlap. The latter imports only uncovered source intervals with
fresh price IDs, clipped to merge time. Every original price, captured invoice
quote and invoice line stays intact; stock and movement history are unchanged.
Reporting assignments move to the destination, with their original classification
stored relationally. Replaced destination product rules retain every original
rule identity, label and timestamp in the receipt, including rules beyond the first
preview page. The source is archived as a durable alias, excluded from new
selectors, and cannot be edited, restored or newly referenced. Trusted invoice
recovery and offline request replay continue to use the original durable quote.

Prices, overlap conflicts and product rules are paged at 50 rows per section.
Confirmation requires explicit policies, resolved human labels and review of all
pages. A server token binds actor, workspace, both periods and a frozen cutoff,
and expires after five minutes. Its fingerprint hashes affected mutable planning
rows and retained-history ownership counts without aggregating historical
payloads. Refresh after expiry, concurrent changes, unresolved conflicts or a
retryable readiness failure; acknowledgment is invalidated before retry.

Merge apply and child writers coordinate on only the selected period rows.
Assignment/price writers recheck aliases after waiting; unrelated periods and
workspaces remain available. Missing schema/guards return 503 and perform no
merge. Historical provenance foreign keys are deferred until transaction end,
allowing whole-workspace cascade ordering while rejecting standalone removal of
referenced periods and original/imported quotes.

Regression coverage: `inventory-season-merge.sql` exercises actual migration,
history, trusted recovery and transaction-boundary constraints;
`inventory-season-merge-concurrency.mjs` uses independent PostgreSQL clients for
writer rejection and unrelated CRUD. Both run in the existing Inventory contract
CI lifecycle, including official type generation and owned cleanup. Route,
internal API transport and dialog tests cover authorization, tokens, errors,
policies, expiry and pagination. App builds and Rust tests require exact-commit CI;
source and mocked UI checks do not establish browser or production behavior.

The season test’s cascade control retains quote tombstones after trusted invoice
deletion. Deletion of a workspace that still contains a live invoice has a separate
parent-schema defect: the invoice product unit foreign key attempts a null default
against a non-null column. An isolated parent-schema control reproduced that
error before applying the new season migrations. The expected-defect control is
retained as local validation evidence, outside the passing season CI contract. Season merging does not alter that existing schema.

Season merge previews and review pages are scoped to the authenticated Inventory
actor and workspace. Switching accounts resets the visible review and prevents
late responses from completing another account's dialog. Before submitting,
Inventory saves one immutable actor/workspace-bound request (original source,
destination, policies and preview token) in the current tab's session storage.
Storage failure prevents sending. Closing/reopening or reloading that tab
restores an unresolved request; it is never replaced by a fresh preview.

For an ambiguous response or authentication pause, Check original merge replays
the exact saved request. The existing apply RPC checks its committed receipt
before preview expiry or merged-source validation, so a lost success response
can be reconciled after the token expires or source is archived. A conflict or
validation response allows fresh review only for a first known attempt. Once
uncertainty exists, even a retry 409 retains the original request: a lock timeout
can occur while its earlier transaction still commits. Recovery is manual and
per tab; it does not claim a cross-tab journal or new offline navigation support.
Mounted regressions in `use-season-merge-recovery.test.tsx` and
`use-inventory-season-merge-review.test.tsx` cover scope changes, late responses,
remount recovery, storage failures and lost-response/conflict/receipt sequences
with synthetic transport. Existing isolated SQL receipt replay coverage remains
the database evidence; browser/device and exact-CI evidence remain separate.

## Cleared checkout search

The web Inventory sale dialog restores its loaded product catalog immediately when
product search is cleared. Category and warehouse choices and scheduled price
eligibility derive from those restored products. A previous empty response or
in-flight response cannot replace the unfiltered catalog. During a new search,
loaded products remain searchable while the server query settles. This changes
search selection only; current season price validation and existing cart snapshots
remain authoritative. Mounted regressions in
`season-sale-integration.test.tsx` cover settled empty search, clearing an in-flight
search, restored category/warehouse choices, and the original scheduled prices in
the submitted sale.

The checkout category and warehouse pickers use the full authorized workspace
form options, independent of product search, pagination, or season price coverage.
Selecting a category queries its products across pages; search results have their
own pagination. Categories with no currently eligible stock remain selectable and
show an empty product result rather than disappearing from the picker. The
mounted unloaded-category regression uses synthetic data outside the initial
catalog page and verifies searchable choices, category-scoped requests, and the
second page. This does not imply every product has a current scheduled price.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.