@happyvertical/smrt-commerce 0.30.0

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 (101) hide show
  1. package/AGENTS.md +44 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +146 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/collections/ContractCollection.d.ts +87 -0
  8. package/dist/collections/ContractCollection.d.ts.map +1 -0
  9. package/dist/collections/CustomerCollection.d.ts +58 -0
  10. package/dist/collections/CustomerCollection.d.ts.map +1 -0
  11. package/dist/collections/FulfillmentCollection.d.ts +75 -0
  12. package/dist/collections/FulfillmentCollection.d.ts.map +1 -0
  13. package/dist/collections/InvoiceCollection.d.ts +162 -0
  14. package/dist/collections/InvoiceCollection.d.ts.map +1 -0
  15. package/dist/collections/InvoiceLineItemCollection.d.ts +90 -0
  16. package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -0
  17. package/dist/collections/PaymentAllocationCollection.d.ts +86 -0
  18. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -0
  19. package/dist/collections/PaymentCollection.d.ts +96 -0
  20. package/dist/collections/PaymentCollection.d.ts.map +1 -0
  21. package/dist/collections/PaymentIntentCollection.d.ts +66 -0
  22. package/dist/collections/PaymentIntentCollection.d.ts.map +1 -0
  23. package/dist/collections/PayoutCollection.d.ts +47 -0
  24. package/dist/collections/PayoutCollection.d.ts.map +1 -0
  25. package/dist/collections/VendorCollection.d.ts +59 -0
  26. package/dist/collections/VendorCollection.d.ts.map +1 -0
  27. package/dist/collections/index.d.ts +15 -0
  28. package/dist/collections/index.d.ts.map +1 -0
  29. package/dist/index.d.ts +5 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +5308 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/manifest.json +13852 -0
  34. package/dist/models/Contract.d.ts +425 -0
  35. package/dist/models/Contract.d.ts.map +1 -0
  36. package/dist/models/ContractLineItem.d.ts +92 -0
  37. package/dist/models/ContractLineItem.d.ts.map +1 -0
  38. package/dist/models/Customer.d.ts +98 -0
  39. package/dist/models/Customer.d.ts.map +1 -0
  40. package/dist/models/Fulfillment.d.ts +99 -0
  41. package/dist/models/Fulfillment.d.ts.map +1 -0
  42. package/dist/models/FulfillmentLineItem.d.ts +42 -0
  43. package/dist/models/FulfillmentLineItem.d.ts.map +1 -0
  44. package/dist/models/Invoice.d.ts +326 -0
  45. package/dist/models/Invoice.d.ts.map +1 -0
  46. package/dist/models/InvoiceLineItem.d.ts +120 -0
  47. package/dist/models/InvoiceLineItem.d.ts.map +1 -0
  48. package/dist/models/Payment.d.ts +269 -0
  49. package/dist/models/Payment.d.ts.map +1 -0
  50. package/dist/models/PaymentAllocation.d.ts +93 -0
  51. package/dist/models/PaymentAllocation.d.ts.map +1 -0
  52. package/dist/models/PaymentIntent.d.ts +341 -0
  53. package/dist/models/PaymentIntent.d.ts.map +1 -0
  54. package/dist/models/Payout.d.ts +200 -0
  55. package/dist/models/Payout.d.ts.map +1 -0
  56. package/dist/models/Vendor.d.ts +153 -0
  57. package/dist/models/Vendor.d.ts.map +1 -0
  58. package/dist/models/index.d.ts +17 -0
  59. package/dist/models/index.d.ts.map +1 -0
  60. package/dist/playground.d.ts +2 -0
  61. package/dist/playground.d.ts.map +1 -0
  62. package/dist/playground.js +108 -0
  63. package/dist/playground.js.map +1 -0
  64. package/dist/smrt-knowledge.json +5494 -0
  65. package/dist/svelte/components/InvoiceActions.svelte +191 -0
  66. package/dist/svelte/components/InvoiceActions.svelte.d.ts +26 -0
  67. package/dist/svelte/components/InvoiceActions.svelte.d.ts.map +1 -0
  68. package/dist/svelte/components/InvoiceCard.svelte +233 -0
  69. package/dist/svelte/components/InvoiceCard.svelte.d.ts +16 -0
  70. package/dist/svelte/components/InvoiceCard.svelte.d.ts.map +1 -0
  71. package/dist/svelte/components/InvoiceHeader.svelte +258 -0
  72. package/dist/svelte/components/InvoiceHeader.svelte.d.ts +26 -0
  73. package/dist/svelte/components/InvoiceHeader.svelte.d.ts.map +1 -0
  74. package/dist/svelte/components/InvoiceLineItems.svelte +322 -0
  75. package/dist/svelte/components/InvoiceLineItems.svelte.d.ts +24 -0
  76. package/dist/svelte/components/InvoiceLineItems.svelte.d.ts.map +1 -0
  77. package/dist/svelte/components/InvoiceTotals.svelte +193 -0
  78. package/dist/svelte/components/InvoiceTotals.svelte.d.ts +27 -0
  79. package/dist/svelte/components/InvoiceTotals.svelte.d.ts.map +1 -0
  80. package/dist/svelte/components/UnbilledItems.svelte +355 -0
  81. package/dist/svelte/components/UnbilledItems.svelte.d.ts +18 -0
  82. package/dist/svelte/components/UnbilledItems.svelte.d.ts.map +1 -0
  83. package/dist/svelte/i18n.d.ts +19 -0
  84. package/dist/svelte/i18n.d.ts.map +1 -0
  85. package/dist/svelte/i18n.js +19 -0
  86. package/dist/svelte/index.d.ts +40 -0
  87. package/dist/svelte/index.d.ts.map +1 -0
  88. package/dist/svelte/index.js +43 -0
  89. package/dist/svelte/playground.d.ts +103 -0
  90. package/dist/svelte/playground.d.ts.map +1 -0
  91. package/dist/svelte/playground.js +103 -0
  92. package/dist/svelte/types.d.ts +47 -0
  93. package/dist/svelte/types.d.ts.map +1 -0
  94. package/dist/svelte/types.js +4 -0
  95. package/dist/types/index.d.ts +234 -0
  96. package/dist/types/index.d.ts.map +1 -0
  97. package/dist/ui.d.ts +10 -0
  98. package/dist/ui.d.ts.map +1 -0
  99. package/dist/ui.js +85 -0
  100. package/dist/ui.js.map +1 -0
  101. package/package.json +87 -0
