@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.
- package/CHANGELOG.md +906 -751
- 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 +1 -1
- 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 +2 -2
- package/dist/proration.mjs +18 -6
- 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 -232
- 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) &&
|
|
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
|
-
/**
|
|
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 };
|