@classytic/primitives 0.7.0 → 0.7.2

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 CHANGED
@@ -3,6 +3,56 @@
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.2] - 2026-05-30
7
+
8
+ ### Added — `BankTransaction` per-account routing + pending lifecycle fields
9
+
10
+ Three additive optional fields on `BankTransaction` (`/bank-transaction`):
11
+
12
+ - **`sourceAccountId?: string`** — the vendor-side account this row belongs to
13
+ (Plaid `account_id`, Xero `BankAccount.AccountID`). Sync feeds return
14
+ transactions spanning multiple accounts in one batch; this lets consumers
15
+ route each row to the right account per-transaction (multi-account sync)
16
+ instead of forcing the whole batch onto one account. Statement formats
17
+ (single-account files) leave it undefined — the account lives on
18
+ `BankStatement.account`.
19
+ - **`pending?: boolean`** — the entry hasn't posted/cleared yet (real-time
20
+ feeds like Plaid `pending: true`; statement formats leave it undefined).
21
+ Consumers must treat pending rows as provisional — don't reconcile/post
22
+ them as final.
23
+ - **`supersedesExternalId?: string`** — set on a POSTED row that replaces an
24
+ earlier PENDING one, carrying the pending row's `externalId` (Plaid
25
+ `pending_transaction_id`). Pending and posted rows have different
26
+ `externalId`s, so consumers MUST use this back-pointer to drop/replace the
27
+ superseded pending row on ingest — otherwise both persist as duplicates.
28
+
29
+ Backward-compatible (both optional). Closes a fidelity gap where Plaid's
30
+ pending state was dropped at the canonical boundary, so downstream consumers
31
+ (`@classytic/fin-io` Plaid mapper, `@classytic/revenue` import) couldn't
32
+ distinguish provisional rows or dedupe pending→posted.
33
+
34
+ ## [0.7.1] - 2026-05-26
35
+
36
+ ### Added — workflow + scheduling + composition primitives
37
+
38
+ Four new flat subpaths (additive):
39
+
40
+ - **`/status-history`** — append-only status transition log primitive.
41
+ - **`/condition`** — declarative condition expressions for workflow gates.
42
+ - **`/mixin`** — composition helpers for primitive object assembly.
43
+ - **`/sla-policy`** — SLA policy definitions paired with the existing `/sla` runtime.
44
+
45
+ ### Added — payment event coverage
46
+
47
+ Expanded `PAYMENT_EVENT_TYPE` catalogue to cover auth/capture and dispute
48
+ lifecycles: `AUTHORIZED`, `CAPTURED`, `AUTH_VOIDED`, `DISPUTED`,
49
+ `DISPUTE_WON`, `DISPUTE_LOST`, `SETTLED`.
50
+
51
+ `PaymentRefundedPayload` and `PaymentReversedPayload` now carry
52
+ `originalAmount` (and `reversedAmount` / `isPartial` on reversed) so
53
+ consumers can detect partial vs full operations without a follow-up
54
+ Payment lookup.
55
+
6
56
  ## [0.7.0] - 2026-05-26
7
57
 
8
58
  ### Added — payment domain primitives