@@ -0,0 +1,341 @@
1
+ import { SmrtObject } from '@happyvertical/smrt-core';
2
+ import { PaymentIntentStatus, PaymentOption } from '../types/index.js';
3
+ export declare class PaymentIntent extends SmrtObject {
4
+ /**
5
+ * Tenant ID for multi-tenant isolation. Nullable so the same model
6
+ * can serve both per-tenant SaaS deployments and single-tenant /
7
+ * global setups.
8
+ */
9
+ tenantId: string | null;
10
+ /**
11
+ * Plain string reference to the upstream `Sku` (from
12
+ * `@happyvertical/smrt-products`) being purchased. Cross-package
13
+ * reference — plain string, not `@foreignKey()`, to avoid the
14
+ * circular dependency the framework warns against.
15
+ *
16
+ * Required for the marketplace flow; other consumers can leave it
17
+ * empty if they don't model a catalog.
18
+ */
19
+ skuId: string;
20
+ /**
21
+ * Caller-supplied scope for the idempotency-key natural key. The
22
+ * marketplace sets this to a `Sku` id, but the field is intentionally
23
+ * abstract so other consumers (subscription tiers, donation tiers,
24
+ * tipping flows) can scope idempotency by whatever granularity makes
25
+ * sense.
26
+ *
27
+ * Empty string is permitted but disables the natural-key dedup —
28
+ * every retry then creates a fresh row.
29
+ */
30
+ offeringRef: string;
31
+ /**
32
+ * The buyer's email address. The licensee on any downstream
33
+ * `LicenseSale` (or similar rights-issuance row) is identified by
34
+ * this email — high-tier purchases also create a `Customer` record
35
+ * and set {@link customerId}, but most purchases get away with email
36
+ * alone.
37
+ */
38
+ licenseeEmail: string;
39
+ /**
40
+ * Optional link to a `Customer` row for purchases that warrant a
41
+ * full customer record (high-tier licensing, recurring buyers,
42
+ * etc.). Cross-model reference is kept as a plain string to match
43
+ * the rest of the package's cross-package convention.
44
+ */
45
+ customerId: string;
46
+ /**
47
+ * The payment rails the buyer can choose between to satisfy this
48
+ * intent. Stored as a JSON column. See {@link PaymentOption} for
49
+ * the per-option shape. Empty array means the intent is invalid /
50
+ * mis-built; callers should reject save in that case.
51
+ */
52
+ paymentOptions: PaymentOption[];
53
+ /**
54
+ * USD price locked at quote time. Stored at decimal precision.
55
+ * Independent of any specific option's native-currency amount;
56
+ * `usdPriceLocked` is the canonical "what this costs in USD"
57
+ * number used downstream for reporting, tax, and drift accounting.
58
+ */
59
+ usdPriceLocked: number;
60
+ /**
61
+ * Length of the price-lock window in milliseconds. Defaults to 15
62
+ * minutes; consumers can override per-intent. Stored so a later
63
+ * audit can see the original window even after {@link priceLockExpiresAt}
64
+ * has passed.
65
+ */
66
+ priceLockWindowMs: number;
67
+ /**
68
+ * Wall-clock time at which the price lock expires. Set by the
69
+ * constructor (or by `expire()` if you want to short-circuit a
70
+ * specific intent). Once `Date.now()` passes this value and the
71
+ * status is still `AWAITING_PAYMENT`, callers should treat the
72
+ * intent as expired and refuse to record a payment against it.
73
+ */
74
+ priceLockExpiresAt: Date | null;
75
+ /**
76
+ * Current status — see {@link PaymentIntentStatus} for the state
77
+ * machine. Mutate via the dedicated `markPaid` / `markIssued` /
78
+ * `expire` / `cancel` / `retire` helpers rather than assigning
79
+ * directly, so the helpers' invariant checks run.
80
+ */
81
+ status: PaymentIntentStatus;
82
+ /**
83
+ * Caller-supplied idempotency key. Combined with `tenantId`,
84
+ * `offeringRef`, and `licenseeEmail`, this forms the natural key
85
+ * registered in `conflictColumns` — a retried `create` with the
86
+ * same tuple upserts the existing row instead of creating a
87
+ * duplicate.
88
+ *
89
+ * Empty string disables natural-key dedup (every retry creates a
90
+ * fresh row).
91
+ */
92
+ idempotencyKey: string;
93
+ /**
94
+ * `backendId` of the option that satisfied the intent. Set by
95
+ * {@link markPaid}; remains empty for non-paid intents. Used by
96
+ * {@link isOptionRetired} to flag inbound funds on the other
97
+ * options as "needs refund".
98
+ */
99
+ paidOptionBackendId: string;
100
+ /**
101
+ * Plain string reference to the `Payment` row that satisfied the
102
+ * intent. Cross-model reference matches the rest of the
103
+ * package's convention.
104
+ */
105
+ paymentId: string;
106
+ /**
107
+ * When the intent transitioned to `PAID`.
108
+ */
109
+ paidAt: Date | null;
110
+ /**
111
+ * When the intent transitioned to `ISSUED`.
112
+ */
113
+ issuedAt: Date | null;
114
+ /**
115
+ * When the intent transitioned to `EXPIRED`.
116
+ */
117
+ expiredAt: Date | null;
118
+ /**
119
+ * When the intent transitioned to `CANCELLED`.
120
+ */
121
+ cancelledAt: Date | null;
122
+ /**
123
+ * When the intent transitioned to `RETIRED`.
124
+ */
125
+ retiredAt: Date | null;
126
+ /**
127
+ * Optional human-readable notes (e.g., support tickets, refund
128
+ * memos, cancellation reasons). Append-only by convention.
129
+ */
130
+ notes: string;
131
+ constructor(options?: any);
132
+ /**
133
+ * Re-normalize `paymentOptions` after the framework's
134
+ * `initializePropertiesFromOptions` pass has overwritten the
135
+ * constructor-set values with the raw cloned input, and stamp a
136
+ * default `priceLockExpiresAt` so newly-created intents have a
137
+ * concrete expiry without callers needing to compute it.
138
+ */
139
+ initialize(): Promise<this>;
140
+ /**
141
+ * Save-time state-machine guard (S5 audit #1390 round 2).
142
+ *
143
+ * `status`, `paymentId`, and `paidOptionBackendId` are all mass-assignable on
144
+ * the generated update route, and the sync `markPaid` helper trusts its
145
+ * arguments. Two distinct attacks follow:
146
+ *
147
+ * 1. **Forge a PAID intent** — set `status: 'paid'` (or call bare `markPaid`)
148
+ * against a bogus / pending / non-matching Payment. This falsifies
149
+ * downstream "this was paid for" rights issuance.
150
+ * 2. **Repoint an already-PAID intent** — leave `status: 'paid'` but swap
151
+ * `paymentId` / `paidOptionBackendId` to a bogus Payment after the fact.
152
+ * Round 1 only verified the *transition into* PAID, so an already-PAID
153
+ * row could be silently repointed with no re-verify. Likewise an intent
154
+ * forced straight to `ISSUED` (the terminal happy-path that gates rights
155
+ * issuance) was never required to have been backed by a real Payment.
156
+ *
157
+ * This guard therefore:
158
+ * - validates the status transition is legal (no `awaiting_payment → issued`
159
+ * skips, no reviving terminal states);
160
+ * - re-verifies the backing Payment on **every** save where the persisted
161
+ * status is PAID or ISSUED — not just the transition edge — so a repoint
162
+ * of the paid backing fields is caught.
163
+ *
164
+ * Re-verifying an unchanged PAID/ISSUED row is cheap (one Payment load) and
165
+ * closes the repoint hole; freezing the backing fields would instead block
166
+ * legitimate corrections, so we re-verify.
167
+ */
168
+ save(): Promise<this>;
169
+ /**
170
+ * Load the authoritative persisted row for this intent (S5 audit #1390 round
171
+ * 4). Returns `undefined` when the intent has no `id` or no row exists yet
172
+ * (truly new). Reading the DB directly — rather than trusting the
173
+ * {@link loadedIntentStatus} WeakMap, which is only populated when
174
+ * {@link initialize} hydrated the row — defeats the poisonable-prior-state
175
+ * vector where `create({ id: <existing>, _skipLoad: true })` produces an
176
+ * un-hydrated instance whose WeakMap entry is missing. A create-onto-existing
177
+ * is thus correctly treated as an update.
178
+ */
179
+ private loadPersistedRow;
180
+ /**
181
+ * Reject any change to the settled backing fields of an already-PAID/ISSUED
182
+ * intent (codex HIGH#2). `paymentOptions` is compared structurally because
183
+ * the winning option's `nativeAmount` is exactly what the reconciliation in
184
+ * {@link assertBackedByCompletedPayment} binds against — letting it drift
185
+ * would let a repointed `paymentId` match a doctored option.
186
+ */
187
+ private assertBackingFieldsUnchanged;
188
+ /**
189
+ * Reject an illegal status flip done via raw field assignment. Compares the
190
+ * about-to-be-written status against the status the row was loaded with.
191
+ * No-op re-saves (status unchanged) and brand-new rows are allowed (the
192
+ * backing-Payment verification still runs separately for PAID/ISSUED).
193
+ */
194
+ private assertStatusTransition;
195
+ /**
196
+ * Verify the intent's `paidOptionBackendId` / `paymentId` reference a real,
197
+ * COMPLETED, amount-matching Payment. Throws otherwise. Shared by
198
+ * {@link verifyAndMarkPaid} (pre-transition) and the save-time guard
199
+ * (catch-all for raw mass-assignment).
200
+ */
201
+ private assertBackedByCompletedPayment;
202
+ /**
203
+ * Reconcile a COMPLETED Payment against the winning {@link PaymentOption}
204
+ * (S5 audit #1390). Beyond the amount check, the Payment must have arrived on
205
+ * the SAME rail and currency the option quoted — otherwise an unrelated
206
+ * completed payment that merely shares a numeric amount (e.g. a USD 199
207
+ * payment) could satisfy a different option (e.g. a `base-usdc` 199 option),
208
+ * marking the intent paid on the wrong rail with no matching funds. Compares
209
+ * the Payment's `backendId` (rail) and native currency (falling back to its
210
+ * settlement currency) against the option's `backendId` / `currency`, then
211
+ * the native amount.
212
+ *
213
+ * Shared by {@link verifyAndMarkPaid} (pre-transition) and
214
+ * {@link assertBackedByCompletedPayment} (save-time catch-all) so both gates
215
+ * enforce the identical invariant.
216
+ */
217
+ private reconcilePaymentWithOption;
218
+ isAwaitingPayment(): boolean;
219
+ isPaid(): boolean;
220
+ isIssued(): boolean;
221
+ isCancelled(): boolean;
222
+ isRetired(): boolean;
223
+ /**
224
+ * Terminal-state predicate: any status other than `AWAITING_PAYMENT`
225
+ * is terminal (the intent will not accept further state changes
226
+ * except the explicit `markIssued` after `PAID`).
227
+ */
228
+ isTerminal(): boolean;
229
+ /**
230
+ * `Date.now()`-based predicate: has the price lock window passed?
231
+ * Independent of `status` so callers can detect a stale-but-not-
232
+ * yet-marked-EXPIRED intent (and call `expire()` to advance it).
233
+ */
234
+ isExpired(): boolean;
235
+ /**
236
+ * Transition the intent to `PAID`, naming which option satisfied it
237
+ * and which `Payment` row carries the funds. Implicitly retires the
238
+ * other options — callers can check {@link isOptionRetired} to flag
239
+ * later inbound funds for refund.
240
+ *
241
+ * Throws when called on a non-`AWAITING_PAYMENT` intent so a stale
242
+ * caller can't accidentally overwrite a winning option with a
243
+ * losing one.
244
+ */
245
+ markPaid(args: {
246
+ backendId: string;
247
+ paymentId: string;
248
+ }): void;
249
+ /**
250
+ * Verify-then-transition variant of {@link markPaid} (S5 audit #1390).
251
+ *
252
+ * `markPaid` trusts the caller-supplied `paymentId` / `backendId` without
253
+ * checking the referenced `Payment` actually exists, is `COMPLETED`, or
254
+ * carries the amount the winning option quoted. That lets a caller mark an
255
+ * intent PAID against a non-existent or still-pending payment. This method
256
+ * loads the `Payment` row and enforces:
257
+ *
258
+ * - the Payment exists,
259
+ * - it is `COMPLETED` (not pending / failed / cancelled / refunded),
260
+ * - it arrived on the same rail (`backendId`) and currency the option
261
+ * quoted (so an unrelated completed payment that merely shares a numeric
262
+ * amount can't satisfy a different option),
263
+ * - and its amount reconciles with the winning option's `nativeAmount`
264
+ * (within sub-cent tolerance), falling back to the option vs. the
265
+ * Payment's settlement `amount` when no native rail is recorded.
266
+ *
267
+ * Only after those pass does it delegate to the sync `markPaid` for the
268
+ * status-machine invariants.
269
+ *
270
+ * @param args.backendId backendId of the option that was satisfied
271
+ * @param args.paymentId id of the Payment row carrying the funds (required)
272
+ */
273
+ verifyAndMarkPaid(args: {
274
+ backendId: string;
275
+ paymentId: string;
276
+ }): Promise<void>;
277
+ /**
278
+ * Transition a `PAID` intent to `ISSUED` once the downstream rights
279
+ * / contract / fulfillment have been created. Idempotent — calling
280
+ * twice is a no-op so callers don't have to guard against retries.
281
+ */
282
+ markIssued(): void;
283
+ /**
284
+ * Move an `AWAITING_PAYMENT` intent to `EXPIRED`. Safe to call on
285
+ * an already-expired intent (no-op). Throws on terminal non-
286
+ * expired states so a confused caller can't undo a paid / issued
287
+ * intent.
288
+ */
289
+ expire(): void;
290
+ /**
291
+ * Cancel an open intent. Only valid from `AWAITING_PAYMENT` —
292
+ * a paid / issued intent is no longer the buyer's to cancel; that
293
+ * path is a refund, modelled separately on `Payment`.
294
+ */
295
+ cancel(reason?: string): void;
296
+ /**
297
+ * Mark a paid intent as retired — used when a previously-recorded
298
+ * payment was reversed (chargeback, refund) and the intent should
299
+ * not be considered "satisfied" anymore. Distinct from `cancel()`
300
+ * which only applies to never-paid intents.
301
+ */
302
+ retire(reason?: string): void;
303
+ /**
304
+ * Return the option whose `backendId` matches, or `undefined` if
305
+ * no such option is on this intent. Useful for routing inbound
306
+ * payments to the right option's `nativeAmount` / `payTo` for
307
+ * verification.
308
+ */
309
+ getOption(backendId: string): PaymentOption | undefined;
310
+ /**
311
+ * `true` once the intent is paid and the given option was NOT the
312
+ * winning one. Consumers use this to flag inbound funds to retired
313
+ * options for refund. Returns `false` for the winning option, for
314
+ * unpaid intents, and for an unknown `backendId`.
315
+ */
316
+ isOptionRetired(backendId: string): boolean;
317
+ /**
318
+ * The options that were NOT selected at payment time. Returns an
319
+ * empty array for unpaid intents so callers can iterate without
320
+ * special-casing.
321
+ */
322
+ getRetiredOptions(): PaymentOption[];
323
+ /**
324
+ * Defensive coercion for `paymentOptions` input. Accepts either an
325
+ * already-parsed array or a JSON string (e.g. from a row whose
326
+ * column was hand-edited or migrated from a different schema).
327
+ * Drops entries that don't carry the required `backendId` /
328
+ * `currency` / `payTo` / `nativeAmount` quartet so downstream
329
+ * routing code can rely on the invariant.
330
+ */
331
+ private static normalizePaymentOptions;
332
+ /**
333
+ * Coerce a Date-ish input (Date / number / ISO string / null /
334
+ * undefined) into a `Date | null`. The framework hands us strings
335
+ * when hydrating from SQLite, numbers when round-tripping through
336
+ * JSON, and Date instances from the application.
337
+ */
338
+ private static coerceDate;
339
+ }
340
+ export default PaymentIntent;
341
+ //# sourceMappingURL=PaymentIntent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaymentIntent.d.ts","sourceRoot":"","sources":["../../src/models/PaymentIntent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,UAAU,EAAQ,MAAM,0BAA0B,CAAC;AAE5D,OAAO,EACL,mBAAmB,EACnB,KAAK,aAAa,EAEnB,MAAM,mBAAmB,CAAC;AAsD3B,qBAsCa,aAAc,SAAQ,UAAU;IAC3C;;;;OAIG;IAEH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B;;;;;;;;OAQG;IACH,KAAK,EAAE,MAAM,CAAM;IAEnB;;;;;;;;;OASG;IACH,WAAW,EAAE,MAAM,CAAM;IAEzB;;;;;;OAMG;IACH,aAAa,EAAE,MAAM,CAAM;IAE3B;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAM;IAExB;;;;;OAKG;IACH,cAAc,EAAE,aAAa,EAAE,CAAM;IAErC;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAO;IAE7B;;;;;OAKG;IACH,iBAAiB,EAAE,MAAM,CAAgC;IAEzD;;;;;;OAMG;IACH,kBAAkB,EAAE,IAAI,GAAG,IAAI,CAAQ;IAEvC;;;;;OAKG;IACH,MAAM,EAAE,mBAAmB,CAAwC;IAEnE;;;;;;;;;OASG;IACH,cAAc,EAAE,MAAM,CAAM;IAE5B;;;;;OAKG;IACH,mBAAmB,EAAE,MAAM,CAAM;IAEjC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAM;IAEvB;;OAEG;IACH,MAAM,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE3B;;OAEG;IACH,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE7B;;OAEG;IACH,SAAS,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE9B;;OAEG;IACH,WAAW,EAAE,IAAI,GAAG,IAAI,CAAQ;IAEhC;;OAEG;IACH,SAAS,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE9B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAM;gBAEP,OAAO,GAAE,GAAQ;IAyC7B;;;;;;OAMG;IACY,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB1C;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAqCpC;;;;;;;;;OASG;YACW,gBAAgB;IAW9B;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IAuCpC;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAa9B;;;;;OAKG;YACW,8BAA8B;IAqC5C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,0BAA0B;IAiDlC,iBAAiB,IAAI,OAAO;IAI5B,MAAM,IAAI,OAAO;IAIjB,QAAQ,IAAI,OAAO;IAInB,WAAW,IAAI,OAAO;IAItB,SAAS,IAAI,OAAO;IAIpB;;;;OAIG;IACH,UAAU,IAAI,OAAO;IAIrB;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAQpB;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAsC9D;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,iBAAiB,CAAC,IAAI,EAAE;QAC5B,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCjB;;;;OAIG;IACH,UAAU,IAAI,IAAI;IAWlB;;;;;OAKG;IACH,MAAM,IAAI,IAAI;IAWd;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAe7B;;;;;OAKG;IACH,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAoB7B;;;;;OAKG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAIvD;;;;;OAKG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAO3C;;;;OAIG;IACH,iBAAiB,IAAI,aAAa,EAAE;IASpC;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IA0CtC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;CAS1B;AAED,eAAe,aAAa,CAAC"}
@@ -0,0 +1,200 @@
1
+ import { SmrtObject } from '@happyvertical/smrt-core';
2
+ import { PayoutStatus } from '../types/index.js';
3
+ export declare class Payout extends SmrtObject {
4
+ /**
5
+ * Tenant ID for multi-tenant isolation. Nullable so global / single-
6
+ * tenant deployments work too.
7
+ */
8
+ tenantId: string | null;
9
+ /**
10
+ * The source {@link Payment} that funded this payout. Plain string
11
+ * reference (FK convention: `@foreignKey('ModelName')` would be
12
+ * fine within the package, but `Payment` lives in this same
13
+ * package — staying with a string keeps the dependency graph
14
+ * clean and matches how `PaymentIntent` references `Payment`).
15
+ */
16
+ paymentId: string;
17
+ /**
18
+ * The destination {@link Vendor}. Foreign-key constrained because
19
+ * Vendor lives in this same package and a hard reference catches
20
+ * dangling payouts at insert time.
21
+ */
22
+ vendorId: string;
23
+ /**
24
+ * Total funds moved, in the originating payment's native currency.
25
+ * Stored at decimal precision (matches `Payment.amount` /
26
+ * `Payment.nativeAmount` convention).
27
+ */
28
+ grossAmount: number;
29
+ /**
30
+ * Operator's take from `grossAmount`. Must equal
31
+ * `grossAmount - supplierNet` at save time — the model enforces
32
+ * the invariant via `validateAmounts()`.
33
+ */
34
+ operatorFee: number;
35
+ /**
36
+ * Net funds the supplier receives. Must equal
37
+ * `grossAmount - operatorFee` at save time.
38
+ */
39
+ supplierNet: number;
40
+ /**
41
+ * Native currency the funds are denominated in — payout-rail-
42
+ * qualified, matching `Payment.nativeCurrency` and
43
+ * `Vendor.payoutAddresses` keys (`USDC-base`, `BTC`, `USD-stripe`,
44
+ * etc.).
45
+ */
46
+ currency: string;
47
+ /**
48
+ * The `PaymentBackend` adapter id used to send the payout. For a
49
+ * marketplace this typically matches the source `Payment.backendId`
50
+ * (currency-match settlement, no conversion); other consumers can
51
+ * use a different backend if they bridge currencies elsewhere.
52
+ */
53
+ backendId: string;
54
+ /**
55
+ * Outgoing chain transaction hash, gateway settlement id, or
56
+ * (for not-yet-broadcast BTC payouts) a PSBT identifier. Set when
57
+ * the payout transitions to `SENT`. Empty until then.
58
+ */
59
+ backendTxRef: string;
60
+ /**
61
+ * Current status — see {@link PayoutStatus} for the state machine.
62
+ * Mutate via `markSent` / `markConfirmed` / `markFailed` /
63
+ * `resetFromFailed` rather than direct assignment so the
64
+ * transition invariants run.
65
+ */
66
+ status: PayoutStatus;
67
+ /**
68
+ * When the payout transitioned to `SENT`.
69
+ */
70
+ sentAt: Date | null;
71
+ /**
72
+ * When the payout transitioned to `CONFIRMED`.
73
+ */
74
+ confirmedAt: Date | null;
75
+ /**
76
+ * When the payout transitioned to `FAILED`.
77
+ */
78
+ failedAt: Date | null;
79
+ /**
80
+ * Caller-supplied human-readable failure reason. Empty for non-
81
+ * failed payouts. Cleared by `resetFromFailed()`.
82
+ */
83
+ failureReason: string;
84
+ /**
85
+ * Append-only operator notes — chargeback memos, manual-retry
86
+ * justifications, etc.
87
+ */
88
+ notes: string;
89
+ constructor(options?: any);
90
+ /**
91
+ * Re-coerce timestamp fields after the framework reapplies raw option
92
+ * values during initialization.
93
+ */
94
+ initialize(): Promise<this>;
95
+ isPending(): boolean;
96
+ isSent(): boolean;
97
+ isConfirmed(): boolean;
98
+ isFailed(): boolean;
99
+ /**
100
+ * Transition `PENDING → SENT`. Requires `backendTxRef` — without a
101
+ * way to identify the outgoing transaction the payout cannot be
102
+ * tracked further. Throws on any other source status so a confused
103
+ * caller can't accidentally roll back a confirmed payout.
104
+ */
105
+ markSent(backendTxRef: string): void;
106
+ /**
107
+ * Transition `SENT → CONFIRMED`. Throws if called from any other
108
+ * status — confirmations on a pending payout would mean a chain
109
+ * confirmation arrived before the application even recorded the
110
+ * outgoing tx, which is a code bug worth surfacing rather than
111
+ * silently fixing.
112
+ */
113
+ markConfirmed(): void;
114
+ /**
115
+ * Move the payout to `FAILED`. Valid from `PENDING` (couldn't even
116
+ * broadcast) or `SENT` (broadcast but chain rejected). Confirmed
117
+ * payouts cannot fail — that path is a refund, modelled separately.
118
+ */
119
+ markFailed(reason: string): void;
120
+ /**
121
+ * Operator-driven reset: move a `FAILED` payout back to `PENDING`
122
+ * after fixing whatever broke. Clears the failure metadata and the
123
+ * old `backendTxRef` (the next attempt will have a new one).
124
+ * Distinct from `markFailed` reflexivity — the issue spec says
125
+ * "failed is terminal but allows manual reset via explicit method".
126
+ */
127
+ resetFromFailed(): void;
128
+ /**
129
+ * Throws if the gross/fee/net invariant doesn't hold. Tolerates a
130
+ * sub-cent rounding fuzz (`EPSILON = 0.01`) to match the rest of
131
+ * the smrt-ledgers package's tolerance.
132
+ */
133
+ validateAmounts(): void;
134
+ /**
135
+ * Save-time state-machine + settlement guard (S5 audit #1390 round 2).
136
+ *
137
+ * `status`, `backendTxRef`, and the amount triple are all mass-assignable on
138
+ * the generated create/update routes. The amount invariant alone (round 1)
139
+ * still let a caller raw-flip a persisted payout to `CONFIRMED` / `SENT`
140
+ * (skipping the chain steps) or remit a `grossAmount` larger than the source
141
+ * Payment actually brought in — money that never arrived.
142
+ *
143
+ * This guard adds:
144
+ * - **Transitions on an existing row must be legal** per
145
+ * {@link PAYOUT_STATUS_TRANSITIONS} (no `pending → confirmed` skip on a
146
+ * raw update, no reviving a CONFIRMED payout). Brand-new rows may be
147
+ * created in any status (collection/import/query fixtures), but advancing
148
+ * a persisted row is constrained to the legal edges the helpers enforce.
149
+ * - **SENT requires a backendTxRef** (matches `markSent`'s invariant) so a
150
+ * sent payout is always traceable, regardless of how the status was set.
151
+ * - **grossAmount is capped by the source Payment** when that Payment
152
+ * resolves: a payout can never remit more than the funding Payment
153
+ * settled. A `paymentId` that doesn't resolve (synthetic id, Payment-less
154
+ * deployment) skips the cap — the source Payment is a plain-string,
155
+ * cross-model reference by design and may legitimately be a manually
156
+ * recorded (non-COMPLETED) row, so the cap is best-effort rather than a
157
+ * hard existence requirement.
158
+ *
159
+ * The amount-invariant check (`validateAmounts`) still runs first as the
160
+ * arithmetic floor.
161
+ */
162
+ save(): Promise<this>;
163
+ /**
164
+ * Resolve the AUTHORITATIVE prior status from the database (S5 audit #1390
165
+ * round 4). The {@link loadedPayoutStatus} WeakMap is only populated when
166
+ * {@link initialize} hydrated the row, so a `create({ id: <existing>,
167
+ * _skipLoad: true })` upsert produces an instance with a missing WeakMap
168
+ * entry — trusting it would treat the write as a brand-new row and skip the
169
+ * transition + CONFIRMED-genesis guards. Reading the persisted row directly
170
+ * makes a create-onto-existing behave as an update. `undefined` means no row
171
+ * exists (genuinely new).
172
+ */
173
+ private resolvePriorStatus;
174
+ /**
175
+ * Reject an illegal status flip on an existing row. Brand-new payouts (no
176
+ * prior) may start in any status — collection imports and query fixtures
177
+ * legitimately seed SENT / CONFIRMED rows — but advancing a *persisted* row
178
+ * is constrained to the legal edges the helpers enforce, so a raw update
179
+ * can't skip `pending → confirmed` or revive a terminal CONFIRMED.
180
+ */
181
+ private assertStatusTransition;
182
+ /**
183
+ * Cap `grossAmount` by the source {@link Payment}'s settled funds. A payout
184
+ * that remits more than the Payment that funded it brought in is money the
185
+ * operator never received.
186
+ *
187
+ * HARD requirement (S5 audit #1390 round 4, codex HIGH#3): the source Payment
188
+ * MUST resolve. Round 3 made the cap best-effort — a `paymentId` that didn't
189
+ * resolve silently skipped the cap, so a caller could remit an arbitrary
190
+ * `grossAmount` simply by pointing at a non-existent (or empty) payment.
191
+ * A payout's whole purpose is to remit funds that *arrived* via a Payment;
192
+ * without a resolvable source there is no funded amount to cap against, so we
193
+ * reject rather than skip. `PayoutCollection.createFromPayment()` always wires
194
+ * a real `paymentId`, so this never blocks the supported creation path.
195
+ */
196
+ private assertCappedBySourcePayment;
197
+ private static coerceDate;
198
+ }
199
+ export default Payout;
200
+ //# sourceMappingURL=Payout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Payout.d.ts","sourceRoot":"","sources":["../../src/models/Payout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAc,UAAU,EAAQ,MAAM,0BAA0B,CAAC;AAExE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAmCjD,qBAsBa,MAAO,SAAQ,UAAU;IACpC;;;OAGG;IAEH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAM;IAEvB;;;;OAIG;IAEH,QAAQ,EAAE,MAAM,CAAM;IAEtB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAO;IAE1B;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAO;IAE1B;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAO;IAE1B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAM;IAEtB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAM;IAEvB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAM;IAE1B;;;;;OAKG;IACH,MAAM,EAAE,YAAY,CAAwB;IAE5C;;OAEG;IACH,MAAM,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE3B;;OAEG;IACH,WAAW,EAAE,IAAI,GAAG,IAAI,CAAQ;IAEhC;;OAEG;IACH,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE7B;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAM;IAE3B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAM;gBAEP,OAAO,GAAE,GAAQ;IA2B7B;;;OAGG;IACY,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAa1C,SAAS,IAAI,OAAO;IAIpB,MAAM,IAAI,OAAO;IAIjB,WAAW,IAAI,OAAO;IAItB,QAAQ,IAAI,OAAO;IAInB;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;IAcpC;;;;;;OAMG;IACH,aAAa,IAAI,IAAI;IAUrB;;;;OAIG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAchC;;;;;;OAMG;IACH,eAAe,IAAI,IAAI;IAevB;;;;OAIG;IACH,eAAe,IAAI,IAAI;IA0BvB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCpC;;;;;;;;;OASG;YACW,kBAAkB;IAchC;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IAa9B;;;;;;;;;;;;;OAaG;YACW,2BAA2B;IAoCzC,OAAO,CAAC,MAAM,CAAC,UAAU;CAS1B;AAED,eAAe,MAAM,CAAC"}