@classytic/primitives 0.6.0 → 0.7.1
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 +50 -9
- package/dist/payment-allocation-status.d.mts +47 -0
- package/dist/payment-allocation-status.mjs +39 -0
- package/dist/payment-events.d.mts +271 -0
- package/dist/payment-events.mjs +23 -0
- package/dist/payment-gateway.d.mts +28 -7
- package/dist/payment-method-kind.d.mts +48 -0
- package/dist/payment-method-kind.mjs +43 -0
- package/dist/status-history.d.mts +3 -4
- package/package.json +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,13 +3,55 @@
|
|
|
3
3
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
4
|
adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [0.7.1] - 2026-05-26
|
|
7
|
+
|
|
8
|
+
### Added — workflow + scheduling + composition primitives
|
|
9
|
+
|
|
10
|
+
Four new flat subpaths (additive):
|
|
11
|
+
|
|
12
|
+
- **`/status-history`** — append-only status transition log primitive.
|
|
13
|
+
- **`/condition`** — declarative condition expressions for workflow gates.
|
|
14
|
+
- **`/mixin`** — composition helpers for primitive object assembly.
|
|
15
|
+
- **`/sla-policy`** — SLA policy definitions paired with the existing `/sla` runtime.
|
|
16
|
+
|
|
17
|
+
### Added — payment event coverage
|
|
18
|
+
|
|
19
|
+
Expanded `PAYMENT_EVENT_TYPE` catalogue to cover auth/capture and dispute
|
|
20
|
+
lifecycles: `AUTHORIZED`, `CAPTURED`, `AUTH_VOIDED`, `DISPUTED`,
|
|
21
|
+
`DISPUTE_WON`, `DISPUTE_LOST`, `SETTLED`.
|
|
22
|
+
|
|
23
|
+
`PaymentRefundedPayload` and `PaymentReversedPayload` now carry
|
|
24
|
+
`originalAmount` (and `reversedAmount` / `isPartial` on reversed) so
|
|
25
|
+
consumers can detect partial vs full operations without a follow-up
|
|
26
|
+
Payment lookup.
|
|
27
|
+
|
|
28
|
+
## [0.7.0] - 2026-05-26
|
|
29
|
+
|
|
30
|
+
### Added — payment domain primitives
|
|
31
|
+
|
|
32
|
+
Three new subpaths shared across every payment-aware package
|
|
33
|
+
(`@classytic/invoice`, `@classytic/revenue`, `@classytic/order`, future
|
|
34
|
+
POS / AP / subscription packages). Each ships under a flat subpath so
|
|
35
|
+
consumers tree-shake what they don't import.
|
|
36
|
+
|
|
37
|
+
- **`/payment-method-kind`** — `PaymentMethodKind` universal vocabulary
|
|
38
|
+
(kind vs host-registered `methodCode`) so packages can speak `kind`
|
|
39
|
+
while hosts speak `code`.
|
|
40
|
+
- **`/payment-allocation-status`** — orthogonal allocation +
|
|
41
|
+
reconciliation status enums for payment legs (`unallocated`,
|
|
42
|
+
`partial`, `allocated`, `over_allocated`, plus reconciliation states).
|
|
43
|
+
- **`/payment-events`** — pure TS payload contracts for payment domain
|
|
44
|
+
events. Wire-only (no Zod, no runtime). Anchored on `Money` +
|
|
45
|
+
`PaymentMethodKind`; causation/correlation stay on `EventMeta`.
|
|
46
|
+
|
|
47
|
+
No breaking changes.
|
|
48
|
+
|
|
6
49
|
## [0.6.0] - 2026-05-17
|
|
7
50
|
|
|
8
51
|
### Added — five new primitives for CRM / workflow domains
|
|
9
52
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
each ships under its own flat subpath import (no breaking changes).
|
|
53
|
+
Five new primitives covering gaps the existing surface didn't address.
|
|
54
|
+
Each ships under its own flat subpath import (no breaking changes).
|
|
13
55
|
|
|
14
56
|
- **`/phone`** — `PhoneNumber` value object + `parsePhone(input)` that
|
|
15
57
|
normalizes free-form input to E.164, captures the country calling code
|
|
@@ -20,10 +62,9 @@ each ships under its own flat subpath import (no breaking changes).
|
|
|
20
62
|
|
|
21
63
|
- **`/status-history`** — Generic, immutable status-change log:
|
|
22
64
|
`StatusChangeEntry<TStatus>`, `appendStatus()`, `timeInStatus()`,
|
|
23
|
-
`lastTransitionTo()`. Captures `durationInPriorMs` at write time
|
|
24
|
-
(
|
|
25
|
-
|
|
26
|
-
Pairs naturally with `/state-machine`.
|
|
65
|
+
`lastTransitionTo()`. Captures `durationInPriorMs` at write time so
|
|
66
|
+
funnel-velocity dashboards ("avg time in Qualification") work without
|
|
67
|
+
re-walking timestamps. Pairs naturally with `/state-machine`.
|
|
27
68
|
|
|
28
69
|
- **`/condition`** — Tiny JSON-serializable predicate DSL: `FieldCondition`
|
|
29
70
|
(`{ field, op, value }`) composed via `AllCondition` / `AnyCondition`
|
|
@@ -37,8 +78,8 @@ each ships under its own flat subpath import (no breaking changes).
|
|
|
37
78
|
`withMixin<T, K, M>()` / `getMixin()` / `hasMixin()` / `withoutMixin()`.
|
|
38
79
|
Lets a `Contact` carry per-domain payloads (`mixins.customer`,
|
|
39
80
|
`mixins.lead-fit`) on a typed `mixins` namespace without bloating the
|
|
40
|
-
base type.
|
|
41
|
-
|
|
81
|
+
base type. Plain object composition — no decorators, no global
|
|
82
|
+
hierarchy, no proxies.
|
|
42
83
|
|
|
43
84
|
- **`/sla-policy`** — Higher-order SLA layered on the existing `/sla`:
|
|
44
85
|
priority matrix (`urgent` / `high` / `normal` → durations), first-
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/money/payment-allocation-status.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Allocation + reconciliation status enums for payment legs.
|
|
4
|
+
*
|
|
5
|
+
* Two orthogonal dimensions sit on every PaymentAllocation row (or
|
|
6
|
+
* equivalent in any package that settles money against an invoice /
|
|
7
|
+
* order / claim):
|
|
8
|
+
*
|
|
9
|
+
* 1. **Allocation status** — has this payment been *applied* against
|
|
10
|
+
* the obligation(s) it's meant to settle? (`unallocated`,
|
|
11
|
+
* `partial`, `allocated`, `over_allocated`)
|
|
12
|
+
* 2. **Reconciliation status** — has this allocation been *matched*
|
|
13
|
+
* to a bank statement line / processor settlement record?
|
|
14
|
+
* (`unreconciled`, `matched`, `manual`, `disputed`)
|
|
15
|
+
*
|
|
16
|
+
* Both are needed because a payment can be allocated (the AR ledger
|
|
17
|
+
* thinks it's done) without being reconciled (the bank statement
|
|
18
|
+
* hasn't been imported / matched yet), and vice versa.
|
|
19
|
+
*
|
|
20
|
+
* Zero runtime, zero deps.
|
|
21
|
+
*/
|
|
22
|
+
declare const PAYMENT_ALLOCATION_STATUS: {
|
|
23
|
+
/** No portion of the payment has been applied to any obligation yet. */readonly UNALLOCATED: "unallocated"; /** Some of the payment is applied; remainder is available credit. */
|
|
24
|
+
readonly PARTIAL: "partial"; /** Fully applied — `sum(allocations) === payment.totalAmount`. */
|
|
25
|
+
readonly ALLOCATED: "allocated";
|
|
26
|
+
/**
|
|
27
|
+
* Sum of allocations exceeds the payment total — invariant violation
|
|
28
|
+
* that should be flagged for operator review, never silently accepted.
|
|
29
|
+
*/
|
|
30
|
+
readonly OVER_ALLOCATED: "over_allocated";
|
|
31
|
+
};
|
|
32
|
+
type PaymentAllocationStatus = (typeof PAYMENT_ALLOCATION_STATUS)[keyof typeof PAYMENT_ALLOCATION_STATUS];
|
|
33
|
+
declare const RECONCILIATION_STATUS: {
|
|
34
|
+
/** No bank statement line / settlement record has been linked yet. */readonly UNRECONCILED: "unreconciled"; /** Auto-matched to a `BankTransaction` by amount + date + reference. */
|
|
35
|
+
readonly MATCHED: "matched"; /** Operator-confirmed match (auto-match failed or was overridden). */
|
|
36
|
+
readonly MANUAL: "manual";
|
|
37
|
+
/**
|
|
38
|
+
* Match was contested — amount mismatch, duplicate claim, chargeback
|
|
39
|
+
* in flight. Halts further automated processing until resolved.
|
|
40
|
+
*/
|
|
41
|
+
readonly DISPUTED: "disputed";
|
|
42
|
+
};
|
|
43
|
+
type ReconciliationStatus = (typeof RECONCILIATION_STATUS)[keyof typeof RECONCILIATION_STATUS];
|
|
44
|
+
declare const isPaymentAllocationStatus: (value: unknown) => value is PaymentAllocationStatus;
|
|
45
|
+
declare const isReconciliationStatus: (value: unknown) => value is ReconciliationStatus;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { PAYMENT_ALLOCATION_STATUS, PaymentAllocationStatus, RECONCILIATION_STATUS, ReconciliationStatus, isPaymentAllocationStatus, isReconciliationStatus };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/money/payment-allocation-status.ts
|
|
2
|
+
/**
|
|
3
|
+
* Allocation + reconciliation status enums for payment legs.
|
|
4
|
+
*
|
|
5
|
+
* Two orthogonal dimensions sit on every PaymentAllocation row (or
|
|
6
|
+
* equivalent in any package that settles money against an invoice /
|
|
7
|
+
* order / claim):
|
|
8
|
+
*
|
|
9
|
+
* 1. **Allocation status** — has this payment been *applied* against
|
|
10
|
+
* the obligation(s) it's meant to settle? (`unallocated`,
|
|
11
|
+
* `partial`, `allocated`, `over_allocated`)
|
|
12
|
+
* 2. **Reconciliation status** — has this allocation been *matched*
|
|
13
|
+
* to a bank statement line / processor settlement record?
|
|
14
|
+
* (`unreconciled`, `matched`, `manual`, `disputed`)
|
|
15
|
+
*
|
|
16
|
+
* Both are needed because a payment can be allocated (the AR ledger
|
|
17
|
+
* thinks it's done) without being reconciled (the bank statement
|
|
18
|
+
* hasn't been imported / matched yet), and vice versa.
|
|
19
|
+
*
|
|
20
|
+
* Zero runtime, zero deps.
|
|
21
|
+
*/
|
|
22
|
+
const PAYMENT_ALLOCATION_STATUS = {
|
|
23
|
+
UNALLOCATED: "unallocated",
|
|
24
|
+
PARTIAL: "partial",
|
|
25
|
+
ALLOCATED: "allocated",
|
|
26
|
+
OVER_ALLOCATED: "over_allocated"
|
|
27
|
+
};
|
|
28
|
+
const RECONCILIATION_STATUS = {
|
|
29
|
+
UNRECONCILED: "unreconciled",
|
|
30
|
+
MATCHED: "matched",
|
|
31
|
+
MANUAL: "manual",
|
|
32
|
+
DISPUTED: "disputed"
|
|
33
|
+
};
|
|
34
|
+
const ALLOCATION_VALUES = new Set(Object.values(PAYMENT_ALLOCATION_STATUS));
|
|
35
|
+
const RECONCILIATION_VALUES = new Set(Object.values(RECONCILIATION_STATUS));
|
|
36
|
+
const isPaymentAllocationStatus = (value) => typeof value === "string" && ALLOCATION_VALUES.has(value);
|
|
37
|
+
const isReconciliationStatus = (value) => typeof value === "string" && RECONCILIATION_VALUES.has(value);
|
|
38
|
+
//#endregion
|
|
39
|
+
export { PAYMENT_ALLOCATION_STATUS, RECONCILIATION_STATUS, isPaymentAllocationStatus, isReconciliationStatus };
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { Money } from "./money.mjs";
|
|
2
|
+
import { PaymentMethodKind } from "./payment-method-kind.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/events/payment-events.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Canonical event-name constants. Packages SHOULD publish events
|
|
7
|
+
* keyed by these strings so consumers can subscribe by type without
|
|
8
|
+
* importing per-package catalogues.
|
|
9
|
+
*/
|
|
10
|
+
declare const PAYMENT_EVENT_TYPE: {
|
|
11
|
+
readonly INITIATED: "payment.initiated";
|
|
12
|
+
readonly SUCCEEDED: "payment.succeeded";
|
|
13
|
+
readonly FAILED: "payment.failed";
|
|
14
|
+
readonly REFUNDED: "payment.refunded";
|
|
15
|
+
readonly REVERSED: "payment.reversed";
|
|
16
|
+
readonly RECONCILED: "payment.reconciled";
|
|
17
|
+
readonly AUTHORIZED: "payment.authorized";
|
|
18
|
+
readonly CAPTURED: "payment.captured";
|
|
19
|
+
readonly AUTH_VOIDED: "payment.auth_voided";
|
|
20
|
+
readonly DISPUTED: "payment.disputed";
|
|
21
|
+
readonly DISPUTE_WON: "payment.dispute_won";
|
|
22
|
+
readonly DISPUTE_LOST: "payment.dispute_lost";
|
|
23
|
+
readonly SETTLED: "payment.settled";
|
|
24
|
+
};
|
|
25
|
+
type PaymentEventType = (typeof PAYMENT_EVENT_TYPE)[keyof typeof PAYMENT_EVENT_TYPE];
|
|
26
|
+
/**
|
|
27
|
+
* A payment attempt has been started against a provider — intent
|
|
28
|
+
* created, checkout session opened, manual record drafted. The
|
|
29
|
+
* obligation is NOT settled yet; consumers should treat this as
|
|
30
|
+
* "money in flight" not "money received".
|
|
31
|
+
*/
|
|
32
|
+
interface PaymentInitiatedPayload {
|
|
33
|
+
/** Stable payment identifier owned by the emitting package. */
|
|
34
|
+
paymentId: string;
|
|
35
|
+
/**
|
|
36
|
+
* Provider's reference for this attempt — Stripe payment intent id,
|
|
37
|
+
* bKash sessionId, internal sequence number for manual entries.
|
|
38
|
+
*/
|
|
39
|
+
providerRef?: string;
|
|
40
|
+
/** Provider name — `'stripe'`, `'bkash'`, `'manual'`, host-defined. */
|
|
41
|
+
providerCode: string;
|
|
42
|
+
amount: Money;
|
|
43
|
+
methodKind: PaymentMethodKind;
|
|
44
|
+
/**
|
|
45
|
+
* Host-specific method code from the host's PaymentMethod registry
|
|
46
|
+
* (e.g. `'bkash'`, `'stripe_card'`). Optional because not every host
|
|
47
|
+
* runs a registry; when absent, `methodKind` is the only classifier.
|
|
48
|
+
*/
|
|
49
|
+
methodCode?: string;
|
|
50
|
+
occurredAt: Date;
|
|
51
|
+
metadata?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A payment has cleared — funds are confirmed received by the merchant
|
|
55
|
+
* (gateway success, manual confirmation, bank credit). This is the
|
|
56
|
+
* trigger for AR settlement, invoice paymentStatus updates, and
|
|
57
|
+
* downstream fulfilment unlocks.
|
|
58
|
+
*/
|
|
59
|
+
interface PaymentSucceededPayload {
|
|
60
|
+
paymentId: string;
|
|
61
|
+
providerRef?: string;
|
|
62
|
+
providerCode: string;
|
|
63
|
+
amount: Money;
|
|
64
|
+
methodKind: PaymentMethodKind;
|
|
65
|
+
methodCode?: string;
|
|
66
|
+
occurredAt: Date;
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A payment attempt failed before clearing — card declined, wallet
|
|
71
|
+
* timeout, manual rejection. The obligation remains unsettled;
|
|
72
|
+
* consumers may retry, prompt the customer, or mark the invoice
|
|
73
|
+
* overdue.
|
|
74
|
+
*/
|
|
75
|
+
interface PaymentFailedPayload {
|
|
76
|
+
paymentId: string;
|
|
77
|
+
providerRef?: string;
|
|
78
|
+
providerCode: string;
|
|
79
|
+
amount: Money;
|
|
80
|
+
methodKind: PaymentMethodKind;
|
|
81
|
+
methodCode?: string;
|
|
82
|
+
/** Human-readable reason from the provider or operator. */
|
|
83
|
+
reason: string;
|
|
84
|
+
/** Machine code for failure classification — e.g. `'card_declined'`. */
|
|
85
|
+
reasonCode?: string;
|
|
86
|
+
occurredAt: Date;
|
|
87
|
+
metadata?: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A previously-succeeded payment has been refunded — partial or full.
|
|
91
|
+
* Distinct from {@link PaymentReversedPayload}: a refund returns money
|
|
92
|
+
* to the customer (post-settlement); a reversal undoes the settlement
|
|
93
|
+
* itself (pre- or post-clearing, often due to operator error).
|
|
94
|
+
*/
|
|
95
|
+
interface PaymentRefundedPayload {
|
|
96
|
+
paymentId: string;
|
|
97
|
+
/** Refund identifier (separate from `paymentId`). */
|
|
98
|
+
refundId: string;
|
|
99
|
+
providerRef?: string;
|
|
100
|
+
providerCode: string;
|
|
101
|
+
/** Amount refunded in this event — may be less than the original payment. */
|
|
102
|
+
refundedAmount: Money;
|
|
103
|
+
/** Original payment total — present so consumers can compute remaining refundable without a Payment lookup. */
|
|
104
|
+
originalAmount: Money;
|
|
105
|
+
/** `true` if this refund returns less than the original payment total. */
|
|
106
|
+
isPartial: boolean;
|
|
107
|
+
reason?: string;
|
|
108
|
+
occurredAt: Date;
|
|
109
|
+
metadata?: Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A payment has been reversed — the settlement is being unwound,
|
|
113
|
+
* typically because of operator error, duplicate capture, or
|
|
114
|
+
* pre-clearing cancellation. Allocations referencing this payment
|
|
115
|
+
* MUST be unwound by consumers; AR balance restores to pre-payment
|
|
116
|
+
* state.
|
|
117
|
+
*/
|
|
118
|
+
interface PaymentReversedPayload {
|
|
119
|
+
paymentId: string;
|
|
120
|
+
providerRef?: string;
|
|
121
|
+
providerCode: string;
|
|
122
|
+
/** Amount being unwound — equals the original payment for full reversal, less for partial. */
|
|
123
|
+
reversedAmount: Money;
|
|
124
|
+
/** Original payment total — present so consumers can detect partial vs full without a Payment lookup. */
|
|
125
|
+
originalAmount: Money;
|
|
126
|
+
/** `true` when reversedAmount < originalAmount. */
|
|
127
|
+
isPartial: boolean;
|
|
128
|
+
reason: string;
|
|
129
|
+
occurredAt: Date;
|
|
130
|
+
metadata?: Record<string, unknown>;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* An allocation (the row that ties a payment to an obligation — invoice,
|
|
134
|
+
* order, claim) has been reconciled against a bank-statement line or
|
|
135
|
+
* processor settlement record. Triggers GL posting in country-aware
|
|
136
|
+
* ledger bridges and closes the audit loop.
|
|
137
|
+
*/
|
|
138
|
+
interface PaymentReconciledPayload {
|
|
139
|
+
/** Allocation identifier — NOT the payment id (a payment can have many). */
|
|
140
|
+
allocationId: string;
|
|
141
|
+
paymentId: string;
|
|
142
|
+
/**
|
|
143
|
+
* External reference of the matched record — typically a
|
|
144
|
+
* `BankTransaction.externalId`, processor settlement id, or
|
|
145
|
+
* operator-supplied note.
|
|
146
|
+
*/
|
|
147
|
+
externalRef: string;
|
|
148
|
+
reconciledAt: Date;
|
|
149
|
+
/** Actor id who confirmed the match (operator) or `'system'` for auto-match. */
|
|
150
|
+
reconciledBy: string;
|
|
151
|
+
metadata?: Record<string, unknown>;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Gateway has placed an authorisation hold but has NOT captured funds.
|
|
155
|
+
* Distinct from {@link PaymentSucceededPayload} — auth-only flows split
|
|
156
|
+
* "hold" from "settle"; consumers should NOT treat this as cleared funds.
|
|
157
|
+
*/
|
|
158
|
+
interface PaymentAuthorizedPayload {
|
|
159
|
+
paymentId: string;
|
|
160
|
+
providerRef?: string;
|
|
161
|
+
providerCode: string;
|
|
162
|
+
/** Amount the gateway has placed a hold for — may be captured later in full or part. */
|
|
163
|
+
authorizedAmount: Money;
|
|
164
|
+
methodKind: PaymentMethodKind;
|
|
165
|
+
methodCode?: string;
|
|
166
|
+
/** When the auth hold expires if not captured — Stripe defaults to 7 days for cards. */
|
|
167
|
+
expiresAt?: Date;
|
|
168
|
+
occurredAt: Date;
|
|
169
|
+
metadata?: Record<string, unknown>;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Gateway has captured a previously-authorised payment — funds confirmed.
|
|
173
|
+
* Pairs with {@link PaymentAuthorizedPayload} for two-phase flows.
|
|
174
|
+
*/
|
|
175
|
+
interface PaymentCapturedPayload {
|
|
176
|
+
paymentId: string;
|
|
177
|
+
providerRef?: string;
|
|
178
|
+
providerCode: string;
|
|
179
|
+
/** Amount actually captured — `<=` original auth. */
|
|
180
|
+
capturedAmount: Money;
|
|
181
|
+
/** Total authorised — present so consumers can compute remaining holdable. */
|
|
182
|
+
authorizedAmount: Money;
|
|
183
|
+
/** `true` when capturedAmount < authorizedAmount (partial capture). */
|
|
184
|
+
isPartial: boolean;
|
|
185
|
+
methodKind: PaymentMethodKind;
|
|
186
|
+
methodCode?: string;
|
|
187
|
+
occurredAt: Date;
|
|
188
|
+
metadata?: Record<string, unknown>;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Authorisation hold released — uncaptured auth voided before settlement.
|
|
192
|
+
*/
|
|
193
|
+
interface PaymentAuthVoidedPayload {
|
|
194
|
+
paymentId: string;
|
|
195
|
+
providerRef?: string;
|
|
196
|
+
providerCode: string;
|
|
197
|
+
/** Amount released back to the customer's available balance. */
|
|
198
|
+
voidedAmount: Money;
|
|
199
|
+
reason?: string;
|
|
200
|
+
occurredAt: Date;
|
|
201
|
+
metadata?: Record<string, unknown>;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Customer / issuing bank opened a dispute or chargeback against this
|
|
205
|
+
* payment. The merchant typically has a deadline to submit evidence.
|
|
206
|
+
*/
|
|
207
|
+
interface PaymentDisputedPayload {
|
|
208
|
+
paymentId: string;
|
|
209
|
+
/** Provider's dispute / chargeback id. */
|
|
210
|
+
disputeId: string;
|
|
211
|
+
providerRef?: string;
|
|
212
|
+
providerCode: string;
|
|
213
|
+
/** Amount being disputed — may be less than the original payment for partial chargebacks. */
|
|
214
|
+
disputedAmount: Money;
|
|
215
|
+
/** Provider's category — `'fraudulent'`, `'product_not_received'`, `'duplicate'`, etc. */
|
|
216
|
+
reason: string;
|
|
217
|
+
/** Provider lifecycle phase at the time of emit — `'warning_needs_response'`, `'needs_response'`, `'under_review'`. */
|
|
218
|
+
status: string;
|
|
219
|
+
/** When the host must submit evidence by, if applicable. */
|
|
220
|
+
evidenceDueBy?: Date;
|
|
221
|
+
occurredAt: Date;
|
|
222
|
+
metadata?: Record<string, unknown>;
|
|
223
|
+
}
|
|
224
|
+
/** Dispute resolved in merchant's favour — funds restored. */
|
|
225
|
+
interface PaymentDisputeWonPayload {
|
|
226
|
+
paymentId: string;
|
|
227
|
+
disputeId: string;
|
|
228
|
+
providerRef?: string;
|
|
229
|
+
providerCode: string;
|
|
230
|
+
/** Amount restored to the merchant. */
|
|
231
|
+
recoveredAmount: Money;
|
|
232
|
+
occurredAt: Date;
|
|
233
|
+
metadata?: Record<string, unknown>;
|
|
234
|
+
}
|
|
235
|
+
/** Dispute resolved against merchant — funds permanently lost plus fee. */
|
|
236
|
+
interface PaymentDisputeLostPayload {
|
|
237
|
+
paymentId: string;
|
|
238
|
+
disputeId: string;
|
|
239
|
+
providerRef?: string;
|
|
240
|
+
providerCode: string;
|
|
241
|
+
/** Amount permanently debited from the merchant. */
|
|
242
|
+
lostAmount: Money;
|
|
243
|
+
/** Dispute / chargeback fee the merchant pays in addition. */
|
|
244
|
+
feeAmount?: Money;
|
|
245
|
+
occurredAt: Date;
|
|
246
|
+
metadata?: Record<string, unknown>;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Funds have been settled to the merchant bank account by the gateway —
|
|
250
|
+
* the gross-vs-net delta is the processor fee. Use for bank-statement
|
|
251
|
+
* reconciliation in country-aware ledger bridges.
|
|
252
|
+
*/
|
|
253
|
+
interface PaymentSettledPayload {
|
|
254
|
+
paymentId: string;
|
|
255
|
+
providerRef?: string;
|
|
256
|
+
providerCode: string;
|
|
257
|
+
/** Net amount transferred to the merchant bank account (gross - fees). */
|
|
258
|
+
settledAmount: Money;
|
|
259
|
+
/** Provider fee deducted before settlement. */
|
|
260
|
+
feeAmount?: Money;
|
|
261
|
+
/** Provider's payout / settlement batch identifier — useful for bank-statement matching. */
|
|
262
|
+
payoutId?: string;
|
|
263
|
+
/** When the funds become available in the merchant account. */
|
|
264
|
+
expectedArrivalAt?: Date;
|
|
265
|
+
occurredAt: Date;
|
|
266
|
+
metadata?: Record<string, unknown>;
|
|
267
|
+
}
|
|
268
|
+
/** Union of all payment event payload types — convenient for exhaustive switches. */
|
|
269
|
+
type PaymentEventPayload = PaymentInitiatedPayload | PaymentSucceededPayload | PaymentFailedPayload | PaymentRefundedPayload | PaymentReversedPayload | PaymentReconciledPayload | PaymentAuthorizedPayload | PaymentCapturedPayload | PaymentAuthVoidedPayload | PaymentDisputedPayload | PaymentDisputeWonPayload | PaymentDisputeLostPayload | PaymentSettledPayload;
|
|
270
|
+
//#endregion
|
|
271
|
+
export { PAYMENT_EVENT_TYPE, PaymentAuthVoidedPayload, PaymentAuthorizedPayload, PaymentCapturedPayload, PaymentDisputeLostPayload, PaymentDisputeWonPayload, PaymentDisputedPayload, PaymentEventPayload, PaymentEventType, PaymentFailedPayload, PaymentInitiatedPayload, PaymentReconciledPayload, PaymentRefundedPayload, PaymentReversedPayload, PaymentSettledPayload, PaymentSucceededPayload };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/events/payment-events.ts
|
|
2
|
+
/**
|
|
3
|
+
* Canonical event-name constants. Packages SHOULD publish events
|
|
4
|
+
* keyed by these strings so consumers can subscribe by type without
|
|
5
|
+
* importing per-package catalogues.
|
|
6
|
+
*/
|
|
7
|
+
const PAYMENT_EVENT_TYPE = {
|
|
8
|
+
INITIATED: "payment.initiated",
|
|
9
|
+
SUCCEEDED: "payment.succeeded",
|
|
10
|
+
FAILED: "payment.failed",
|
|
11
|
+
REFUNDED: "payment.refunded",
|
|
12
|
+
REVERSED: "payment.reversed",
|
|
13
|
+
RECONCILED: "payment.reconciled",
|
|
14
|
+
AUTHORIZED: "payment.authorized",
|
|
15
|
+
CAPTURED: "payment.captured",
|
|
16
|
+
AUTH_VOIDED: "payment.auth_voided",
|
|
17
|
+
DISPUTED: "payment.disputed",
|
|
18
|
+
DISPUTE_WON: "payment.dispute_won",
|
|
19
|
+
DISPUTE_LOST: "payment.dispute_lost",
|
|
20
|
+
SETTLED: "payment.settled"
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { PAYMENT_EVENT_TYPE };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Money } from "./money.mjs";
|
|
2
|
+
import { PaymentMethodKind } from "./payment-method-kind.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/money/payment-gateway.d.ts
|
|
4
5
|
/**
|
|
@@ -43,6 +44,14 @@ interface CreateIntentParams {
|
|
|
43
44
|
*/
|
|
44
45
|
[key: string]: unknown;
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Closed set of intent lifecycle states the engine recognises. Modelled on
|
|
49
|
+
* Stripe's PaymentIntent status because every other gateway maps cleanly
|
|
50
|
+
* INTO this set; gateways that lack a state simply never emit it.
|
|
51
|
+
*/
|
|
52
|
+
type PaymentIntentStatus = 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'processing' | 'requires_capture' | 'canceled' | 'succeeded';
|
|
53
|
+
/** Closed set of refund lifecycle states the engine recognises. */
|
|
54
|
+
type RefundResultStatus = 'pending' | 'requires_action' | 'succeeded' | 'failed' | 'canceled';
|
|
46
55
|
/**
|
|
47
56
|
* What `provider.createIntent(...)` returns. The engine persists
|
|
48
57
|
* `id` / `paymentIntentId` / `sessionId` on the Transaction's
|
|
@@ -55,12 +64,10 @@ interface PaymentIntent {
|
|
|
55
64
|
/** Lowercase gateway name (`'stripe'`, `'razorpay'`, `'sslcommerz'`, `'bkash'`, `'manual'`). */
|
|
56
65
|
provider: string;
|
|
57
66
|
/**
|
|
58
|
-
* Lifecycle status
|
|
59
|
-
*
|
|
60
|
-
* `'pending'`, `'processing'`, `'requires_action'`, `'succeeded'`,
|
|
61
|
-
* `'failed'`, `'cancelled'`.
|
|
67
|
+
* Lifecycle status — closed union the engine state-machines branch on.
|
|
68
|
+
* Adapters MUST map provider-native strings into this set.
|
|
62
69
|
*/
|
|
63
|
-
status:
|
|
70
|
+
status: PaymentIntentStatus;
|
|
64
71
|
/** Amount + currency that the intent locks in. */
|
|
65
72
|
amount: Money;
|
|
66
73
|
/**
|
|
@@ -80,6 +87,8 @@ interface PaymentIntent {
|
|
|
80
87
|
paymentUrl?: string;
|
|
81
88
|
/** Manual-flow instructions ("transfer to A/c 1234, reference X"). */
|
|
82
89
|
instructions?: string;
|
|
90
|
+
/** Canonical method kind if known at intent creation (customer pre-selected). */
|
|
91
|
+
methodKind?: PaymentMethodKind;
|
|
83
92
|
/** Pass-through metadata echoed from the create call. */
|
|
84
93
|
metadata?: Record<string, unknown>;
|
|
85
94
|
/** Raw provider response — kept for audit. Consumers MUST NOT rely on shape. */
|
|
@@ -108,6 +117,8 @@ interface PaymentResult {
|
|
|
108
117
|
amount?: Money;
|
|
109
118
|
/** When the gateway booked the capture. */
|
|
110
119
|
paidAt?: Date;
|
|
120
|
+
/** Canonical method kind chosen by the customer (typically known post-settlement). */
|
|
121
|
+
methodKind?: PaymentMethodKind;
|
|
111
122
|
/** Pass-through metadata (gateway customer id, fee breakdown, etc.). */
|
|
112
123
|
metadata?: Record<string, unknown>;
|
|
113
124
|
/** Raw response — audit only. */
|
|
@@ -122,7 +133,7 @@ interface RefundResult {
|
|
|
122
133
|
id: string;
|
|
123
134
|
/** Lowercase gateway name. */
|
|
124
135
|
provider: string;
|
|
125
|
-
status:
|
|
136
|
+
status: RefundResultStatus;
|
|
126
137
|
/** Amount reversed, as the gateway reports it. May be partial. */
|
|
127
138
|
amount?: Money;
|
|
128
139
|
/** When the gateway booked the refund. */
|
|
@@ -161,8 +172,18 @@ interface WebhookEvent {
|
|
|
161
172
|
};
|
|
162
173
|
/** When the gateway emitted the event. */
|
|
163
174
|
createdAt?: Date;
|
|
175
|
+
/** Canonical method kind derived from the event payload, when present. */
|
|
176
|
+
methodKind?: PaymentMethodKind;
|
|
177
|
+
/** Provider signature header value — kept for delayed verification / audit replay. */
|
|
178
|
+
signature?: string;
|
|
179
|
+
/** Signature timestamp the provider asserted — for replay-window enforcement. */
|
|
180
|
+
signatureTimestamp?: Date;
|
|
181
|
+
/** Connected-account id for multi-tenant routing — Stripe Connect uses `event.account`. */
|
|
182
|
+
accountId?: string;
|
|
183
|
+
/** Test vs production mode — Stripe's `livemode`. False / undefined = test. */
|
|
184
|
+
livemode?: boolean;
|
|
164
185
|
/** Raw provider payload — audit only. */
|
|
165
186
|
raw?: unknown;
|
|
166
187
|
}
|
|
167
188
|
//#endregion
|
|
168
|
-
export { CreateIntentParams, PaymentIntent, PaymentResult, ProviderCapabilities, RefundResult, WebhookEvent };
|
|
189
|
+
export { CreateIntentParams, PaymentIntent, PaymentIntentStatus, PaymentResult, ProviderCapabilities, RefundResult, RefundResultStatus, WebhookEvent };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/money/payment-method-kind.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Canonical payment method kinds — the universal vocabulary every
|
|
4
|
+
* payment-aware package speaks (`@classytic/invoice`, `@classytic/revenue`,
|
|
5
|
+
* `@classytic/order`, future POS / AP / subscription packages, …).
|
|
6
|
+
*
|
|
7
|
+
* **Kind vs code.** A `PaymentMethodKind` is the universal category
|
|
8
|
+
* (every payment system on earth has these). A `methodCode` is the
|
|
9
|
+
* host-specific string registered in a host's `PaymentMethod` registry
|
|
10
|
+
* (`'bkash'`, `'nagad'`, `'stripe_card'`, `'cash_register_3'`). Packages
|
|
11
|
+
* speak `kind`; hosts speak `code`. The pair lets UIs branch on kind
|
|
12
|
+
* (show wallet form vs card form vs cash form) while the host owns the
|
|
13
|
+
* concrete method catalogue.
|
|
14
|
+
*
|
|
15
|
+
* The enum is curated for distinct lifecycle / accounting / reporting
|
|
16
|
+
* characteristics, not provider variants. Provider-specific identification
|
|
17
|
+
* belongs in a host `methodCode` (e.g. `'stripe_card'`, `'bkash'`,
|
|
18
|
+
* `'klarna'`) and per-payment metadata. Consumers SHOULD handle unknown
|
|
19
|
+
* values defensively (fall through to `'other'` semantics) since the
|
|
20
|
+
* union may grow in future minor versions.
|
|
21
|
+
*
|
|
22
|
+
* Zero runtime, zero deps. Mirror this exactly anywhere a payment leg's
|
|
23
|
+
* "what kind of payment was this" needs to be typed.
|
|
24
|
+
*/
|
|
25
|
+
declare const PAYMENT_METHOD_KIND: {
|
|
26
|
+
/** Credit / debit card — Visa, Mastercard, Amex, RuPay, UnionPay, … */readonly CARD: "card"; /** Standard bank transfer — ACH credit, SEPA credit, IMPS, NEFT, RTGS, wire. */
|
|
27
|
+
readonly BANK_TRANSFER: "bank_transfer"; /** Real-time bank rails — UPI, Pix, FedNow, PromptPay, FPX, GrabPay rails. */
|
|
28
|
+
readonly INSTANT_BANK_TRANSFER: "instant_bank_transfer"; /** Mandate-based bank pull — SEPA Direct Debit, ACH Debit, BACS Direct Debit, AU BECS. */
|
|
29
|
+
readonly DIRECT_DEBIT: "direct_debit"; /** Digital wallet — Apple Pay, Google Pay, Link, PayPal, Cashapp, Alipay, WeChat Pay, Revolut Pay. */
|
|
30
|
+
readonly WALLET: "wallet"; /** Cash-in/out wallet infrastructure in unbanked markets — bKash, Nagad, M-Pesa, MoMo, GCash. */
|
|
31
|
+
readonly MOBILE_MONEY: "mobile_money"; /** Buy now pay later — Klarna, Afterpay, Affirm, Tabby, Tamara. */
|
|
32
|
+
readonly BNPL: "bnpl"; /** Customer-purchased prepaid credit / voucher. */
|
|
33
|
+
readonly GIFT_CARD: "gift_card"; /** Physical cash. */
|
|
34
|
+
readonly CASH: "cash"; /** Paper cheque. */
|
|
35
|
+
readonly CHEQUE: "cheque"; /** Cryptocurrency settlement. */
|
|
36
|
+
readonly CRYPTO: "cryptocurrency";
|
|
37
|
+
/**
|
|
38
|
+
* Operator-recorded settlement that doesn't fit any other kind — e.g.
|
|
39
|
+
* an offline bank deposit confirmed by phone, an in-kind exchange, a
|
|
40
|
+
* manual write-off. Always-valid escape hatch.
|
|
41
|
+
*/
|
|
42
|
+
readonly MANUAL: "manual"; /** Unknown or vendor-specific kind — last resort. Prefer a specific kind. */
|
|
43
|
+
readonly OTHER: "other";
|
|
44
|
+
};
|
|
45
|
+
type PaymentMethodKind = (typeof PAYMENT_METHOD_KIND)[keyof typeof PAYMENT_METHOD_KIND];
|
|
46
|
+
declare const isPaymentMethodKind: (value: unknown) => value is PaymentMethodKind;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { PAYMENT_METHOD_KIND, PaymentMethodKind, isPaymentMethodKind };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/money/payment-method-kind.ts
|
|
2
|
+
/**
|
|
3
|
+
* Canonical payment method kinds — the universal vocabulary every
|
|
4
|
+
* payment-aware package speaks (`@classytic/invoice`, `@classytic/revenue`,
|
|
5
|
+
* `@classytic/order`, future POS / AP / subscription packages, …).
|
|
6
|
+
*
|
|
7
|
+
* **Kind vs code.** A `PaymentMethodKind` is the universal category
|
|
8
|
+
* (every payment system on earth has these). A `methodCode` is the
|
|
9
|
+
* host-specific string registered in a host's `PaymentMethod` registry
|
|
10
|
+
* (`'bkash'`, `'nagad'`, `'stripe_card'`, `'cash_register_3'`). Packages
|
|
11
|
+
* speak `kind`; hosts speak `code`. The pair lets UIs branch on kind
|
|
12
|
+
* (show wallet form vs card form vs cash form) while the host owns the
|
|
13
|
+
* concrete method catalogue.
|
|
14
|
+
*
|
|
15
|
+
* The enum is curated for distinct lifecycle / accounting / reporting
|
|
16
|
+
* characteristics, not provider variants. Provider-specific identification
|
|
17
|
+
* belongs in a host `methodCode` (e.g. `'stripe_card'`, `'bkash'`,
|
|
18
|
+
* `'klarna'`) and per-payment metadata. Consumers SHOULD handle unknown
|
|
19
|
+
* values defensively (fall through to `'other'` semantics) since the
|
|
20
|
+
* union may grow in future minor versions.
|
|
21
|
+
*
|
|
22
|
+
* Zero runtime, zero deps. Mirror this exactly anywhere a payment leg's
|
|
23
|
+
* "what kind of payment was this" needs to be typed.
|
|
24
|
+
*/
|
|
25
|
+
const PAYMENT_METHOD_KIND = {
|
|
26
|
+
CARD: "card",
|
|
27
|
+
BANK_TRANSFER: "bank_transfer",
|
|
28
|
+
INSTANT_BANK_TRANSFER: "instant_bank_transfer",
|
|
29
|
+
DIRECT_DEBIT: "direct_debit",
|
|
30
|
+
WALLET: "wallet",
|
|
31
|
+
MOBILE_MONEY: "mobile_money",
|
|
32
|
+
BNPL: "bnpl",
|
|
33
|
+
GIFT_CARD: "gift_card",
|
|
34
|
+
CASH: "cash",
|
|
35
|
+
CHEQUE: "cheque",
|
|
36
|
+
CRYPTO: "cryptocurrency",
|
|
37
|
+
MANUAL: "manual",
|
|
38
|
+
OTHER: "other"
|
|
39
|
+
};
|
|
40
|
+
const KIND_VALUES = new Set(Object.values(PAYMENT_METHOD_KIND));
|
|
41
|
+
const isPaymentMethodKind = (value) => typeof value === "string" && KIND_VALUES.has(value);
|
|
42
|
+
//#endregion
|
|
43
|
+
export { PAYMENT_METHOD_KIND, isPaymentMethodKind };
|
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
* Pairs naturally with `state-machine`: the machine decides if a transition
|
|
12
12
|
* is legal; this helper records the move that happened.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* from raw timestamps later.
|
|
14
|
+
* Recording `durationInPriorMs` at write time means funnel-velocity reports
|
|
15
|
+
* (avg time spent in "Qualification") fall out of a single field instead of
|
|
16
|
+
* a re-walk over raw timestamps later.
|
|
18
17
|
*
|
|
19
18
|
* @example
|
|
20
19
|
* import { appendStatus, type StatusHistory } from '@classytic/primitives/status-history';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
|
|
5
5
|
"author": "Classytic",
|
|
6
6
|
"license": "MIT",
|
|
@@ -97,6 +97,18 @@
|
|
|
97
97
|
"types": "./dist/payment-gateway.d.mts",
|
|
98
98
|
"default": "./dist/payment-gateway.mjs"
|
|
99
99
|
},
|
|
100
|
+
"./payment-method-kind": {
|
|
101
|
+
"types": "./dist/payment-method-kind.d.mts",
|
|
102
|
+
"default": "./dist/payment-method-kind.mjs"
|
|
103
|
+
},
|
|
104
|
+
"./payment-allocation-status": {
|
|
105
|
+
"types": "./dist/payment-allocation-status.d.mts",
|
|
106
|
+
"default": "./dist/payment-allocation-status.mjs"
|
|
107
|
+
},
|
|
108
|
+
"./payment-events": {
|
|
109
|
+
"types": "./dist/payment-events.d.mts",
|
|
110
|
+
"default": "./dist/payment-events.mjs"
|
|
111
|
+
},
|
|
100
112
|
"./phone": {
|
|
101
113
|
"types": "./dist/phone.d.mts",
|
|
102
114
|
"default": "./dist/phone.mjs"
|