@@ -96,6 +96,41 @@ interface BankTransaction {
96
96
  balanceAfter?: Money;
97
97
  /** Format-specific entry-type code (`'DEBIT'`, `'CREDIT'`, `'CHECK'`, …). */
98
98
  type?: string;
99
+ /**
100
+ * Vendor-side identifier of the account this row belongs to — Plaid
101
+ * `account_id`, Xero `BankAccount.AccountID`. Sync feeds return transactions
102
+ * spanning MULTIPLE accounts in one batch, so this lets a consumer route
103
+ * each row to the right destination account per-transaction (multi-account
104
+ * sync) instead of forcing the whole batch onto one account.
105
+ *
106
+ * Statement formats (OFX / CAMT.053 / MT940 / CSV / IIF) are single-account
107
+ * files — they carry the account on `BankStatement.account` and leave this
108
+ * `undefined`. Distinct from any consumer-side account id (e.g. the host's
109
+ * own `bankAccountId` used in the import unique index).
110
+ */
111
+ sourceAccountId?: string;
112
+ /**
113
+ * The entry has not yet posted / cleared — the bank may still change its
114
+ * amount, description, or drop it entirely. Real-time feeds populate this
115
+ * (Plaid `pending: true`); statement formats (OFX / CAMT.053 / MT940 / CSV)
116
+ * only ever carry settled rows and leave it `undefined`.
117
+ *
118
+ * Consumers MUST treat `pending: true` rows as provisional — do not
119
+ * reconcile or post them as final, and expect a superseding posted row to
120
+ * arrive later (linked via `supersedesExternalId`).
121
+ */
122
+ pending?: boolean;
123
+ /**
124
+ * Set on a POSTED row that replaces an earlier PENDING one: the
125
+ * `externalId` of the pending row this entry supersedes (Plaid
126
+ * `pending_transaction_id`).
127
+ *
128
+ * The pending and posted rows carry DIFFERENT `externalId`s, so dedup on
129
+ * `externalId` alone can't collapse them — consumers MUST use this
130
+ * back-pointer to drop / replace the superseded pending row on ingest, or
131
+ * both versions persist as duplicates.
132
+ */
133
+ supersedesExternalId?: string;
99
134
  /**
100
135
  * Format-specific raw record. Pass-through for audit / debugging.
101
136
  * Consumers MUST NOT rely on this — schema may change between fin-io
@@ -14,6 +14,13 @@ declare const PAYMENT_EVENT_TYPE: {
14
14
  readonly REFUNDED: "payment.refunded";
15
15
  readonly REVERSED: "payment.reversed";
16
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";
17
24
  };
18
25
  type PaymentEventType = (typeof PAYMENT_EVENT_TYPE)[keyof typeof PAYMENT_EVENT_TYPE];
19
26
  /**
@@ -93,6 +100,8 @@ interface PaymentRefundedPayload {
93
100
  providerCode: string;
94
101
  /** Amount refunded in this event — may be less than the original payment. */
95
102
  refundedAmount: Money;
103
+ /** Original payment total — present so consumers can compute remaining refundable without a Payment lookup. */
104
+ originalAmount: Money;
96
105
  /** `true` if this refund returns less than the original payment total. */
97
106
  isPartial: boolean;
98
107
  reason?: string;
@@ -110,6 +119,12 @@ interface PaymentReversedPayload {
110
119
  paymentId: string;
111
120
  providerRef?: string;
112
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;
113
128
  reason: string;
114
129
  occurredAt: Date;
115
130
  metadata?: Record<string, unknown>;
@@ -135,7 +150,122 @@ interface PaymentReconciledPayload {
135
150
  reconciledBy: string;
136
151
  metadata?: Record<string, unknown>;
137
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
+ }
138
268
  /** Union of all payment event payload types — convenient for exhaustive switches. */
139
- type PaymentEventPayload = PaymentInitiatedPayload | PaymentSucceededPayload | PaymentFailedPayload | PaymentRefundedPayload | PaymentReversedPayload | PaymentReconciledPayload;
269
+ type PaymentEventPayload = PaymentInitiatedPayload | PaymentSucceededPayload | PaymentFailedPayload | PaymentRefundedPayload | PaymentReversedPayload | PaymentReconciledPayload | PaymentAuthorizedPayload | PaymentCapturedPayload | PaymentAuthVoidedPayload | PaymentDisputedPayload | PaymentDisputeWonPayload | PaymentDisputeLostPayload | PaymentSettledPayload;
140
270
  //#endregion
141
- export { PAYMENT_EVENT_TYPE, PaymentEventPayload, PaymentEventType, PaymentFailedPayload, PaymentInitiatedPayload, PaymentReconciledPayload, PaymentRefundedPayload, PaymentReversedPayload, PaymentSucceededPayload };
271
+ export { PAYMENT_EVENT_TYPE, PaymentAuthVoidedPayload, PaymentAuthorizedPayload, PaymentCapturedPayload, PaymentDisputeLostPayload, PaymentDisputeWonPayload, PaymentDisputedPayload, PaymentEventPayload, PaymentEventType, PaymentFailedPayload, PaymentInitiatedPayload, PaymentReconciledPayload, PaymentRefundedPayload, PaymentReversedPayload, PaymentSettledPayload, PaymentSucceededPayload };
@@ -10,7 +10,14 @@ const PAYMENT_EVENT_TYPE = {
10
10
  FAILED: "payment.failed",
11
11
  REFUNDED: "payment.refunded",
12
12
  REVERSED: "payment.reversed",
13
- RECONCILED: "payment.reconciled"
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"
14
21
  };
15
22
  //#endregion
16
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 as the gateway reports it. Free-form string —
59
- * engine maps known values to its own state machine. Common values:
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: string;
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: 'succeeded' | 'failed' | 'processing';
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 };
@@ -12,13 +12,25 @@
12
12
  * (show wallet form vs card form vs cash form) while the host owns the
13
13
  * concrete method catalogue.
14
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
+ *
15
22
  * Zero runtime, zero deps. Mirror this exactly anywhere a payment leg's
16
23
  * "what kind of payment was this" needs to be typed.
17
24
  */
18
25
  declare const PAYMENT_METHOD_KIND: {
19
- /** Credit / debit card — Visa, Mastercard, Amex, RuPay, UnionPay, … */readonly CARD: "card"; /** Direct bank transfer — ACH, SEPA, IMPS, NEFT, RTGS, wire, … */
20
- readonly BANK_TRANSFER: "bank_transfer"; /** Digital wallet — bKash, Nagad, PayPal, Apple Pay, Google Pay, Alipay, … */
21
- readonly WALLET: "wallet"; /** Physical cash. */
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. */
22
34
  readonly CASH: "cash"; /** Paper cheque. */
23
35
  readonly CHEQUE: "cheque"; /** Cryptocurrency settlement. */
24
36
  readonly CRYPTO: "cryptocurrency";
@@ -12,13 +12,25 @@
12
12
  * (show wallet form vs card form vs cash form) while the host owns the
13
13
  * concrete method catalogue.
14
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
+ *
15
22
  * Zero runtime, zero deps. Mirror this exactly anywhere a payment leg's
16
23
  * "what kind of payment was this" needs to be typed.
17
24
  */
18
25
  const PAYMENT_METHOD_KIND = {
19
26
  CARD: "card",
20
27
  BANK_TRANSFER: "bank_transfer",
28
+ INSTANT_BANK_TRANSFER: "instant_bank_transfer",
29
+ DIRECT_DEBIT: "direct_debit",
21
30
  WALLET: "wallet",
31
+ MOBILE_MONEY: "mobile_money",
32
+ BNPL: "bnpl",
33
+ GIFT_CARD: "gift_card",
22
34
  CASH: "cash",
23
35
  CHEQUE: "cheque",
24
36
  CRYPTO: "cryptocurrency",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",