@classytic/primitives 0.6.0 → 0.7.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 CHANGED
@@ -3,13 +3,33 @@
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.0] - 2026-05-26
7
+
8
+ ### Added — payment domain primitives
9
+
10
+ Three new subpaths shared across every payment-aware package
11
+ (`@classytic/invoice`, `@classytic/revenue`, `@classytic/order`, future
12
+ POS / AP / subscription packages). Each ships under a flat subpath so
13
+ consumers tree-shake what they don't import.
14
+
15
+ - **`/payment-method-kind`** — `PaymentMethodKind` universal vocabulary
16
+ (kind vs host-registered `methodCode`) so packages can speak `kind`
17
+ while hosts speak `code`.
18
+ - **`/payment-allocation-status`** — orthogonal allocation +
19
+ reconciliation status enums for payment legs (`unallocated`,
20
+ `partial`, `allocated`, `over_allocated`, plus reconciliation states).
21
+ - **`/payment-events`** — pure TS payload contracts for payment domain
22
+ events. Wire-only (no Zod, no runtime). Anchored on `Money` +
23
+ `PaymentMethodKind`; causation/correlation stay on `EventMeta`.
24
+
25
+ No breaking changes.
26
+
6
27
  ## [0.6.0] - 2026-05-17
7
28
 
8
29
  ### Added — five new primitives for CRM / workflow domains
9
30
 
10
- These were extracted after deep-diving Twenty, Huly Platform, Frappe CRM,
11
- and NocoBase. Each fills a gap the existing primitives didn't cover, and
12
- each ships under its own flat subpath import (no breaking changes).
31
+ Five new primitives covering gaps the existing surface didn't address.
32
+ Each ships under its own flat subpath import (no breaking changes).
13
33
 
14
34
  - **`/phone`** — `PhoneNumber` value object + `parsePhone(input)` that
15
35
  normalizes free-form input to E.164, captures the country calling code
@@ -20,10 +40,9 @@ each ships under its own flat subpath import (no breaking changes).
20
40
 
21
41
  - **`/status-history`** — Generic, immutable status-change log:
22
42
  `StatusChangeEntry<TStatus>`, `appendStatus()`, `timeInStatus()`,
