@classytic/primitives 0.5.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 +91 -0
- package/dist/address.d.mts +1 -1
- package/dist/address.mjs +1 -1
- package/dist/approval.d.mts +1 -1
- package/dist/approval.mjs +1 -1
- package/dist/bank-transaction.d.mts +1 -1
- package/dist/{brand-DApaegoR.d.mts → brand-LLXqoh5J.d.mts} +1 -1
- package/dist/brand.d.mts +1 -1
- package/dist/cadence.d.mts +1 -1
- package/dist/cadence.mjs +1 -1
- package/dist/condition.d.mts +76 -0
- package/dist/condition.mjs +131 -0
- package/dist/context.d.mts +1 -1
- package/dist/{currency-rgUAn5KM.d.mts → currency-B4rSdN7D.d.mts} +2 -2
- package/dist/currency.d.mts +1 -1
- package/dist/currency.mjs +1 -1
- package/dist/events.d.mts +1 -1
- package/dist/events.mjs +1 -1
- package/dist/hold.d.mts +1 -1
- package/dist/hold.mjs +1 -1
- package/dist/mixin.d.mts +73 -0
- package/dist/mixin.mjs +47 -0
- package/dist/money.d.mts +2 -2
- package/dist/money.mjs +1 -1
- package/dist/outbox.d.mts +1 -1
- package/dist/outbox.mjs +1 -1
- package/dist/payment-allocation-status.d.mts +47 -0
- package/dist/payment-allocation-status.mjs +39 -0
- package/dist/payment-events.d.mts +141 -0
- package/dist/payment-events.mjs +16 -0
- package/dist/payment-gateway.d.mts +1 -1
- package/dist/payment-method-kind.d.mts +36 -0
- package/dist/payment-method-kind.mjs +31 -0
- package/dist/period.d.mts +1 -1
- package/dist/period.mjs +1 -1
- package/dist/person.d.mts +1 -1
- package/dist/person.mjs +1 -1
- package/dist/phone.d.mts +47 -0
- package/dist/phone.mjs +310 -0
- package/dist/reference.d.mts +1 -1
- package/dist/reference.mjs +1 -1
- package/dist/result.d.mts +1 -1
- package/dist/result.mjs +1 -1
- package/dist/sla-policy.d.mts +116 -0
- package/dist/sla-policy.mjs +120 -0
- package/dist/sla.d.mts +1 -1
- package/dist/sla.mjs +1 -1
- package/dist/split-allocation.d.mts +1 -1
- package/dist/split-allocation.mjs +1 -1
- package/dist/state-machine.d.mts +1 -1
- package/dist/state-machine.mjs +1 -1
- package/dist/status-history.d.mts +85 -0
- package/dist/status-history.mjs +48 -0
- package/package.json +33 -1
|
@@ -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 };
|
package/dist/period.d.mts
CHANGED
package/dist/period.mjs
CHANGED
package/dist/person.d.mts
CHANGED
package/dist/person.mjs
CHANGED
package/dist/phone.d.mts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Result } from "./result.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/identity/phone.d.ts
|
|
4
|
+
type PhoneErrorCode = 'EMPTY' | 'MISSING_PLUS' | 'NOT_E164' | 'TOO_SHORT' | 'TOO_LONG';
|
|
5
|
+
declare class PhoneError extends Error {
|
|
6
|
+
readonly name = "PhoneError";
|
|
7
|
+
readonly code: PhoneErrorCode;
|
|
8
|
+
constructor(code: PhoneErrorCode, message: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parsed phone number. Stored E.164 (always begins with `+`, digits only after).
|
|
12
|
+
* `callingCode` is the 1–3 digit country dial code (`'1'` US, `'44'` UK,
|
|
13
|
+
* `'880'` BD, etc.) — useful for routing without re-parsing.
|
|
14
|
+
*
|
|
15
|
+
* Construction goes through `parsePhone()` (never assemble by hand) so the
|
|
16
|
+
* invariants are guaranteed by the parser.
|
|
17
|
+
*/
|
|
18
|
+
interface PhoneNumber {
|
|
19
|
+
readonly e164: string;
|
|
20
|
+
readonly callingCode: string;
|
|
21
|
+
/** The national-number portion (digits after the calling code). */
|
|
22
|
+
readonly nationalNumber: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse free-form phone input into a normalized `PhoneNumber`. Accepts:
|
|
26
|
+
* - `'+14155550182'` → e164 unchanged
|
|
27
|
+
* - `'+1 (415) 555-0182'` → strips spaces / punctuation
|
|
28
|
+
* - `'+1.415.555.0182'` → strips dots
|
|
29
|
+
*
|
|
30
|
+
* Rejects anything without a leading `+` — we don't guess the country.
|
|
31
|
+
* If the host knows the user's region, prepend the code before calling.
|
|
32
|
+
*/
|
|
33
|
+
declare function parsePhone(input: string): Result<PhoneNumber, PhoneError>;
|
|
34
|
+
/** Convenience: returns the E.164 string. Useful when piping to a transport. */
|
|
35
|
+
declare function formatPhone(phone: PhoneNumber): string;
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort national-format render: groups the national number into 3-digit
|
|
38
|
+
* blocks. Not locale-aware — for display polish, hosts should format per
|
|
39
|
+
* region. Useful as a default in admin UIs.
|
|
40
|
+
*
|
|
41
|
+
* @example formatNational(parsePhone('+14155550182').value!) → '+1 415 555 0182'
|
|
42
|
+
*/
|
|
43
|
+
declare function formatNational(phone: PhoneNumber): string;
|
|
44
|
+
/** Type guard. */
|
|
45
|
+
declare function isPhoneNumber(value: unknown): value is PhoneNumber;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { PhoneError, PhoneErrorCode, PhoneNumber, formatNational, formatPhone, isPhoneNumber, parsePhone };
|
package/dist/phone.mjs
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { err, ok } from "./result.mjs";
|
|
2
|
+
//#region src/identity/phone.ts
|
|
3
|
+
/**
|
|
4
|
+
* Phone number value object.
|
|
5
|
+
*
|
|
6
|
+
* `PersonName` and `Address` are typed; phones have been a plain `string` in
|
|
7
|
+
* `ContactInfo` so far. This primitive closes that gap with a tiny,
|
|
8
|
+
* zero-dependency wrapper that:
|
|
9
|
+
* 1. Normalizes E.164 input (strips formatting, keeps the leading `+`).
|
|
10
|
+
* 2. Captures the country calling code as a discrete field for routing /
|
|
11
|
+
* regional analytics.
|
|
12
|
+
* 3. Surfaces parse failures via `Result<PhoneNumber, PhoneError>` so
|
|
13
|
+
* callers don't deal with thrown errors at value-object construction.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT a full libphonenumber dep — that ships ~500 KB of metadata
|
|
16
|
+
* and is per-locale. We accept E.164 input and validate format; deeper
|
|
17
|
+
* validation (region-of-issue, mobile-vs-landline) is the host's call to
|
|
18
|
+
* layer on top. This keeps `@classytic/primitives` zero-dep and tree-shakeable.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* import { parsePhone, formatPhone, type PhoneNumber } from '@classytic/primitives/phone';
|
|
22
|
+
*
|
|
23
|
+
* const parsed = parsePhone('+1 (415) 555-0182');
|
|
24
|
+
* if (parsed.ok) {
|
|
25
|
+
* formatPhone(parsed.value); // '+14155550182'
|
|
26
|
+
* parsed.value.callingCode; // '1'
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
29
|
+
var PhoneError = class extends Error {
|
|
30
|
+
name = "PhoneError";
|
|
31
|
+
code;
|
|
32
|
+
constructor(code, message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* E.164 country calling codes by length. We accept 1–3 digits and let the
|
|
39
|
+
* longest-prefix-match decide. Source: ITU-T E.164 assignments (snapshot —
|
|
40
|
+
* stable across decades).
|
|
41
|
+
*
|
|
42
|
+
* Kept inline (not exported) because it's an implementation detail of the
|
|
43
|
+
* parser. Length is the only thing we actually need for the split.
|
|
44
|
+
*/
|
|
45
|
+
const E164_PREFIXES_3 = new Set([
|
|
46
|
+
"210",
|
|
47
|
+
"211",
|
|
48
|
+
"212",
|
|
49
|
+
"213",
|
|
50
|
+
"216",
|
|
51
|
+
"218",
|
|
52
|
+
"220",
|
|
53
|
+
"221",
|
|
54
|
+
"222",
|
|
55
|
+
"223",
|
|
56
|
+
"224",
|
|
57
|
+
"225",
|
|
58
|
+
"226",
|
|
59
|
+
"227",
|
|
60
|
+
"228",
|
|
61
|
+
"229",
|
|
62
|
+
"230",
|
|
63
|
+
"231",
|
|
64
|
+
"232",
|
|
65
|
+
"233",
|
|
66
|
+
"234",
|
|
67
|
+
"235",
|
|
68
|
+
"236",
|
|
69
|
+
"237",
|
|
70
|
+
"238",
|
|
71
|
+
"239",
|
|
72
|
+
"240",
|
|
73
|
+
"241",
|
|
74
|
+
"242",
|
|
75
|
+
"243",
|
|
76
|
+
"244",
|
|
77
|
+
"245",
|
|
78
|
+
"246",
|
|
79
|
+
"247",
|
|
80
|
+
"248",
|
|
81
|
+
"249",
|
|
82
|
+
"250",
|
|
83
|
+
"251",
|
|
84
|
+
"252",
|
|
85
|
+
"253",
|
|
86
|
+
"254",
|
|
87
|
+
"255",
|
|
88
|
+
"256",
|
|
89
|
+
"257",
|
|
90
|
+
"258",
|
|
91
|
+
"260",
|
|
92
|
+
"261",
|
|
93
|
+
"262",
|
|
94
|
+
"263",
|
|
95
|
+
"264",
|
|
96
|
+
"265",
|
|
97
|
+
"266",
|
|
98
|
+
"267",
|
|
99
|
+
"268",
|
|
100
|
+
"269",
|
|
101
|
+
"290",
|
|
102
|
+
"291",
|
|
103
|
+
"297",
|
|
104
|
+
"298",
|
|
105
|
+
"299",
|
|
106
|
+
"350",
|
|
107
|
+
"351",
|
|
108
|
+
"352",
|
|
109
|
+
"353",
|
|
110
|
+
"354",
|
|
111
|
+
"355",
|
|
112
|
+
"356",
|
|
113
|
+
"357",
|
|
114
|
+
"358",
|
|
115
|
+
"359",
|
|
116
|
+
"370",
|
|
117
|
+
"371",
|
|
118
|
+
"372",
|
|
119
|
+
"373",
|
|
120
|
+
"374",
|
|
121
|
+
"375",
|
|
122
|
+
"376",
|
|
123
|
+
"377",
|
|
124
|
+
"378",
|
|
125
|
+
"379",
|
|
126
|
+
"380",
|
|
127
|
+
"381",
|
|
128
|
+
"382",
|
|
129
|
+
"383",
|
|
130
|
+
"385",
|
|
131
|
+
"386",
|
|
132
|
+
"387",
|
|
133
|
+
"389",
|
|
134
|
+
"420",
|
|
135
|
+
"421",
|
|
136
|
+
"423",
|
|
137
|
+
"500",
|
|
138
|
+
"501",
|
|
139
|
+
"502",
|
|
140
|
+
"503",
|
|
141
|
+
"504",
|
|
142
|
+
"505",
|
|
143
|
+
"506",
|
|
144
|
+
"507",
|
|
145
|
+
"508",
|
|
146
|
+
"509",
|
|
147
|
+
"590",
|
|
148
|
+
"591",
|
|
149
|
+
"592",
|
|
150
|
+
"593",
|
|
151
|
+
"594",
|
|
152
|
+
"595",
|
|
153
|
+
"596",
|
|
154
|
+
"597",
|
|
155
|
+
"598",
|
|
156
|
+
"599",
|
|
157
|
+
"670",
|
|
158
|
+
"672",
|
|
159
|
+
"673",
|
|
160
|
+
"674",
|
|
161
|
+
"675",
|
|
162
|
+
"676",
|
|
163
|
+
"677",
|
|
164
|
+
"678",
|
|
165
|
+
"679",
|
|
166
|
+
"680",
|
|
167
|
+
"681",
|
|
168
|
+
"682",
|
|
169
|
+
"683",
|
|
170
|
+
"685",
|
|
171
|
+
"686",
|
|
172
|
+
"687",
|
|
173
|
+
"688",
|
|
174
|
+
"689",
|
|
175
|
+
"690",
|
|
176
|
+
"691",
|
|
177
|
+
"692",
|
|
178
|
+
"800",
|
|
179
|
+
"808",
|
|
180
|
+
"850",
|
|
181
|
+
"852",
|
|
182
|
+
"853",
|
|
183
|
+
"855",
|
|
184
|
+
"856",
|
|
185
|
+
"870",
|
|
186
|
+
"878",
|
|
187
|
+
"880",
|
|
188
|
+
"881",
|
|
189
|
+
"882",
|
|
190
|
+
"883",
|
|
191
|
+
"886",
|
|
192
|
+
"888",
|
|
193
|
+
"960",
|
|
194
|
+
"961",
|
|
195
|
+
"962",
|
|
196
|
+
"963",
|
|
197
|
+
"964",
|
|
198
|
+
"965",
|
|
199
|
+
"966",
|
|
200
|
+
"967",
|
|
201
|
+
"968",
|
|
202
|
+
"970",
|
|
203
|
+
"971",
|
|
204
|
+
"972",
|
|
205
|
+
"973",
|
|
206
|
+
"974",
|
|
207
|
+
"975",
|
|
208
|
+
"976",
|
|
209
|
+
"977",
|
|
210
|
+
"979",
|
|
211
|
+
"992",
|
|
212
|
+
"993",
|
|
213
|
+
"994",
|
|
214
|
+
"995",
|
|
215
|
+
"996",
|
|
216
|
+
"998"
|
|
217
|
+
]);
|
|
218
|
+
const E164_PREFIXES_2 = new Set([
|
|
219
|
+
"20",
|
|
220
|
+
"27",
|
|
221
|
+
"30",
|
|
222
|
+
"31",
|
|
223
|
+
"32",
|
|
224
|
+
"33",
|
|
225
|
+
"34",
|
|
226
|
+
"36",
|
|
227
|
+
"39",
|
|
228
|
+
"40",
|
|
229
|
+
"41",
|
|
230
|
+
"43",
|
|
231
|
+
"44",
|
|
232
|
+
"45",
|
|
233
|
+
"46",
|
|
234
|
+
"47",
|
|
235
|
+
"48",
|
|
236
|
+
"49",
|
|
237
|
+
"51",
|
|
238
|
+
"52",
|
|
239
|
+
"53",
|
|
240
|
+
"54",
|
|
241
|
+
"55",
|
|
242
|
+
"56",
|
|
243
|
+
"57",
|
|
244
|
+
"58",
|
|
245
|
+
"60",
|
|
246
|
+
"61",
|
|
247
|
+
"62",
|
|
248
|
+
"63",
|
|
249
|
+
"64",
|
|
250
|
+
"65",
|
|
251
|
+
"66",
|
|
252
|
+
"81",
|
|
253
|
+
"82",
|
|
254
|
+
"84",
|
|
255
|
+
"86",
|
|
256
|
+
"90",
|
|
257
|
+
"91",
|
|
258
|
+
"92",
|
|
259
|
+
"93",
|
|
260
|
+
"94",
|
|
261
|
+
"95",
|
|
262
|
+
"98"
|
|
263
|
+
]);
|
|
264
|
+
const E164_PREFIXES_1 = new Set(["1", "7"]);
|
|
265
|
+
/**
|
|
266
|
+
* Parse free-form phone input into a normalized `PhoneNumber`. Accepts:
|
|
267
|
+
* - `'+14155550182'` → e164 unchanged
|
|
268
|
+
* - `'+1 (415) 555-0182'` → strips spaces / punctuation
|
|
269
|
+
* - `'+1.415.555.0182'` → strips dots
|
|
270
|
+
*
|
|
271
|
+
* Rejects anything without a leading `+` — we don't guess the country.
|
|
272
|
+
* If the host knows the user's region, prepend the code before calling.
|
|
273
|
+
*/
|
|
274
|
+
function parsePhone(input) {
|
|
275
|
+
if (typeof input !== "string" || input.trim().length === 0) return err(new PhoneError("EMPTY", "phone input is empty"));
|
|
276
|
+
const trimmed = input.trim();
|
|
277
|
+
if (!trimmed.startsWith("+")) return err(new PhoneError("MISSING_PLUS", `phone must be E.164 (start with '+'): got ${JSON.stringify(input)}`));
|
|
278
|
+
const digits = trimmed.slice(1).replace(/[^0-9]/g, "");
|
|
279
|
+
if (digits.length === 0) return err(new PhoneError("NOT_E164", "phone has no digits"));
|
|
280
|
+
if (digits.length > 15) return err(new PhoneError("TOO_LONG", `phone exceeds E.164 15-digit limit: ${digits.length} digits`));
|
|
281
|
+
if (digits.length < 7) return err(new PhoneError("TOO_SHORT", `phone too short to be E.164: ${digits.length} digits`));
|
|
282
|
+
const callingCode = E164_PREFIXES_3.has(digits.slice(0, 3)) ? digits.slice(0, 3) : E164_PREFIXES_2.has(digits.slice(0, 2)) ? digits.slice(0, 2) : E164_PREFIXES_1.has(digits.slice(0, 1)) ? digits.slice(0, 1) : null;
|
|
283
|
+
if (callingCode === null) return err(new PhoneError("NOT_E164", `unrecognized country calling code in ${JSON.stringify(input)}`));
|
|
284
|
+
return ok({
|
|
285
|
+
e164: `+${digits}`,
|
|
286
|
+
callingCode,
|
|
287
|
+
nationalNumber: digits.slice(callingCode.length)
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** Convenience: returns the E.164 string. Useful when piping to a transport. */
|
|
291
|
+
function formatPhone(phone) {
|
|
292
|
+
return phone.e164;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Best-effort national-format render: groups the national number into 3-digit
|
|
296
|
+
* blocks. Not locale-aware — for display polish, hosts should format per
|
|
297
|
+
* region. Useful as a default in admin UIs.
|
|
298
|
+
*
|
|
299
|
+
* @example formatNational(parsePhone('+14155550182').value!) → '+1 415 555 0182'
|
|
300
|
+
*/
|
|
301
|
+
function formatNational(phone) {
|
|
302
|
+
const groups = phone.nationalNumber.match(/.{1,3}/g) ?? [];
|
|
303
|
+
return `+${phone.callingCode} ${groups.join(" ")}`.trim();
|
|
304
|
+
}
|
|
305
|
+
/** Type guard. */
|
|
306
|
+
function isPhoneNumber(value) {
|
|
307
|
+
return typeof value === "object" && value !== null && typeof value.e164 === "string" && typeof value.callingCode === "string" && typeof value.nationalNumber === "string" && value.e164.startsWith("+");
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
export { PhoneError, formatNational, formatPhone, isPhoneNumber, parsePhone };
|
package/dist/reference.d.mts
CHANGED
package/dist/reference.mjs
CHANGED
package/dist/result.d.mts
CHANGED
package/dist/result.mjs
CHANGED