@classytic/primitives 0.19.0 → 0.20.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 (45) hide show
  1. package/CHANGELOG.md +906 -751
  2. package/README.md +160 -159
  3. package/dist/approval.d.mts +49 -10
  4. package/dist/approval.mjs +44 -10
  5. package/dist/bank-transaction.d.mts +1 -1
  6. package/dist/cadence-BFQVC1Qp.d.mts +107 -0
  7. package/dist/cadence.d.mts +1 -92
  8. package/dist/cadence.mjs +133 -4
  9. package/dist/calendar-B6fm-FbR.d.mts +46 -0
  10. package/dist/calendar.d.mts +1 -45
  11. package/dist/canonical.mjs +17 -0
  12. package/dist/context.d.mts +28 -1
  13. package/dist/currency.d.mts +2 -2
  14. package/dist/currency.mjs +134 -19
  15. package/dist/event-infra.d.mts +1 -34
  16. package/dist/event-infra.mjs +16 -46
  17. package/dist/idempotency.d.mts +253 -0
  18. package/dist/idempotency.mjs +226 -0
  19. package/dist/memory-outbox.d.mts +64 -0
  20. package/dist/memory-outbox.mjs +172 -0
  21. package/dist/monetization.d.mts +1 -1
  22. package/dist/money-DPo9fLBi.d.mts +347 -0
  23. package/dist/money.d.mts +1 -140
  24. package/dist/money.mjs +6 -15
  25. package/dist/outbox.d.mts +49 -1
  26. package/dist/outbox.mjs +64 -1
  27. package/dist/payment-events.d.mts +132 -4
  28. package/dist/payment-events.mjs +22 -2
  29. package/dist/payment-gateway.d.mts +210 -7
  30. package/dist/payment-gateway.mjs +18 -1
  31. package/dist/period.d.mts +138 -7
  32. package/dist/period.mjs +316 -5
  33. package/dist/person-parse.d.mts +82 -0
  34. package/dist/person-parse.mjs +109 -0
  35. package/dist/proration.d.mts +2 -2
  36. package/dist/proration.mjs +18 -6
  37. package/dist/sla-policy.d.mts +1 -1
  38. package/dist/state-machine.mjs +13 -0
  39. package/dist/suspension.d.mts +1 -1
  40. package/dist/timezone-tlG7ygwx.d.mts +114 -0
  41. package/dist/timezone.d.mts +2 -100
  42. package/dist/timezone.mjs +38 -3
  43. package/dist/unit-cost-rate.d.mts +1 -2
  44. package/package.json +244 -232
  45. package/dist/currency-BCJnJBlb.d.mts +0 -119
package/dist/money.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { minorUnitFactor } from "./currency.mjs";
1
+ import { CurrencyMismatchError, currencyCode, isCurrencyCode, minorUnitFactor } from "./currency.mjs";
2
2
  //#region src/money/money.ts
3
3
  /**
4
4
  * Cast a raw number to {@link MinorUnits}. Pass the currency as the second
@@ -12,26 +12,17 @@ import { minorUnitFactor } from "./currency.mjs";
12
12
  function MinorUnits(amount, _currency) {
13
13
  return amount;
14
14
  }
15
- var CurrencyMismatchError = class extends Error {
16
- name = "CurrencyMismatchError";
17
- left;
18
- right;
19
- constructor(left, right) {
20
- super(`Currency mismatch: ${left} vs ${right}`);
21
- this.left = left;
22
- this.right = right;
23
- }
24
- };
25
15
  /**
26
16
  * Construct {@link Money} from an integer minor-unit amount.
27
17
  *
28
18
  * @throws TypeError if `amount` is not a finite integer.
19
+ * @throws InvalidCurrencyCodeError if `currency` is not ISO 4217 format.
29
20
  */
30
21
  function money(amount, currency) {
31
22
  if (!Number.isFinite(amount) || !Number.isInteger(amount)) throw new TypeError(`Money.amount must be a finite integer in minor units, got ${amount}`);
32
23
  return {
33
24
  amount,
34
- currency
25
+ currency: currencyCode(currency)
35
26
  };
36
27
  }
