@happyvertical/smrt-commerce 0.57.35 → 0.57.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,17 +3,17 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-commerce",
6
- "packageVersion": "0.57.35",
6
+ "packageVersion": "0.57.37",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "4806a800fd54cff63c923d10d1e3425ef5333069c6c4bc871fee58ff272a2cad",
11
- "packageJson": "3d0bcb891837f5391eebb43302093cc73ae71b3dd51cd030d0e61db410309d7a",
10
+ "manifest": "abf5d17dc3303f3fa9a95c8d1240b6aee0e39cd8417e1e4853cd44455d03b4d7",
11
+ "packageJson": "fafa7b9802cbeb5852d16f8b44b865a67675bcb27205885d551f79bc3a92478c",
12
12
  "agents": "8fae989030badeabdb15109fab804aa7910c01570d449046c24a04678fe432a7",
13
13
  "moduleDoc:agents/customer-vendor-ui.md": "2b22e519411a16d8c493496fc168da77b9acd25f6077ea8fbac00cfee3a3501c",
14
14
  "moduleDoc:README.md": "beb2fada61d0babc7aa0528b883a9d47d6c827d876ace1cce04a908338fa01f8",
15
15
  "moduleDoc:agents/invoice-quantity.md": "3a5fbb464c8fe63acc021b2259ba36f052bb22c49b29e503639c5316814e2028",
16
- "moduleDoc:agents/connected-payments.md": "dcffea1cf3f90e84f0bfc87a1d6f617b6158880560dd40bac808693b7f8aaef3",
16
+ "moduleDoc:agents/connected-payments.md": "aa0f6c61179d5b8e780c447ac27c482e3475b57147169e4698dd8492940586c1",
17
17
  "moduleDoc:agents/historical-sales.md": "d7e68bb3a93495c91449b7406a5eefdcddb54fafea83d14d5ba8e2c8d6168de4",
18
18
  "moduleDoc:agents/payment-allocations.md": "679894f603f62522515bce338741bafda3f10d16ef0b817b105dc2e732471fdb",
19
19
  "moduleDoc:agents/tax-codes.md": "6b07282f0ac93db4fd42ddb4a9551d880e24ec2bd2fae9d06c06561313b3cb9f",
@@ -16774,6 +16774,152 @@
16774
16774
  "tags": [],
16775
16775
  "risks": []
16776
16776
  },
16777
+ {
16778
+ "name": "TenantPaymentDispute",
16779
+ "qualifiedName": "@happyvertical/smrt-commerce:TenantPaymentDispute",
16780
+ "collection": "tenantpaymentdisputes",
16781
+ "description": "One authoritative dispute snapshot and its immutable balance evidence.",
16782
+ "tableName": "tenant_payment_disputes",
16783
+ "packageName": "@happyvertical/smrt-commerce",
16784
+ "extends": "SmrtObject",
16785
+ "fields": [
16786
+ {
16787
+ "name": "tenantId",
16788
+ "type": "text",
16789
+ "required": true,
16790
+ "columnType": "UUID"
16791
+ },
16792
+ {
16793
+ "name": "backendId",
16794
+ "type": "text",
16795
+ "required": false,
16796
+ "columnType": "TEXT",
16797
+ "default": ""
16798
+ },
16799
+ {
16800
+ "name": "environment",
16801
+ "type": "text",
16802
+ "required": false,
16803
+ "columnType": "TEXT",
16804
+ "default": "test"
16805
+ },
16806
+ {
16807
+ "name": "providerAccountId",
16808
+ "type": "text",
16809
+ "required": false,
16810
+ "columnType": "TEXT",
16811
+ "default": ""
16812
+ },
16813
+ {
16814
+ "name": "disputeId",
16815
+ "type": "text",
16816
+ "required": false,
16817
+ "columnType": "TEXT",
16818
+ "default": ""
16819
+ },
16820
+ {
16821
+ "name": "paymentId",
16822
+ "type": "foreignKey",
16823
+ "required": false,
16824
+ "related": "Payment",
16825
+ "columnType": "UUID"
16826
+ },
16827
+ {
16828
+ "name": "status",
16829
+ "type": "text",
16830
+ "required": false,
16831
+ "columnType": "TEXT",
16832
+ "default": "unknown"
16833
+ },
16834
+ {
16835
+ "name": "snapshotJson",
16836
+ "type": "text",
16837
+ "required": false,
16838
+ "columnType": "TEXT"
16839
+ }
16840
+ ],
16841
+ "relationships": [
16842
+ {
16843
+ "name": "paymentId",
16844
+ "type": "foreignKey",
16845
+ "required": false,
16846
+ "related": "Payment",
16847
+ "columnType": "UUID"
16848
+ }
16849
+ ],
16850
+ "methods": [
16851
+ "delete",
16852
+ "save"
16853
+ ],
16854
+ "methodSignatures": [
16855
+ {
16856
+ "name": "delete",
16857
+ "async": true,
16858
+ "params": [
16859
+ "_options?: SmrtDeleteOptions"
16860
+ ],
16861
+ "returns": "Promise<void>"
16862
+ },
16863
+ {
16864
+ "name": "save",
16865
+ "async": true,
16866
+ "params": [
16867
+ "options?: SmrtSaveOptions"
16868
+ ],
16869
+ "returns": "Promise<this>"
16870
+ }
16871
+ ],
16872
+ "tenant": {
16873
+ "scoped": true,
16874
+ "mode": "required",
16875
+ "field": "tenantId"
16876
+ },
16877
+ "conflictColumns": [
16878
+ "tenant_id",
16879
+ "slug",
16880
+ "context"
16881
+ ],
16882
+ "surfaces": [],
16883
+ "withheldSurfaces": [
16884
+ {
16885
+ "kind": "api",
16886
+ "operation": "delete",
16887
+ "code": "api-disabled",
16888
+ "reason": "api is disabled",
16889
+ "objectName": "@happyvertical/smrt-commerce:TenantPaymentDispute"
16890
+ },
16891
+ {
16892
+ "kind": "api",
16893
+ "operation": "save",
16894
+ "code": "api-disabled",
16895
+ "reason": "api is disabled",
16896
+ "objectName": "@happyvertical/smrt-commerce:TenantPaymentDispute"
16897
+ }
16898
+ ],
16899
+ "relationshipFeatures": [
16900
+ "foreignKey",
16901
+ "uuidColumns"
16902
+ ],
16903
+ "tags": [],
16904
+ "risks": []
16905
+ },
16906
+ {
16907
+ "name": "TenantPaymentDisputeCollection",
16908
+ "qualifiedName": "@happyvertical/smrt-commerce:TenantPaymentDisputeCollection",
16909
+ "collection": "tenantpaymentdisputes",
16910
+ "tableName": "tenant_payment_disputes",
16911
+ "packageName": "@happyvertical/smrt-commerce",
16912
+ "extends": "SmrtCollection",
16913
+ "fields": [],
16914
+ "relationships": [],
16915
+ "methods": [],
16916
+ "surfaces": [],
16917
+ "relationshipFeatures": [
16918
+ "uuidColumns"
16919
+ ],
16920
+ "tags": [],
16921
+ "risks": []
16922
+ },
16777
16923
  {
16778
16924
  "name": "TenantPaymentWebhookEvent",
16779
16925
  "qualifiedName": "@happyvertical/smrt-commerce:TenantPaymentWebhookEvent",
@@ -16846,6 +16992,13 @@
16846
16992
  "columnType": "TEXT",
16847
16993
  "default": ""
16848
16994
  },
