@happyvertical/smrt-commerce 0.57.10 → 0.57.12

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.
Files changed (63) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.md +61 -0
  3. package/agents/connected-payments.md +135 -0
  4. package/agents/contract-billing-schedules.md +6 -0
  5. package/agents/money-upgrade.md +5 -0
  6. package/dist/chunks/{ContractLineItemCollection-DQZqRiZi.js → ContractLineItemCollection-DkBeBR7a.js} +4 -4
  7. package/dist/chunks/{ContractLineItemCollection-DQZqRiZi.js.map → ContractLineItemCollection-DkBeBR7a.js.map} +1 -1
  8. package/dist/chunks/{DocumentTaxCollection-Dg942m_Z.js → DocumentTaxCollection-D85ut-Fo.js} +14 -10
  9. package/dist/chunks/{DocumentTaxCollection-Dg942m_Z.js.map → DocumentTaxCollection-D85ut-Fo.js.map} +1 -1
  10. package/dist/chunks/{InvoiceCollection-XPcPyibo.js → InvoiceCollection-Datg_eZM.js} +9 -9
  11. package/dist/chunks/{InvoiceCollection-XPcPyibo.js.map → InvoiceCollection-Datg_eZM.js.map} +1 -1
  12. package/dist/chunks/{TaxCodeCollection-CbTBWO8z.js → TaxCodeCollection-CIC1skxr.js} +2 -2
  13. package/dist/chunks/{TaxCodeCollection-CbTBWO8z.js.map → TaxCodeCollection-CIC1skxr.js.map} +1 -1
  14. package/dist/chunks/{TaxComponentCollection-VZ_cGr2n.js → TaxComponentCollection-BHBV-_1J.js} +3 -3
  15. package/dist/chunks/{TaxComponentCollection-VZ_cGr2n.js.map → TaxComponentCollection-BHBV-_1J.js.map} +1 -1
  16. package/dist/chunks/{allocation-integrity-Kr8uceRx.js → allocation-integrity-B2SyO1mu.js} +6 -6
  17. package/dist/chunks/{allocation-integrity-Kr8uceRx.js.map → allocation-integrity-B2SyO1mu.js.map} +1 -1
  18. package/dist/chunks/{contract-money-DGNqQwf0.js → contract-money-C61kGBkU.js} +7 -6
  19. package/dist/chunks/contract-money-C61kGBkU.js.map +1 -0
  20. package/dist/chunks/{invoice-posting-DVSkfSUm.js → invoice-posting-uLfdyLIi.js} +3 -3
  21. package/dist/chunks/{invoice-posting-DVSkfSUm.js.map → invoice-posting-uLfdyLIi.js.map} +1 -1
  22. package/dist/chunks/{retention-accounting-Ct5Df6xx.js → retention-accounting-DGuXzt9g.js} +149 -11
  23. package/dist/chunks/retention-accounting-DGuXzt9g.js.map +1 -0
  24. package/dist/chunks/{tenant-money-barrier-CG78Xxja.js → tenant-money-barrier-DXnJWxiZ.js} +3 -3
  25. package/dist/chunks/{tenant-money-barrier-CG78Xxja.js.map → tenant-money-barrier-DXnJWxiZ.js.map} +1 -1
  26. package/dist/chunks/types-Bbm0Xflc.js.map +1 -1
  27. package/dist/collections/TenantPaymentAccountCollection.d.ts +28 -0
  28. package/dist/collections/TenantPaymentAccountCollection.d.ts.map +1 -0
  29. package/dist/collections/index.d.ts +1 -0
  30. package/dist/collections/index.d.ts.map +1 -1
  31. package/dist/connected-payments-internal.d.ts +5 -0
  32. package/dist/connected-payments-internal.d.ts.map +1 -0
  33. package/dist/connected-payments.d.ts +361 -0
  34. package/dist/connected-payments.d.ts.map +1 -0
  35. package/dist/contract-money.d.ts.map +1 -1
  36. package/dist/index.d.ts +3 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1241 -160
  39. package/dist/index.js.map +1 -1
  40. package/dist/invoices.js +1 -1
  41. package/dist/manifest.json +1404 -129
  42. package/dist/models/Contract.d.ts +5 -1
  43. package/dist/models/Contract.d.ts.map +1 -1
  44. package/dist/models/Payment.d.ts +33 -0
  45. package/dist/models/Payment.d.ts.map +1 -1
  46. package/dist/models/PaymentIntent.d.ts +35 -0
  47. package/dist/models/PaymentIntent.d.ts.map +1 -1
  48. package/dist/models/TenantPaymentAccount.d.ts +121 -0
  49. package/dist/models/TenantPaymentAccount.d.ts.map +1 -0
  50. package/dist/models/TenantPaymentWebhookEvent.d.ts +50 -0
  51. package/dist/models/TenantPaymentWebhookEvent.d.ts.map +1 -0
  52. package/dist/models/index.d.ts +2 -0
  53. package/dist/models/index.d.ts.map +1 -1
  54. package/dist/recipes.d.ts +15 -0
  55. package/dist/recipes.d.ts.map +1 -1
  56. package/dist/smrt-knowledge.json +622 -27
  57. package/dist/types/index.d.ts +16 -0
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/upgrade/money-v2.d.ts +2 -0
  60. package/dist/upgrade/money-v2.d.ts.map +1 -1
  61. package/package.json +16 -16
  62. package/dist/chunks/contract-money-DGNqQwf0.js.map +0 -1
  63. package/dist/chunks/retention-accounting-Ct5Df6xx.js.map +0 -1
package/AGENTS.md CHANGED
@@ -16,6 +16,7 @@ E-commerce with Contract STI hierarchy, invoice lifecycle, payment tracking, pay
16
16
  - **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.
17
17
  - **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).
18
18
  - **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.
19
+ - **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).
19
20
  - **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.
20
21
  - **Fulfillment** / **FulfillmentLineItem**: shipment/delivery tracking.
21
22
 