37
28
  /**
@@ -60,7 +51,7 @@ function fromMajor(major, currency) {
60
51
  const cleaned = Number(raw.toPrecision(15));
61
52
  return {
62
53
  amount: Math.round(Math.abs(cleaned)) * (cleaned < 0 ? -1 : 1),
63
- currency
54
+ currency: currencyCode(currency)
64
55
  };
65
56
  }
66
57
  /** Convert {@link Money} back to its major-unit floating-point representation. */
@@ -100,7 +91,7 @@ function multiplyMoney(m, scalar) {
100
91
  function sumMoney(values, currency) {
101
92
  return values.reduce((acc, v) => addMoney(acc, v), {
102
93
  amount: 0,
103
- currency
94
+ currency: currencyCode(currency)
104
95
  });
105
96
  }
106
97
  /** Strict equality (amount and currency both match). */
@@ -180,7 +171,7 @@ function formatMinorUnits(minor, minorUnitDecimals = 2) {
180
171
  }
181
172
  /** Structural type guard. */
182
173
  function isMoney(value) {
183
- return typeof value === "object" && value !== null && typeof value.amount === "number" && Number.isInteger(value.amount) && typeof value.currency === "string";
174
+ return typeof value === "object" && value !== null && typeof value.amount === "number" && Number.isInteger(value.amount) && isCurrencyCode(value.currency);
184
175
  }
185
176
  //#endregion
186
177
  export { CurrencyMismatchError, MinorUnits, absMoney, addMoney, compareMoney, equalsMoney, formatMinorUnits, fromMajor, isMoney, isNegativeMoney, isPositiveMoney, isZeroMoney, majorToMinorUnits, minorUnitsToMajor, money, multiplyMoney, negateMoney, percentOfMinor, subtractMoney, sumMoney, toMajor };
package/dist/outbox.d.mts CHANGED
@@ -157,6 +157,19 @@ declare class InvalidOutboxEventError extends Error {
157
157
  * `claimPending`.** If the underlying storage has corrupt rows, the
158
158
  * store is responsible for quarantining them.
159
159
  */
160
+ /**
161
+ * The delivery states an outbox row can hold.
162
+ *
163
+ * Named HERE, in the contract, rather than in each store. It went unnamed while every
164
+ * method took an event id and none took a state — `countByStatus` is the first member
165
+ * that makes the vocabulary part of the interface, and two stores had already written
166
+ * the same three strings independently by then.
167
+ *
168
+ * `dead_letter` is terminal until an operator calls `requeue`; it is a durable statement
169
+ * that something is owed, not a deletion.
170
+ */
171
+ declare const OUTBOX_STATUSES: readonly ["pending", "delivered", "dead_letter"];
172
+ type OutboxStatus = (typeof OUTBOX_STATUSES)[number];
160
173
  interface OutboxStore {
161
174
  /**
162
175
  * Declares that `save` ENLISTS `options.session` in the caller's transaction,
@@ -242,6 +255,41 @@ interface OutboxStore {
242
255
  * @returns Number of purged events.
243
256
  */
244
257
  purge?(olderThanMs: number): Promise<number>;
258
+ /**
259
+ * Re-drive ONE dead-lettered event: back to `pending`, attempts reset, lease cleared,
260
+ * immediately visible.
261
+ *
262
+ * The other half of `fail({ deadLetter: true })`. Dead-lettering is only a durable
263
+ * statement that something is owed; a store that can dead-letter but not requeue leaves
264
+ * an operator who has fixed the root cause with no way to act on the worklist except
265
+ * hand-editing rows in the database.
266
+ *
267
+ * MUST be scoped to `dead_letter` — requeuing a `pending` row would reset the attempt
268
+ * count on an event that is merely backing off, defeating the retry policy, and
269
+ * requeuing a `delivered` row would republish an event every subscriber already saw.
270
+ *
271
+ * @returns `true` if a dead-lettered event was requeued, `false` if no such event
272
+ * exists — NOT an error. An operator re-driving a worklist races the relay, and an
273
+ * id that has already been dealt with is an ordinary outcome.
274
+ */
275
+ requeue?(eventId: string): Promise<boolean>;
276
+ /**
277
+ * How many events are in `status`. The health surface behind an operator dashboard or
278
+ * an alert (`dead_letter > 0` means money or access may be unrecorded).
279
+ */
280
+ countByStatus?(status: OutboxStatus): Promise<number>;
281
+ /**
282
+ * Age in ms of the OLDEST pending event, or `null` when nothing is pending.
283
+ *
284
+ * The single most useful outbox health signal, and the one a naive dashboard misses: a
285
+ * pending COUNT looks identical whether the queue is draining briskly or has been
286
+ * wedged for an hour behind one poison row. Age distinguishes them.
287
+ *
288
+ * Measured from the event's creation, not its last attempt — the question being asked
289
+ * is "how long has this event gone undelivered", and resetting the clock on each retry
290
+ * would make a permanently-failing row look permanently fresh.
291
+ */
292
+ oldestPendingAgeMs?(): Promise<number | null>;
245
293
  }
246
294
  //#endregion
247
- export { InvalidOutboxEventError, OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxErrorInfo, OutboxFailOptions, OutboxFailureContext, OutboxFailureDecision, OutboxFailurePolicy, OutboxOwnershipError, OutboxStore, OutboxWriteOptions };
295
+ export { InvalidOutboxEventError, OUTBOX_STATUSES, OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxErrorInfo, OutboxFailOptions, OutboxFailureContext, OutboxFailureDecision, OutboxFailurePolicy, OutboxOwnershipError, OutboxStatus, OutboxStore, OutboxWriteOptions };
package/dist/outbox.mjs CHANGED
@@ -26,5 +26,68 @@ var InvalidOutboxEventError = class extends Error {
26
26
  super(`Invalid outbox event: ${reason}`);
27
27
  }
28
28
  };
29
+ /**
30
+ * Durable storage contract for the transactional outbox pattern.
31
+ *
32
+ * **Required methods**: `save`, `getPending`, `acknowledge`.
33
+ *
34
+ * **Optional capabilities** — stores opt-in to richer semantics:
35
+ *
36
+ * - `claimPending` — lease-based work claim for multi-worker relay
37
+ * - `fail` — failure tracking with retry scheduling and dead-letter
38
+ * - `getDeadLettered` — typed DLQ replay surface
39
+ * - `purge` — acknowledged event cleanup
40
+ *
41
+ * Arc's runtime detects capabilities at runtime and degrades gracefully for
42
+ * legacy stores that only implement the required methods.
43
+ *
44
+ * ## Required semantics
45
+ *
46
+ * Implementations MUST honor these contracts — arc's relay depends on them
47
+ * for correctness of at-least-once delivery:
48
+ *
49
+ * 1. **`save` must reject invalid events.** If `event.type` or `event.meta.id`
50
+ * is missing/empty, throw — do not persist. Callers pre-validate but
51
+ * stores should defend against direct-save code paths.
52
+ *
53
+ * 2. **`claimPending` must be atomic.** Two workers calling `claimPending`
54
+ * concurrently must never receive the same event. SQL: `SELECT ... FOR
55
+ * UPDATE SKIP LOCKED`; Mongo: `findOneAndUpdate` with `{ leaseOwner:
56
+ * null }` in the match condition.
57
+ *
58
+ * 3. **`acknowledge` / `fail` must throw {@link OutboxOwnershipError} on
59
+ * ownership mismatch.** When `options.consumerId` is provided and does
60
+ * not match the current lease owner, throw — never silently no-op. The
61
+ * relay relies on this signal to avoid counting hijacked events as
62
+ * successfully relayed.
63
+ *
64
+ * 4. **`acknowledge` on an unknown `eventId` is a no-op.** Keeps the relay
65
+ * idempotent when the store has been purged or the event was manually
66
+ * removed. Do NOT throw `OutboxOwnershipError` in this case.
67
+ *
68
+ * 5. **`fail` must deterministically update lease/visibility.** On success,
69
+ * the event MUST become re-claimable (immediately or at `retryAt`) or
70
+ * transition to dead-letter. Lease owner should be cleared.
71
+ *
72
+ * 6. **Malformed events must never be returned by `getPending` /
73
+ * `claimPending`.** If the underlying storage has corrupt rows, the
74
+ * store is responsible for quarantining them.
75
+ */
76
+ /**
77
+ * The delivery states an outbox row can hold.
78
+ *
79
+ * Named HERE, in the contract, rather than in each store. It went unnamed while every
80
+ * method took an event id and none took a state — `countByStatus` is the first member
81
+ * that makes the vocabulary part of the interface, and two stores had already written
82
+ * the same three strings independently by then.
83
+ *
84
+ * `dead_letter` is terminal until an operator calls `requeue`; it is a durable statement
85
+ * that something is owed, not a deletion.
86
+ */
87
+ const OUTBOX_STATUSES = [
88
+ "pending",
89
+ "delivered",
90
+ "dead_letter"
91
+ ];
29
92
  //#endregion
30
- export { InvalidOutboxEventError, OutboxOwnershipError };
93
+ export { InvalidOutboxEventError, OUTBOX_STATUSES, OutboxOwnershipError };
@@ -1,5 +1,6 @@
1
- import { Money } from "./money.mjs";
1
+ import { n as Money } from "./money-DPo9fLBi.mjs";
2
2
  import { PaymentMethodKind } from "./payment-method-kind.mjs";
3
+ import { ProviderUnknownCause } from "./payment-gateway.mjs";
3
4
 
4
5
  //#region src/events/payment-events.d.ts
5
6
  /**
@@ -21,6 +22,19 @@ declare const PAYMENT_EVENT_TYPE: {
21
22
  readonly DISPUTE_WON: "payment.dispute_won";
22
23
  readonly DISPUTE_LOST: "payment.dispute_lost";
23
24
  readonly SETTLED: "payment.settled";
25
+ /**
26
+ * The provider command produced NO observable outcome — timeout, abort,
27
+ * network fault, or a provider that holds no record and refuses to guess.
28
+ *
29
+ * This member exists because the port contract (`ProviderCommandResult` in
30
+ * `/payment-gateway`) is three-valued — `confirmed | declined | unknown` —
31
+ * while this catalogue was two-valued. An adapter that hit the third case had
32
+ * nowhere to publish it, so the only shapes on the bus were SUCCEEDED and
33
+ * FAILED, and an unobserved outcome had to be forced into one of them.
34
+ * Forcing it into FAILED is the one that double-charges: it licenses a retry
35
+ * for a command that may already have moved money.
36
+ */
37
+ readonly UNKNOWN: "payment.unknown";
24
38
  };