23
- `lastTransitionTo()`. Captures `durationInPriorMs` at write time
24
- (Frappe's "status change log" pattern), so funnel-velocity dashboards
25
- ("avg time in Qualification") work without re-walking timestamps.
26
- Pairs naturally with `/state-machine`.
43
+ `lastTransitionTo()`. Captures `durationInPriorMs` at write time so
44
+ funnel-velocity dashboards ("avg time in Qualification") work without
45
+ re-walking timestamps. Pairs naturally with `/state-machine`.
27
46
 
28
47
  - **`/condition`** — Tiny JSON-serializable predicate DSL: `FieldCondition`
29
48
  (`{ field, op, value }`) composed via `AllCondition` / `AnyCondition`
@@ -37,8 +56,8 @@ each ships under its own flat subpath import (no breaking changes).
37
56
  `withMixin<T, K, M>()` / `getMixin()` / `hasMixin()` / `withoutMixin()`.
38
57
  Lets a `Contact` carry per-domain payloads (`mixins.customer`,
39
58
  `mixins.lead-fit`) on a typed `mixins` namespace without bloating the
40
- base type. Distilled from Huly's mixin idea minus the decorators,
41
- global hierarchy, and proxies.
59
+ base type. Plain object composition — no decorators, no global
60
+ hierarchy, no proxies.
42
61
 
43
62
  - **`/sla-policy`** — Higher-order SLA layered on the existing `/sla`:
44
63
  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,141 @@
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
+ };
18
+ type PaymentEventType = (typeof PAYMENT_EVENT_TYPE)[keyof typeof PAYMENT_EVENT_TYPE];
19
+ /**
20
+ * A payment attempt has been started against a provider — intent
21
+ * created, checkout session opened, manual record drafted. The
22
+ * obligation is NOT settled yet; consumers should treat this as
23
+ * "money in flight" not "money received".
24
+ */
25
+ interface PaymentInitiatedPayload {
26
+ /** Stable payment identifier owned by the emitting package. */
27
+ paymentId: string;
28
+ /**
29
+ * Provider's reference for this attempt — Stripe payment intent id,
30
+ * bKash sessionId, internal sequence number for manual entries.
31
+ */
32
+ providerRef?: string;
33
+ /** Provider name — `'stripe'`, `'bkash'`, `'manual'`, host-defined. */
34
+ providerCode: string;
35
+ amount: Money;
36
+ methodKind: PaymentMethodKind;
37
+ /**
38
+ * Host-specific method code from the host's PaymentMethod registry
39
+ * (e.g. `'bkash'`, `'stripe_card'`). Optional because not every host
40
+ * runs a registry; when absent, `methodKind` is the only classifier.
41
+ */
42
+ methodCode?: string;
43
+ occurredAt: Date;
44
+ metadata?: Record<string, unknown>;
45
+ }
46
+ /**
47
+ * A payment has cleared — funds are confirmed received by the merchant
48
+ * (gateway success, manual confirmation, bank credit). This is the
49
+ * trigger for AR settlement, invoice paymentStatus updates, and
50
+ * downstream fulfilment unlocks.
51
+ */
52
+ interface PaymentSucceededPayload {
53
+ paymentId: string;
54
+ providerRef?: string;
55
+ providerCode: string;
56
+ amount: Money;
57
+ methodKind: PaymentMethodKind;
58
+ methodCode?: string;
59
+ occurredAt: Date;
60
+ metadata?: Record<string, unknown>;
61
+ }
62
+ /**
63
+ * A payment attempt failed before clearing — card declined, wallet
64
+ * timeout, manual rejection. The obligation remains unsettled;
65
+ * consumers may retry, prompt the customer, or mark the invoice
66
+ * overdue.
67
+ */
68
+ interface PaymentFailedPayload {
69
+ paymentId: string;
70
+ providerRef?: string;
71
+ providerCode: string;
72
+ amount: Money;
73
+ methodKind: PaymentMethodKind;
74
+ methodCode?: string;
75
+ /** Human-readable reason from the provider or operator. */
76
+ reason: string;
77
+ /** Machine code for failure classification — e.g. `'card_declined'`. */
78
+ reasonCode?: string;
79
+ occurredAt: Date;
80
+ metadata?: Record<string, unknown>;
81
+ }
82
+ /**
83
+ * A previously-succeeded payment has been refunded — partial or full.
84
+ * Distinct from {@link PaymentReversedPayload}: a refund returns money
85
+ * to the customer (post-settlement); a reversal undoes the settlement
86
+ * itself (pre- or post-clearing, often due to operator error).
87
+ */
88
+ interface PaymentRefundedPayload {
89
+ paymentId: string;
90
+ /** Refund identifier (separate from `paymentId`). */
91
+ refundId: string;
92
+ providerRef?: string;
93
+ providerCode: string;
94
+ /** Amount refunded in this event — may be less than the original payment. */
95
+ refundedAmount: Money;
96
+ /** `true` if this refund returns less than the original payment total. */
97
+ isPartial: boolean;
98
+ reason?: string;
99
+ occurredAt: Date;
100
+ metadata?: Record<string, unknown>;
101
+ }
102
+ /**
103
+ * A payment has been reversed — the settlement is being unwound,
104
+ * typically because of operator error, duplicate capture, or
105
+ * pre-clearing cancellation. Allocations referencing this payment
106
+ * MUST be unwound by consumers; AR balance restores to pre-payment
107
+ * state.
108
+ */
109
+ interface PaymentReversedPayload {
110
+ paymentId: string;
111
+ providerRef?: string;
112
+ providerCode: string;
113
+ reason: string;
114
+ occurredAt: Date;
115
+ metadata?: Record<string, unknown>;
116
+ }
117
+ /**
118
+ * An allocation (the row that ties a payment to an obligation — invoice,
119
+ * order, claim) has been reconciled against a bank-statement line or
120
+ * processor settlement record. Triggers GL posting in country-aware
121
+ * ledger bridges and closes the audit loop.
122
+ */
123
+ interface PaymentReconciledPayload {
124
+ /** Allocation identifier — NOT the payment id (a payment can have many). */
125
+ allocationId: string;
126
+ paymentId: string;
127
+ /**
128
+ * External reference of the matched record — typically a
129
+ * `BankTransaction.externalId`, processor settlement id, or
130
+ * operator-supplied note.
131
+ */
132
+ externalRef: string;
133
+ reconciledAt: Date;
134
+ /** Actor id who confirmed the match (operator) or `'system'` for auto-match. */
135
+ reconciledBy: string;
136
+ metadata?: Record<string, unknown>;
137
+ }
138
+ /** Union of all payment event payload types — convenient for exhaustive switches. */
139
+ type PaymentEventPayload = PaymentInitiatedPayload | PaymentSucceededPayload | PaymentFailedPayload | PaymentRefundedPayload | PaymentReversedPayload | PaymentReconciledPayload;
140
+ //#endregion
141
+ export { PAYMENT_EVENT_TYPE, PaymentEventPayload, PaymentEventType, PaymentFailedPayload, PaymentInitiatedPayload, PaymentReconciledPayload, PaymentRefundedPayload, PaymentReversedPayload, PaymentSucceededPayload };
@@ -0,0 +1,16 @@
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
+ };
15
+ //#endregion
16
+ export { PAYMENT_EVENT_TYPE };
@@ -0,0 +1,36 @@
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
+ * Zero runtime, zero deps. Mirror this exactly anywhere a payment leg's
16
+ * "what kind of payment was this" needs to be typed.
17
+ */
18
+ 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. */
22
+ readonly CASH: "cash"; /** Paper cheque. */
23
+ readonly CHEQUE: "cheque"; /** Cryptocurrency settlement. */
24
+ readonly CRYPTO: "cryptocurrency";
25
+ /**
26
+ * Operator-recorded settlement that doesn't fit any other kind — e.g.
27
+ * an offline bank deposit confirmed by phone, an in-kind exchange, a
28
+ * manual write-off. Always-valid escape hatch.
29
+ */
30
+ readonly MANUAL: "manual"; /** Unknown or vendor-specific kind — last resort. Prefer a specific kind. */
31
+ readonly OTHER: "other";
32
+ };
33
+ type PaymentMethodKind = (typeof PAYMENT_METHOD_KIND)[keyof typeof PAYMENT_METHOD_KIND];
34
+ declare const isPaymentMethodKind: (value: unknown) => value is PaymentMethodKind;
35
+ //#endregion
36
+ export { PAYMENT_METHOD_KIND, PaymentMethodKind, isPaymentMethodKind };
@@ -0,0 +1,31 @@
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
+ * Zero runtime, zero deps. Mirror this exactly anywhere a payment leg's
16
+ * "what kind of payment was this" needs to be typed.
17
+ */
18
+ const PAYMENT_METHOD_KIND = {
19
+ CARD: "card",
20
+ BANK_TRANSFER: "bank_transfer",
21
+ WALLET: "wallet",
22
+ CASH: "cash",
23
+ CHEQUE: "cheque",
24
+ CRYPTO: "cryptocurrency",
25
+ MANUAL: "manual",
26
+ OTHER: "other"
27
+ };
28
+ const KIND_VALUES = new Set(Object.values(PAYMENT_METHOD_KIND));
29
+ const isPaymentMethodKind = (value) => typeof value === "string" && KIND_VALUES.has(value);
30
+ //#endregion
31
+ 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
- * Frappe's "status change log" with `duration_in_prior_status` was the
15
- * inspiration — funnel velocity reports (avg time spent in "Qualification")
16
- * are free when you store the duration at write time instead of recomputing
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.6.0",
3
+ "version": "0.7.0",
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"