Skip to main content

Overview

apps/finance and the Finance experience inside apps/web are paired Finance hosts. They must stay 1:1 for product features, data behavior, mutations, permissions, route behavior, and user-facing workflow updates. Canonical workspace routes include:
  • /{wsId}
  • /{wsId}/transactions
  • /{wsId}/wallets
  • /{wsId}/invoices
  • /{wsId}/categories
  • /{wsId}/tags
  • /{wsId}/recurring
  • /{wsId}/budgets
  • /{wsId}/analytics
  • /{wsId}/debts
apps/web /{wsId}/finance/* routes render the same shared Finance product surface inside the normal platform dashboard shell. apps/finance keeps the shorter standalone route shape without the /finance segment. Use route prefixes instead of forks:
  • apps/web: financePrefix="/finance" and FinanceRouteProvider prefix="/finance".
  • apps/finance: financePrefix="" and FinanceRouteProvider prefix="".
Legacy nested category paths, such as /{wsId}/finance/transactions/categories, should redirect inside apps/web to /{wsId}/finance/categories while preserving query strings.

API Ownership

  • apps/finance owns the standalone workspace shell, Finance UI route wrappers, local /verify-token handoff, host-local auth/session routes, and build-info.
  • apps/web owns platform-shell Finance route wrappers under /{wsId}/finance/*.
  • apps/finance owns the hard-cutover product handlers for transactions and categories, wallets and checkpoints, budgets, debts, recurring transactions, invoices, charts and overview reporting, and Inventory reconciliation. It also has local supporting handlers for promotions, inventory products, settings, linked products/promotions, and workspace/user lookup needs.
  • The local recurring, invoice, and wallet examples include /api/v1/workspaces/:wsId/finance/recurring-transactions, /api/v1/workspaces/:wsId/finance/invoices, and /api/workspaces/:wsId/wallets. Exact local handlers win before the /api/:path* fallback rewrite is considered.
  • Deliberately central exceptions remain in apps/web: the legacy /api/v1/workspaces/:wsId/wallets list/create route, wallet-role whitelist routes, shared workspace storage routes used by transaction attachments, and the Finance exchange-rate cron. Unmatched requests reach Web through the fallback rewrite.
  • Keep shared callers on @tuturuuu/internal-api. Finance-owned helpers use the Finance API base URL for server calls, while helpers for the deliberate Web exceptions remain on the central API origin; browser calls stay same-origin.
  • Finance-local protected handlers accept the Finance app-session actor and enforce route-level workspace and product permissions. Forwarded Web exceptions require the coordinated Web-issued session and retain their own authorization checks.
  • Finance transaction attachment reads must authorize through the same authenticated transaction visibility path as normal transaction reads. Do not grant storage list, metadata, or signed-read URL access from coarse view_transactions or update_transactions permissions alone; reuse get_wallet_transactions_with_permissions with p_transaction_ids so wallet whitelists, viewing windows, and granular income/expense permissions stay aligned.
  • Finance transaction attachment uploads have server-side limits: 10 files per transaction and 50 MB per file. The signed-upload route must reject over-limit declared sizes before issuing a URL, and finalize must inspect the actual stored object size/count and delete over-limit uploads before any follow-up processing.
  • Finance transaction type filters must not classify confidential amount signs for callers without view_confidential_amount. RPCs that accept p_transaction_type should apply income/expense predicates only when the row is non-confidential or the caller can view confidential amounts; otherwise typed filters should omit those redacted rows instead of using the raw amount sign.
  • Finance invoice customer IDs must be validated against workspace_users with the route workspace before insert, and admin-backed invoice reads should resolve customer display fields with an explicit ws_id filter instead of a nested service-role join on customer_id.
When adding new Finance behavior, update the shared Finance UI/helper first, then wire both host wrappers in the same change. Add or extend the route in apps/finance when it belongs to a hard-cutover family; preserve the explicit Web exceptions until their dependent platform callers migrate. Expose shared access in packages/internal-api and consume it through TanStack Query or shared server components.

Inventory sales reconciliation

Finance owns the Inventory reconciliation inbox and its protected API under /api/workspaces/:wsId/finance/inventory-reconciliation. Access to pending entries, provider mappings, provider history synchronization, manual adjustments, and bulk link/unlink actions requires manage_finance. Every provider-confirmed Polar, Square POS, or Square Terminal event is first stored as an immutable private.inventory_finance_entries source row. The source row affects balances only after an atomic database RPC links it to a currency-compatible wallet transaction. Missing wallets stay pending and are never included in income, wallet, or net-total calculations. Provider-and-currency mappings take precedence over the Inventory revenue wallet fallback and the Finance default wallet. A wallet must use the entry currency. Category resolution uses the unanimous product category first, then the provider mapping, the Inventory default, and finally uncategorized. Deleting or explicitly unlinking a provider transaction removes only the ledger row and returns the immutable source entry to pending. Provider, reference, signed amount, and occurrence date remain provider-controlled; wallet, category, tags, description, and confidentiality remain editable. For rollout and recovery:
  1. Deploy the database migration before Inventory, Finance, or shared-package consumers.
  2. Verify historical completed provider sales appear in the pending inbox. The migration never auto-posts unmatched historical sales.
  3. Configure provider and currency mappings, inspect pending counts by currency, and only then use bulk linking.
  4. Run the bounded provider-history sync explicitly to discover historical refunds and Square disputes.
  5. If a ledger row was deleted accidentally, relink the still-present source entry. Do not recreate provider events manually.
  6. Use audited manual adjustments only when the provider has no supported event, such as a Polar chargeback.