16995
+ {
16996
+ "name": "paymentId",
16997
+ "type": "text",
16998
+ "required": false,
16999
+ "columnType": "TEXT",
17000
+ "default": ""
17001
+ },
16849
17002
  {
16850
17003
  "name": "applicationFeeJson",
16851
17004
  "type": "text",
@@ -22042,12 +22195,12 @@
22042
22195
  ],
22043
22196
  "prompts": [],
22044
22197
  "relationshipsV2": {
22045
- "foreignKeyFields": 47,
22198
+ "foreignKeyFields": 48,
22046
22199
  "crossPackageRefFields": 19,
22047
22200
  "junctionCollections": 0,
22048
22201
  "hierarchicalObjects": 0,
22049
22202
  "polymorphicAssociations": 0,
22050
- "uuidColumns": 270
22203
+ "uuidColumns": 276
22051
22204
  },
22052
22205
  "agentDoc": "# @happyvertical/smrt-commerce\n\nE-commerce with Contract STI hierarchy, invoice lifecycle, payment tracking, payout remittance, and optional ledger integration.\n\n## Models\n\n- **Customer** / **Vendor**: linked to Profile via string ID (not FK). `contacts` (JSON) is written via [`savePartyContacts()`](agents/customer-vendor-ui.md#contact-rows-3501). Customer has creditLimit, paymentTerms, customerType (DTC / WHOLESALE / RETAIL). Vendor has leadTimeDays, minimumOrder, and `payoutAddresses: Record<string, string>` — a flat map from payout-rail-qualified currency code (`USDC-base`, `BTC`, `USD-stripe`, ...) to destination string (EVM address, BTC address, Stripe Connect account id, IBAN). Use `getPayoutAddress(currency)` for a `Map.get`-style lookup; missing entries return `undefined` (caller decides skip-vs-error).\n- **Contract**: concrete STI base. Abstract **SalesContract** owns `customerId`; **PurchaseContract** owns `vendorId`. `channelId` is open-ended. Before upgrade, run `preflightCommercePartyLinks(db)` and repair linked base rows, stray parties, and wrong-branch links. See [README](README.md).\n- **WholesaleOrder**: B2B order. Conventional pairing — customer has `customerType: 'wholesale'`, NET-30/60 terms, delivered via wholesale-portal channel.\n- **ProductionOrder**: manufacturing equivalent of a PurchaseOrder — commission your factory to make finished goods. Consumes raw materials per BOM (`@happyvertical/smrt-manufacturing`) and produces SKU stock (`@happyvertical/smrt-inventory`).\n- **Cart**: transient order-in-progress. Same shape as Order; the application promotes the row from `_meta_type: Cart` to `_meta_type: Order` at checkout instead of copying data between tables.\n- **LicenseSale**: industry-neutral licensing primitive. Carries an *immutable* rights snapshot (`rightsMedium`, `rightsDistributionScope`, `rightsExclusivity`, `rightsDuration`, `rightsTerritory`, `rightsSublicensing`, `rightsDerivatives` — typed `Meta<T>` fields), licensee identity (`licenseeEmail`, optional `licenseeLegalEntity` / `licenseeJurisdiction`), and a signed-PDF reference (`pdfUrl`, `pdfHash`, optional `onChainHashRegistryRef`). Once saved at `ContractStatus.ACCEPTED`, the rights snapshot is frozen — mutating any of the seven rights fields and re-saving throws. The only legal transition out of ACCEPTED is `revoke()` (moves to CANCELLED without touching rights). Useful for stock media, music licensing, code-asset marketplaces, license keys, anywhere rights are sold for a fee.\n- **ContractLineItem**: items on contracts.\n- **Invoice**: status machine `DRAFT → SENT → VIEWED → PARTIAL → PAID` (also OVERDUE, CANCELLED, WRITTEN_OFF). `recognizeRevenue()` creates balanced AR journal entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable).\n- **InvoiceLineItem**: fractional-quantity line items with safe minor-unit validation/recalculation on save; the existing-column migration is documented in [invoice quantity persistence](agents/invoice-quantity.md).\n- **Payment** / **PaymentAllocation**: tracks payments against invoices (status: see Gotchas). Carries optional backend-adapter fields for PaymentBackend-routed flows: `backendId` (rail adapter id — `base-usdc`, `btc`, `stripe`, distinct from `externalProvider` which names an accounting sync destination), `backendTxRef` (chain tx hash or gateway settlement id), `nativeAmount` / `nativeCurrency` (what actually arrived), `usdAtQuote` / `usdAtConfirmation` (drift accounting for volatile-currency rails). `Payment.usdDrift()` returns the confirmation - quote delta, or `0` when either side is unset.\n- **PaymentInstrument**: reusable saved payment method (\"card on file\"). Stores only provider references and non-sensitive display metadata (`providerCustomerId`, `providerPaymentMethodId`, brand / last4 / expiry), never raw card data. `isDefault` is domain-managed, not API-writable: `PaymentInstrumentCollection.setDefaultForCustomer(customerId, instrumentId)` validates the target and clears the customer's other defaults (one per customer).\n- **PaymentIntent**: short-lived pre-payment commitment with multi-option semantics. Locks a USD price for a fixed window (default 15 minutes) and lists one or more `PaymentOption`s describing different rails (`backendId`, `currency`, `payTo`, `nativeAmount`, optional `chain` / `memo` / `x402Capable` / `expiresAt`). First option to receive payment wins; the others are implicitly retired. State machine `awaiting_payment → paid → (issued | retired)` plus `expired` and `cancelled`. Mutate via dedicated `markPaid` / `markIssued` / `expire` / `cancel` / `retire` helpers — direct status assignment bypasses the invariant checks. Idempotency: see the natural-key Gotcha.\n- **TenantPaymentAccount**: tenant-owned Stripe Connect account; direct charges with an application fee, account→tenant webhook routing, refunds, no platform fallback. Write only through `connected-payments.ts`; see [connected payments](agents/connected-payments.md).\n- **Payout**: operator-to-supplier remittance. Distinct from Payment because direction, status machine, and chain semantics differ. Status machine `pending → sent → confirmed → failed` (failed is terminal but resettable via `resetFromFailed()` after fixing the underlying problem). References source `paymentId` (plain string) and destination `vendorId` (foreign key). Amount invariant `supplierNet === grossAmount - operatorFee` enforced exactly on save (integer minor units, no tolerance — #2401). `PayoutCollection.createFromPayment()` is the typical entry point; it pulls native amount / currency from the source Payment.\n- **Fulfillment** / **FulfillmentLineItem**: shipment/delivery tracking.\n\n## Recipes (#3590)\n\n`src/recipes.ts` declares four minimal `SmrtRecipe`s, emitted into the built\n`manifest.json` and `smrt-knowledge.json` `recipes`: `commerce.customers`\n(Customer), `commerce.vendors` (Vendor), `commerce.sales` (Order, requires\ncustomers), `commerce.purchases` (PurchaseOrder, requires vendors). Headers\nonly; line items, fulfilment, invoices, and payments are later recipes. Each\nhas user-facing help in `src/<name>.recipe.md` (#3591), and the fields those\nrecipes show carry `@field({ description })` text; keep both in plain\nlanguage for end users and update them when a recipe's fields change. See\n[core recipes](../core/agents/recipes.md).\n\nSee [historical issued sales](agents/historical-sales.md) for exact original source\nmoney, immutable source children and combined historical/live customer funds.\nSee [payment allocation integrity](agents/payment-allocations.md) for atomic splits,\nretry identity, parent corrections and supported dialects.\nSee [tax codes](agents/tax-codes.md) for tenant-owned rate components, default codes,\nand the standard codes every tenant gets.\nSee [document money persistence](agents/document-money.md) for the one money\nmodel: tax-code resolution, atomic aggregate writes, issued snapshots, and\nfrozen fields. Every Invoice and Contract money write needs a tenant. Host\nbootstrap is `ensureCommerceTenant(db, tenantId, { cookbook })`, which returns\n`{ tenantId }`. Money data written by an earlier version is refused with\n`COMMERCE_DATA_NEEDS_UPGRADE` until `smrt db:upgrade-commerce-money` converts\nit; see [money data upgrade](agents/money-upgrade.md).\nSee [Contract money persistence](agents/contract-money.md) for the Contract\naggregate and purchase recoverability.\n\n## Ledger Integration\n\n`@happyvertical/smrt-ledgers` is a regular dependency, loaded lazily via dynamic `import()` (never a static import) so the package graph stays a DAG (#1582). Invoice stores `arJournalId` and `revenueJournalId` as string references. `recognizeRevenue()` creates a balanced AR journal entry; `getArJournal()` returns null when no journal has been recognized yet.\n\n## Billing-period close (#3060)\n\n`src/billing/` closes periods from smrt-subscriptions charges; see the README\nfor usage.\n\n- **Port, not provider.** Period close, events, and credit checkout call only\n the `BillingProvider` port (integer minor units). `stripe.ts` adapts\n `@happyvertical/accounting`, which takes currency *major* units; convert with\n `units.ts` and never hand-roll a Stripe call here. `units.ts` exponents are\n an explicit ISO 4217 table matching the SDK's, never `Intl` display digits\n (CLDR differs for IQD, MGA, … and drifts by ICU release). The SDK's\n `*Minor` fields are already ISO minor units. Capabilities a rail may lack\n (`chargeSavedPaymentMethod`, `createSetupCheckout`, `getCheckout`,\n `setDefaultPaymentMethod`, `markInvoiceUncollectible`) are optional port\n methods: detect them by presence, and keep additions optional (#3139).\n `sendAndReconcile` refuses a provider invoice whose subtotal or total\n disagrees with the local invoice.\n- **Replay safety is identity.** Close, invoice, line, payment, allocation,\n and claim ids are `deterministicId()`s; `BillingLineSource` (conflict key\n `source_type, source_id`) is the only double-billing guard, so never bill a\n source without claiming it, and never widen that key. Each close step is\n persisted before the next; `acquireCloseLease()` serializes workers. A\n close that nets to a credit keeps its claims and ends `carried_forward`;\n its subtotal is a `credit_carry_forward` source for the payer's next billed\n close, which marks the carried close `completed`.\n- **Cycles and flat-plan coverage (#3116).** `cycles.ts` is the schedule\n (pure; `BillingAccount.billingAnchorAt`, null = calendar months). A close\n with no period closes each payer's `lastEndedBillingPeriod()`. Flat-plan\n claims are the exception to \"id from the source\": their `sourceId` is\n `<subscriptionId>:seq:<chainSequence>` and their `periodStart/End` is the\n exact time they bill. `claimFlatWindow()` reads the last number, then the\n coverage (claims ending after the window start), claims one gap as number\n n + 1, and retries on conflict; reading in that order is what makes it\n safe. That numbering, not the period, stops overlaps when schedules or\n payers change — never claim a flat plan outside it, and keep both reads\n bounded (no whole-history scans). Each numbered claim is its own invoice\n line keyed `lineKey|periodStart` (a function of the claim alone, so a\n resumed close rebuilds the same line ids). Proration is\n `prorateMinorUnits()` (half up, BigInt) against the period's full cycle.\n- **System tables.** `BillingAccount`, `BillingPeriodClose`, and\n `BillingLineSource` are not tenant-scoped and have no generated surface.\n Collections stay unexported; models are root exports (#3082). Invoices,\n payments, and journals are written in the seller's tenant; payer-owned\n subscriptions and credit grants in a system context, only after\n verification.\n- **Events** use smrt-jobs' delivery inbox under provider\n `<name>-billing:<sellerTenantId>` and a provider-filtered\n `ForgeProjectionRuntime`, so a runtime never claims another seller's\n events; any other runtime sharing the inbox must pass `providers` too. Only\n events this package acts on are stored: foreign checkouts are `ignored`,\n and only `smrt_*` checkout metadata is kept; it is\n HMAC-signed with the webhook secret at creation (empty values are omitted:\n Stripe drops them), so another integration on the same account cannot forge\n a credit purchase or a card setup; credit is granted only when the\n collected total equals it plus provider tax. Checkout amounts come from the\n re-read session (`getCheckout()`), never the Stripe event, whose amounts are\n in Stripe's unit. Rotating the webhook secret strands in-flight checkouts\n (up to 24 h). `observe()` re-reads provider state, so ordering never depends\n on delivery order; the only provider writes in `observe()` are idempotent\n (making a saved card the default). The standing hook runs inside the event\n transaction and must be idempotent.\n- **Card on file and top-ups (#3139).** A saved card is the payer customer's\n default `PaymentInstrument` (seller tenant, id from the provider method).\n `autoChargeInvoices` bills such payers `charge_automatically`, decided once\n and saved as `Invoice.collectionMethod` before the first push, which every\n retry reuses (never recompute it: a lost push response would record a\n method the provider was not asked for, #3190); activation follows `paid`, never send. An auto top-up attempt\n is a `PENDING` `Payment` (`reference` `auto-top-up:<policyId>`, id from the\n policy and `count()` of its attempts) inserted before the charge, so racing\n evaluations derive one id and one charge; the charge key is derived from\n that id. `applyAutoTopUpOutcome()` must run in a transaction and starts with\n a conditional `UPDATE … WHERE status = 'pending'` row lock, so the hook and\n the `payment` webhook settle once; the grant is keyed by the charge key.\n Never settle an attempt outside that function. Taxed accounts are charged\n with provider tax by default (#3194): the attempt row holds the credit\n (the charge subtotal), the tax is journaled to tax payable at settlement,\n and the attempt's tax decision is recorded in its `notes` so a re-drive\n repeats the same charge request (the provider refuses a key reused with\n different tax). `claimAttempt()` is the only place that decides a\n skip and reports it (`onAutoTopUpSkipped`); `cardOnFile()` and period close\n use `defaultCardInstrument()` — never copy that predicate. A provider write-off moves the local invoice to\n `WRITTEN_OFF` (a later payment, by card or on a rail (#3188), is recorded\n and reinstates the payer; the status stays, so \"paid in full\" for a\n written-off invoice is read from its allocations).\n- **Billing invoices are money-model invoices.** The provider taxes them\n (`providerTaxAmount`, server-managed, not API-writable, added to line tax),\n so every billing line is an explicit `taxMode: 'override'` to the seller's\n `out-of-scope` code. Charges are written before credits so the running\n subtotal never goes negative, and provider tax is written in the save that\n issues the invoice. Revenue posting is idempotent. `ledger.bookCurrency` is\n required (ISO 4217, validated by `BillingRuntime`) and must equal the\n billing currency. `toAccountingInput({ mapping })` emits major units.\n- **Payment rails (#3138).** A runtime has one issuing `provider` and any\n number of `paymentProviders` (unique names, one inbox namespace each; a\n provider with `capabilities.issuesInvoices === false` can never issue).\n `observe()` resolves the provider from the delivery namespace, never the\n payload. Crypto rails implement the port over `@happyvertical/payments`'\n `CryptoCheckoutGateway`; never call BTCPay (or any gateway) from here.\n Settlement is decided by the pure `decideAttempt()` from gateway state and\n `paymentPolicy` — the gateway's `settled` is authoritative and nothing here\n counts confirmations. A `BillingPaymentAttempt` (`(provider, checkout_id)`\n deterministic id) records every state and settles once; grants and payments\n are keyed by checkout id. Invoice payments close the issuer's invoice out of\n band in `observe()` (idempotent) before `project()` records the payment, and\n `settlePaidInvoice` skips (records nothing for) a `paidOutOfBand` invoice\n the rail latched with `outOfBandRequestedAt`; other out-of-band closes are\n recorded as `OTHER` payments. Rails never save cards or charge tax: a taxed\n credit purchase on a rail needs an explicit `automaticTax: false`. Exceptions are flags for operators;\n nothing reverses or refunds automatically.\n- **Known limits**: write-offs post no bad-debt journal (AR stays until paid\n or journaled by an operator); discounts still reach Stripe as negative lines (the SDK's\n line `discount` would change the lines a replayed push reconciles, so it\n needs its own migration); `RetailCharge` has no adjustment ledger. Flat\n plans are monthly, in arrears; plan changes are not prorated (#3119).\n\n## Cross-Package References\n\n- `customerId` → `@foreignKey('Customer')` (hard reference within package)\n- `vendorId` → `@foreignKey('Vendor')` on PurchaseContract only\n- `profileId` → plain string to smrt-profiles\n- `arJournalId`, `revenueJournalId` → plain string to smrt-ledgers\n- `skuId` (on `PaymentIntent`, `LicenseSale`) → plain string to smrt-products\n- `paymentId` (on `PaymentIntent`, `Payout`, `LicenseSale`) → plain string to `Payment` (same package, but kept as plain string for cross-model consistency)\n- `vendorId` on `Payout` → `@foreignKey(Vendor)` (hard reference within package)\n- Billing records reference tenants with `@crossPackageRef('@happyvertical/smrt-users:Tenant')` and charges, plan periods, and adjustments by plain `sourceId`\n\n## Gotchas\n\n- **Optional tenancy**: all models `@TenantScoped({ mode: 'optional' })` + nullable tenantId\n- **Money units and arithmetic**: money fields use signed integer minor units; rates and quantities remain decimal where their contracts require them. Exact rounding, tax-code resolution, safe-integer validation, and journal units are defined in the [commercial money model](agents/money-model.md) and [money calculator](agents/money-calculator.md). Do not reintroduce floating-point tolerances on money paths.\n- **Money documents need a tenant**: models are optionally tenant-scoped, but every Invoice and Contract money write requires one (`MoneyValidationError` `INVALID_MONEY_INPUT`, `Commerce money documents require a tenant`).\n- **Earlier-version money data is refused**: never add a read or conversion path for it. Detection and the one-shot upgrade are in [money data upgrade](agents/money-upgrade.md).\n- **Book currency is required**: `recognizeRevenue()` and `cancelWithRevenueReversal()` take a required `bookCurrency`, which must equal the invoice currency.\n- **Asset-native amounts stay outside line rescales**: `Payment.nativeAmount` and `PaymentIntent.paymentOptions[].nativeAmount` are per-asset values identified by `nativeCurrency`; normalize each asset to its own minor unit before any type conversion, and never apply the commerce ×100 line-money rescale to them.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax comes from tax codes**: the server resolves each line's code and computes line tax and header totals; client totals and a line's stored `taxRate` are never inputs\n- **Profile linking**: separate `ProfileCollection.create()` needed to fetch actual Profile object\n- **PaymentIntent natural key**: `conflictColumns: ['tenant_id', 'offering_ref', 'licensee_email', 'idempotency_key']` — a retried `create` with the same tuple upserts the existing row. Use `PaymentIntentCollection.getOrCreateByIdempotencyKey()` to branch on `{ intent, created }`. Empty natural-key inputs (e.g. blank `idempotencyKey`) disable dedup by design.\n- **Payout state-machine guards**: `markSent` requires a non-empty `backendTxRef`; `markConfirmed` only valid from `SENT`; `markFailed` only valid from `PENDING` / `SENT` (a confirmed payout can't fail — that path is a refund). `resetFromFailed()` is the dedicated escape hatch from `FAILED` back to `PENDING` after an operator fixes the underlying problem; it clears `backendTxRef` so the next attempt picks up a fresh one.\n- **LicenseSale immutability**: rights snapshot freezes on save-with-status-ACCEPTED. The captured snapshot lives in a module-scoped `WeakMap<LicenseSale, string>` so it doesn't interact with the schema or round-trip through `_meta_data`. Drafts (status != ACCEPTED) remain mutable. To \"change\" an issued license: `revoke()` it, then issue a new `LicenseSale` row.\n- **Vendor.payoutAddresses normalization**: the constructor accepts either a `Record<string, string>` or a pre-serialized JSON string. `initialize()` re-normalizes after the framework's option-override pass; `save()` re-normalizes defensively against direct field assignment. Non-string values inside the input map are silently dropped to preserve the typed invariant downstream.\n\n## Commercial UI\n\n`CustomerSelect`/`VendorSelect` are the registered selectors for Customer and\nVendor (`selects` in `src/ui.ts`); models carry `ui.widget` / `ui.format` hints. See\n[customer/vendor UI](agents/customer-vendor-ui.md#selectors-3602).\n\nSee [commercial UI composition](agents/commercial-ui.md) for native form recovery, amount compatibility and backend ownership. Package-owned guides under `agents/` ship with the package.\n\n## Quote and estimate UI\n\n`src/svelte/quote-types.ts` separates unparsed draft strings from explicit\nminor-unit revision DTOs. Native row submitters, caller-owned retry identity and\nextension conventions are documented in [agents/quote-ui.md](agents/quote-ui.md).\n\n## Purchasing UI\n\n`PurchaseOrderEditor` preserves allocation IDs and explicit zeroes, including\nretained reduction rows; the server validates review fingerprints before recording.\nSee [agents/purchasing-ui.md](agents/purchasing-ui.md) for caller responsibilities.\n\n## Exact cost billing\n\nPure signed cost billing and explicit tax rounding are available through the\nindependent `cost-billing` subpath; see [contract](agents/cost-billing.md).\n\nSee [contract billing schedules](agents/contract-billing-schedules.md) for exact\napproved milestone capacity, immutable amendments and atomic canonical invoice issue.\n\n## Customer funds (#4079)\n\nSee [customer advances](agents/customer-funds.md) for explicitly configured\nunapplied-AR/deposit-liability receipts, conserved application/refund capacity,\nreal liability reclassification, final-invoice attestation and mutation guards.\nUse the public commands; `Payment.customerFundsJson` is command-owned.\n\nSee [observed purchase prices](agents/observed-purchase-prices.md) for incomplete\noriginal supplier prices, explicit unknown totals and reviewed completion.\nSee [received sales agreement money](agents/received-sales-contract-money.md) for\nexact complete source Order/Agreement evidence without synthetic lines or tax recognition.\n\nSee [ordinary invoice accounting adjustments](agents/ordinary-accounting.md) for\nnon-retained provider credit applications, recorded refunds and durable recovery.\n",
