apps/inventory and runs locally through Portless at
https://inventory.tuturuuu.localhost. Production is intended for
https://inventory.tuturuuu.com.
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. 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 ininventory_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.
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 ininventory_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: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
- 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/moneyand enter with the sharedMoneyInput. 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.
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-periodsPATCH/DELETE /api/v1/workspaces/:wsId/inventory/sales-periods/:periodIdGET/POST /api/v1/workspaces/:wsId/inventory/sales-periods/:periodId/pricesPUT /api/v1/workspaces/:wsId/inventory/sales/:saleId/periodGET /api/v1/workspaces/:wsId/inventory/sales?period_id=:periodId
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
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.