package/README.md CHANGED
@@ -698,6 +698,67 @@ await billing.processEvents();
698
698
  See [`AGENTS.md`](./AGENTS.md#billing-period-close-3060) for invariants and
699
699
  known limits.
700
700
 
701
+ ## Tenant-owned connected payment accounts
702
+
703
+ A tenant (for example a co-op) can be paid into its own Stripe Connect
704
+ Standard account, so its customers' money never passes through the platform.
705
+ Checkouts are direct charges on the tenant's account; the platform's share is
706
+ an application fee. A tenant without an enabled account is refused — there is
707
+ no fallback to the platform account.
708
+
709
+ ```ts
710
+ import { SecretService } from '@happyvertical/smrt-secrets';
711
+ import {
712
+ createConnectedCheckout,
713
+ createStripeConnectGateway,
714
+ createTenantOnboardingLink,
715
+ handleConnectedPaymentWebhook,
716
+ onboardTenantPaymentAccount,
717
+ refundConnectedPayment,
718
+ } from '@happyvertical/smrt-commerce';
719
+
720
+ const gateway = await createStripeConnectGateway({
721
+ secrets: await SecretService.create({ db }),
722
+ secretsTenantId: installationTenantId,
723
+ environment: 'live',
724
+ adapterOptions: { successUrl, cancelUrl, defaultCurrency: 'CAD' },
725
+ });
726
+
727
+ // Onboarding: create the tenant's Standard account, then send its treasurer
728
+ // to Stripe. The link is a bearer secret: redirect, never log or store.
729
+ await onboardTenantPaymentAccount({ db, gateway, tenantId, environment: 'live', country: 'CA' });
730
+ const { url } = await createTenantOnboardingLink({
731
+ db, gateway, tenantId, environment: 'live', returnUrl, refreshUrl,
732
+ });
733
+
734
+ // Checkout: the fee comes from the tenant's fee policy, in integer minor units.
735
+ const { intent, checkoutUrl } = await createConnectedCheckout({
736
+ db, gateway, tenantId, environment: 'live',
737
+ offeringRef: 'membership-2027', licenseeEmail, idempotencyKey,
738
+ amount: 10_000, currency: 'CAD', applicationFeeAmount: 500,
739
+ });
740
+
741
+ // Webhooks (Connect endpoint; use endpoint: 'platform' for application_fee.*).
742
+ const result = await handleConnectedPaymentWebhook({
743
+ db, gateway, environment: 'live', payload: rawBody,
744
+ signature: request.headers.get('Stripe-Signature') ?? undefined,
745
+ resolveSettlementAccounts: ({ tenantId }) => ledgerAccountsFor(tenantId),
746
+ });
747
+
748
+ // Refunds go to the tenant's account; the fee-refund choice is explicit.
749
+ await refundConnectedPayment({
750
+ db, gateway, tenantId, environment: 'live', paymentId, amount: 4_000,
751
+ refundApplicationFee: true, idempotencyKey, resolveSettlementAccounts,
752
+ });
753
+ ```
754
+
755
+ The settled `Payment` books cash net of the fee and the fee as an expense, so
756
+ the fee never becomes a payable to the platform. Webhook signatures are
757
+ verified before routing, events are routed by connected account to the
758
+ owning tenant, quotes are bound to that tenant before settling, and processed
759
+ event ids are stored. Secrets, endpoints, and limits:
760
+ [connected payments](agents/connected-payments.md).
761
+
701
762
  ## Dependencies
702
763
 
703
764
  - `@happyvertical/smrt-core` -- ORM and code generation
@@ -0,0 +1,135 @@
1
+ # Tenant-owned connected payment accounts (#3831)
2
+
3
+ A tenant (for example a co-op) is paid into its own Stripe Connect **Standard**
4
+ account. Checkouts are direct charges on that account, the platform's share is
5
+ an application fee, and the platform never holds the tenant's money. Source:
6
+ `src/connected-payments.ts`, `src/models/TenantPaymentAccount.ts`,
7
+ `src/models/TenantPaymentWebhookEvent.ts`.
8
+
9
+ ## Model
10
+
11
+ `TenantPaymentAccount { tenantId, backendId, environment, providerAccountId,
12
+ status, chargesEnabled, detailsSubmitted, payoutsEnabled, capabilitiesJson,
13
+ capabilitiesObservedAt, feePolicyRef, disabledAt, disabledReason }`.
14
+
15
+ - Tenant-scoped (`required`), native UUID tenant ids. Two unique indexes:
16
+ `(tenantId, backendId, environment)` and
17
+ `(backendId, environment, providerAccountId)`, so the database refuses a
18
+ second owner of a provider account even if a command check were bypassed.
19
+ - `status`: `pending` (details not submitted), `restricted` (details
20
+ submitted, charges or payouts off), `enabled` (charges, details and payouts
21
+ all on), `disabled` (operator; refreshes keep it off). Only `enabled` takes
22
+ new charges.
23
+ - Read-only generated surface (REST, MCP and CLI: `list`/`get` and read methods only); never deleted. `save()` refuses any write outside the
24
+ commands, any change of tenant/backend/environment, and a provider account
25
+ change outside the relink command.
26
+ - Connected `Payment` rows and connected `PaymentIntent` creation/option
27
+ writes need the commands' internal authority (`connected-payments-internal.ts`,
28
+ per tenant), so no generated REST/MCP/CLI write can forge or advance them;
29
+ connected payments and refunds are never deleted. A checkout's identity
30
+ includes its environment (`PaymentIntent.connectedEnvironment`): the same
31
+ `acct_` id exists in test and live.
32
+ - `TenantPaymentWebhookEvent` (`_smrt_tenant_payment_webhook_events`, system
33
+ table, unique `(backendId, environment, eventId)`) stores processed event ids;
34
+ the SDK's dedupe is in memory only.
35
+ - `PaymentIntent` and `Payment` gain server-managed `connectedAccountId` and
36
+ `applicationFeeCents` (`Payment` also `refundOfPaymentId` and
37
+ `connectedRefundState`), frozen once settled. A connected intent settles only
38
+ with a Payment on the same account and fee, and its account, fee and payment
39
+ options (which name the provider session settlement re-reads) are frozen
40
+ from creation, so a generated update cannot repoint it. A connected
41
+ `Payment` (charge or refund) keeps its kind, amounts, currency, provider
42
+ references, account, fee and refunded payment from its first write, even
43
+ while pending.
44
+
45
+ ## Commands
46
+
47
+ All take an explicit `tenantId` and check ownership against it, so a
48
+ privileged or system caller is refused the same way. There is no fallback to
49
+ the platform account.
50
+
51
+ | Command | Does |
52
+ |---|---|
53
+ | `onboardTenantPaymentAccount` | Create the Standard account (per-tenant idempotency key) and store it; returns the existing one on retry. |
54
+ | `createTenantOnboardingLink` | Single-use onboarding URL. A bearer secret: redirect, never log or store. |
55
+ | `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). |
56
+ | `refreshTenantPaymentAccount` | Snapshot from `getAccountCapabilities`, re-derive status. |
57
+ | `setTenantPaymentAccountDisabled`, `setTenantPaymentFeePolicy` | Operator switches. |
58
+ | `requireEnabledTenantPaymentAccount` | Throws `NO_ENABLED_ACCOUNT` unless enabled. |
59
+ | `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. |
60
+ | `handleConnectedPaymentWebhook` | See below. |
61
+ | `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. |
62
+ | `createStripeConnectGateway` | Platform `StripeAdapter` from smrt-secrets. |
63
+
64
+ ## Webhooks
65
+
66
+ `handleConnectedPaymentWebhook({ db, gateway, environment, endpoint, payload,
67
+ signature, resolveSettlementAccounts })`:
68
+
69
+ 1. The SDK verifies the signature **before** routing, then maps the event's
70
+ connected account to its `TenantPaymentAccount` (cross-tenant read under
71
+ `withSystemContext`, the one reviewed cross-tenant path). Unknown accounts
72
+ throw the SDK's `PaymentNotOwnedError`.
73
+ 2. The event's `livemode` must match `environment` (`ENVIRONMENT_MISMATCH`).
74
+ 3. A stored event id returns `{ duplicate: true }` with its first outcome (and the `application_fee` details, so a host can still book them).
75
+ 4. `account.updated` applies the snapshot unless an earlier second than the
76
+ stored one (`stale_account_update`). `application_fee.*` (platform endpoint,
77
+ `endpoint: 'platform'`) is routed to the tenant and reported, not booked.
78
+ A payment event loads the quote **in the resolved tenant** and refuses
79
+ (`QUOTE_NOT_OWNED`) unless it belongs to that tenant and account, then
80
+ re-reads the Checkout Session from the provider, so delivery order never
81
+ matters. A provider-confirmed checkout settles even when its notice
82
+ arrives after the price lock (the session expires with the lock); a
83
+ payment confirmed for a quote already expired or cancelled is reported
84
+ `late_payment` and posts nothing. `account.updated` reloads the row and
85
+ refuses an event for a provider account the row no longer holds; a
86
+ concurrent operator write fails the revision-guarded save (retried by the
87
+ provider). Refund updates (`charge.refund.updated`, `refund.*`) book or fail
88
+ a pending refund recorded by `refundConnectedPayment` on that account;
89
+ terminal states never move back. Refunds made by the command carry
90
+ `metadata.smrt_refund_id` and are matched by it (an unknown one throws
91
+ `REFUND_NOT_RECORDED`; an amount differing from the row throws
92
+ `AMOUNT_MISMATCH`). Refunds made elsewhere are reported. Settlement records a `Payment` (deterministic id from the intent)
93
+ through `recordPayment()` and marks the intent paid. `refunded` events are
94
+ reported (`refund_reported`), never booked automatically.
95
+ 5. The event id is stored only after processing succeeds; a refusal throws,
96
+ so respond non-2xx and nothing unprocessed is acknowledged.
97
+
98
+ ## Accounting
99
+
100
+ The direct charge arrives net of the fee, which the provider withheld at
101
+ source: `recordPayment()` debits cash `amount - fee`, debits
102
+ `applicationFeeAccountId` `fee`, and credits receivables `amount`. The fee is
103
+ an expense already paid, never a payable to the platform. A payment with a fee
104
+ and no `applicationFeeAccountId` is refused. Refund journals reverse the
105
+ refunded amount; a refunded application fee is reported by
106
+ `application_fee.refunded` for the host to book.
107
+
108
+ ## Operator setup
109
+
110
+ - smrt-secrets (held by the installation tenant, `secretsTenantId`):
111
+ `stripe-connect-<env>-secret-key` (`sk_`/`rk_` key of that environment),
112
+ `stripe-connect-<env>-webhook-secret` (platform endpoint),
113
+ `stripe-connect-<env>-connect-webhook-secret` (Connect endpoint). Values
114
+ never appear in errors.
115
+ - Stripe: one platform endpoint (`application_fee.created`,
116
+ `application_fee.refunded`) and one Connect endpoint (`account.updated`,
117
+ `checkout.session.completed`, `checkout.session.async_payment_succeeded`,
118
+ `checkout.session.async_payment_failed`, `checkout.session.expired`,
119
+ `charge.refunded`, `charge.refund.updated`), per environment.
120
+ - Each tenant needs ledger accounts for cash, receivables and the platform fee
121
+ expense, returned by `resolveSettlementAccounts`.
122
+
123
+ ## Known limits
124
+
125
+ A `requested` refund the provider never made (a request lost in transport
126
+ and not retried within 23 hours, or one the provider rejected — the SDK does
127
+ not expose the HTTP status, so a rejection cannot be told from an unreadable
128
+ success) holds its amount and books nothing until an operator resolves it.
129
+ Automatic reconciliation needs an SDK refund read and error status
130
+ ([sdk#1430](https://github.com/happyvertical/sdk/issues/1430)).
131
+
132
+ Relinking strands open checkouts on the old account and refuses refunds of
133
+ its payments (`ACCOUNT_NOT_OWNED`). Concurrent refunds with different keys are
134
+ capped by the provider, not locally. Payments confirmed after an intent
135
+ expired or was cancelled are reported `ignored` for an operator.
@@ -65,3 +65,9 @@ claim rejects. A parent rollback reverses both invoice issue and schedule claim.
65
65
  A detached read DTO cannot modify persisted state. Ordinary Contract writes and
66
66
  deletes cannot erase schedule history. Host authorization precedes these commands;
67
67
  none is exposed as an unauthenticated generated mutation.
68
+
69
+ `billingScheduleJson` is `''` when a Contract has no schedule, and the
70
+ column defaults to `''`. A row written before that default holds NULL, which
71
+ hydrates and saves as `''`, so an ordinary save of such a Contract succeeds;
72
+ any other change to the field through `save()` is still refused (#4155).
73
+ `smrt db:upgrade-commerce-money` stores `''` for those NULL rows.
@@ -81,6 +81,11 @@ each failing document. A tenant whose earlier
81
81
  migration never finished also fails; roll it back with the version that started
82
82
  it, or reset its data. Converted tenants are skipped on a rerun.
83
83
 
84
+ Every run also stores `''` for each Contract whose `billing_schedule_json` is
85
+ NULL (only that tenant's with `tenantId`) and reports the count in
86
+ `billingSchedulesBackfilled` (#4155). The runtime already reads NULL as no
87
+ schedule, so this is tidying, not a prerequisite.
88
+
84
89
  Once no tenant needs the upgrade, the old columns and table are dropped. The
85
90
  upgrade leaves no mode flag. Read `failures` and `remaining` on the result; the
86
91
  CLI exits nonzero on either. New `rate-<percent>` components have no liability
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { MoneyValidationError } from "../money.js";
3
3
  import { a as assertCommerceMoneySchemaCurrent, f as DOCUMENT_MONEY_READ_REBIND, p as DOCUMENT_MONEY_WRITE } from "./detect-DAE-_NAk.js";
4
- import { n as withTenantMoneyCollectionRead } from "./tenant-money-barrier-CG78Xxja.js";
4
+ import { n as withTenantMoneyCollectionRead } from "./tenant-money-barrier-DXnJWxiZ.js";
5
5
  import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
6
6
  import { TenantScoped, getTenantId, queryGlobal, queryWithGlobals, tenantId } from "@happyvertical/smrt-tenancy";
7
7
  //#region src/models/ContractLineItem.ts
@@ -116,7 +116,7 @@ var ContractLineItem = class extends SmrtObject {
116
116
  return this;
117
117
  }
118
118
  async save() {
119
- const { saveContractLineItem } = await import("./contract-money-DGNqQwf0.js");
119
+ const { saveContractLineItem } = await import("./contract-money-C61kGBkU.js");
120
120
  return await saveContractLineItem(this);
121
121
  }
122
122
  /** @internal Contract aggregate persistence only. */
@@ -126,7 +126,7 @@ var ContractLineItem = class extends SmrtObject {
126
126
  return await super.save();
127
127
  }
128
128
  async delete(options = {}) {
129
- const { deleteContractLineItem } = await import("./contract-money-DGNqQwf0.js");
129
+ const { deleteContractLineItem } = await import("./contract-money-C61kGBkU.js");
130
130
  await deleteContractLineItem(this, options);
131
131
  }
132
132
  /** @internal Contract aggregate persistence only. */
@@ -257,4 +257,4 @@ var ContractLineItemCollection = class extends SmrtCollection {
257
257
  //#endregion
258
258
  export { ContractLineItemCollection_exports as n, ContractLineItem as r, ContractLineItemCollection as t };
259
259
 
260
- //# sourceMappingURL=ContractLineItemCollection-DQZqRiZi.js.map
260
+ //# sourceMappingURL=ContractLineItemCollection-DkBeBR7a.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ContractLineItemCollection-DQZqRiZi.js","names":["field"],"sources":["../../src/models/ContractLineItem.ts","../../src/collections/ContractLineItemCollection.ts"],"sourcesContent":["/**\n * ContractLineItem model - individual items on a contract\n * @packageDocumentation\n */\n\nimport {\n crossPackageRef,\n field,\n foreignKey,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/smrt-core/migrations';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport {\n DOCUMENT_MONEY_READ_REBIND,\n DOCUMENT_MONEY_WRITE,\n type DocumentMoneyReadRebindCapability,\n type DocumentMoneyWriteCapability,\n} from '../document-money-internal.js';\nimport { MoneyValidationError } from '../money.js';\nimport type {\n ContractLineItemOptions,\n ContractTaxComponentSnapshot,\n} from '../types/index.js';\n\n/**\n * ContractLineItem represents a single line item on a contract.\n *\n * Line items contain the details of what is being sold/purchased,\n * including quantity, pricing, and optional subscription details.\n *\n * @example\n * ```typescript\n * const lineItem = await lineItems.create({\n * contractId: order.id,\n * description: 'Widget Pro',\n * quantity: 10,\n * unitPrice: 4999, // $49.99 in cents\n * taxRate: 0.08\n * });\n * ```\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n})\nexport class ContractLineItem extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-commerce\";\n /** @internal Collection read guard returns this snapshot on its root DB. */\n rebindMoneyRead(\n capability: DocumentMoneyReadRebindCapability,\n db: DatabaseInterface,\n ): this {\n if (capability !== DOCUMENT_MONEY_READ_REBIND)\n throw new Error(\n 'Contract line read rebinding requires the owning capability',\n );\n this._db = db;\n this.options.db = db;\n return this;\n }\n\n /**\n * Tenant ID for multi-tenant isolation\n * Nullable to support both tenant-scoped and global line items\n */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * Parent contract\n */\n @field({ description: 'The order, quote or agreement this line belongs to.' })\n @foreignKey('Contract', { role: 'items' })\n contractId: string = '';\n\n /**\n * Item description\n */\n @field({ description: 'What is being sold or bought on this line.' })\n description: string = '';\n\n /**\n * Quantity ordered. Decimal on purpose — contracts price fractional\n * quantities (hours, weight, bandwidth), as do invoice line items.\n */\n @field({ description: 'How many.' })\n quantity: number = 1.0;\n\n /**\n * Unit price before discount, in **integer minor units** (cents, satoshis),\n * in the document's pricing mode: tax-inclusive when its `lineAmountType`\n * is `inclusive`.\n *\n * The `= 0` initializer is load-bearing: SMRT maps an integer literal to\n * INTEGER and a decimal literal to DECIMAL. Money is exact, so `$49.99` is\n * `4999` (#2401).\n */\n @field({\n description:\n \"The price of one, before discount; includes tax when the document's prices include tax.\",\n ui: { format: 'money' },\n })\n unitPrice: number = 0;\n\n /**\n * Discount amount (flat, not percentage), in integer minor units (#2401).\n */\n @field({ ui: { format: 'money' } })\n discount: number = 0;\n\n discountType: 'none' | 'percent' | 'amount' = 'none';\n\n @field({ type: 'decimal' })\n discountRate = '0.0';\n\n /**\n * Tax rate as decimal (e.g., 0.08 for 8%). A rate is inherently fractional,\n * so it stays DECIMAL — INTEGER would truncate every meaningful value.\n */\n @field({\n description:\n 'The stored fractional tax rate for this line (0.05 means 5%).',\n })\n taxRate: number = 0.0;\n\n taxMode: 'inherit' | 'override' = 'inherit';\n\n @foreignKey('TaxCode', { nullable: true })\n taxCodeId: string | null = null;\n\n taxCodeCode: string | null = null;\n\n taxCodeName: string | null = null;\n\n taxComponentsJson: string | null = null;\n\n get taxComponents(): ContractTaxComponentSnapshot[] {\n if (this.taxComponentsJson === null) return [];\n try {\n const value: unknown = JSON.parse(this.taxComponentsJson);\n if (!Array.isArray(value)) return [];\n return value.filter(\n (entry): entry is ContractTaxComponentSnapshot =>\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as ContractTaxComponentSnapshot).taxComponentId ===\n 'string' &&\n typeof (entry as ContractTaxComponentSnapshot).name === 'string' &&\n typeof (entry as ContractTaxComponentSnapshot).percent === 'string' &&\n Number.isSafeInteger(\n (entry as ContractTaxComponentSnapshot).taxMinor,\n ),\n );\n } catch {\n return [];\n }\n }\n\n set taxComponents(value: ContractTaxComponentSnapshot[]) {\n this.taxComponentsJson = JSON.stringify(value);\n }\n\n /** Exact sum of frozen component taxes, in integer minor units. */\n @field({ ui: { format: 'money' } })\n taxAmount = 0;\n\n /** Stable issue/freeze identity, independent of lifecycle updated_at. */\n moneySnapshotId = '';\n\n /**\n * Line amount in integer minor units: net of discount and before tax for\n * `exclusive` / `none` documents, and tax-inclusive for `inclusive` ones;\n * {@link taxAmount} holds the tax. Derived by the Contract money aggregate\n * on every save.\n */\n @field({\n description:\n \"The line amount: net of discount, and including tax only when the document's prices include tax.\",\n ui: { format: 'money' },\n })\n amount: number = 0;\n\n /**\n * Optional product reference (cross-package, plain string)\n */\n @field({\n description: 'The product this line is for, if it is one you list.',\n })\n @crossPackageRef('@happyvertical/smrt-products:Product')\n productId: string = '';\n\n /**\n * SKU or item code\n */\n sku: string = '';\n\n /**\n * Start date (for lease/subscription items)\n */\n startDate: Date | null = null;\n\n /**\n * End date (for lease/subscription items)\n */\n endDate: Date | null = null;\n\n /**\n * Billing period (for subscriptions: 'monthly', 'yearly', etc.)\n */\n billingPeriod: string = '';\n\n /**\n * Sort order within the contract\n */\n sortOrder: number = 0;\n\n constructor(options: ContractLineItemOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.contractId !== undefined) this.contractId = options.contractId;\n if (options.description !== undefined)\n this.description = options.description;\n if (options.quantity !== undefined) this.quantity = options.quantity;\n if (options.unitPrice !== undefined) this.unitPrice = options.unitPrice;\n if (options.discount !== undefined) this.discount = options.discount;\n if (options.discountType !== undefined)\n this.discountType = options.discountType;\n if (options.discountRate !== undefined)\n this.discountRate = options.discountRate;\n if (options.taxRate !== undefined) this.taxRate = options.taxRate;\n if (options.taxMode !== undefined) this.taxMode = options.taxMode;\n if (options.taxCodeId !== undefined) this.taxCodeId = options.taxCodeId;\n if (options.taxCodeCode !== undefined)\n this.taxCodeCode = options.taxCodeCode;\n if (options.taxCodeName !== undefined)\n this.taxCodeName = options.taxCodeName;\n if (options.taxComponentsJson !== undefined)\n this.taxComponentsJson = options.taxComponentsJson;\n if (options.taxComponents !== undefined)\n this.taxComponents = structuredClone(options.taxComponents);\n if (options.taxAmount !== undefined) this.taxAmount = options.taxAmount;\n if (options.moneySnapshotId !== undefined)\n this.moneySnapshotId = options.moneySnapshotId;\n if (options.amount !== undefined) this.amount = options.amount;\n if (options.productId !== undefined) this.productId = options.productId;\n if (options.sku !== undefined) this.sku = options.sku;\n if (options.startDate !== undefined) this.startDate = options.startDate;\n if (options.endDate !== undefined) this.endDate = options.endDate;\n if (options.billingPeriod !== undefined)\n this.billingPeriod = options.billingPeriod;\n if (options.sortOrder !== undefined) this.sortOrder = options.sortOrder;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n // A row read from the database must not be old-format money data.\n if (this.isPersisted) {\n const { assertHydratedMoneyRowCurrent } = await import(\n '../upgrade/detect.js'\n );\n await assertHydratedMoneyRowCurrent(this);\n }\n return this;\n }\n\n override async save(): Promise<this> {\n const { saveContractLineItem } = await import('../contract-money.js');\n return (await saveContractLineItem(this)) as this;\n }\n\n /** @internal Contract aggregate persistence only. */\n async saveDerived(capability: DocumentMoneyWriteCapability): Promise<this> {\n if (capability !== DOCUMENT_MONEY_WRITE)\n throw new Error('Contract line money write capability is required');\n // Money is integer minor units: refuse a major-unit price before it can\n // reach the database (#2401), ahead of the aggregate recompute.\n for (const [field, value] of [\n ['unitPrice', this.unitPrice],\n ['discount', this.discount],\n ] as const)\n if (!Number.isSafeInteger(value))\n throw new MoneyValidationError(\n 'UNSAFE_MONEY',\n `Contract line ${field} ${value} must be a safe integer number of minor units`,\n );\n return (await super.save()) as this;\n }\n\n override async delete(\n options: { expectedUpdatedAt?: Date | string } = {},\n ): Promise<void> {\n const { deleteContractLineItem } = await import('../contract-money.js');\n await deleteContractLineItem(this, options);\n }\n\n /** @internal Contract aggregate persistence only. */\n async deleteDerived(\n capability: DocumentMoneyWriteCapability,\n options: { expectedUpdatedAt?: Date | string } = {},\n ): Promise<void> {\n if (capability !== DOCUMENT_MONEY_WRITE)\n throw new Error('Contract line money write capability is required');\n await super.delete(options);\n }\n\n /**\n * Check if this is a subscription/recurring item\n */\n isRecurring(): boolean {\n return !!this.billingPeriod && !!this.startDate;\n }\n\n /**\n * Check if subscription is active\n */\n isSubscriptionActive(): boolean {\n if (!this.isRecurring()) return false;\n if (!this.startDate) return false;\n\n const now = new Date();\n if (now < this.startDate) return false;\n if (this.endDate && now > this.endDate) return false;\n\n return true;\n }\n}\n\nexport default ContractLineItem;\n","/**\n * ContractLineItemCollection - Collection manager for ContractLineItem objects\n * @packageDocumentation\n */\n\nimport {\n SmrtCollection,\n type SmrtListOptions,\n type SmrtObject,\n type SmrtSelectedRow,\n type SmrtSelectField,\n type SmrtStiReadScope,\n type SmrtWhereClause,\n} from '@happyvertical/smrt-core';\nimport {\n getTenantId,\n queryGlobal,\n queryWithGlobals,\n} from '@happyvertical/smrt-tenancy';\nimport { DOCUMENT_MONEY_READ_REBIND } from '../document-money-internal.js';\nimport { ContractLineItem } from '../models/ContractLineItem.js';\nimport { withTenantMoneyCollectionRead } from '../tenant-money-barrier.js';\nimport { assertCommerceMoneySchemaCurrent } from '../upgrade/detect.js';\n\nexport class ContractLineItemCollection extends SmrtCollection<ContractLineItem> {\n static readonly _itemClass = ContractLineItem;\n\n private async withMoneyRead<T>(operation: () => Promise<T>): Promise<T> {\n const rootDb = this.db;\n const tenantId = getTenantId();\n if (!tenantId) {\n // Without a tenant, still refuse a database with old money storage.\n await assertCommerceMoneySchemaCurrent(rootDb);\n return operation();\n }\n const result = await withTenantMoneyCollectionRead(\n rootDb,\n tenantId,\n (boundDb) => this.withDatabase(boundDb, operation),\n );\n if (result instanceof ContractLineItem)\n result.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);\n else if (Array.isArray(result))\n for (const item of result)\n if (item instanceof ContractLineItem)\n item.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);\n return result;\n }\n\n override async get(\n filter: string | SmrtWhereClause<ContractLineItem>,\n options: Parameters<SmrtCollection<ContractLineItem>['get']>[1] = {},\n ): Promise<ContractLineItem | null> {\n return this.withMoneyRead(() => super.get(filter, options));\n }\n\n override async list<\n const Select extends readonly SmrtSelectField<ContractLineItem>[],\n >(\n options: Omit<SmrtListOptions<ContractLineItem>, 'select' | 'stiScope'> & {\n select: Select;\n include?: never;\n stiScope: SmrtStiReadScope;\n },\n ): Promise<Record<string, unknown>[]>;\n override async list<\n const Select extends readonly SmrtSelectField<ContractLineItem>[],\n >(\n options: Omit<SmrtListOptions<ContractLineItem>, 'select' | 'stiScope'> & {\n select: Select;\n include?: never;\n stiScope?: undefined;\n },\n ): Promise<SmrtSelectedRow<ContractLineItem, Select>[]>;\n override async list(\n options: Omit<SmrtListOptions<ContractLineItem>, 'select' | 'stiScope'> & {\n select?: undefined;\n stiScope: SmrtStiReadScope;\n },\n ): Promise<SmrtObject[]>;\n override async list(options?: undefined): Promise<ContractLineItem[]>;\n override async list(\n options: Omit<SmrtListOptions<ContractLineItem>, 'select' | 'stiScope'> & {\n select?: undefined;\n stiScope?: undefined;\n },\n ): Promise<ContractLineItem[]>;\n override async list(\n options:\n | (Omit<SmrtListOptions<ContractLineItem>, 'select'> & {\n select?: undefined;\n })\n | undefined,\n ): Promise<SmrtObject[]>;\n override async list(\n options: SmrtListOptions<ContractLineItem> | undefined,\n ): Promise<SmrtObject[] | Record<string, unknown>[]>;\n override async list(\n options: SmrtListOptions<ContractLineItem> = {},\n ): Promise<SmrtObject[] | Record<string, unknown>[]> {\n return this.withMoneyRead(() => super.list(options));\n }\n\n /**\n * Find contract line items by their parent contract.\n *\n * @param contractId - Contract ID\n * @returns Array of contract line items, sorted by sortOrder\n */\n async findByContract(contractId: string): Promise<ContractLineItem[]> {\n return await this.list({\n where: { contractId },\n orderBy: 'sortOrder ASC',\n });\n }\n\n // ============================================================================\n // Tenant Helper Methods\n // ============================================================================\n\n /**\n * Find all contract line items belonging to a specific tenant\n *\n * @param tenantId - Tenant ID\n * @returns Array of contract line items for the tenant\n */\n async findByTenant(tenantId: string): Promise<ContractLineItem[]> {\n return this.list({ where: { tenantId } });\n }\n\n /**\n * Find all global contract line items (not associated with any tenant).\n *\n * Routes through the shared tenant-global helper so it does not throw under\n * an active tenant context (an explicit `tenant_id IS NULL` filter would be\n * flagged as an isolation violation). (#1600)\n *\n * @returns Array of global contract line items\n */\n async findGlobal(): Promise<ContractLineItem[]> {\n return queryGlobal<ContractLineItem>(this);\n }\n\n /**\n * Find contract line items for a tenant including global line items.\n *\n * Fails closed if an active tenant context requests a different tenant's\n * rows; the admin/system path keeps the cross-tenant capability. (#1600)\n *\n * @param tenantId - Tenant ID\n * @returns Array of tenant-specific and global contract line items\n */\n async findWithGlobals(tenantId: string): Promise<ContractLineItem[]> {\n return queryWithGlobals<ContractLineItem>(\n this,\n tenantId,\n 'ContractLineItem.findWithGlobals',\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ContractLineItemCollection-DkBeBR7a.js","names":["field"],"sources":["../../src/models/ContractLineItem.ts","../../src/collections/ContractLineItemCollection.ts"],"sourcesContent":["/**\n * ContractLineItem model - individual items on a contract\n * @packageDocumentation\n */\n\nimport {\n crossPackageRef,\n field,\n foreignKey,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/smrt-core/migrations';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport {\n DOCUMENT_MONEY_READ_REBIND,\n DOCUMENT_MONEY_WRITE,\n type DocumentMoneyReadRebindCapability,\n type DocumentMoneyWriteCapability,\n} from '../document-money-internal.js';\nimport { MoneyValidationError } from '../money.js';\nimport type {\n ContractLineItemOptions,\n ContractTaxComponentSnapshot,\n} from '../types/index.js';\n\n/**\n * ContractLineItem represents a single line item on a contract.\n *\n * Line items contain the details of what is being sold/purchased,\n * including quantity, pricing, and optional subscription details.\n *\n * @example\n * ```typescript\n * const lineItem = await lineItems.create({\n * contractId: order.id,\n * description: 'Widget Pro',\n * quantity: 10,\n * unitPrice: 4999, // $49.99 in cents\n * taxRate: 0.08\n * });\n * ```\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n})\nexport class ContractLineItem extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-commerce\";\n /** @internal Collection read guard returns this snapshot on its root DB. */\n rebindMoneyRead(\n capability: DocumentMoneyReadRebindCapability,\n db: DatabaseInterface,\n ): this {\n if (capability !== DOCUMENT_MONEY_READ_REBIND)\n throw new Error(\n 'Contract line read rebinding requires the owning capability',\n );\n this._db = db;\n this.options.db = db;\n return this;\n }\n\n /**\n * Tenant ID for multi-tenant isolation\n * Nullable to support both tenant-scoped and global line items\n */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * Parent contract\n */\n @field({ description: 'The order, quote or agreement this line belongs to.' })\n @foreignKey('Contract', { role: 'items' })\n contractId: string = '';\n\n /**\n * Item description\n */\n @field({ description: 'What is being sold or bought on this line.' })\n description: string = '';\n\n /**\n * Quantity ordered. Decimal on purpose — contracts price fractional\n * quantities (hours, weight, bandwidth), as do invoice line items.\n */\n @field({ description: 'How many.' })\n quantity: number = 1.0;\n\n /**\n * Unit price before discount, in **integer minor units** (cents, satoshis),\n * in the document's pricing mode: tax-inclusive when its `lineAmountType`\n * is `inclusive`.\n *\n * The `= 0` initializer is load-bearing: SMRT maps an integer literal to\n * INTEGER and a decimal literal to DECIMAL. Money is exact, so `$49.99` is\n * `4999` (#2401).\n */\n @field({\n description:\n \"The price of one, before discount; includes tax when the document's prices include tax.\",\n ui: { format: 'money' },\n })\n unitPrice: number = 0;\n\n /**\n * Discount amount (flat, not percentage), in integer minor units (#2401).\n */\n @field({ ui: { format: 'money' } })\n discount: number = 0;\n\n discountType: 'none' | 'percent' | 'amount' = 'none';\n\n @field({ type: 'decimal' })\n discountRate = '0.0';\n\n /**\n * Tax rate as decimal (e.g., 0.08 for 8%). A rate is inherently fractional,\n * so it stays DECIMAL — INTEGER would truncate every meaningful value.\n */\n @field({\n description:\n 'The stored fractional tax rate for this line (0.05 means 5%).',\n })\n taxRate: number = 0.0;\n\n taxMode: 'inherit' | 'override' = 'inherit';\n\n @foreignKey('TaxCode', { nullable: true })\n taxCodeId: string | null = null;\n\n taxCodeCode: string | null = null;\n\n taxCodeName: string | null = null;\n\n taxComponentsJson: string | null = null;\n\n get taxComponents(): ContractTaxComponentSnapshot[] {\n if (this.taxComponentsJson === null) return [];\n try {\n const value: unknown = JSON.parse(this.taxComponentsJson);\n if (!Array.isArray(value)) return [];\n return value.filter(\n (entry): entry is ContractTaxComponentSnapshot =>\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as ContractTaxComponentSnapshot).taxComponentId ===\n 'string' &&\n typeof (entry as ContractTaxComponentSnapshot).name === 'string' &&\n typeof (entry as ContractTaxComponentSnapshot).percent === 'string' &&\n Number.isSafeInteger(\n (entry as ContractTaxComponentSnapshot).taxMinor,\n ),\n );\n } catch {\n return [];\n }\n }\n\n set taxComponents(value: ContractTaxComponentSnapshot[]) {\n this.taxComponentsJson = JSON.stringify(value);\n }\n\n /** Exact sum of frozen component taxes, in integer minor units. */\n @field({ ui: { format: 'money' } })\n taxAmount = 0;\n\n /** Stable issue/freeze identity, independent of lifecycle updated_at. */\n moneySnapshotId = '';\n\n /**\n * Line amount in integer minor units: net of discount and before tax for\n * `exclusive` / `none` documents, and tax-inclusive for `inclusive` ones;\n * {@link taxAmount} holds the tax. Derived by the Contract money aggregate\n * on every save.\n */\n @field({\n description:\n \"The line amount: net of discount, and including tax only when the document's prices include tax.\",\n ui: { format: 'money' },\n })\n amount: number = 0;\n\n /**\n * Optional product reference (cross-package, plain string)\n */\n @field({\n description: 'The product this line is for, if it is one you list.',\n })\n @crossPackageRef('@happyvertical/smrt-products:Product')\n productId: string = '';\n\n /**\n * SKU or item code\n */\n sku: string = '';\n\n /**\n * Start date (for lease/subscription items)\n */\n startDate: Date | null = null;\n\n /**\n * End date (for lease/subscription items)\n */\n endDate: Date | null = null;\n\n /**\n * Billing period (for subscriptions: 'monthly', 'yearly', etc.)\n */\n billingPeriod: string = '';\n\n /**\n * Sort order within the contract\n */\n sortOrder: number = 0;\n\n constructor(options: ContractLineItemOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.contractId !== undefined) this.contractId = options.contractId;\n if (options.description !== undefined)\n this.description = options.description;\n if (options.quantity !== undefined) this.quantity = options.quantity;\n if (options.unitPrice !== undefined) this.unitPrice = options.unitPrice;\n if (options.discount !== undefined) this.discount = options.discount;\n if (options.discountType !== undefined)\n this.discountType = options.discountType;\n if (options.discountRate !== undefined)\n this.discountRate = options.discountRate;\n if (options.taxRate !== undefined) this.taxRate = options.taxRate;\n if (options.taxMode !== undefined) this.taxMode = options.taxMode;\n if (options.taxCodeId !== undefined) this.taxCodeId = options.taxCodeId;\n if (options.taxCodeCode !== undefined)\n this.taxCodeCode = options.taxCodeCode;\n if (options.taxCodeName !== undefined)\n this.taxCodeName = options.taxCodeName;\n if (options.taxComponentsJson !== undefined)\n this.taxComponentsJson = options.taxComponentsJson;\n if (options.taxComponents !== undefined)\n this.taxComponents = structuredClone(options.taxComponents);\n if (options.taxAmount !== undefined) this.taxAmount = options.taxAmount;\n if (options.moneySnapshotId !== undefined)\n this.moneySnapshotId = options.moneySnapshotId;\n if (options.amount !== undefined) this.amount = options.amount;\n if (options.productId !== undefined) this.productId = options.productId;\n if (options.sku !== undefined) this.sku = options.sku;\n if (options.startDate !== undefined) this.startDate = options.startDate;\n if (options.endDate !== undefined) this.endDate = options.endDate;\n if (options.billingPeriod !== undefined)\n this.billingPeriod = options.billingPeriod;\n if (options.sortOrder !== undefined) this.sortOrder = options.sortOrder;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n // A row read from the database must not be old-format money data.\n if (this.isPersisted) {\n const { assertHydratedMoneyRowCurrent } = await import(\n '../upgrade/detect.js'\n );\n await assertHydratedMoneyRowCurrent(this);\n }\n return this;\n }\n\n override async save(): Promise<this> {\n const { saveContractLineItem } = await import('../contract-money.js');\n return (await saveContractLineItem(this)) as this;\n }\n\n /** @internal Contract aggregate persistence only. */\n async saveDerived(capability: DocumentMoneyWriteCapability): Promise<this> {\n if (capability !== DOCUMENT_MONEY_WRITE)\n throw new Error('Contract line money write capability is required');\n // Money is integer minor units: refuse a major-unit price before it can\n // reach the database (#2401), ahead of the aggregate recompute.\n for (const [field, value] of [\n ['unitPrice', this.unitPrice],\n ['discount', this.discount],\n ] as const)\n if (!Number.isSafeInteger(value))\n throw new MoneyValidationError(\n 'UNSAFE_MONEY',\n `Contract line ${field} ${value} must be a safe integer number of minor units`,\n );\n return (await super.save()) as this;\n }\n\n override async delete(\n options: { expectedUpdatedAt?: Date | string } = {},\n ): Promise<void> {\n const { deleteContractLineItem } = await import('../contract-money.js');\n await deleteContractLineItem(this, options);\n }\n\n /** @internal Contract aggregate persistence only. */\n async deleteDerived(\n capability: DocumentMoneyWriteCapability,\n options: { expectedUpdatedAt?: Date | string } = {},\n ): Promise<void> {\n if (capability !== DOCUMENT_MONEY_WRITE)\n throw new Error('Contract line money write capability is required');\n await super.delete(options);\n }\n\n /**\n * Check if this is a subscription/recurring item\n */\n isRecurring(): boolean {\n return !!this.billingPeriod && !!this.startDate;\n }\n\n /**\n * Check if subscription is active\n */\n isSubscriptionActive(): boolean {\n if (!this.isRecurring()) return false;\n if (!this.startDate) return false;\n\n const now = new Date();\n if (now < this.startDate) return false;\n if (this.endDate && now > this.endDate) return false;\n\n return true;\n }\n}\n\nexport default ContractLineItem;\n","/**\n * ContractLineItemCollection - Collection manager for ContractLineItem objects\n * @packageDocumentation\n */\n\nimport {\n SmrtCollection,\n type SmrtListOptions,\n type SmrtObject,\n type SmrtSelectedRow,\n type SmrtSelectField,\n type SmrtStiReadScope,\n type SmrtWhereClause,\n} from '@happyvertical/smrt-core';\nimport {\n getTenantId,\n queryGlobal,\n queryWithGlobals,\n} from '@happyvertical/smrt-tenancy';\nimport { DOCUMENT_MONEY_READ_REBIND } from '../document-money-internal.js';\nimport { ContractLineItem } from '../models/ContractLineItem.js';\nimport { withTenantMoneyCollectionRead } from '../tenant-money-barrier.js';\nimport { assertCommerceMoneySchemaCurrent } from '../upgrade/detect.js';\n\nexport class ContractLineItemCollection extends SmrtCollection<ContractLineItem> {\n static readonly _itemClass = ContractLineItem;\n\n private async withMoneyRead<T>(operation: () => Promise<T>): Promise<T> {\n const rootDb = this.db;\n const tenantId = getTenantId();\n if (!tenantId) {\n // Without a tenant, still refuse a database with old money storage.\n await assertCommerceMoneySchemaCurrent(rootDb);\n return operation();\n }\n const result = await withTenantMoneyCollectionRead(\n rootDb,\n tenantId,\n (boundDb) => this.withDatabase(boundDb, operation),\n );\n if (result instanceof ContractLineItem)\n result.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);\n else if (Array.isArray(result))\n for (const item of result)\n if (item instanceof ContractLineItem)\n item.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);\n return result;\n }\n\n override async get(\n filter: string | SmrtWhereClause<ContractLineItem>,\n options: Parameters<SmrtCollection<ContractLineItem>['get']>[1] = {},\n ): Promise<ContractLineItem | null> {\n return this.withMoneyRead(() => super.get(filter, options));\n }\n\n override async list<\n const Select extends readonly SmrtSelectField<ContractLineItem>[],\n >(\n options: Omit<SmrtListOptions<ContractLineItem>, 'select' | 'stiScope'> & {\n select: Select;\n include?: never;\n stiScope: SmrtStiReadScope;\n },\n ): Promise<Record<string, unknown>[]>;\n override async list<\n const Select extends readonly SmrtSelectField<ContractLineItem>[],\n >(\n options: Omit<SmrtListOptions<ContractLineItem>, 'select' | 'stiScope'> & {\n select: Select;\n include?: never;\n stiScope?: undefined;\n },\n ): Promise<SmrtSelectedRow<ContractLineItem, Select>[]>;\n override async list(\n options: Omit<SmrtListOptions<ContractLineItem>, 'select' | 'stiScope'> & {\n select?: undefined;\n stiScope: SmrtStiReadScope;\n },\n ): Promise<SmrtObject[]>;\n override async list(options?: undefined): Promise<ContractLineItem[]>;\n override async list(\n options: Omit<SmrtListOptions<ContractLineItem>, 'select' | 'stiScope'> & {\n select?: undefined;\n stiScope?: undefined;\n },\n ): Promise<ContractLineItem[]>;\n override async list(\n options:\n | (Omit<SmrtListOptions<ContractLineItem>, 'select'> & {\n select?: undefined;\n })\n | undefined,\n ): Promise<SmrtObject[]>;\n override async list(\n options: SmrtListOptions<ContractLineItem> | undefined,\n ): Promise<SmrtObject[] | Record<string, unknown>[]>;\n override async list(\n options: SmrtListOptions<ContractLineItem> = {},\n ): Promise<SmrtObject[] | Record<string, unknown>[]> {\n return this.withMoneyRead(() => super.list(options));\n }\n\n /**\n * Find contract line items by their parent contract.\n *\n * @param contractId - Contract ID\n * @returns Array of contract line items, sorted by sortOrder\n */\n async findByContract(contractId: string): Promise<ContractLineItem[]> {\n return await this.list({\n where: { contractId },\n orderBy: 'sortOrder ASC',\n });\n }\n\n // ============================================================================\n // Tenant Helper Methods\n // ============================================================================\n\n /**\n * Find all contract line items belonging to a specific tenant\n *\n * @param tenantId - Tenant ID\n * @returns Array of contract line items for the tenant\n */\n async findByTenant(tenantId: string): Promise<ContractLineItem[]> {\n return this.list({ where: { tenantId } });\n }\n\n /**\n * Find all global contract line items (not associated with any tenant).\n *\n * Routes through the shared tenant-global helper so it does not throw under\n * an active tenant context (an explicit `tenant_id IS NULL` filter would be\n * flagged as an isolation violation). (#1600)\n *\n * @returns Array of global contract line items\n */\n async findGlobal(): Promise<ContractLineItem[]> {\n return queryGlobal<ContractLineItem>(this);\n }\n\n /**\n * Find contract line items for a tenant including global line items.\n *\n * Fails closed if an active tenant context requests a different tenant's\n * rows; the admin/system path keeps the cross-tenant capability. (#1600)\n *\n * @param tenantId - Tenant ID\n * @returns Array of tenant-specific and global contract line items\n */\n async findWithGlobals(tenantId: string): Promise<ContractLineItem[]> {\n return queryWithGlobals<ContractLineItem>(\n this,\n tenantId,\n 'ContractLineItem.findWithGlobals',\n );\n }\n}\n"],"mappings":"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"}
@@ -4,11 +4,11 @@ import { MoneyValidationError, assertIntegerMinorUnits, calculateMoneyDocument,
4
4
  import { n as withHistoricalCommerceWrite, t as assertHistoricalCommerceWrite } from "./historical-commerce-internal-C9ButfuH.js";
5
5
  import { t as allocateFrozenCredit } from "./credit-allocation-yspz0Lab.js";
6
6
  import { _ as withDocumentMoneyTransaction, a as assertCommerceMoneySchemaCurrent, f as DOCUMENT_MONEY_READ_REBIND, g as withDocumentMoneyObjectTransaction, h as lockDocumentMoneyParents, p as DOCUMENT_MONEY_WRITE } from "./detect-DAE-_NAk.js";
7
- import { s as parseInvoiceRetention, t as InvoiceCollection } from "./InvoiceCollection-XPcPyibo.js";
8
- import { n as withTenantMoneyCollectionRead, r as withTenantMoneyRuntime, t as lockTenantMoneyRuntime } from "./tenant-money-barrier-CG78Xxja.js";
9
- import { t as TaxCodeCollection } from "./TaxCodeCollection-CbTBWO8z.js";
10
- import { t as ContractLineItemCollection } from "./ContractLineItemCollection-DQZqRiZi.js";
11
- import { t as TaxComponentCollection } from "./TaxComponentCollection-VZ_cGr2n.js";
7
+ import { s as parseInvoiceRetention, t as InvoiceCollection } from "./InvoiceCollection-Datg_eZM.js";
8
+ import { n as withTenantMoneyCollectionRead, r as withTenantMoneyRuntime, t as lockTenantMoneyRuntime } from "./tenant-money-barrier-DXnJWxiZ.js";
9
+ import { t as TaxCodeCollection } from "./TaxCodeCollection-CIC1skxr.js";
10
+ import { t as ContractLineItemCollection } from "./ContractLineItemCollection-DkBeBR7a.js";
11
+ import { t as TaxComponentCollection } from "./TaxComponentCollection-BHBV-_1J.js";
12
12
  import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
13
13
  import { TenantScoped, getTenantId, queryGlobal, queryWithGlobals, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
14
14
  import { ISO_4217_MINOR_UNITS } from "@happyvertical/smrt-ui/currency";
@@ -1226,7 +1226,7 @@ var Contract = class extends SmrtObject {
1226
1226
  * Calculate and update totals from line items
1227
1227
  */
1228
1228
  async recalculateTotals() {
1229
- const { recalculateContractTotals } = await import("./contract-money-DGNqQwf0.js");
1229
+ const { recalculateContractTotals } = await import("./contract-money-C61kGBkU.js");
1230
1230
  await recalculateContractTotals(this);
1231
1231
  }
1232
1232
  /**
@@ -1237,6 +1237,7 @@ var Contract = class extends SmrtObject {
1237
1237
  */
1238
1238
  async initialize() {
1239
1239
  await super.initialize();
1240
+ this.billingScheduleJson ??= "";
1240
1241
  if (this.isPersisted) {
1241
1242
  const { assertHydratedMoneyRowCurrent } = await import("./detect-DAE-_NAk.js").then((n) => n.c);
1242
1243
  await assertHydratedMoneyRowCurrent(this);
@@ -1256,12 +1257,12 @@ var Contract = class extends SmrtObject {
1256
1257
  * write surface (#2401).
1257
1258
  */
1258
1259
  async save() {
1259
- const { saveContract } = await import("./contract-money-DGNqQwf0.js");
1260
+ const { saveContract } = await import("./contract-money-C61kGBkU.js");
1260
1261
  return await saveContract(this);
1261
1262
  }
1262
1263
  /** Received purchase monetary history cannot be deleted. */
1263
1264
  async delete(options = {}) {
1264
- const { deleteContract } = await import("./contract-money-DGNqQwf0.js");
1265
+ const { deleteContract } = await import("./contract-money-C61kGBkU.js");
1265
1266
  await deleteContract(this, options);
1266
1267
  }
1267
1268
  /** @internal Contract aggregate persistence only. */
@@ -1343,7 +1344,10 @@ __decorateClass$1([field({
1343
1344
  ui: { widget: "currency" }
1344
1345
  })], Contract.prototype, "currency", 2);
1345
1346
  __decorateClass$1([field({ description: "Whether amounts were calculated or recorded from reviewed original paperwork." })], Contract.prototype, "moneyAuthority", 2);
1346
- __decorateClass$1([field({ type: "text" })], Contract.prototype, "billingScheduleJson", 2);
1347
+ __decorateClass$1([field({
1348
+ type: "text",
1349
+ default: ""
1350
+ })], Contract.prototype, "billingScheduleJson", 2);
1347
1351
  __decorateClass$1([field({
1348
1352
  description: "The date it was issued or placed.",
1349
1353
  ui: { format: "date" }
@@ -4395,4 +4399,4 @@ var DocumentTaxCollection = class extends SmrtCollection {
4395
4399
  //#endregion
4396
4400
  export { DEFAULT_PARTY_CONTACT_FIELD_NAMES as $, PurchaseContractCollection as A, PurchaseOrder as B, refreshIssuedCreditBalance as C, issueContractMilestoneInvoice as D, getContractBillingSchedule as E, Lease as F, isContractSnapshotReportEligible as G, SupplierQuote as H, LicenseSale as I, HistoricalSalesLine as J, Vendor as K, Order as L, Cart as M, Contract as N, ContractCollection as O, Estimate as P, Customer as Q, ProductionOrder as R, previewInvoiceMoney as S, approveContractBillingSchedule as T, WholesaleOrder as U, SalesContract as V, classifyContractPurchase as W, InvoiceLineItem as X, InvoiceLineItemCollection as Y, CustomerCollection as Z, historicalText as _, getReceivedSalesContractMoney as a, normalizePartyContacts as at, getInvoiceMoneyFacts as b, validateReceivedPurchaseMoneyFacts as c, validatePartyContactRows as ct, HistoricalSalesTaxClassificationRequired as d, PARTY_CONTACT_MAX_LENGTHS as et, listHistoricalSalesHeaderFacts as f, historicalCalendarDate as g, recordHistoricalSalesMoney as h, getReceivedPurchaseMoney as i, applyPartyContactIntent as it, Agreement as j, SalesContractCollection as k, VendorCollection as l, readHistoricalSalesLines as m, DocumentTaxCollection_exports as n, PartyContactPayloadError as nt, recordReceivedPurchaseMoney as o, readPartyContactRows as ot, getHistoricalSalesMoney as p, HistoricalSalesLineCollection as q, ReceivedPurchaseTaxClassificationRequired as r, PartyContactValidationError as rt, recordReceivedSalesContractMoney as s, savePartyContacts as st, DocumentTaxCollection as t, PartyContactPartyNotFoundError as tt, DocumentTax as u, validateHistoricalSalesMoneyFacts as v, allocateContractMilestones as w, lockAndReloadInvoice as x, document_money_exports as y, PurchaseContract as z };
4397
4401
 
4398
- //# sourceMappingURL=DocumentTaxCollection-Dg942m_Z.js.map
4402
+ //# sourceMappingURL=DocumentTaxCollection-D85ut-Fo.js.map