@classytic/primitives 0.18.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.
- package/CHANGELOG.md +906 -715
- package/README.md +160 -159
- package/dist/approval.d.mts +49 -10
- package/dist/approval.mjs +44 -10
- package/dist/bank-transaction.d.mts +1 -1
- package/dist/cadence-BFQVC1Qp.d.mts +107 -0
- package/dist/cadence.d.mts +1 -92
- package/dist/cadence.mjs +133 -4
- package/dist/calendar-B6fm-FbR.d.mts +46 -0
- package/dist/calendar.d.mts +1 -45
- package/dist/canonical.mjs +17 -0
- package/dist/context.d.mts +28 -1
- package/dist/currency.d.mts +2 -2
- package/dist/currency.mjs +134 -19
- package/dist/event-infra.d.mts +1 -34
- package/dist/event-infra.mjs +16 -46
- package/dist/idempotency.d.mts +253 -0
- package/dist/idempotency.mjs +226 -0
- package/dist/memory-outbox.d.mts +64 -0
- package/dist/memory-outbox.mjs +172 -0
- package/dist/monetization.d.mts +195 -0
- package/dist/monetization.mjs +79 -0
- package/dist/money-DPo9fLBi.d.mts +347 -0
- package/dist/money.d.mts +1 -140
- package/dist/money.mjs +6 -15
- package/dist/outbox.d.mts +49 -1
- package/dist/outbox.mjs +64 -1
- package/dist/payment-events.d.mts +132 -4
- package/dist/payment-events.mjs +22 -2
- package/dist/payment-gateway.d.mts +210 -7
- package/dist/payment-gateway.mjs +18 -1
- package/dist/period.d.mts +138 -7
- package/dist/period.mjs +316 -5
- package/dist/person-parse.d.mts +82 -0
- package/dist/person-parse.mjs +109 -0
- package/dist/proration.d.mts +76 -0
- package/dist/proration.mjs +132 -0
- package/dist/sla-policy.d.mts +1 -1
- package/dist/state-machine.mjs +13 -0
- package/dist/suspension.d.mts +1 -1
- package/dist/timezone-tlG7ygwx.d.mts +114 -0
- package/dist/timezone.d.mts +2 -100
- package/dist/timezone.mjs +38 -3
- package/dist/unit-cost-rate.d.mts +1 -2
- package/package.json +244 -224
- package/dist/currency-BCJnJBlb.d.mts +0 -119
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
|
-
/**
|
|
269
|
-
|
|
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 };
|
package/dist/payment-events.mjs
CHANGED
|
@@ -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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
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
3
|
|
|
4
4
|
//#region src/money/payment-gateway.d.ts
|
|
@@ -25,7 +25,7 @@ interface ProviderCapabilities {
|
|
|
25
25
|
* Arguments to `provider.createIntent(...)`. Engine constructs this
|
|
26
26
|
* from the host's checkout payload and the resolved currency. The
|
|
27
27
|
* `metadata` blob is opaque pass-through — providers MAY forward it to
|
|
28
|
-
* the upstream gateway (Stripe attaches it to
|
|
28
|
+
* the upstream gateway (Stripe attaches it to its own PaymentIntent), but
|
|
29
29
|
* MUST NOT mutate or interpret it.
|
|
30
30
|
*/
|
|
31
31
|
interface CreateIntentParams {
|
|
@@ -49,7 +49,7 @@ interface CreateIntentParams {
|
|
|
49
49
|
* Stripe's PaymentIntent status because every other gateway maps cleanly
|
|
50
50
|
* INTO this set; gateways that lack a state simply never emit it.
|
|
51
51
|
*/
|
|
52
|
-
type
|
|
52
|
+
type ProviderIntentStatus = 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'processing' | 'requires_capture' | 'canceled' | 'succeeded';
|
|
53
53
|
/** Closed set of refund lifecycle states the engine recognises. */
|
|
54
54
|
type RefundResultStatus = 'pending' | 'requires_action' | 'succeeded' | 'failed' | 'canceled';
|
|
55
55
|
/**
|
|
@@ -58,7 +58,17 @@ type RefundResultStatus = 'pending' | 'requires_action' | 'succeeded' | 'failed'
|
|
|
58
58
|
* `gateway` block so subsequent `verify` / webhook lookups can find
|
|
59
59
|
* the row.
|
|
60
60
|
*/
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* A PROVIDER-side intent reference — what the gateway calls this payment.
|
|
63
|
+
*
|
|
64
|
+
* Named `ProviderIntent`, not `PaymentIntent`, because the domain aggregate that models the
|
|
65
|
+
* customer's obligation is also called a payment intent, and one name cannot span both layers.
|
|
66
|
+
* This is the narrow thing: a gateway-scoped id, the provider's own status mapped onto our closed
|
|
67
|
+
* union, and the amount it locked in. It belongs to ONE attempt against ONE provider account.
|
|
68
|
+
*
|
|
69
|
+
* See `spine/docs/payments-architecture.md` §3.3.
|
|
70
|
+
*/
|
|
71
|
+
interface ProviderIntent {
|
|
62
72
|
/** Unique gateway-scoped id for this intent (`pi_xxx`, `ch_xxx`, etc.). */
|
|
63
73
|
id: string;
|
|
64
74
|
/** Lowercase gateway name (`'stripe'`, `'razorpay'`, `'sslcommerz'`, `'bkash'`, `'manual'`). */
|
|
@@ -67,7 +77,7 @@ interface PaymentIntent {
|
|
|
67
77
|
* Lifecycle status — closed union the engine state-machines branch on.
|
|
68
78
|
* Adapters MUST map provider-native strings into this set.
|
|
69
79
|
*/
|
|
70
|
-
status:
|
|
80
|
+
status: ProviderIntentStatus;
|
|
71
81
|
/** Amount + currency that the intent locks in. */
|
|
72
82
|
amount: Money;
|
|
73
83
|
/**
|
|
@@ -104,7 +114,7 @@ interface PaymentResult {
|
|
|
104
114
|
/** Lowercase gateway name. */
|
|
105
115
|
provider: string;
|
|
106
116
|
/**
|
|
107
|
-
* Closed status set — narrower than `
|
|
117
|
+
* Closed status set — narrower than `ProviderIntent.status` because
|
|
108
118
|
* verification has settled the lifecycle.
|
|
109
119
|
*
|
|
110
120
|
* - `'succeeded'` — payment captured, funds owed to merchant.
|
|
@@ -185,5 +195,198 @@ interface WebhookEvent {
|
|
|
185
195
|
/** Raw provider payload — audit only. */
|
|
186
196
|
raw?: unknown;
|
|
187
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* The contract a payment provider must satisfy — declared HERE, in primitives, so an
|
|
200
|
+
* adapter can be written against it with **no runtime dependency on the engine**.
|
|
201
|
+
*
|
|
202
|
+
* ## Why this moved
|
|
203
|
+
*
|
|
204
|
+
* `@classytic/revenue-manual` imported the abstract `PaymentProvider` CLASS from
|
|
205
|
+
* `@classytic/revenue` — a runtime value, not a type. That coupled every adapter to the
|
|
206
|
+
* engine package: an adapter could not be versioned or published independently, and the
|
|
207
|
+
* dependency ran adapter → engine while the engine also registers adapters.
|
|
208
|
+
*
|
|
209
|
+
* This is the same correction made for `OutboxStore`: primitives owns the CONTRACT, the
|
|
210
|
+
* engine adapts to it, and a kit or a third party can implement it knowing only this file.
|
|
211
|
+
*
|
|
212
|
+
* ## Extending vs implementing
|
|
213
|
+
*
|
|
214
|
+
* `revenue`'s abstract `PaymentProvider` class now *implements* this interface and stays
|
|
215
|
+
* available for the config plumbing it provides (`config`, `defaultCurrency`). Existing
|
|
216
|
+
* adapters that `extends PaymentProvider` are unaffected. New adapters should `implement
|
|
217
|
+
* PaymentProviderPort` directly and depend only on primitives.
|
|
218
|
+
*
|
|
219
|
+
* See `spine/docs/payments-architecture.md` §3.4.1 and phase 1.
|
|
220
|
+
*/
|
|
221
|
+
/**
|
|
222
|
+
* How reconciliation asks a provider about a specific REFUND. The refund is identified
|
|
223
|
+
* by the original `paymentId` plus the refund's `idempotencyKey` (the value the provider
|
|
224
|
+
* deduped on); `refundRef` is the gateway refund id when the engine observed one.
|
|
225
|
+
*/
|
|
226
|
+
interface RefundStatusQuery {
|
|
227
|
+
paymentId: string;
|
|
228
|
+
idempotencyKey: string;
|
|
229
|
+
refundRef?: string;
|
|
230
|
+
}
|
|
231
|
+
interface PaymentProviderPort {
|
|
232
|
+
/** Stable lowercase provider name — the registry key. */
|
|
233
|
+
readonly name: string;
|
|
234
|
+
/**
|
|
235
|
+
* Create a provider-side intent for an amount.
|
|
236
|
+
*
|
|
237
|
+
* `command` is REQUIRED, not optional. Creating an intent is side-effecting upstream: a
|
|
238
|
+
* timed-out create can leave a live intent at the gateway with no local record, and a
|
|
239
|
+
* retry without a stable idempotency key produces a SECOND one. Making the envelope
|
|
240
|
+
* mandatory is what stops an adapter quietly omitting it.
|
|
241
|
+
*/
|
|
242
|
+
createIntent(params: CreateIntentParams, command: PaymentCommandContext): Promise<ProviderIntent>;
|
|
243
|
+
/**
|
|
244
|
+
* Confirm an intent actually succeeded at the provider.
|
|
245
|
+
*
|
|
246
|
+
* A provider that CANNOT know — a stateless manual provider, for instance — must not
|
|
247
|
+
* answer `succeeded`. Reporting success for something it never observed is how a
|
|
248
|
+
* reconciliation path gets a false positive.
|
|
249
|
+
*/
|
|
250
|
+
verifyPayment(intentId: string, command?: PaymentCommandContext): Promise<PaymentResult>;
|
|
251
|
+
/**
|
|
252
|
+
* Current provider-side status. Same honesty requirement as `verifyPayment`.
|
|
253
|
+
*
|
|
254
|
+
* `command` is OPTIONAL here and on `verifyPayment`: both are reads, so there is nothing
|
|
255
|
+
* to make idempotent. It is accepted purely for correlation across our logs and the
|
|
256
|
+
* provider's.
|
|
257
|
+
*/
|
|
258
|
+
getStatus(intentId: string, command?: PaymentCommandContext): Promise<PaymentResult>;
|
|
259
|
+
/**
|
|
260
|
+
* Reverse a payment, in full or in part.
|
|
261
|
+
*
|
|
262
|
+
* `command` is REQUIRED and carries the idempotency key. It used to be an optional field
|
|
263
|
+
* inside `options`, which made the single most dangerous operation on this port the one
|
|
264
|
+
* where deduplication was easiest to forget. Providers whose gateway supports an
|
|
265
|
+
* idempotency key MUST forward it; those without may ignore it, and the engine still
|
|
266
|
+
* guards with its own claim and a unique index.
|
|
267
|
+
*/
|
|
268
|
+
refund(paymentId: string, amount: number | null | undefined, command: PaymentCommandContext, options?: {
|
|
269
|
+
reason?: string;
|
|
270
|
+
}): Promise<RefundResult>;
|
|
271
|
+
/**
|
|
272
|
+
* Current status of a REFUND (not the payment). OPTIONAL and feature-detected: only
|
|
273
|
+
* a provider that can look a refund up implements it. Reconciliation MUST use this,
|
|
274
|
+
* never `getStatus` (which reports the PAYMENT) — asking the payment about a refund
|
|
275
|
+
* misclassifies a successful charge as a successful reversal. The query carries the
|
|
276
|
+
* original `paymentId` and the refund's `idempotencyKey` (and `refundRef` when we
|
|
277
|
+
* observed one) so the provider can find the specific reversal it deduped. A provider
|
|
278
|
+
* without this cannot be auto-reconciled — the engine then RETAINS the reservation.
|
|
279
|
+
*/
|
|
280
|
+
getRefundStatus?(query: RefundStatusQuery, command?: PaymentCommandContext): Promise<PaymentResult>;
|
|
281
|
+
handleWebhook(payload: unknown, headers?: Record<string, string>): Promise<WebhookEvent>;
|
|
282
|
+
/**
|
|
283
|
+
* Reject a forged webhook. Returning `true` unconditionally is only acceptable for a
|
|
284
|
+
* provider with no webhook transport at all (manual/dev).
|
|
285
|
+
*/
|
|
286
|
+
verifyWebhookSignature(payload: unknown, signature: string): boolean;
|
|
287
|
+
/** Declared capabilities. Callers gate on these rather than probing by failing. */
|
|
288
|
+
getCapabilities(): ProviderCapabilities;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* `setDefaultCurrency` is deliberately NOT on the port.
|
|
292
|
+
*
|
|
293
|
+
* A default currency is ENGINE configuration, not part of the provider execution contract,
|
|
294
|
+
* and every `Money` already carries its own currency. Requiring the mutator would make each
|
|
295
|
+
* adapter stateful and awkward to share across accounts — the same instance cannot then
|
|
296
|
+
* serve two accounts with different defaults.
|
|
297
|
+
*
|
|
298
|
+
* Adapters that want the convenience expose it and the registry feature-detects it; the
|
|
299
|
+
* rest simply read the currency off the amount they were handed.
|
|
300
|
+
*/
|
|
301
|
+
interface DefaultCurrencyAware {
|
|
302
|
+
setDefaultCurrency(currency: string): void;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The envelope every money-changing provider command carries.
|
|
306
|
+
*
|
|
307
|
+
* Idempotency used to be supplied on `refund` alone. It belongs on every command: a
|
|
308
|
+
* retried authorize or capture double-charges just as readily as a retried refund, and the
|
|
309
|
+
* retry is usually the client's, not ours.
|
|
310
|
+
*
|
|
311
|
+
* Adapters whose gateway supports an idempotency key MUST forward it. The engine still keeps
|
|
312
|
+
* its own atomic claim/result store, because not every provider offers one and a provider's
|
|
313
|
+
* word is not a guarantee we control.
|
|
314
|
+
*/
|
|
315
|
+
interface PaymentCommandContext {
|
|
316
|
+
/**
|
|
317
|
+
* Caller-supplied, stable across retries of the SAME logical operation. Two different
|
|
318
|
+
* operations must never share one; one operation retried must never change it.
|
|
319
|
+
*/
|
|
320
|
+
idempotencyKey: string;
|
|
321
|
+
/** Correlates this attempt across our logs, the provider's, and the audit trail. */
|
|
322
|
+
requestId: string;
|
|
323
|
+
/** Our reference for the thing being paid for — order number, invoice number. */
|
|
324
|
+
merchantReference: string;
|
|
325
|
+
/**
|
|
326
|
+
* Branch / tenant scope. OPTIONAL: a revenue engine can run unscoped
|
|
327
|
+
* (single-tenant / company-global), where there is no branch id to carry —
|
|
328
|
+
* so this is absent rather than an empty or asserted string. Scoped engines
|
|
329
|
+
* always set it; consumers that require scope must check, not cast.
|
|
330
|
+
*/
|
|
331
|
+
organizationId?: string;
|
|
332
|
+
/** Cancels an in-flight command (a cashier abandoning a terminal prompt, a shutdown). */
|
|
333
|
+
signal?: AbortSignal;
|
|
334
|
+
}
|
|
335
|
+
/** Why a provider said no. A DECISION, distinct from an unanswered question. */
|
|
336
|
+
interface PaymentDecline {
|
|
337
|
+
/** Normalized reason. Adapters map their vendor's vocabulary onto this. */
|
|
338
|
+
reason: 'insufficient_funds' | 'card_declined' | 'expired' | 'invalid_details' | 'limit_exceeded' | 'duplicate' | 'not_permitted' | 'provider_error' | 'other';
|
|
339
|
+
/** The provider's own code and message, preserved verbatim for support and reconciliation. */
|
|
340
|
+
providerCode?: string;
|
|
341
|
+
providerMessage?: string;
|
|
342
|
+
/** Whether trying again could plausibly succeed. `false` for a hard decline. */
|
|
343
|
+
retryable: boolean;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The outcome of a provider command. **Three-valued, and the third one is the point.**
|
|
347
|
+
*
|
|
348
|
+
* A timeout is NOT a failure. When a capture request times out, the money may well have
|
|
349
|
+
* moved — we simply did not hear back. Treating that as `declined` and retrying is the
|
|
350
|
+
* canonical way to double-charge a customer, and it is invisible until reconciliation.
|
|
351
|
+
*
|
|
352
|
+
* `unknown` also covers a provider that legitimately cannot answer: a stateless manual
|
|
353
|
+
* provider asked for the status of a payment it never stored. Today's two-valued shape
|
|
354
|
+
* forces such a provider to invent an answer, and `revenue-manual` invented `succeeded`.
|
|
355
|
+
*
|
|
356
|
+
* **The only correct response to `unknown` is to reconcile status before acting** — never
|
|
357
|
+
* a blind retry.
|
|
358
|
+
*/
|
|
359
|
+
type ProviderCommandResult<T> = {
|
|
360
|
+
outcome: 'confirmed';
|
|
361
|
+
value: T;
|
|
362
|
+
} | {
|
|
363
|
+
outcome: 'declined';
|
|
364
|
+
error: PaymentDecline;
|
|
365
|
+
} | {
|
|
366
|
+
outcome: 'unknown'; /** Whatever handle we have for asking again later. */
|
|
367
|
+
providerReference?: string;
|
|
368
|
+
/**
|
|
369
|
+
* NORMALIZED reason we do not know — never a raw exception message.
|
|
370
|
+
*
|
|
371
|
+
* Vendor errors routinely embed request URLs, tokens, account identifiers and
|
|
372
|
+
* fragments of the request body. This value is persisted on the transaction, returned
|
|
373
|
+
* to callers and read by operators, so it must be safe to store and display. The raw
|
|
374
|
+
* error belongs in a redacted diagnostic log, not here.
|
|
375
|
+
*/
|
|
376
|
+
causeCode?: ProviderUnknownCause;
|
|
377
|
+
};
|
|
378
|
+
/** Why an outcome went unobserved. Coarse on purpose — it drives handling, not diagnosis. */
|
|
379
|
+
type ProviderUnknownCause = 'timeout' | 'aborted' | 'network' | 'status_unavailable' | 'unclassified';
|
|
380
|
+
/**
|
|
381
|
+
* Thrown by a provider that cannot answer a status query, rather than guessing.
|
|
382
|
+
*
|
|
383
|
+
* Classified as `unknown`. The engine's correct reaction is to consult the stored record:
|
|
384
|
+
* for a manual method there is no external money-movement authority, so the record IS the
|
|
385
|
+
* authority (see `spine/docs/payments-architecture.md` §1).
|
|
386
|
+
*/
|
|
387
|
+
declare class ProviderStatusUnavailableError extends Error {
|
|
388
|
+
readonly providerName: string;
|
|
389
|
+
constructor(providerName: string, message?: string);
|
|
390
|
+
}
|
|
188
391
|
//#endregion
|
|
189
|
-
export { CreateIntentParams,
|
|
392
|
+
export { CreateIntentParams, DefaultCurrencyAware, PaymentCommandContext, PaymentDecline, PaymentProviderPort, PaymentResult, ProviderCapabilities, ProviderCommandResult, ProviderIntent, ProviderIntentStatus, ProviderStatusUnavailableError, ProviderUnknownCause, RefundResult, RefundResultStatus, RefundStatusQuery, WebhookEvent };
|
package/dist/payment-gateway.mjs
CHANGED
|
@@ -1 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/money/payment-gateway.ts
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by a provider that cannot answer a status query, rather than guessing.
|
|
4
|
+
*
|
|
5
|
+
* Classified as `unknown`. The engine's correct reaction is to consult the stored record:
|
|
6
|
+
* for a manual method there is no external money-movement authority, so the record IS the
|
|
7
|
+
* authority (see `spine/docs/payments-architecture.md` §1).
|
|
8
|
+
*/
|
|
9
|
+
var ProviderStatusUnavailableError = class extends Error {
|
|
10
|
+
providerName;
|
|
11
|
+
constructor(providerName, message) {
|
|
12
|
+
super(message ?? `provider "${providerName}" cannot report status for this payment — it holds no record of it. Read the stored transaction instead of asking the provider.`);
|
|
13
|
+
this.name = "ProviderStatusUnavailableError";
|
|
14
|
+
this.providerName = providerName;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ProviderStatusUnavailableError };
|