25
39
  type PaymentEventType = (typeof PAYMENT_EVENT_TYPE)[keyof typeof PAYMENT_EVENT_TYPE];
26
40
  /**
@@ -30,6 +44,8 @@ type PaymentEventType = (typeof PAYMENT_EVENT_TYPE)[keyof typeof PAYMENT_EVENT_T
30
44
  * "money in flight" not "money received".
31
45
  */
32
46
  interface PaymentInitiatedPayload {
47
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
48
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.INITIATED;
33
49
  /** Stable payment identifier owned by the emitting package. */
34
50
  paymentId: string;
35
51
  /**
@@ -57,6 +73,8 @@ interface PaymentInitiatedPayload {
57
73
  * downstream fulfilment unlocks.
58
74
  */
59
75
  interface PaymentSucceededPayload {
76
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
77
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.SUCCEEDED;
60
78
  paymentId: string;
61
79
  providerRef?: string;
62
80
  providerCode: string;
@@ -73,6 +91,8 @@ interface PaymentSucceededPayload {
73
91
  * overdue.
74
92
  */
75
93
  interface PaymentFailedPayload {
94
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
95
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.FAILED;
76
96
  paymentId: string;
77
97
  providerRef?: string;
78
98
  providerCode: string;
@@ -93,6 +113,8 @@ interface PaymentFailedPayload {
93
113
  * itself (pre- or post-clearing, often due to operator error).
94
114
  */
95
115
  interface PaymentRefundedPayload {
116
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
117
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.REFUNDED;
96
118
  paymentId: string;
97
119
  /** Refund identifier (separate from `paymentId`). */
98
120
  refundId: string;
@@ -116,6 +138,8 @@ interface PaymentRefundedPayload {
116
138
  * state.
117
139
  */
118
140
  interface PaymentReversedPayload {
141
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
142
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.REVERSED;
119
143
  paymentId: string;
120
144
  providerRef?: string;
121
145
  providerCode: string;
@@ -136,6 +160,8 @@ interface PaymentReversedPayload {
136
160
  * ledger bridges and closes the audit loop.
137
161
  */
138
162
  interface PaymentReconciledPayload {
163
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
164
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.RECONCILED;
139
165
  /** Allocation identifier — NOT the payment id (a payment can have many). */
140
166
  allocationId: string;
141
167
  paymentId: string;
@@ -156,6 +182,8 @@ interface PaymentReconciledPayload {
156
182
  * "hold" from "settle"; consumers should NOT treat this as cleared funds.
157
183
  */
158
184
  interface PaymentAuthorizedPayload {
185
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
186
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.AUTHORIZED;
159
187
  paymentId: string;
160
188
  providerRef?: string;
161
189
  providerCode: string;
@@ -173,6 +201,8 @@ interface PaymentAuthorizedPayload {
173
201
  * Pairs with {@link PaymentAuthorizedPayload} for two-phase flows.
174
202
  */
175
203
  interface PaymentCapturedPayload {
204
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
205
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.CAPTURED;
176
206
  paymentId: string;
177
207
  providerRef?: string;
178
208
  providerCode: string;
@@ -191,6 +221,8 @@ interface PaymentCapturedPayload {
191
221
  * Authorisation hold released — uncaptured auth voided before settlement.
192
222
  */
193
223
  interface PaymentAuthVoidedPayload {
224
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
225
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.AUTH_VOIDED;
194
226
  paymentId: string;
195
227
  providerRef?: string;
196
228
  providerCode: string;
@@ -205,6 +237,8 @@ interface PaymentAuthVoidedPayload {
205
237
  * payment. The merchant typically has a deadline to submit evidence.
206
238
  */
207
239
  interface PaymentDisputedPayload {
240
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
241
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.DISPUTED;
208
242
  paymentId: string;
209
243
  /** Provider's dispute / chargeback id. */
210
244
  disputeId: string;
@@ -223,6 +257,8 @@ interface PaymentDisputedPayload {
223
257
  }
224
258
  /** Dispute resolved in merchant's favour — funds restored. */
225
259
  interface PaymentDisputeWonPayload {
260
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
261
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.DISPUTE_WON;
226
262
  paymentId: string;
227
263
  disputeId: string;
228
264
  providerRef?: string;
@@ -234,6 +270,8 @@ interface PaymentDisputeWonPayload {
234
270
  }
235
271
  /** Dispute resolved against merchant — funds permanently lost plus fee. */
236
272
  interface PaymentDisputeLostPayload {
273
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
274
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.DISPUTE_LOST;
237
275
  paymentId: string;
238
276
  disputeId: string;
239
277
  providerRef?: string;
@@ -251,6 +289,8 @@ interface PaymentDisputeLostPayload {
251
289
  * reconciliation in country-aware ledger bridges.
252
290
  */
253
291
  interface PaymentSettledPayload {
292
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
293
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.SETTLED;
254
294
  paymentId: string;
255
295
  providerRef?: string;
256
296
  providerCode: string;
@@ -265,7 +305,95 @@ interface PaymentSettledPayload {
265
305
  occurredAt: Date;
266
306
  metadata?: Record<string, unknown>;
267
307
  }
268
- /** Union of all payment event payload types — convenient for exhaustive switches. */
269
- type PaymentEventPayload = PaymentInitiatedPayload | PaymentSucceededPayload | PaymentFailedPayload | PaymentRefundedPayload | PaymentReversedPayload | PaymentReconciledPayload | PaymentAuthorizedPayload | PaymentCapturedPayload | PaymentAuthVoidedPayload | PaymentDisputedPayload | PaymentDisputeWonPayload | PaymentDisputeLostPayload | PaymentSettledPayload;
308
+ /**
309
+ * The provider command produced no observable outcome. **This is not a
310
+ * failure**, and it is not a success — it is the absence of an answer.
311
+ *
312
+ * The obligation's state is genuinely unknown: the capture may have landed at
313
+ * the gateway with the acknowledgement lost in transit. The ONLY correct
314
+ * reaction is to reconcile the payment's real status before acting. A blind
315
+ * retry is a double charge; marking it failed and releasing the order is a
316
+ * shipped-but-unpaid order. Both are silent.
317
+ *
318
+ * Consumers MUST NOT treat this as a terminal state. It is a prompt to go and
319
+ * find out, and it resolves later into `payment.succeeded` or `payment.failed`.
320
+ */
321
+ interface PaymentUnknownPayload {
322
+ /** Discriminant — narrows {@link PaymentEventPayload}. */
323
+ readonly eventType: typeof PAYMENT_EVENT_TYPE.UNKNOWN;
324
+ paymentId: string;
325
+ providerRef?: string;
326
+ providerCode: string;
327
+ /** Which command went unanswered — the retry policy differs per verb. */
328
+ operation: 'create_intent' | 'verify' | 'authorize' | 'capture' | 'refund' | 'void';
329
+ /** The amount at stake, when the command carried one. */
330
+ amount?: Money;
331
+ methodKind?: PaymentMethodKind;
332
+ methodCode?: string;
333
+ /**
334
+ * NORMALIZED reason the outcome was not observed — mirrors
335
+ * `ProviderUnknownCause` from `/payment-gateway`. Never a raw vendor error
336
+ * string: this value is persisted and displayed, and vendor errors embed
337
+ * request URLs, tokens and body fragments.
338
+ */
339
+ causeCode: ProviderUnknownCause;
340
+ /** The idempotency key the command carried — what reconciliation asks with. */
341
+ idempotencyKey?: string;
342
+ occurredAt: Date;
343
+ metadata?: Record<string, unknown>;
344
+ }
345
+ /**
346
+ * Union of all payment event payload types.
347
+ *
348
+ * DISCRIMINATED on `eventType`. Before the discriminant existed,
349
+ * `PaymentInitiatedPayload` and `PaymentSucceededPayload` were structurally
350
+ * IDENTICAL — so a handler typed for "funds received" accepted "money in
351
+ * flight" with no error anywhere, and TypeScript could not tell them apart even
352
+ * in principle. Structural identity between two events that mean opposite
353
+ * things is precisely where a nominal tag has to carry the meaning.
354
+ */
355
+ type PaymentEventPayload = PaymentInitiatedPayload | PaymentSucceededPayload | PaymentFailedPayload | PaymentRefundedPayload | PaymentReversedPayload | PaymentReconciledPayload | PaymentAuthorizedPayload | PaymentCapturedPayload | PaymentAuthVoidedPayload | PaymentDisputedPayload | PaymentDisputeWonPayload | PaymentDisputeLostPayload | PaymentSettledPayload | PaymentUnknownPayload;
356
+ /** Event type → its payload. The lookup a typed publisher/handler pair needs. */
357
+ interface PaymentEventPayloadMap {
358
+ 'payment.initiated': PaymentInitiatedPayload;
359
+ 'payment.succeeded': PaymentSucceededPayload;
360
+ 'payment.failed': PaymentFailedPayload;
361
+ 'payment.refunded': PaymentRefundedPayload;
362
+ 'payment.reversed': PaymentReversedPayload;
363
+ 'payment.reconciled': PaymentReconciledPayload;
364
+ 'payment.authorized': PaymentAuthorizedPayload;
365
+ 'payment.captured': PaymentCapturedPayload;
366
+ 'payment.auth_voided': PaymentAuthVoidedPayload;
367
+ 'payment.disputed': PaymentDisputedPayload;
368
+ 'payment.dispute_won': PaymentDisputeWonPayload;
369
+ 'payment.dispute_lost': PaymentDisputeLostPayload;
370
+ 'payment.settled': PaymentSettledPayload;
371
+ 'payment.unknown': PaymentUnknownPayload;
372
+ }
373
+ type Assert<T extends true> = T;
374
+ /** Every declared event type has a payload in the map. */
375
+ type _EveryTypeMapped = Assert<PaymentEventType extends keyof PaymentEventPayloadMap ? true : false>;
376
+ /** The map declares no key that is not a real event type. */
377
+ type _NoStrayMapKeys = Assert<keyof PaymentEventPayloadMap extends PaymentEventType ? true : false>;
378
+ /** Each payload's own `eventType` literal matches the key it is filed under. */
379
+ type _DiscriminantMatchesKey = Assert<{ [K in keyof PaymentEventPayloadMap]: PaymentEventPayloadMap[K] extends {
380
+ eventType: K;
381
+ } ? true : false }[keyof PaymentEventPayloadMap] extends true ? true : false>;
382
+ type PaymentEventContractGuards = [_EveryTypeMapped, _NoStrayMapKeys, _DiscriminantMatchesKey];
383
+ /**
384
+ * Narrow a payload by event type at RUNTIME, with the compile-time narrowing
385
+ * to match. `if (isPaymentEvent(PAYMENT_EVENT_TYPE.SUCCEEDED, p)) { … }` —
386
+ * inside the branch `p` is `PaymentSucceededPayload` and cannot be an
387
+ * initiated payload, which is the whole point of the discriminant.
388
+ */
389
+ declare function isPaymentEvent<T extends PaymentEventType>(type: T, payload: PaymentEventPayload): payload is PaymentEventPayloadMap[T];
390
+ /**
391
+ * Is this payload a CLEARED-FUNDS event?
392
+ *
393
+ * The named answer to the question that used to be answerable only by
394
+ * structural shape — and answered wrong, because `initiated` had the same
395
+ * shape. `captured` counts; `initiated`, `authorized` and `unknown` do not.
396
+ */
397
+ declare function isFundsReceived(payload: PaymentEventPayload): boolean;
270
398
  //#endregion
271
- export { PAYMENT_EVENT_TYPE, PaymentAuthVoidedPayload, PaymentAuthorizedPayload, PaymentCapturedPayload, PaymentDisputeLostPayload, PaymentDisputeWonPayload, PaymentDisputedPayload, PaymentEventPayload, PaymentEventType, PaymentFailedPayload, PaymentInitiatedPayload, PaymentReconciledPayload, PaymentRefundedPayload, PaymentReversedPayload, PaymentSettledPayload, PaymentSucceededPayload };
399
+ export { PAYMENT_EVENT_TYPE, PaymentAuthVoidedPayload, PaymentAuthorizedPayload, PaymentCapturedPayload, PaymentDisputeLostPayload, PaymentDisputeWonPayload, PaymentDisputedPayload, PaymentEventContractGuards, PaymentEventPayload, PaymentEventPayloadMap, PaymentEventType, PaymentFailedPayload, PaymentInitiatedPayload, PaymentReconciledPayload, PaymentRefundedPayload, PaymentReversedPayload, PaymentSettledPayload, PaymentSucceededPayload, PaymentUnknownPayload, isFundsReceived, isPaymentEvent };
@@ -17,7 +17,27 @@ const PAYMENT_EVENT_TYPE = {
17
17
  DISPUTED: "payment.disputed",
18
18
  DISPUTE_WON: "payment.dispute_won",
19
19
  DISPUTE_LOST: "payment.dispute_lost",
20
- SETTLED: "payment.settled"
20
+ SETTLED: "payment.settled",
21
+ UNKNOWN: "payment.unknown"
21
22
  };
23
+ /**
24
+ * Narrow a payload by event type at RUNTIME, with the compile-time narrowing
25
+ * to match. `if (isPaymentEvent(PAYMENT_EVENT_TYPE.SUCCEEDED, p)) { … }` —
26
+ * inside the branch `p` is `PaymentSucceededPayload` and cannot be an
27
+ * initiated payload, which is the whole point of the discriminant.
28
+ */
29
+ function isPaymentEvent(type, payload) {
30
+ return payload.eventType === type;
31
+ }
32
+ /**
33
+ * Is this payload a CLEARED-FUNDS event?
34
+ *
35
+ * The named answer to the question that used to be answerable only by
36
+ * structural shape — and answered wrong, because `initiated` had the same
37
+ * shape. `captured` counts; `initiated`, `authorized` and `unknown` do not.
38
+ */
39
+ function isFundsReceived(payload) {
40
+ return payload.eventType === PAYMENT_EVENT_TYPE.SUCCEEDED || payload.eventType === PAYMENT_EVENT_TYPE.CAPTURED;
41
+ }
22
42
  //#endregion
23
- export { PAYMENT_EVENT_TYPE };
43
+ export { PAYMENT_EVENT_TYPE, isFundsReceived, isPaymentEvent };