22053
22206
  "moduleDocs": [
@@ -22069,7 +22222,7 @@
22069
22222
  {
22070
22223
  "path": "agents/connected-payments.md",
22071
22224
  "module": "connected-payments",
22072
- "content": "# Tenant-owned connected payment accounts (#3831)\n\nA tenant (for example a co-op) is paid into its own Stripe Connect **Standard**\naccount. Checkouts are direct charges on that account, the platform's share is\nan application fee, and the platform never holds the tenant's money. Source:\n`src/connected-payments.ts`, `src/models/TenantPaymentAccount.ts`,\n`src/models/TenantPaymentWebhookEvent.ts`.\n\n## Model\n\n`TenantPaymentAccount { tenantId, backendId, environment, providerAccountId,\nstatus, chargesEnabled, detailsSubmitted, payoutsEnabled, capabilitiesJson,\ncapabilitiesObservedAt, feePolicyRef, disabledAt, disabledReason }`.\n\n- Tenant-scoped (`required`), native UUID tenant ids. Two unique indexes:\n `(tenantId, backendId, environment)` and\n `(backendId, environment, providerAccountId)`, so the database refuses a\n second owner of a provider account even if a command check were bypassed.\n- `status`: `pending` (details not submitted), `restricted` (details\n submitted, charges or payouts off), `enabled` (charges, details and payouts\n all on), `disabled` (operator; refreshes keep it off). Only `enabled` takes\n new charges.\n- Read-only generated surface (REST, MCP and CLI: `list`/`get` and read methods only); never deleted. `save()` refuses any write outside the\n commands, any change of tenant/backend/environment, and a provider account\n change outside the relink command.\n- Connected `Payment` rows and connected `PaymentIntent` creation/option\n writes need the commands' internal authority (`connected-payments-internal.ts`,\n per tenant), so no generated REST/MCP/CLI write can forge or advance them;\n connected payments and refunds are never deleted. A checkout's identity\n includes its environment (`PaymentIntent.connectedEnvironment`): the same\n `acct_` id exists in test and live.\n- `TenantPaymentWebhookEvent` (`_smrt_tenant_payment_webhook_events`, system\n table, unique `(backendId, environment, eventId)`) stores processed event ids;\n the SDK's dedupe is in memory only.\n- `PaymentIntent` and `Payment` gain server-managed `connectedAccountId` and\n `applicationFeeCents` (`Payment` also `refundOfPaymentId` and\n `connectedRefundState`), frozen once settled. A connected intent settles only\n with a Payment on the same account and fee, and its account, fee and payment\n options (which name the provider session settlement re-reads) are frozen\n from creation, so a generated update cannot repoint it. A connected\n `Payment` (charge or refund) keeps its kind, amounts, currency, provider\n references, account, fee and refunded payment from its first write, even\n while pending.\n\n## Commands\n\nAll take an explicit `tenantId` and check ownership against it, so a\nprivileged or system caller is refused the same way. There is no fallback to\nthe platform account.\n\n| Command | Does |\n|---|---|\n| `onboardTenantPaymentAccount` | Create the Standard account (per-tenant idempotency key) and store it; returns the existing one on retry. |\n| `createTenantOnboardingLink` | Single-use onboarding URL. A bearer secret: redirect, never log or store. |\n| `linkTenantPaymentAccount` | Link an existing account; `replace: true` relinks. Refused (`ACCOUNT_IN_USE`) when another tenant holds it or its `smrt_tenant_id` metadata names another tenant. An account whose metadata names no tenant needs `ownershipVerified: true` (the host proved control, for example Stripe OAuth). |\n| `refreshTenantPaymentAccount` | Snapshot from `getAccountCapabilities`, re-derive status. |\n| `setTenantPaymentAccountDisabled`, `setTenantPaymentFeePolicy` | Operator switches. |\n| `requireEnabledTenantPaymentAccount` | Throws `NO_ENABLED_ACCOUNT` unless enabled. |\n| `createConnectedCheckout` | Checkout on the tenant's account with `stripeAccount` + `applicationFeeAmount` (integer minor units from the caller's fee policy; `0` for none; below the amount). The intent (account, environment, fee, expiry) is recorded before the provider call and the option naming the provider session is added once, so a retry replays the same request (the expiry is part of the provider idempotency key) and recovers a lost answer; once the recorded expiry is under 31 minutes away the retry asks for a fresh session with a renewed expiry (the lost URL never reached the buyer). The requested amount and currency are recorded too: a retry with a different price, environment, account or fee is refused; a retry of a paid checkout returns it with `settled: true` (no URL; never charge again); a retry of an unpaid closed (expired, cancelled or lapsed) quote gets `QUOTE_CLOSED`; an explicit `expiresAt` too close to replay must be replaced by the caller. |\n| `handleConnectedPaymentWebhook` | See below. |\n| `refundConnectedPayment` | Refund on the tenant's account; `refundApplicationFee` is required; partial and full. The refund row is written (`requested`, holding its amount) before the provider is asked; only a provider-`succeeded` refund is journaled, `pending` waits for the refund update, `failed` (including a first request the SDK refused before sending) releases the amount; a re-sent request must repeat its `refundApplicationFee` (stored in the frozen `applicationFeeRefunded`). A retry with the same key returns a recorded result, or re-sends a lost request with the same provider idempotency key within 23 hours (older lost requests wait for the refund update). Once booked refunds cover the payment it is marked `REFUNDED` and its intent retired. |\n| `createStripeConnectGateway` | Platform `StripeAdapter` from smrt-secrets. |\n\n## Webhooks\n\n`handleConnectedPaymentWebhook({ db, gateway, environment, endpoint, payload,\nsignature, resolveSettlementAccounts })`:\n\n1. The SDK verifies the signature **before** routing, then maps the event's\n connected account to its `TenantPaymentAccount` (cross-tenant read under\n `withSystemContext`, the one reviewed cross-tenant path). Unknown accounts\n throw the SDK's `PaymentNotOwnedError`.\n2. The event's `livemode` must match `environment` (`ENVIRONMENT_MISMATCH`).\n3. A stored event id returns `{ duplicate: true }` with its first outcome (and the `application_fee` details, so a host can still book them).\n4. `account.updated` applies the snapshot unless an earlier second than the\n stored one (`stale_account_update`). `application_fee.*` (platform endpoint,\n `endpoint: 'platform'`) is routed to the tenant and reported, not booked.\n A payment event loads the quote **in the resolved tenant** and refuses\n (`QUOTE_NOT_OWNED`) unless it belongs to that tenant and account, then\n re-reads the Checkout Session from the provider, so delivery order never\n matters. A provider-confirmed checkout settles even when its notice\n arrives after the price lock (the session expires with the lock); a\n payment confirmed for a quote already expired or cancelled is reported\n `late_payment` and posts nothing. `account.updated` reloads the row and\n refuses an event for a provider account the row no longer holds; a\n concurrent operator write fails the revision-guarded save (retried by the\n provider). Refund updates (`charge.refund.updated`, `refund.*`) book or fail\n a pending refund recorded by `refundConnectedPayment` on that account;\n terminal states never move back. Refunds made by the command carry\n `metadata.smrt_refund_id` and are matched by it (an unknown one throws\n `REFUND_NOT_RECORDED`; an amount differing from the row throws\n `AMOUNT_MISMATCH`). Refunds made elsewhere are reported. Settlement records a `Payment` (deterministic id from the intent)\n through `recordPayment()` and marks the intent paid. `refunded` events are\n reported (`refund_reported`), never booked automatically.\n5. The event id is stored only after processing succeeds; a refusal throws,\n so respond non-2xx and nothing unprocessed is acknowledged.\n\n## Accounting\n\nThe direct charge arrives net of the fee, which the provider withheld at\nsource: `recordPayment()` debits cash `amount - fee`, debits\n`applicationFeeAccountId` `fee`, and credits receivables `amount`. The fee is\nan expense already paid, never a payable to the platform. A payment with a fee\nand no `applicationFeeAccountId` is refused. Refund journals reverse the\nrefunded amount; a refunded application fee is reported by\n`application_fee.refunded` for the host to book.\n\n## Operator setup\n\n- smrt-secrets (held by the installation tenant, `secretsTenantId`):\n `stripe-connect-<env>-secret-key` (`sk_`/`rk_` key of that environment),\n `stripe-connect-<env>-webhook-secret` (platform endpoint),\n `stripe-connect-<env>-connect-webhook-secret` (Connect endpoint). Values\n never appear in errors.\n- Stripe: one platform endpoint (`application_fee.created`,\n `application_fee.refunded`) and one Connect endpoint (`account.updated`,\n `checkout.session.completed`, `checkout.session.async_payment_succeeded`,\n `checkout.session.async_payment_failed`, `checkout.session.expired`,\n `charge.refunded`, `charge.refund.updated`), per environment.\n- Each tenant needs ledger accounts for cash, receivables and the platform fee\n expense, returned by `resolveSettlementAccounts`.\n\n## Known limits\n\nA `requested` refund the provider never made (a request lost in transport\nand not retried within 23 hours, or one the provider rejected — the SDK does\nnot expose the HTTP status, so a rejection cannot be told from an unreadable\nsuccess) holds its amount and books nothing until an operator resolves it.\nAutomatic reconciliation needs an SDK refund read and error status\n([sdk#1430](https://github.com/happyvertical/sdk/issues/1430)).\n\nRelinking strands open checkouts on the old account and refuses refunds of\nits payments (`ACCOUNT_NOT_OWNED`). Concurrent refunds with different keys are\ncapped by the provider, not locally. Payments confirmed after an intent\nexpired or was cancelled are reported `ignored` for an operator.\n\n## Frozen fee-policy provenance (#4263)\n\nAt the authoritative price calculation, pass `feePolicyRef` to\n`createConnectedCheckout`: an opaque immutable version reference (for example\n`bakery-membership:version:1`). It is host-attested evidence, not a lookup or an\nauthorization grant; the host must resolve and authorize the policy within the\nexplicit tenant before calling. This command validates account ownership and\nnever reads another tenant's policy. Nonempty references cannot contain leading\nor trailing whitespace. Omission stores the empty string for unversioned hosts.\n\nThe intent's server-managed `connectedFeePolicyRef` freezes **before** the\nprovider request, alongside price and fee, including when that request fails.\nEvery retry must repeat the original reference, even when a new agreement\nversion yields the same cents. A mismatch is refused before any provider call.\nKeep `offeringRef` and `idempotencyKey` tied to the purchase; changing them to\nencode an amendment would create another charge. Amendments to the account's\nlive `feePolicyRef` cannot rewrite a checkout. The reference follows settlement\nto `Payment` and any command-created refunds and cannot be edited there.\nExisting checkouts with no reference accept only an omitted reference on retry;\nthey are never retrospectively attributed to the current account policy.\n"
22225
+ "content": "# Tenant-owned connected payment accounts (#3831)\n\nA tenant (for example a co-op) is paid into its own Stripe Connect **Standard**\naccount. Checkouts are direct charges on that account, the platform's share is\nan application fee, and the platform never holds the tenant's money. Source:\n`src/connected-payments.ts`, `src/models/TenantPaymentAccount.ts`,\n`src/models/TenantPaymentWebhookEvent.ts`.\n\n## Model\n\n`TenantPaymentAccount { tenantId, backendId, environment, providerAccountId,\nstatus, chargesEnabled, detailsSubmitted, payoutsEnabled, capabilitiesJson,\ncapabilitiesObservedAt, feePolicyRef, disabledAt, disabledReason }`.\n\n- Tenant-scoped (`required`), native UUID tenant ids. Two unique indexes:\n `(tenantId, backendId, environment)` and\n `(backendId, environment, providerAccountId)`, so the database refuses a\n second owner of a provider account even if a command check were bypassed.\n- `status`: `pending` (details not submitted), `restricted` (details\n submitted, charges or payouts off), `enabled` (charges, details and payouts\n all on), `disabled` (operator; refreshes keep it off). Only `enabled` takes\n new charges.\n- Read-only generated surface (REST, MCP and CLI: `list`/`get` and read methods only); never deleted. `save()` refuses any write outside the\n commands, any change of tenant/backend/environment, and a provider account\n change outside the relink command.\n- Connected `Payment` rows and connected `PaymentIntent` creation/option\n writes need the commands' internal authority (`connected-payments-internal.ts`,\n per tenant), so no generated REST/MCP/CLI write can forge or advance them;\n connected payments and refunds are never deleted. A checkout's identity\n includes its environment (`PaymentIntent.connectedEnvironment`): the same\n `acct_` id exists in test and live.\n- `TenantPaymentWebhookEvent` (`_smrt_tenant_payment_webhook_events`, system\n table, unique `(backendId, environment, eventId)`) stores processed event ids;\n the SDK's dedupe is in memory only.\n- `PaymentIntent` and `Payment` gain server-managed `connectedAccountId` and\n `applicationFeeCents` (`Payment` also `refundOfPaymentId` and\n `connectedRefundState`), frozen once settled. A connected intent settles only\n with a Payment on the same account and fee, and its account, fee and payment\n options (which name the provider session settlement re-reads) are frozen\n from creation, so a generated update cannot repoint it. A connected\n `Payment` (charge or refund) keeps its kind, amounts, currency, provider\n references, account, fee and refunded payment from its first write, even\n while pending.\n\n## Commands\n\nAll take an explicit `tenantId` and check ownership against it, so a\nprivileged or system caller is refused the same way. There is no fallback to\nthe platform account.\n\n| Command | Does |\n|---|---|\n| `onboardTenantPaymentAccount` | Create the Standard account (per-tenant idempotency key) and store it; returns the existing one on retry. |\n| `createTenantOnboardingLink` | Single-use onboarding URL. A bearer secret: redirect, never log or store. |\n| `linkTenantPaymentAccount` | Link an existing account; `replace: true` relinks. Refused (`ACCOUNT_IN_USE`) when another tenant holds it or its `smrt_tenant_id` metadata names another tenant. An account whose metadata names no tenant needs `ownershipVerified: true` (the host proved control, for example Stripe OAuth). |\n| `refreshTenantPaymentAccount` | Snapshot from `getAccountCapabilities`, re-derive status. |\n| `setTenantPaymentAccountDisabled`, `setTenantPaymentFeePolicy` | Operator switches. |\n| `requireEnabledTenantPaymentAccount` | Throws `NO_ENABLED_ACCOUNT` unless enabled. |\n| `createConnectedCheckout` | Checkout on the tenant's account with `stripeAccount` + `applicationFeeAmount` (integer minor units from the caller's fee policy; `0` for none; below the amount). The intent (account, environment, fee, expiry) is recorded before the provider call and the option naming the provider session is added once, so a retry replays the same request (the expiry is part of the provider idempotency key) and recovers a lost answer; once the recorded expiry is under 31 minutes away the retry asks for a fresh session with a renewed expiry (the lost URL never reached the buyer). The requested amount and currency are recorded too: a retry with a different price, environment, account or fee is refused; a retry of a paid checkout returns it with `settled: true` (no URL; never charge again); a retry of an unpaid closed (expired, cancelled or lapsed) quote gets `QUOTE_CLOSED`; an explicit `expiresAt` too close to replay must be replaced by the caller. |\n| `handleConnectedPaymentWebhook` | See below. |\n| `refundConnectedPayment` | Refund on the tenant's account; `refundApplicationFee` is required; partial and full. The refund row is written (`requested`, holding its amount) before the provider is asked; only a provider-`succeeded` refund is journaled, `pending` waits for the refund update, `failed` (including a first request the SDK refused before sending) releases the amount; a re-sent request must repeat its `refundApplicationFee` (stored in the frozen `applicationFeeRefunded`). A retry with the same key returns a recorded result, or re-sends a lost request with the same provider idempotency key within 23 hours (older lost requests wait for the refund update). Once booked refunds cover the payment it is marked `REFUNDED` and its intent retired. |\n| `createStripeConnectGateway` | Platform `StripeAdapter` from smrt-secrets. |\n\n## Webhooks\n\n`handleConnectedPaymentWebhook({ db, gateway, environment, endpoint, payload,\nsignature, resolveSettlementAccounts })`:\n\n1. The SDK verifies the signature **before** routing, then maps the event's\n connected account to its `TenantPaymentAccount` (cross-tenant read under\n `withSystemContext`, the one reviewed cross-tenant path). Unknown accounts\n throw the SDK's `PaymentNotOwnedError`.\n2. The event's `livemode` must match `environment` (`ENVIRONMENT_MISMATCH`).\n3. A stored event id returns `{ duplicate: true }` with its first outcome (and the `application_fee` details, so a host can still book them).\n4. `account.updated` applies the snapshot unless an earlier second than the\n stored one (`stale_account_update`). `application_fee.*` (platform endpoint,\n `endpoint: 'platform'`) is routed to the tenant and reported, not booked.\n A payment event loads the quote **in the resolved tenant** and refuses\n (`QUOTE_NOT_OWNED`) unless it belongs to that tenant and account, then\n re-reads the Checkout Session from the provider, so delivery order never\n matters. A provider-confirmed checkout settles even when its notice\n arrives after the price lock (the session expires with the lock); a\n payment confirmed for a quote already expired or cancelled is reported\n `late_payment` and posts nothing. `account.updated` reloads the row and\n refuses an event for a provider account the row no longer holds; a\n concurrent operator write fails the revision-guarded save (retried by the\n provider). Refund updates (`charge.refund.updated`, `refund.*`) book or fail\n a pending refund recorded by `refundConnectedPayment` on that account;\n terminal states never move back. Refunds made by the command carry\n `metadata.smrt_refund_id` and are matched by it (an unknown one throws\n `REFUND_NOT_RECORDED`; an amount differing from the row throws\n `AMOUNT_MISMATCH`). Refunds made elsewhere are reported. Settlement records a `Payment` (deterministic id from the intent)\n through `recordPayment()` and marks the intent paid. `refunded` events are\n reported (`refund_reported`), never booked automatically.\n5. Connected `charge.dispute.*` events re-read the typed dispute through the\n public SDK (`gateway.getDispute`, optional for older custom gateways but\n required for dispute delivery), scoped to the verified account and environment. They locate one\n settled payment in that tenant by provider payment-intent/charge reference;\n when none exists, `gateway.lookupCheckout` resolves authoritative ownership\n by PaymentIntent/Charge in that same account and environment. Only a complete\n provider `no_match` is acknowledged as `dispute_reported`, without any journal\n or dispute projection. A matched quote must belong to a persisted intent in\n that tenant/account/environment (and match the saved session when present);\n a missing local settlement then refuses delivery (`DISPUTE_NOT_RECORDED`)\n until settlement arrives, including when checkout creation lost its response\n and saved no option. Unknown quote/tenant/account/environment, conflicting\n session identity, ambiguous or incomplete lookup and provider failure refuse\n delivery. An older custom gateway without `lookupCheckout` refuses this\n zero-settlement case with `CONFIGURATION`; it never guesses from absent rows.\n Unknown statuses or balance transaction kinds,\n currency conversions, changed evidence and missing fee configuration refuse\n delivery for operator reconciliation. No dispute is acknowledged as ignored.\n6. The event id is stored only after processing succeeds; a refusal throws,\n so respond non-2xx and nothing unprocessed is acknowledged.\n\n## Accounting\n\nThe direct charge arrives net of the fee, which the provider withheld at\nsource: `recordPayment()` debits cash `amount - fee`, debits\n`applicationFeeAccountId` `fee`, and credits receivables `amount`. The fee is\nan expense already paid, never a payable to the platform. A payment with a fee\nand no `applicationFeeAccountId` is refused. Refund journals reverse the\nrefunded amount; a refunded application fee is reported by\n`application_fee.refunded` for the host to book.\n\n## Dispute accounting and recovery (#4262)\n\n`TenantPaymentDispute` is a tenant-owned, command-written projection with no\ngenerated API/CLI/MCP surface. Its retained snapshot records provider lifecycle\nand all posted balance transaction evidence. A payment revision fence serializes\nreconciliation before the authoritative provider read. Every new provider\nbalance transaction gets one deterministic, posted journal in the same database\ntransaction as the snapshot. A crash rolls back both; a crash saving the inbox\nreplays the snapshot safely. Duplicate event ids return `paymentId` too. A\nredelivery whose historical outcome is `ignored` is reconciled and updates that\ninbox outcome; other processed outcomes stay deduplicated.\n\nPosts use provider integer minor units, never lifecycle status as money proof:\ncash changes by `net`, receivables by `-amount`, dispute fee expense by `fee`.\nFor example, a 10,000 withdrawal with a 1,500 fee credits cash 11,500, debits AR\n10,000 and expense 1,500. A reinstatement posts the opposite balances from its\nown provider transaction. Configure `disputeFeeAccountId` on the settlement\naccounts whenever fees occur. The provider's balance transaction must be an\n`adjustment`, with optional category `dispute` or `dispute_reversal`, and in the\nsettlement currency. Unknown contracts fail closed until supported explicitly.\n\nOriginal payment, invoice allocations, payouts, refund rows and settlement\njournals remain historical evidence. Reconciliation records the resulting\nreceivable even when the invoice was already paid or a supplier payout confirmed;\nit does not collect the debt, unwind an allocation, reverse a supplier remittance,\nor send a provider refund. Hosts must show/recover that receivable operationally.\nLifecycle updates with no balance evidence record status and post no money.\n\nBefore deployment, run the normal commerce schema migration to add\n`tenant_payment_disputes` and the webhook event's optional `paymentId` column.\nReplay previously ignored deliveries by having the provider redeliver the original\nsigned Connect webhook to the updated endpoint (a fresh valid signature is\nrequired; do not bypass signature validation or manually delete inbox history).\nOrdering uses current provider state, so redelivering an older event after a\nclosed dispute cannot revert its postings. Retain the original connected account\nmapping: relinking remains an explicit operation that strands old-account events.\nThe complete Payment/ledger contract runs on SQLite and PostgreSQL; the existing\nDuckDB checkout-only lane does not support Payment's full FK closure.\n\n## Operator setup\n\n- smrt-secrets (held by the installation tenant, `secretsTenantId`):\n `stripe-connect-<env>-secret-key` (`sk_`/`rk_` key of that environment),\n `stripe-connect-<env>-webhook-secret` (platform endpoint),\n `stripe-connect-<env>-connect-webhook-secret` (Connect endpoint). Values\n never appear in errors.\n- Stripe: one platform endpoint (`application_fee.created`,\n `application_fee.refunded`) and one Connect endpoint (`account.updated`,\n `checkout.session.completed`, `checkout.session.async_payment_succeeded`,\n `checkout.session.async_payment_failed`, `checkout.session.expired`,\n `charge.refunded`, `charge.refund.updated`, `charge.dispute.created`,\n `charge.dispute.updated`, `charge.dispute.closed`,\n `charge.dispute.funds_withdrawn`, `charge.dispute.funds_reinstated`), per environment.\n- Each tenant needs ledger accounts for cash, receivables and the platform fee\n expense, returned by `resolveSettlementAccounts`.\n\n## Known limits\n\nA `requested` refund the provider never made (a request lost in transport\nand not retried within 23 hours, or one the provider rejected — the SDK does\nnot expose the HTTP status, so a rejection cannot be told from an unreadable\nsuccess) holds its amount and books nothing until an operator resolves it.\nAutomatic reconciliation needs an SDK refund read and error status\n([sdk#1430](https://github.com/happyvertical/sdk/issues/1430)).\n\nRelinking strands open checkouts on the old account and refuses refunds of\nits payments (`ACCOUNT_NOT_OWNED`). Concurrent refunds with different keys are\ncapped by the provider, not locally. Payments confirmed after an intent\nexpired or was cancelled are reported `ignored` for an operator.\n\n## Frozen fee-policy provenance (#4263)\n\nAt the authoritative price calculation, pass `feePolicyRef` to\n`createConnectedCheckout`: an opaque immutable version reference (for example\n`bakery-membership:version:1`). It is host-attested evidence, not a lookup or an\nauthorization grant; the host must resolve and authorize the policy within the\nexplicit tenant before calling. This command validates account ownership and\nnever reads another tenant's policy. Nonempty references cannot contain leading\nor trailing whitespace. Omission stores the empty string for unversioned hosts.\n\nThe intent's server-managed `connectedFeePolicyRef` freezes **before** the\nprovider request, alongside price and fee, including when that request fails.\nEvery retry must repeat the original reference, even when a new agreement\nversion yields the same cents. A mismatch is refused before any provider call.\nKeep `offeringRef` and `idempotencyKey` tied to the purchase; changing them to\nencode an amendment would create another charge. Amendments to the account's\nlive `feePolicyRef` cannot rewrite a checkout. The reference follows settlement\nto `Payment` and any command-created refunds and cannot be edited there.\nExisting checkouts with no reference accept only an omitted reference on retry;\nthey are never retrospectively attributed to the current account policy.\n"
22073
22226
  },
22074
22227
  {
22075
22228
  "path": "agents/historical-sales.md",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-commerce",
3
- "version": "0.57.35",
3
+ "version": "0.57.37",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Commerce models for the SMRT framework - contracts, fulfillments, payments",
6
6
  "type": "module",
@@ -63,20 +63,20 @@
63
63
  "access": "public"
64
64
  },
65
65
  "dependencies": {
66
- "@happyvertical/accounting": "^0.108.0",
67
- "@happyvertical/payments": "^0.108.0",
68
- "@happyvertical/smrt-agents": "0.57.35",
69
- "@happyvertical/smrt-core": "0.57.35",
70
- "@happyvertical/smrt-jobs": "0.57.35",
71
- "@happyvertical/smrt-ledgers": "0.57.35",
72
- "@happyvertical/smrt-products": "0.57.35",
73
- "@happyvertical/smrt-reports": "0.57.35",
74
- "@happyvertical/smrt-subscriptions": "0.57.35",
75
- "@happyvertical/smrt-svelte": "0.57.35",
76
- "@happyvertical/smrt-tenancy": "0.57.35",
77
- "@happyvertical/smrt-types": "0.57.35",
78
- "@happyvertical/smrt-ui": "0.57.35",
79
- "@happyvertical/sql": "^0.108.0"
66
+ "@happyvertical/accounting": "^0.110.0",
67
+ "@happyvertical/payments": "^0.110.0",
68
+ "@happyvertical/smrt-agents": "0.57.37",
69
+ "@happyvertical/smrt-core": "0.57.37",
70
+ "@happyvertical/smrt-jobs": "0.57.37",
71
+ "@happyvertical/smrt-ledgers": "0.57.37",
72
+ "@happyvertical/smrt-products": "0.57.37",
73
+ "@happyvertical/smrt-reports": "0.57.37",
74
+ "@happyvertical/smrt-subscriptions": "0.57.37",
75
+ "@happyvertical/smrt-svelte": "0.57.37",
76
+ "@happyvertical/smrt-tenancy": "0.57.37",
77
+ "@happyvertical/smrt-types": "0.57.37",
78
+ "@happyvertical/smrt-ui": "0.57.37",
79
+ "@happyvertical/sql": "^0.110.0"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "svelte": "^5.57.0"
@@ -87,7 +87,7 @@
87
87
  }
88
88
  },
89
89
  "devDependencies": {
90
- "@happyvertical/smrt-vitest": "0.57.35",
90
+ "@happyvertical/smrt-vitest": "0.57.37",
91
91
  "@sveltejs/package": "^2.5.8",
92
92
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
93
93
  "@testing-library/jest-dom": "^7.0.1",