@classytic/primitives 0.19.0 → 0.21.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 +920 -751
- package/README.md +160 -159
- package/dist/approval.d.mts +50 -11
- package/dist/approval.mjs +44 -10
- package/dist/bank-transaction.d.mts +1 -2
- package/dist/{brand-LLXqoh5J.d.mts → brand-Lj_75Y_r.d.mts} +6 -6
- package/dist/brand.d.mts +1 -1
- package/dist/cadence-5X6TiD49.d.mts +107 -0
- package/dist/cadence.d.mts +1 -92
- package/dist/cadence.mjs +135 -6
- package/dist/calendar-DJWSbcgE.d.mts +46 -0
- package/dist/calendar.d.mts +1 -45
- package/dist/calendar.mjs +2 -2
- package/dist/canonical.mjs +17 -0
- package/dist/condition.mjs +2 -2
- package/dist/context.d.mts +28 -2
- package/dist/currency.d.mts +2 -2
- package/dist/currency.mjs +134 -19
- package/dist/environment.d.mts +85 -0
- package/dist/environment.mjs +48 -0
- package/dist/event-infra.d.mts +16 -32
- package/dist/event-infra.mjs +16 -46
- package/dist/idempotency.d.mts +263 -0
- package/dist/idempotency.mjs +226 -0
- package/dist/identity-verification.d.mts +0 -1
- package/dist/memory-outbox.d.mts +63 -0
- package/dist/memory-outbox.mjs +172 -0
- package/dist/mixin.d.mts +1 -1
- package/dist/monetization.d.mts +1 -2
- package/dist/money-CfAvfNiM.d.mts +346 -0
- package/dist/money.d.mts +1 -140
- package/dist/money.mjs +6 -15
- package/dist/otp.mjs +1 -1
- package/dist/outbox.d.mts +49 -2
- package/dist/outbox.mjs +64 -1
- package/dist/payment-allocation-status.d.mts +10 -4
- package/dist/payment-allocation-status.mjs +14 -0
- package/dist/payment-events.d.mts +132 -5
- package/dist/payment-events.mjs +34 -2
- package/dist/payment-gateway.d.mts +211 -8
- package/dist/payment-gateway.mjs +18 -1
- package/dist/payment-method-kind.d.mts +23 -11
- package/dist/payment-method-kind.mjs +17 -0
- package/dist/period.d.mts +149 -7
- package/dist/period.mjs +316 -5
- package/dist/person-parse.d.mts +81 -0
- package/dist/person-parse.mjs +109 -0
- package/dist/phone.d.mts +0 -1
- package/dist/phone.mjs +3 -3
- package/dist/proration.d.mts +2 -3
- package/dist/proration.mjs +18 -6
- package/dist/retention.mjs +3 -3
- package/dist/shelf-life.mjs +1 -1
- package/dist/sla-policy.d.mts +1 -2
- package/dist/state-diagram.d.mts +0 -1
- package/dist/state-diagram.mjs +1 -1
- package/dist/state-machine.mjs +13 -0
- package/dist/suspension.d.mts +6 -4
- package/dist/suspension.mjs +3 -2
- package/dist/timezone-CxUjxmRU.d.mts +113 -0
- package/dist/timezone.d.mts +2 -100
- package/dist/timezone.mjs +38 -3
- package/dist/unit-cost-rate.d.mts +1 -3
- package/package.json +249 -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/otp.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { createHmac, randomInt, timingSafeEqual } from "node:crypto";
|
|
|
26
26
|
* modeled here so the verify primitive stays stateless between attempts.
|
|
27
27
|
*/
|
|
28
28
|
const DEFAULT_LENGTH = 6;
|
|
29
|
-
const DEFAULT_TTL_MS =
|
|
29
|
+
const DEFAULT_TTL_MS = 3e5;
|
|
30
30
|
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
31
31
|
function hmac(code, secret) {
|
|
32
32
|
return createHmac("sha256", secret).update(code).digest("hex");
|
package/dist/outbox.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { DeadLetteredEvent, DomainEvent } from "./events.mjs";
|
|
2
|
-
|
|
3
2
|
//#region src/events/outbox.d.ts
|
|
4
3
|
/**
|
|
5
4
|
* Options passed to {@link OutboxStore.save} for richer write semantics.
|
|
@@ -157,6 +156,19 @@ declare class InvalidOutboxEventError extends Error {
|
|
|
157
156
|
* `claimPending`.** If the underlying storage has corrupt rows, the
|
|
158
157
|
* store is responsible for quarantining them.
|
|
159
158
|
*/
|
|
159
|
+
/**
|
|
160
|
+
* The delivery states an outbox row can hold.
|
|
161
|
+
*
|
|
162
|
+
* Named HERE, in the contract, rather than in each store. It went unnamed while every
|
|
163
|
+
* method took an event id and none took a state — `countByStatus` is the first member
|
|
164
|
+
* that makes the vocabulary part of the interface, and two stores had already written
|
|
165
|
+
* the same three strings independently by then.
|
|
166
|
+
*
|
|
167
|
+
* `dead_letter` is terminal until an operator calls `requeue`; it is a durable statement
|
|
168
|
+
* that something is owed, not a deletion.
|
|
169
|
+
*/
|
|
170
|
+
declare const OUTBOX_STATUSES: readonly ["pending", "delivered", "dead_letter"];
|
|
171
|
+
type OutboxStatus = (typeof OUTBOX_STATUSES)[number];
|
|
160
172
|
interface OutboxStore {
|
|
161
173
|
/**
|
|
162
174
|
* Declares that `save` ENLISTS `options.session` in the caller's transaction,
|
|
@@ -242,6 +254,41 @@ interface OutboxStore {
|
|
|
242
254
|
* @returns Number of purged events.
|
|
243
255
|
*/
|
|
244
256
|
purge?(olderThanMs: number): Promise<number>;
|
|
257
|
+
/**
|
|
258
|
+
* Re-drive ONE dead-lettered event: back to `pending`, attempts reset, lease cleared,
|
|
259
|
+
* immediately visible.
|
|
260
|
+
*
|
|
261
|
+
* The other half of `fail({ deadLetter: true })`. Dead-lettering is only a durable
|
|
262
|
+
* statement that something is owed; a store that can dead-letter but not requeue leaves
|
|
263
|
+
* an operator who has fixed the root cause with no way to act on the worklist except
|
|
264
|
+
* hand-editing rows in the database.
|
|
265
|
+
*
|
|
266
|
+
* MUST be scoped to `dead_letter` — requeuing a `pending` row would reset the attempt
|
|
267
|
+
* count on an event that is merely backing off, defeating the retry policy, and
|
|
268
|
+
* requeuing a `delivered` row would republish an event every subscriber already saw.
|
|
269
|
+
*
|
|
270
|
+
* @returns `true` if a dead-lettered event was requeued, `false` if no such event
|
|
271
|
+
* exists — NOT an error. An operator re-driving a worklist races the relay, and an
|
|
272
|
+
* id that has already been dealt with is an ordinary outcome.
|
|
273
|
+
*/
|
|
274
|
+
requeue?(eventId: string): Promise<boolean>;
|
|
275
|
+
/**
|
|
276
|
+
* How many events are in `status`. The health surface behind an operator dashboard or
|
|
277
|
+
* an alert (`dead_letter > 0` means money or access may be unrecorded).
|
|
278
|
+
*/
|
|
279
|
+
countByStatus?(status: OutboxStatus): Promise<number>;
|
|
280
|
+
/**
|
|
281
|
+
* Age in ms of the OLDEST pending event, or `null` when nothing is pending.
|
|
282
|
+
*
|
|
283
|
+
* The single most useful outbox health signal, and the one a naive dashboard misses: a
|
|
284
|
+
* pending COUNT looks identical whether the queue is draining briskly or has been
|
|
285
|
+
* wedged for an hour behind one poison row. Age distinguishes them.
|
|
286
|
+
*
|
|
287
|
+
* Measured from the event's creation, not its last attempt — the question being asked
|
|
288
|
+
* is "how long has this event gone undelivered", and resetting the clock on each retry
|
|
289
|
+
* would make a permanently-failing row look permanently fresh.
|
|
290
|
+
*/
|
|
291
|
+
oldestPendingAgeMs?(): Promise<number | null>;
|
|
245
292
|
}
|
|
246
293
|
//#endregion
|
|
247
|
-
export { InvalidOutboxEventError, OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxErrorInfo, OutboxFailOptions, OutboxFailureContext, OutboxFailureDecision, OutboxFailurePolicy, OutboxOwnershipError, OutboxStore, OutboxWriteOptions };
|
|
294
|
+
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 };
|
|
@@ -20,8 +20,11 @@
|
|
|
20
20
|
* Zero runtime, zero deps.
|
|
21
21
|
*/
|
|
22
22
|
declare const PAYMENT_ALLOCATION_STATUS: {
|
|
23
|
-
/** No portion of the payment has been applied to any obligation yet. */
|
|
24
|
-
readonly
|
|
23
|
+
/** No portion of the payment has been applied to any obligation yet. */
|
|
24
|
+
readonly UNALLOCATED: "unallocated";
|
|
25
|
+
/** Some of the payment is applied; remainder is available credit. */
|
|
26
|
+
readonly PARTIAL: "partial";
|
|
27
|
+
/** Fully applied — `sum(allocations) === payment.totalAmount`. */
|
|
25
28
|
readonly ALLOCATED: "allocated";
|
|
26
29
|
/**
|
|
27
30
|
* Sum of allocations exceeds the payment total — invariant violation
|
|
@@ -31,8 +34,11 @@ declare const PAYMENT_ALLOCATION_STATUS: {
|
|
|
31
34
|
};
|
|
32
35
|
type PaymentAllocationStatus = (typeof PAYMENT_ALLOCATION_STATUS)[keyof typeof PAYMENT_ALLOCATION_STATUS];
|
|
33
36
|
declare const RECONCILIATION_STATUS: {
|
|
34
|
-
/** No bank statement line / settlement record has been linked yet. */
|
|
35
|
-
readonly
|
|
37
|
+
/** No bank statement line / settlement record has been linked yet. */
|
|
38
|
+
readonly UNRECONCILED: "unreconciled";
|
|
39
|
+
/** Auto-matched to a `BankTransaction` by amount + date + reference. */
|
|
40
|
+
readonly MATCHED: "matched";
|
|
41
|
+
/** Operator-confirmed match (auto-match failed or was overridden). */
|
|
36
42
|
readonly MANUAL: "manual";
|
|
37
43
|
/**
|
|
38
44
|
* Match was contested — amount mismatch, duplicate claim, chargeback
|
|
@@ -20,15 +20,29 @@
|
|
|
20
20
|
* Zero runtime, zero deps.
|
|
21
21
|
*/
|
|
22
22
|
const PAYMENT_ALLOCATION_STATUS = {
|
|
23
|
+
/** No portion of the payment has been applied to any obligation yet. */
|
|
23
24
|
UNALLOCATED: "unallocated",
|
|
25
|
+
/** Some of the payment is applied; remainder is available credit. */
|
|
24
26
|
PARTIAL: "partial",
|
|
27
|
+
/** Fully applied — `sum(allocations) === payment.totalAmount`. */
|
|
25
28
|
ALLOCATED: "allocated",
|
|
29
|
+
/**
|
|
30
|
+
* Sum of allocations exceeds the payment total — invariant violation
|
|
31
|
+
* that should be flagged for operator review, never silently accepted.
|
|
32
|
+
*/
|
|
26
33
|
OVER_ALLOCATED: "over_allocated"
|
|
27
34
|
};
|
|
28
35
|
const RECONCILIATION_STATUS = {
|
|
36
|
+
/** No bank statement line / settlement record has been linked yet. */
|
|
29
37
|
UNRECONCILED: "unreconciled",
|
|
38
|
+
/** Auto-matched to a `BankTransaction` by amount + date + reference. */
|
|
30
39
|
MATCHED: "matched",
|
|
40
|
+
/** Operator-confirmed match (auto-match failed or was overridden). */
|
|
31
41
|
MANUAL: "manual",
|
|
42
|
+
/**
|
|
43
|
+
* Match was contested — amount mismatch, duplicate claim, chargeback
|
|
44
|
+
* in flight. Halts further automated processing until resolved.
|
|
45
|
+
*/
|
|
32
46
|
DISPUTED: "disputed"
|
|
33
47
|
};
|
|
34
48
|
const ALLOCATION_VALUES = new Set(Object.values(PAYMENT_ALLOCATION_STATUS));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Money } from "./money.mjs";
|
|
1
|
+
import { n as Money } from "./money-CfAvfNiM.mjs";
|
|
2
2
|
import { PaymentMethodKind } from "./payment-method-kind.mjs";
|
|
3
|
-
|
|
3
|
+
import { ProviderUnknownCause } from "./payment-gateway.mjs";
|
|
4
4
|
//#region src/events/payment-events.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Canonical event-name constants. Packages SHOULD publish events
|
|
@@ -21,6 +21,19 @@ declare const PAYMENT_EVENT_TYPE: {
|
|
|
21
21
|
readonly DISPUTE_WON: "payment.dispute_won";
|
|
22
22
|
readonly DISPUTE_LOST: "payment.dispute_lost";
|
|
23
23
|
readonly SETTLED: "payment.settled";
|
|
24
|
+
/**
|
|
25
|
+
* The provider command produced NO observable outcome — timeout, abort,
|
|
26
|
+
* network fault, or a provider that holds no record and refuses to guess.
|
|
27
|
+
*
|
|
28
|
+
* This member exists because the port contract (`ProviderCommandResult` in
|
|
29
|
+
* `/payment-gateway`) is three-valued — `confirmed | declined | unknown` —
|
|
30
|
+
* while this catalogue was two-valued. An adapter that hit the third case had
|
|
31
|
+
* nowhere to publish it, so the only shapes on the bus were SUCCEEDED and
|
|
32
|
+
* FAILED, and an unobserved outcome had to be forced into one of them.
|
|
33
|
+
* Forcing it into FAILED is the one that double-charges: it licenses a retry
|
|
34
|
+
* for a command that may already have moved money.
|
|
35
|
+
*/
|
|
36
|
+
readonly UNKNOWN: "payment.unknown";
|
|
24
37
|
};
|
|
25
38
|
type PaymentEventType = (typeof PAYMENT_EVENT_TYPE)[keyof typeof PAYMENT_EVENT_TYPE];
|
|
26
39
|
/**
|
|
@@ -30,6 +43,8 @@ type PaymentEventType = (typeof PAYMENT_EVENT_TYPE)[keyof typeof PAYMENT_EVENT_T
|
|
|
30
43
|
* "money in flight" not "money received".
|
|
31
44
|
*/
|
|
32
45
|
interface PaymentInitiatedPayload {
|
|
46
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
47
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.INITIATED;
|
|
33
48
|
/** Stable payment identifier owned by the emitting package. */
|
|
34
49
|
paymentId: string;
|
|
35
50
|
/**
|
|
@@ -57,6 +72,8 @@ interface PaymentInitiatedPayload {
|
|
|
57
72
|
* downstream fulfilment unlocks.
|
|
58
73
|
*/
|
|
59
74
|
interface PaymentSucceededPayload {
|
|
75
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
76
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.SUCCEEDED;
|
|
60
77
|
paymentId: string;
|
|
61
78
|
providerRef?: string;
|
|
62
79
|
providerCode: string;
|
|
@@ -73,6 +90,8 @@ interface PaymentSucceededPayload {
|
|
|
73
90
|
* overdue.
|
|
74
91
|
*/
|
|
75
92
|
interface PaymentFailedPayload {
|
|
93
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
94
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.FAILED;
|
|
76
95
|
paymentId: string;
|
|
77
96
|
providerRef?: string;
|
|
78
97
|
providerCode: string;
|
|
@@ -93,6 +112,8 @@ interface PaymentFailedPayload {
|
|
|
93
112
|
* itself (pre- or post-clearing, often due to operator error).
|
|
94
113
|
*/
|
|
95
114
|
interface PaymentRefundedPayload {
|
|
115
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
116
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.REFUNDED;
|
|
96
117
|
paymentId: string;
|
|
97
118
|
/** Refund identifier (separate from `paymentId`). */
|
|
98
119
|
refundId: string;
|
|
@@ -116,6 +137,8 @@ interface PaymentRefundedPayload {
|
|
|
116
137
|
* state.
|
|
117
138
|
*/
|
|
118
139
|
interface PaymentReversedPayload {
|
|
140
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
141
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.REVERSED;
|
|
119
142
|
paymentId: string;
|
|
120
143
|
providerRef?: string;
|
|
121
144
|
providerCode: string;
|
|
@@ -136,6 +159,8 @@ interface PaymentReversedPayload {
|
|
|
136
159
|
* ledger bridges and closes the audit loop.
|
|
137
160
|
*/
|
|
138
161
|
interface PaymentReconciledPayload {
|
|
162
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
163
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.RECONCILED;
|
|
139
164
|
/** Allocation identifier — NOT the payment id (a payment can have many). */
|
|
140
165
|
allocationId: string;
|
|
141
166
|
paymentId: string;
|
|
@@ -156,6 +181,8 @@ interface PaymentReconciledPayload {
|
|
|
156
181
|
* "hold" from "settle"; consumers should NOT treat this as cleared funds.
|
|
157
182
|
*/
|
|
158
183
|
interface PaymentAuthorizedPayload {
|
|
184
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
185
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.AUTHORIZED;
|
|
159
186
|
paymentId: string;
|
|
160
187
|
providerRef?: string;
|
|
161
188
|
providerCode: string;
|
|
@@ -173,6 +200,8 @@ interface PaymentAuthorizedPayload {
|
|
|
173
200
|
* Pairs with {@link PaymentAuthorizedPayload} for two-phase flows.
|
|
174
201
|
*/
|
|
175
202
|
interface PaymentCapturedPayload {
|
|
203
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
204
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.CAPTURED;
|
|
176
205
|
paymentId: string;
|
|
177
206
|
providerRef?: string;
|
|
178
207
|
providerCode: string;
|
|
@@ -191,6 +220,8 @@ interface PaymentCapturedPayload {
|
|
|
191
220
|
* Authorisation hold released — uncaptured auth voided before settlement.
|
|
192
221
|
*/
|
|
193
222
|
interface PaymentAuthVoidedPayload {
|
|
223
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
224
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.AUTH_VOIDED;
|
|
194
225
|
paymentId: string;
|
|
195
226
|
providerRef?: string;
|
|
196
227
|
providerCode: string;
|
|
@@ -205,6 +236,8 @@ interface PaymentAuthVoidedPayload {
|
|
|
205
236
|
* payment. The merchant typically has a deadline to submit evidence.
|
|
206
237
|
*/
|
|
207
238
|
interface PaymentDisputedPayload {
|
|
239
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
240
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.DISPUTED;
|
|
208
241
|
paymentId: string;
|
|
209
242
|
/** Provider's dispute / chargeback id. */
|
|
210
243
|
disputeId: string;
|
|
@@ -223,6 +256,8 @@ interface PaymentDisputedPayload {
|
|
|
223
256
|
}
|
|
224
257
|
/** Dispute resolved in merchant's favour — funds restored. */
|
|
225
258
|
interface PaymentDisputeWonPayload {
|
|
259
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
260
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.DISPUTE_WON;
|
|
226
261
|
paymentId: string;
|
|
227
262
|
disputeId: string;
|
|
228
263
|
providerRef?: string;
|
|
@@ -234,6 +269,8 @@ interface PaymentDisputeWonPayload {
|
|
|
234
269
|
}
|
|
235
270
|
/** Dispute resolved against merchant — funds permanently lost plus fee. */
|
|
236
271
|
interface PaymentDisputeLostPayload {
|
|
272
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
273
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.DISPUTE_LOST;
|
|
237
274
|
paymentId: string;
|
|
238
275
|
disputeId: string;
|
|
239
276
|
providerRef?: string;
|
|
@@ -251,6 +288,8 @@ interface PaymentDisputeLostPayload {
|
|
|
251
288
|
* reconciliation in country-aware ledger bridges.
|
|
252
289
|
*/
|
|
253
290
|
interface PaymentSettledPayload {
|
|
291
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
292
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.SETTLED;
|
|
254
293
|
paymentId: string;
|
|
255
294
|
providerRef?: string;
|
|
256
295
|
providerCode: string;
|
|
@@ -265,7 +304,95 @@ interface PaymentSettledPayload {
|
|
|
265
304
|
occurredAt: Date;
|
|
266
305
|
metadata?: Record<string, unknown>;
|
|
267
306
|
}
|
|
268
|
-
/**
|
|
269
|
-
|
|
307
|
+
/**
|
|
308
|
+
* The provider command produced no observable outcome. **This is not a
|
|
309
|
+
* failure**, and it is not a success — it is the absence of an answer.
|
|
310
|
+
*
|
|
311
|
+
* The obligation's state is genuinely unknown: the capture may have landed at
|
|
312
|
+
* the gateway with the acknowledgement lost in transit. The ONLY correct
|
|
313
|
+
* reaction is to reconcile the payment's real status before acting. A blind
|
|
314
|
+
* retry is a double charge; marking it failed and releasing the order is a
|
|
315
|
+
* shipped-but-unpaid order. Both are silent.
|
|
316
|
+
*
|
|
317
|
+
* Consumers MUST NOT treat this as a terminal state. It is a prompt to go and
|
|
318
|
+
* find out, and it resolves later into `payment.succeeded` or `payment.failed`.
|
|
319
|
+
*/
|
|
320
|
+
interface PaymentUnknownPayload {
|
|
321
|
+
/** Discriminant — narrows {@link PaymentEventPayload}. */
|
|
322
|
+
readonly eventType: typeof PAYMENT_EVENT_TYPE.UNKNOWN;
|
|
323
|
+
paymentId: string;
|
|
324
|
+
providerRef?: string;
|
|
325
|
+
providerCode: string;
|
|
326
|
+
/** Which command went unanswered — the retry policy differs per verb. */
|
|
327
|
+
operation: 'create_intent' | 'verify' | 'authorize' | 'capture' | 'refund' | 'void';
|
|
328
|
+
/** The amount at stake, when the command carried one. */
|
|
329
|
+
amount?: Money;
|
|
330
|
+
methodKind?: PaymentMethodKind;
|
|
331
|
+
methodCode?: string;
|
|
332
|
+
/**
|
|
333
|
+
* NORMALIZED reason the outcome was not observed — mirrors
|
|
334
|
+
* `ProviderUnknownCause` from `/payment-gateway`. Never a raw vendor error
|
|
335
|
+
* string: this value is persisted and displayed, and vendor errors embed
|
|
336
|
+
* request URLs, tokens and body fragments.
|
|
337
|
+
*/
|
|
338
|
+
causeCode: ProviderUnknownCause;
|
|
339
|
+
/** The idempotency key the command carried — what reconciliation asks with. */
|
|
340
|
+
idempotencyKey?: string;
|
|
341
|
+
occurredAt: Date;
|
|
342
|
+
metadata?: Record<string, unknown>;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Union of all payment event payload types.
|
|
346
|
+
*
|
|
347
|
+
* DISCRIMINATED on `eventType`. Before the discriminant existed,
|
|
348
|
+
* `PaymentInitiatedPayload` and `PaymentSucceededPayload` were structurally
|
|
349
|
+
* IDENTICAL — so a handler typed for "funds received" accepted "money in
|
|
350
|
+
* flight" with no error anywhere, and TypeScript could not tell them apart even
|
|
351
|
+
* in principle. Structural identity between two events that mean opposite
|
|
352
|
+
* things is precisely where a nominal tag has to carry the meaning.
|
|
353
|
+
*/
|
|
354
|
+
type PaymentEventPayload = PaymentInitiatedPayload | PaymentSucceededPayload | PaymentFailedPayload | PaymentRefundedPayload | PaymentReversedPayload | PaymentReconciledPayload | PaymentAuthorizedPayload | PaymentCapturedPayload | PaymentAuthVoidedPayload | PaymentDisputedPayload | PaymentDisputeWonPayload | PaymentDisputeLostPayload | PaymentSettledPayload | PaymentUnknownPayload;
|
|
355
|
+
/** Event type → its payload. The lookup a typed publisher/handler pair needs. */
|
|
356
|
+
interface PaymentEventPayloadMap {
|
|
357
|
+
'payment.initiated': PaymentInitiatedPayload;
|
|
358
|
+
'payment.succeeded': PaymentSucceededPayload;
|
|
359
|
+
'payment.failed': PaymentFailedPayload;
|
|
360
|
+
'payment.refunded': PaymentRefundedPayload;
|
|
361
|
+
'payment.reversed': PaymentReversedPayload;
|
|
362
|
+
'payment.reconciled': PaymentReconciledPayload;
|
|
363
|
+
'payment.authorized': PaymentAuthorizedPayload;
|
|
364
|
+
'payment.captured': PaymentCapturedPayload;
|
|
365
|
+
'payment.auth_voided': PaymentAuthVoidedPayload;
|
|
366
|
+
'payment.disputed': PaymentDisputedPayload;
|
|
367
|
+
'payment.dispute_won': PaymentDisputeWonPayload;
|
|
368
|
+
'payment.dispute_lost': PaymentDisputeLostPayload;
|
|
369
|
+
'payment.settled': PaymentSettledPayload;
|
|
370
|
+
'payment.unknown': PaymentUnknownPayload;
|
|
371
|
+
}
|
|
372
|
+
type Assert<T extends true> = T;
|
|
373
|
+
/** Every declared event type has a payload in the map. */
|
|
374
|
+
type _EveryTypeMapped = Assert<PaymentEventType extends keyof PaymentEventPayloadMap ? true : false>;
|
|
375
|
+
/** The map declares no key that is not a real event type. */
|
|
376
|
+
type _NoStrayMapKeys = Assert<keyof PaymentEventPayloadMap extends PaymentEventType ? true : false>;
|
|
377
|
+
/** Each payload's own `eventType` literal matches the key it is filed under. */
|
|
378
|
+
type _DiscriminantMatchesKey = Assert<{ [K in keyof PaymentEventPayloadMap]: PaymentEventPayloadMap[K] extends {
|
|
379
|
+
eventType: K;
|
|
380
|
+
} ? true : false; }[keyof PaymentEventPayloadMap] extends true ? true : false>;
|
|
381
|
+
type PaymentEventContractGuards = [_EveryTypeMapped, _NoStrayMapKeys, _DiscriminantMatchesKey];
|
|
382
|
+
/**
|
|
383
|
+
* Narrow a payload by event type at RUNTIME, with the compile-time narrowing
|
|
384
|
+
* to match. `if (isPaymentEvent(PAYMENT_EVENT_TYPE.SUCCEEDED, p)) { … }` —
|
|
385
|
+
* inside the branch `p` is `PaymentSucceededPayload` and cannot be an
|
|
386
|
+
* initiated payload, which is the whole point of the discriminant.
|
|
387
|
+
*/
|
|
388
|
+
declare function isPaymentEvent<T extends PaymentEventType>(type: T, payload: PaymentEventPayload): payload is PaymentEventPayloadMap[T];
|
|
389
|
+
/**
|
|
390
|
+
* Is this payload a CLEARED-FUNDS event?
|
|
391
|
+
*
|
|
392
|
+
* The named answer to the question that used to be answerable only by
|
|
393
|
+
* structural shape — and answered wrong, because `initiated` had the same
|
|
394
|
+
* shape. `captured` counts; `initiated`, `authorized` and `unknown` do not.
|
|
395
|
+
*/
|
|
396
|
+
declare function isFundsReceived(payload: PaymentEventPayload): boolean;
|
|
270
397
|
//#endregion
|
|
271
|
-
export { PAYMENT_EVENT_TYPE, PaymentAuthVoidedPayload, PaymentAuthorizedPayload, PaymentCapturedPayload, PaymentDisputeLostPayload, PaymentDisputeWonPayload, PaymentDisputedPayload, PaymentEventPayload, PaymentEventType, PaymentFailedPayload, PaymentInitiatedPayload, PaymentReconciledPayload, PaymentRefundedPayload, PaymentReversedPayload, PaymentSettledPayload, PaymentSucceededPayload };
|
|
398
|
+
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,39 @@ 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
|
+
/**
|
|
22
|
+
* The provider command produced NO observable outcome — timeout, abort,
|
|
23
|
+
* network fault, or a provider that holds no record and refuses to guess.
|
|
24
|
+
*
|
|
25
|
+
* This member exists because the port contract (`ProviderCommandResult` in
|
|
26
|
+
* `/payment-gateway`) is three-valued — `confirmed | declined | unknown` —
|
|
27
|
+
* while this catalogue was two-valued. An adapter that hit the third case had
|
|
28
|
+
* nowhere to publish it, so the only shapes on the bus were SUCCEEDED and
|
|
29
|
+
* FAILED, and an unobserved outcome had to be forced into one of them.
|
|
30
|
+
* Forcing it into FAILED is the one that double-charges: it licenses a retry
|
|
31
|
+
* for a command that may already have moved money.
|
|
32
|
+
*/
|
|
33
|
+
UNKNOWN: "payment.unknown"
|
|
21
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Narrow a payload by event type at RUNTIME, with the compile-time narrowing
|
|
37
|
+
* to match. `if (isPaymentEvent(PAYMENT_EVENT_TYPE.SUCCEEDED, p)) { … }` —
|
|
38
|
+
* inside the branch `p` is `PaymentSucceededPayload` and cannot be an
|
|
39
|
+
* initiated payload, which is the whole point of the discriminant.
|
|
40
|
+
*/
|
|
41
|
+
function isPaymentEvent(type, payload) {
|
|
42
|
+
return payload.eventType === type;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Is this payload a CLEARED-FUNDS event?
|
|
46
|
+
*
|
|
47
|
+
* The named answer to the question that used to be answerable only by
|
|
48
|
+
* structural shape — and answered wrong, because `initiated` had the same
|
|
49
|
+
* shape. `captured` counts; `initiated`, `authorized` and `unknown` do not.
|
|
50
|
+
*/
|
|
51
|
+
function isFundsReceived(payload) {
|
|
52
|
+
return payload.eventType === PAYMENT_EVENT_TYPE.SUCCEEDED || payload.eventType === PAYMENT_EVENT_TYPE.CAPTURED;
|
|
53
|
+
}
|
|
22
54
|
//#endregion
|
|
23
|
-
export { PAYMENT_EVENT_TYPE };
|
|
55
|
+
export { PAYMENT_EVENT_TYPE, isFundsReceived, isPaymentEvent };
|