platform-types 1.10.852 → 1.10.854
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/dist/account.d.ts +6 -0
- package/dist/accountAllocation.d.ts +125 -0
- package/dist/accountAllocation.js +193 -0
- package/dist/cjs/accountAllocation.js +198 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/payment.js +44 -0
- package/dist/cjs/serviceOrder.js +5 -0
- package/dist/cjs/serviceTracking.js +64 -0
- package/dist/cjs/storefrontWebLiteralKeys.js +64 -9
- package/dist/customer.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/payment.d.ts +73 -1
- package/dist/payment.js +43 -1
- package/dist/print.d.ts +6 -0
- package/dist/product.d.ts +7 -0
- package/dist/serviceOrder.d.ts +11 -1
- package/dist/serviceOrder.js +5 -0
- package/dist/serviceTracking.d.ts +216 -0
- package/dist/serviceTracking.js +61 -0
- package/dist/store.d.ts +14 -1
- package/dist/storefrontWebLiteralKeys.js +64 -9
- package/dist/userActivity.d.ts +16 -1
- package/package.json +1 -1
package/dist/account.d.ts
CHANGED
|
@@ -188,6 +188,12 @@ declare global {
|
|
|
188
188
|
* payment): the obligation whose refund the debit books. Absent on every other row.
|
|
189
189
|
*/
|
|
190
190
|
refundObligationId?: string;
|
|
191
|
+
/**
|
|
192
|
+
* On a link-credit reversal row (SK `LINKREV#{reversedAccountId}`, `paymentRef*` naming the linked
|
|
193
|
+
* payment): the ledger row it reverses — the payment's credit, or that credit's FX adjustment.
|
|
194
|
+
* Absent on every other row.
|
|
195
|
+
*/
|
|
196
|
+
reversesAccountId?: string;
|
|
191
197
|
}
|
|
192
198
|
/**
|
|
193
199
|
* `POST /account` — a new customer-ledger movement. Mirrors the request side of
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cuenta-corriente allocator — which customer debits are still open, derived
|
|
3
|
+
* on read from `ACCOUNT#{storeId}` rows, in INTEGER MINOR UNITS. One
|
|
4
|
+
* implementation shared by the api, the operator app and the storefront, so the
|
|
5
|
+
* three agree on which debit a payment can still settle.
|
|
6
|
+
*
|
|
7
|
+
* No allocation entity is stored: a credit carries no pointer to the debit it
|
|
8
|
+
* paid, so every reader re-derives it, and two readers that derive it
|
|
9
|
+
* differently tell the customer two different things. The rules:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Row order is `createdAt` ascending, NEVER `dated`.** A backdated row is
|
|
12
|
+
* allocated where it was written. Ties: a debit before a credit (an order's
|
|
13
|
+
* debit and its auto-linked payment can share a millisecond), then
|
|
14
|
+
* `accountId`, so the result never depends on the order DynamoDB returned.
|
|
15
|
+
* 2. **A credit stamped with an `orderId` serves that order's open debits
|
|
16
|
+
* first**, oldest first, and only debits already on the ledger — it cannot
|
|
17
|
+
* reach a debit of that order written after it.
|
|
18
|
+
* 3. **Everything else applies oldest-first**: an unstamped credit, and the rest
|
|
19
|
+
* of a stamped one once its order is paid (or has no debit yet).
|
|
20
|
+
* 4. **Credit with no open debit to land on waits unapplied** and is drained by
|
|
21
|
+
* the next debit, so a deposit taken before the order is booked still pays
|
|
22
|
+
* it. What is never drained is reported as `unappliedMinor`.
|
|
23
|
+
*
|
|
24
|
+
* Rows it reads, and how:
|
|
25
|
+
*
|
|
26
|
+
* - A `deleted` row, or one whose `createdAt` is past `asOf`, is skipped.
|
|
27
|
+
* - A synthesised opening row (`ACC000001`, `ACC000001-<suffix>` — the "Saldo
|
|
28
|
+
* anterior" a statement leads with) is a movement of its `balance`: positive
|
|
29
|
+
* is an orderless debit, negative an unstamped credit, and any `orderId` on
|
|
30
|
+
* it is ignored. A `balance` on any OTHER row is a projected running total
|
|
31
|
+
* and is ignored.
|
|
32
|
+
* - A row whose `debit` is above zero is a debit (its `credit` is then not
|
|
33
|
+
* read); otherwise a row whose `credit` is above zero at the ledger scale is
|
|
34
|
+
* a credit; anything else moves nothing. A stored `BALANCE-` checkpoint
|
|
35
|
+
* carries neither, so it moves nothing — the ledger must be read from its
|
|
36
|
+
* first row instead, because FIFO over a ledger with a missing debit is
|
|
37
|
+
* wrong.
|
|
38
|
+
* - A refund-in-transit debit (`RIT#…`) is an ordinary debit stamped with its
|
|
39
|
+
* order. It is reported like any other item; a consumer offering debits to
|
|
40
|
+
* pay excludes it by its `accountId` if it must.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ **ONE ledger bucket — `ledgerCurrency` — never one per row label.** Every
|
|
43
|
+
* writer stores `debit`/`credit` already converted into the store's ledger
|
|
44
|
+
* denomination; a legacy manual row's `currency` labels what the operator
|
|
45
|
+
* TYPED, so bucketing by it files pesos as dollars. Opening rows follow the
|
|
46
|
+
* same rule, a legacy `ACC000001-usd` seed included. Every item therefore
|
|
47
|
+
* carries `ledgerCurrency`.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ **Exact, with no display tolerance.** An item is `paid` only at zero minor
|
|
50
|
+
* units. A reader that rounds to fewer decimals than the ledger scale (a store
|
|
51
|
+
* pricing in whole units on a two-decimal ledger) can call "paid" what this
|
|
52
|
+
* calls `partial`; that tolerance is the reader's to apply, on top of these
|
|
53
|
+
* figures. Amounts are quantised once per row — half away from zero after
|
|
54
|
+
* normalising the scaled value to 15 significant digits, the api ledger's own
|
|
55
|
+
* rule — and never summed as floats. A missing or non-finite amount is zero.
|
|
56
|
+
*
|
|
57
|
+
* Pure: no clock, no I/O, input rows never mutated.
|
|
58
|
+
*/
|
|
59
|
+
/** An item's allocation state, as a code — the reader owns the label. */
|
|
60
|
+
export declare const ACCOUNT_ITEM_STATUSES: readonly ["open", "partial", "paid"];
|
|
61
|
+
export type AccountItemStatus = (typeof ACCOUNT_ITEM_STATUSES)[number];
|
|
62
|
+
/** The fields the allocator reads — a stored row and either wire projection all satisfy it. */
|
|
63
|
+
export type AllocatableAccountRow = Pick<Account, 'accountId' | 'dated'> & Partial<Pick<Account, 'createdAt' | 'orderId' | 'dueDate' | 'details' | 'debit' | 'credit' | 'balance' | 'deleted'>>;
|
|
64
|
+
export interface AccountAllocationOptions {
|
|
65
|
+
/** The store's ledger denomination (`store.config.displayCurrency` as a catalogId). Lowercased on output. */
|
|
66
|
+
ledgerCurrency: string;
|
|
67
|
+
/** Decimal places of `ledgerCurrency`'s minor unit, an integer 0–4. Defaults to 2. */
|
|
68
|
+
ledgerScale?: number;
|
|
69
|
+
/** Unix ms; rows written after it are left out. Omitted, every row counts. */
|
|
70
|
+
asOf?: number;
|
|
71
|
+
}
|
|
72
|
+
/** One debit row after allocation. */
|
|
73
|
+
export interface AllocatedAccountItem {
|
|
74
|
+
/** The debit row's `Account.accountId`. */
|
|
75
|
+
accountId: string;
|
|
76
|
+
/** Absent for orderless debt — an empty `orderId` included, which allocation treats as orderless too. */
|
|
77
|
+
orderId?: string;
|
|
78
|
+
/** `AccountLedgerAllocation.currency`. */
|
|
79
|
+
currency: string;
|
|
80
|
+
/** `YYYYMMDD` ledger day the debit is dated — what an aging read buckets. */
|
|
81
|
+
dated: number;
|
|
82
|
+
/** `YYYYMMDD` due day stamped on the row. Absent means unspecified — never inferred from `dated`. */
|
|
83
|
+
dueDate?: number;
|
|
84
|
+
/** The row's `Account.details`, verbatim when the row carries the key. */
|
|
85
|
+
details?: string;
|
|
86
|
+
/** The row's `createdAt` — the axis allocation ran on. */
|
|
87
|
+
createdAt: number;
|
|
88
|
+
/** The debit, in minor units of `currency`. */
|
|
89
|
+
originalMinor: number;
|
|
90
|
+
/** How much of it credits have covered. */
|
|
91
|
+
paidMinor: number;
|
|
92
|
+
/** `originalMinor - paidMinor`, never negative. */
|
|
93
|
+
remainingMinor: number;
|
|
94
|
+
status: AccountItemStatus;
|
|
95
|
+
}
|
|
96
|
+
/** Every debit of one order, summed. */
|
|
97
|
+
export interface AllocatedAccountOrder {
|
|
98
|
+
originalMinor: number;
|
|
99
|
+
paidMinor: number;
|
|
100
|
+
remainingMinor: number;
|
|
101
|
+
status: AccountItemStatus;
|
|
102
|
+
}
|
|
103
|
+
export interface AccountLedgerAllocation {
|
|
104
|
+
/** The ledger bucket every amount here is in: `ledgerCurrency`, lowercased. */
|
|
105
|
+
currency: string;
|
|
106
|
+
/** Decimal places of one minor unit of `currency`. */
|
|
107
|
+
scale: number;
|
|
108
|
+
/** Every debit row, in allocation order, paid ones included. */
|
|
109
|
+
items: AllocatedAccountItem[];
|
|
110
|
+
/** The items carrying an `orderId`, summed per order. */
|
|
111
|
+
orders: Record<string, AllocatedAccountOrder>;
|
|
112
|
+
/** Credit no debit absorbed — owed to the customer. Never negative. */
|
|
113
|
+
unappliedMinor: number;
|
|
114
|
+
/** Debits minus credits: positive is owed by the customer. Equals the open items' total minus `unappliedMinor`. */
|
|
115
|
+
saldoMinor: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Allocate a customer's ledger. Pass the WHOLE ledger — every movement from the
|
|
119
|
+
* first, or a synthesised opening row standing in for what precedes the rows —
|
|
120
|
+
* since a debit missing from the input leaves a later credit landing on the
|
|
121
|
+
* wrong one. Throws a `RangeError` on a scale outside 0–4.
|
|
122
|
+
*/
|
|
123
|
+
export declare const allocateAccountLedger: (rows: readonly AllocatableAccountRow[], options: AccountAllocationOptions) => AccountLedgerAllocation;
|
|
124
|
+
/** The items of an allocation that still owe something, in allocation order. */
|
|
125
|
+
export declare const openAccountItems: (allocation: AccountLedgerAllocation) => AllocatedAccountItem[];
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cuenta-corriente allocator — which customer debits are still open, derived
|
|
3
|
+
* on read from `ACCOUNT#{storeId}` rows, in INTEGER MINOR UNITS. One
|
|
4
|
+
* implementation shared by the api, the operator app and the storefront, so the
|
|
5
|
+
* three agree on which debit a payment can still settle.
|
|
6
|
+
*
|
|
7
|
+
* No allocation entity is stored: a credit carries no pointer to the debit it
|
|
8
|
+
* paid, so every reader re-derives it, and two readers that derive it
|
|
9
|
+
* differently tell the customer two different things. The rules:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Row order is `createdAt` ascending, NEVER `dated`.** A backdated row is
|
|
12
|
+
* allocated where it was written. Ties: a debit before a credit (an order's
|
|
13
|
+
* debit and its auto-linked payment can share a millisecond), then
|
|
14
|
+
* `accountId`, so the result never depends on the order DynamoDB returned.
|
|
15
|
+
* 2. **A credit stamped with an `orderId` serves that order's open debits
|
|
16
|
+
* first**, oldest first, and only debits already on the ledger — it cannot
|
|
17
|
+
* reach a debit of that order written after it.
|
|
18
|
+
* 3. **Everything else applies oldest-first**: an unstamped credit, and the rest
|
|
19
|
+
* of a stamped one once its order is paid (or has no debit yet).
|
|
20
|
+
* 4. **Credit with no open debit to land on waits unapplied** and is drained by
|
|
21
|
+
* the next debit, so a deposit taken before the order is booked still pays
|
|
22
|
+
* it. What is never drained is reported as `unappliedMinor`.
|
|
23
|
+
*
|
|
24
|
+
* Rows it reads, and how:
|
|
25
|
+
*
|
|
26
|
+
* - A `deleted` row, or one whose `createdAt` is past `asOf`, is skipped.
|
|
27
|
+
* - A synthesised opening row (`ACC000001`, `ACC000001-<suffix>` — the "Saldo
|
|
28
|
+
* anterior" a statement leads with) is a movement of its `balance`: positive
|
|
29
|
+
* is an orderless debit, negative an unstamped credit, and any `orderId` on
|
|
30
|
+
* it is ignored. A `balance` on any OTHER row is a projected running total
|
|
31
|
+
* and is ignored.
|
|
32
|
+
* - A row whose `debit` is above zero is a debit (its `credit` is then not
|
|
33
|
+
* read); otherwise a row whose `credit` is above zero at the ledger scale is
|
|
34
|
+
* a credit; anything else moves nothing. A stored `BALANCE-` checkpoint
|
|
35
|
+
* carries neither, so it moves nothing — the ledger must be read from its
|
|
36
|
+
* first row instead, because FIFO over a ledger with a missing debit is
|
|
37
|
+
* wrong.
|
|
38
|
+
* - A refund-in-transit debit (`RIT#…`) is an ordinary debit stamped with its
|
|
39
|
+
* order. It is reported like any other item; a consumer offering debits to
|
|
40
|
+
* pay excludes it by its `accountId` if it must.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ **ONE ledger bucket — `ledgerCurrency` — never one per row label.** Every
|
|
43
|
+
* writer stores `debit`/`credit` already converted into the store's ledger
|
|
44
|
+
* denomination; a legacy manual row's `currency` labels what the operator
|
|
45
|
+
* TYPED, so bucketing by it files pesos as dollars. Opening rows follow the
|
|
46
|
+
* same rule, a legacy `ACC000001-usd` seed included. Every item therefore
|
|
47
|
+
* carries `ledgerCurrency`.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ **Exact, with no display tolerance.** An item is `paid` only at zero minor
|
|
50
|
+
* units. A reader that rounds to fewer decimals than the ledger scale (a store
|
|
51
|
+
* pricing in whole units on a two-decimal ledger) can call "paid" what this
|
|
52
|
+
* calls `partial`; that tolerance is the reader's to apply, on top of these
|
|
53
|
+
* figures. Amounts are quantised once per row — half away from zero after
|
|
54
|
+
* normalising the scaled value to 15 significant digits, the api ledger's own
|
|
55
|
+
* rule — and never summed as floats. A missing or non-finite amount is zero.
|
|
56
|
+
*
|
|
57
|
+
* Pure: no clock, no I/O, input rows never mutated.
|
|
58
|
+
*/
|
|
59
|
+
/** An item's allocation state, as a code — the reader owns the label. */
|
|
60
|
+
export const ACCOUNT_ITEM_STATUSES = ['open', 'partial', 'paid'];
|
|
61
|
+
const OPENING_ROW_ID = 'ACC000001';
|
|
62
|
+
const DEFAULT_SCALE = 2;
|
|
63
|
+
const MAX_SCALE = 4;
|
|
64
|
+
const isOpeningRow = (accountId) => accountId === OPENING_ROW_ID || accountId.startsWith(`${OPENING_ROW_ID}-`);
|
|
65
|
+
const finiteOrZero = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
|
|
66
|
+
const toMinorUnits = (amount, scale) => {
|
|
67
|
+
const rounded = Math.round(Number(Math.abs(amount * 10 ** scale).toPrecision(15)));
|
|
68
|
+
// Never `-0`.
|
|
69
|
+
return amount < 0 && rounded !== 0 ? -rounded : rounded;
|
|
70
|
+
};
|
|
71
|
+
const statusOf = (originalMinor, paidMinor) => {
|
|
72
|
+
if (originalMinor - paidMinor <= 0)
|
|
73
|
+
return 'paid';
|
|
74
|
+
return paidMinor > 0 ? 'partial' : 'open';
|
|
75
|
+
};
|
|
76
|
+
const toMove = (row, scale) => {
|
|
77
|
+
const createdAt = finiteOrZero(row.createdAt);
|
|
78
|
+
if (isOpeningRow(row.accountId)) {
|
|
79
|
+
const opening = toMinorUnits(finiteOrZero(row.balance), scale);
|
|
80
|
+
return {
|
|
81
|
+
row,
|
|
82
|
+
createdAt,
|
|
83
|
+
isDebit: opening > 0,
|
|
84
|
+
debitMinor: Math.max(opening, 0),
|
|
85
|
+
creditMinor: Math.max(-opening, 0),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const debit = finiteOrZero(row.debit);
|
|
89
|
+
return {
|
|
90
|
+
row,
|
|
91
|
+
createdAt,
|
|
92
|
+
// The RAW amount decides the kind, as the operator app does: a debit too small to reach one
|
|
93
|
+
// minor unit is still a (zero) debit, and its `credit` is still not read.
|
|
94
|
+
isDebit: debit > 0,
|
|
95
|
+
debitMinor: toMinorUnits(debit, scale),
|
|
96
|
+
creditMinor: toMinorUnits(finiteOrZero(row.credit), scale),
|
|
97
|
+
...(row.orderId ? { orderId: row.orderId } : {}),
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
const compareMoves = (a, b) => a.createdAt - b.createdAt || Number(b.isDebit) - Number(a.isDebit) || a.row.accountId.localeCompare(b.row.accountId);
|
|
101
|
+
/** Serves `debits` in order; returns the credit left over. */
|
|
102
|
+
const applyFifo = (debits, creditMinor) => {
|
|
103
|
+
let remaining = creditMinor;
|
|
104
|
+
for (const debit of debits) {
|
|
105
|
+
if (remaining <= 0)
|
|
106
|
+
break;
|
|
107
|
+
const open = debit.totalMinor - debit.paidMinor;
|
|
108
|
+
if (open <= 0)
|
|
109
|
+
continue;
|
|
110
|
+
const applied = Math.min(open, remaining);
|
|
111
|
+
debit.paidMinor += applied;
|
|
112
|
+
remaining -= applied;
|
|
113
|
+
}
|
|
114
|
+
return remaining;
|
|
115
|
+
};
|
|
116
|
+
const toItem = (tracker, currency) => {
|
|
117
|
+
const { row } = tracker;
|
|
118
|
+
return {
|
|
119
|
+
accountId: row.accountId,
|
|
120
|
+
...(tracker.orderId ? { orderId: tracker.orderId } : {}),
|
|
121
|
+
currency,
|
|
122
|
+
dated: row.dated,
|
|
123
|
+
...(row.dueDate === undefined ? {} : { dueDate: row.dueDate }),
|
|
124
|
+
...(row.details === undefined ? {} : { details: row.details }),
|
|
125
|
+
createdAt: tracker.createdAt,
|
|
126
|
+
originalMinor: tracker.totalMinor,
|
|
127
|
+
paidMinor: tracker.paidMinor,
|
|
128
|
+
remainingMinor: Math.max(tracker.totalMinor - tracker.paidMinor, 0),
|
|
129
|
+
status: statusOf(tracker.totalMinor, tracker.paidMinor),
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const summariseOrders = (items) => {
|
|
133
|
+
const orders = {};
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
if (!item.orderId)
|
|
136
|
+
continue;
|
|
137
|
+
const originalMinor = (orders[item.orderId]?.originalMinor ?? 0) + item.originalMinor;
|
|
138
|
+
const paidMinor = (orders[item.orderId]?.paidMinor ?? 0) + item.paidMinor;
|
|
139
|
+
orders[item.orderId] = {
|
|
140
|
+
originalMinor,
|
|
141
|
+
paidMinor,
|
|
142
|
+
remainingMinor: Math.max(originalMinor - paidMinor, 0),
|
|
143
|
+
status: statusOf(originalMinor, paidMinor),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return orders;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Allocate a customer's ledger. Pass the WHOLE ledger — every movement from the
|
|
150
|
+
* first, or a synthesised opening row standing in for what precedes the rows —
|
|
151
|
+
* since a debit missing from the input leaves a later credit landing on the
|
|
152
|
+
* wrong one. Throws a `RangeError` on a scale outside 0–4.
|
|
153
|
+
*/
|
|
154
|
+
export const allocateAccountLedger = (rows, options) => {
|
|
155
|
+
const scale = options.ledgerScale ?? DEFAULT_SCALE;
|
|
156
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
157
|
+
throw new RangeError(`Ledger scale must be an integer in 0-${MAX_SCALE}, got ${scale}`);
|
|
158
|
+
}
|
|
159
|
+
const currency = options.ledgerCurrency.toLowerCase();
|
|
160
|
+
const { asOf } = options;
|
|
161
|
+
const moves = rows
|
|
162
|
+
.filter((row) => !row.deleted && (asOf === undefined || finiteOrZero(row.createdAt) <= asOf))
|
|
163
|
+
.map((row) => toMove(row, scale))
|
|
164
|
+
.sort(compareMoves);
|
|
165
|
+
const debits = [];
|
|
166
|
+
let unappliedMinor = 0;
|
|
167
|
+
let saldoMinor = 0;
|
|
168
|
+
for (const move of moves) {
|
|
169
|
+
if (move.isDebit) {
|
|
170
|
+
saldoMinor += move.debitMinor;
|
|
171
|
+
debits.push({
|
|
172
|
+
row: move.row,
|
|
173
|
+
createdAt: move.createdAt,
|
|
174
|
+
...(move.orderId ? { orderId: move.orderId } : {}),
|
|
175
|
+
totalMinor: move.debitMinor,
|
|
176
|
+
paidMinor: 0,
|
|
177
|
+
});
|
|
178
|
+
unappliedMinor = applyFifo(debits, unappliedMinor);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (move.creditMinor <= 0)
|
|
182
|
+
continue;
|
|
183
|
+
saldoMinor -= move.creditMinor;
|
|
184
|
+
const spare = move.orderId
|
|
185
|
+
? applyFifo(debits.filter((debit) => debit.orderId === move.orderId), move.creditMinor)
|
|
186
|
+
: move.creditMinor;
|
|
187
|
+
unappliedMinor = applyFifo(debits, unappliedMinor + spare);
|
|
188
|
+
}
|
|
189
|
+
const items = debits.map((tracker) => toItem(tracker, currency));
|
|
190
|
+
return { currency, scale, items, orders: summariseOrders(items), unappliedMinor, saldoMinor };
|
|
191
|
+
};
|
|
192
|
+
/** The items of an allocation that still owe something, in allocation order. */
|
|
193
|
+
export const openAccountItems = (allocation) => allocation.items.filter((item) => item.remainingMinor > 0);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The cuenta-corriente allocator — which customer debits are still open, derived
|
|
4
|
+
* on read from `ACCOUNT#{storeId}` rows, in INTEGER MINOR UNITS. One
|
|
5
|
+
* implementation shared by the api, the operator app and the storefront, so the
|
|
6
|
+
* three agree on which debit a payment can still settle.
|
|
7
|
+
*
|
|
8
|
+
* No allocation entity is stored: a credit carries no pointer to the debit it
|
|
9
|
+
* paid, so every reader re-derives it, and two readers that derive it
|
|
10
|
+
* differently tell the customer two different things. The rules:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Row order is `createdAt` ascending, NEVER `dated`.** A backdated row is
|
|
13
|
+
* allocated where it was written. Ties: a debit before a credit (an order's
|
|
14
|
+
* debit and its auto-linked payment can share a millisecond), then
|
|
15
|
+
* `accountId`, so the result never depends on the order DynamoDB returned.
|
|
16
|
+
* 2. **A credit stamped with an `orderId` serves that order's open debits
|
|
17
|
+
* first**, oldest first, and only debits already on the ledger — it cannot
|
|
18
|
+
* reach a debit of that order written after it.
|
|
19
|
+
* 3. **Everything else applies oldest-first**: an unstamped credit, and the rest
|
|
20
|
+
* of a stamped one once its order is paid (or has no debit yet).
|
|
21
|
+
* 4. **Credit with no open debit to land on waits unapplied** and is drained by
|
|
22
|
+
* the next debit, so a deposit taken before the order is booked still pays
|
|
23
|
+
* it. What is never drained is reported as `unappliedMinor`.
|
|
24
|
+
*
|
|
25
|
+
* Rows it reads, and how:
|
|
26
|
+
*
|
|
27
|
+
* - A `deleted` row, or one whose `createdAt` is past `asOf`, is skipped.
|
|
28
|
+
* - A synthesised opening row (`ACC000001`, `ACC000001-<suffix>` — the "Saldo
|
|
29
|
+
* anterior" a statement leads with) is a movement of its `balance`: positive
|
|
30
|
+
* is an orderless debit, negative an unstamped credit, and any `orderId` on
|
|
31
|
+
* it is ignored. A `balance` on any OTHER row is a projected running total
|
|
32
|
+
* and is ignored.
|
|
33
|
+
* - A row whose `debit` is above zero is a debit (its `credit` is then not
|
|
34
|
+
* read); otherwise a row whose `credit` is above zero at the ledger scale is
|
|
35
|
+
* a credit; anything else moves nothing. A stored `BALANCE-` checkpoint
|
|
36
|
+
* carries neither, so it moves nothing — the ledger must be read from its
|
|
37
|
+
* first row instead, because FIFO over a ledger with a missing debit is
|
|
38
|
+
* wrong.
|
|
39
|
+
* - A refund-in-transit debit (`RIT#…`) is an ordinary debit stamped with its
|
|
40
|
+
* order. It is reported like any other item; a consumer offering debits to
|
|
41
|
+
* pay excludes it by its `accountId` if it must.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ **ONE ledger bucket — `ledgerCurrency` — never one per row label.** Every
|
|
44
|
+
* writer stores `debit`/`credit` already converted into the store's ledger
|
|
45
|
+
* denomination; a legacy manual row's `currency` labels what the operator
|
|
46
|
+
* TYPED, so bucketing by it files pesos as dollars. Opening rows follow the
|
|
47
|
+
* same rule, a legacy `ACC000001-usd` seed included. Every item therefore
|
|
48
|
+
* carries `ledgerCurrency`.
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ **Exact, with no display tolerance.** An item is `paid` only at zero minor
|
|
51
|
+
* units. A reader that rounds to fewer decimals than the ledger scale (a store
|
|
52
|
+
* pricing in whole units on a two-decimal ledger) can call "paid" what this
|
|
53
|
+
* calls `partial`; that tolerance is the reader's to apply, on top of these
|
|
54
|
+
* figures. Amounts are quantised once per row — half away from zero after
|
|
55
|
+
* normalising the scaled value to 15 significant digits, the api ledger's own
|
|
56
|
+
* rule — and never summed as floats. A missing or non-finite amount is zero.
|
|
57
|
+
*
|
|
58
|
+
* Pure: no clock, no I/O, input rows never mutated.
|
|
59
|
+
*/
|
|
60
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
|
+
exports.openAccountItems = exports.allocateAccountLedger = exports.ACCOUNT_ITEM_STATUSES = void 0;
|
|
62
|
+
/** An item's allocation state, as a code — the reader owns the label. */
|
|
63
|
+
exports.ACCOUNT_ITEM_STATUSES = ['open', 'partial', 'paid'];
|
|
64
|
+
const OPENING_ROW_ID = 'ACC000001';
|
|
65
|
+
const DEFAULT_SCALE = 2;
|
|
66
|
+
const MAX_SCALE = 4;
|
|
67
|
+
const isOpeningRow = (accountId) => accountId === OPENING_ROW_ID || accountId.startsWith(`${OPENING_ROW_ID}-`);
|
|
68
|
+
const finiteOrZero = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
|
|
69
|
+
const toMinorUnits = (amount, scale) => {
|
|
70
|
+
const rounded = Math.round(Number(Math.abs(amount * 10 ** scale).toPrecision(15)));
|
|
71
|
+
// Never `-0`.
|
|
72
|
+
return amount < 0 && rounded !== 0 ? -rounded : rounded;
|
|
73
|
+
};
|
|
74
|
+
const statusOf = (originalMinor, paidMinor) => {
|
|
75
|
+
if (originalMinor - paidMinor <= 0)
|
|
76
|
+
return 'paid';
|
|
77
|
+
return paidMinor > 0 ? 'partial' : 'open';
|
|
78
|
+
};
|
|
79
|
+
const toMove = (row, scale) => {
|
|
80
|
+
const createdAt = finiteOrZero(row.createdAt);
|
|
81
|
+
if (isOpeningRow(row.accountId)) {
|
|
82
|
+
const opening = toMinorUnits(finiteOrZero(row.balance), scale);
|
|
83
|
+
return {
|
|
84
|
+
row,
|
|
85
|
+
createdAt,
|
|
86
|
+
isDebit: opening > 0,
|
|
87
|
+
debitMinor: Math.max(opening, 0),
|
|
88
|
+
creditMinor: Math.max(-opening, 0),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const debit = finiteOrZero(row.debit);
|
|
92
|
+
return {
|
|
93
|
+
row,
|
|
94
|
+
createdAt,
|
|
95
|
+
// The RAW amount decides the kind, as the operator app does: a debit too small to reach one
|
|
96
|
+
// minor unit is still a (zero) debit, and its `credit` is still not read.
|
|
97
|
+
isDebit: debit > 0,
|
|
98
|
+
debitMinor: toMinorUnits(debit, scale),
|
|
99
|
+
creditMinor: toMinorUnits(finiteOrZero(row.credit), scale),
|
|
100
|
+
...(row.orderId ? { orderId: row.orderId } : {}),
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const compareMoves = (a, b) => a.createdAt - b.createdAt || Number(b.isDebit) - Number(a.isDebit) || a.row.accountId.localeCompare(b.row.accountId);
|
|
104
|
+
/** Serves `debits` in order; returns the credit left over. */
|
|
105
|
+
const applyFifo = (debits, creditMinor) => {
|
|
106
|
+
let remaining = creditMinor;
|
|
107
|
+
for (const debit of debits) {
|
|
108
|
+
if (remaining <= 0)
|
|
109
|
+
break;
|
|
110
|
+
const open = debit.totalMinor - debit.paidMinor;
|
|
111
|
+
if (open <= 0)
|
|
112
|
+
continue;
|
|
113
|
+
const applied = Math.min(open, remaining);
|
|
114
|
+
debit.paidMinor += applied;
|
|
115
|
+
remaining -= applied;
|
|
116
|
+
}
|
|
117
|
+
return remaining;
|
|
118
|
+
};
|
|
119
|
+
const toItem = (tracker, currency) => {
|
|
120
|
+
const { row } = tracker;
|
|
121
|
+
return {
|
|
122
|
+
accountId: row.accountId,
|
|
123
|
+
...(tracker.orderId ? { orderId: tracker.orderId } : {}),
|
|
124
|
+
currency,
|
|
125
|
+
dated: row.dated,
|
|
126
|
+
...(row.dueDate === undefined ? {} : { dueDate: row.dueDate }),
|
|
127
|
+
...(row.details === undefined ? {} : { details: row.details }),
|
|
128
|
+
createdAt: tracker.createdAt,
|
|
129
|
+
originalMinor: tracker.totalMinor,
|
|
130
|
+
paidMinor: tracker.paidMinor,
|
|
131
|
+
remainingMinor: Math.max(tracker.totalMinor - tracker.paidMinor, 0),
|
|
132
|
+
status: statusOf(tracker.totalMinor, tracker.paidMinor),
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
const summariseOrders = (items) => {
|
|
136
|
+
const orders = {};
|
|
137
|
+
for (const item of items) {
|
|
138
|
+
if (!item.orderId)
|
|
139
|
+
continue;
|
|
140
|
+
const originalMinor = (orders[item.orderId]?.originalMinor ?? 0) + item.originalMinor;
|
|
141
|
+
const paidMinor = (orders[item.orderId]?.paidMinor ?? 0) + item.paidMinor;
|
|
142
|
+
orders[item.orderId] = {
|
|
143
|
+
originalMinor,
|
|
144
|
+
paidMinor,
|
|
145
|
+
remainingMinor: Math.max(originalMinor - paidMinor, 0),
|
|
146
|
+
status: statusOf(originalMinor, paidMinor),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return orders;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Allocate a customer's ledger. Pass the WHOLE ledger — every movement from the
|
|
153
|
+
* first, or a synthesised opening row standing in for what precedes the rows —
|
|
154
|
+
* since a debit missing from the input leaves a later credit landing on the
|
|
155
|
+
* wrong one. Throws a `RangeError` on a scale outside 0–4.
|
|
156
|
+
*/
|
|
157
|
+
const allocateAccountLedger = (rows, options) => {
|
|
158
|
+
const scale = options.ledgerScale ?? DEFAULT_SCALE;
|
|
159
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
160
|
+
throw new RangeError(`Ledger scale must be an integer in 0-${MAX_SCALE}, got ${scale}`);
|
|
161
|
+
}
|
|
162
|
+
const currency = options.ledgerCurrency.toLowerCase();
|
|
163
|
+
const { asOf } = options;
|
|
164
|
+
const moves = rows
|
|
165
|
+
.filter((row) => !row.deleted && (asOf === undefined || finiteOrZero(row.createdAt) <= asOf))
|
|
166
|
+
.map((row) => toMove(row, scale))
|
|
167
|
+
.sort(compareMoves);
|
|
168
|
+
const debits = [];
|
|
169
|
+
let unappliedMinor = 0;
|
|
170
|
+
let saldoMinor = 0;
|
|
171
|
+
for (const move of moves) {
|
|
172
|
+
if (move.isDebit) {
|
|
173
|
+
saldoMinor += move.debitMinor;
|
|
174
|
+
debits.push({
|
|
175
|
+
row: move.row,
|
|
176
|
+
createdAt: move.createdAt,
|
|
177
|
+
...(move.orderId ? { orderId: move.orderId } : {}),
|
|
178
|
+
totalMinor: move.debitMinor,
|
|
179
|
+
paidMinor: 0,
|
|
180
|
+
});
|
|
181
|
+
unappliedMinor = applyFifo(debits, unappliedMinor);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (move.creditMinor <= 0)
|
|
185
|
+
continue;
|
|
186
|
+
saldoMinor -= move.creditMinor;
|
|
187
|
+
const spare = move.orderId
|
|
188
|
+
? applyFifo(debits.filter((debit) => debit.orderId === move.orderId), move.creditMinor)
|
|
189
|
+
: move.creditMinor;
|
|
190
|
+
unappliedMinor = applyFifo(debits, unappliedMinor + spare);
|
|
191
|
+
}
|
|
192
|
+
const items = debits.map((tracker) => toItem(tracker, currency));
|
|
193
|
+
return { currency, scale, items, orders: summariseOrders(items), unappliedMinor, saldoMinor };
|
|
194
|
+
};
|
|
195
|
+
exports.allocateAccountLedger = allocateAccountLedger;
|
|
196
|
+
/** The items of an allocation that still owe something, in allocation order. */
|
|
197
|
+
const openAccountItems = (allocation) => allocation.items.filter((item) => item.remainingMinor > 0);
|
|
198
|
+
exports.openAccountItems = openAccountItems;
|
package/dist/cjs/index.js
CHANGED
|
@@ -23,6 +23,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
23
23
|
// the published ESM build keeps the extension Node's resolver demands — bare
|
|
24
24
|
// `export * from "./account"` would throw ERR_MODULE_NOT_FOUND at import time.
|
|
25
25
|
__exportStar(require("./account.js"), exports);
|
|
26
|
+
__exportStar(require("./accountAllocation.js"), exports);
|
|
26
27
|
__exportStar(require("./afip.js"), exports);
|
|
27
28
|
__exportStar(require("./ai.js"), exports);
|
|
28
29
|
__exportStar(require("./api.js"), exports);
|
|
@@ -103,6 +104,7 @@ __exportStar(require("./rma.js"), exports);
|
|
|
103
104
|
__exportStar(require("./roles.js"), exports);
|
|
104
105
|
__exportStar(require("./sentry.js"), exports);
|
|
105
106
|
__exportStar(require("./serviceOrder.js"), exports);
|
|
107
|
+
__exportStar(require("./serviceTracking.js"), exports);
|
|
106
108
|
__exportStar(require("./serviceTemplate.js"), exports);
|
|
107
109
|
__exportStar(require("./socket.js"), exports);
|
|
108
110
|
__exportStar(require("./stock.js"), exports);
|
package/dist/cjs/payment.js
CHANGED
|
@@ -1,2 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PAYMENT_CREDIT_REVERSAL_LOCK_REASONS = exports.PAYMENT_CREDIT_REVERSAL_ERROR_CODES = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The credit reversal's refusals (`PaymentCreditReversalRequest`). Each writes nothing.
|
|
6
|
+
*
|
|
7
|
+
* - `ADMIN_REQUIRED` · 403 — the token is below the ADMIN tier (an `ACCOUNTANT` is `403 ROLE_FORBIDDEN`, earlier).
|
|
8
|
+
* - `INVALID_INPUT` · 400 — the path's `source` is not `mp`, `stripe` or `mp_movement`.
|
|
9
|
+
* - `VALIDATION_FAILED` · 400 — the body is not `{ mode, reason }` or the reason is refused.
|
|
10
|
+
* - `PAYMENT_NOT_FOUND` · 404 — the store has no such payment.
|
|
11
|
+
* - `LINK_LOCKED` · 409 — `reason` is one of {@link PAYMENT_CREDIT_REVERSAL_LOCK_REASONS}.
|
|
12
|
+
* - `CUSTOMER_MERGED` · 409 — the credit's owner is mid-merge (or the payment carries a historical
|
|
13
|
+
* merged-refund flag); `data.mergedInto` names the survivor.
|
|
14
|
+
* - `CUSTOMER_ANONYMIZED` · 409 — a legacy credit naming no customer, on an erased customer.
|
|
15
|
+
* - `CUSTOMER_NOT_FOUND` · 404 — the customer whose balance the credit moved is gone.
|
|
16
|
+
*/
|
|
17
|
+
exports.PAYMENT_CREDIT_REVERSAL_ERROR_CODES = [
|
|
18
|
+
'ADMIN_REQUIRED',
|
|
19
|
+
'INVALID_INPUT',
|
|
20
|
+
'VALIDATION_FAILED',
|
|
21
|
+
'PAYMENT_NOT_FOUND',
|
|
22
|
+
'LINK_LOCKED',
|
|
23
|
+
'CUSTOMER_MERGED',
|
|
24
|
+
'CUSTOMER_ANONYMIZED',
|
|
25
|
+
'CUSTOMER_NOT_FOUND',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Every `reason` of the credit reversal's `409 LINK_LOCKED`:
|
|
29
|
+
*
|
|
30
|
+
* - `credit-reversed` — this credit was already reversed (`reversalAccountId` names the row): a second
|
|
31
|
+
* reversal of the same credit, a replay included.
|
|
32
|
+
* - `not-credited` — the payment's `accountId` names no credit of its own (never credited, or a pointer to
|
|
33
|
+
* a row this payment did not write), or the payment names no customer (un-linked by hand).
|
|
34
|
+
* - `refund-obligation` — a refund obligation already debited part of this credit as in transit
|
|
35
|
+
* (`refundObligationId`); reversing it all would debit that share twice.
|
|
36
|
+
* - `edited-ledger-pointer` — the payment's `fxAdjustmentAccountId` names a row that is not its own FX
|
|
37
|
+
* adjustment for the credit's customer, so the delta to reverse cannot be named.
|
|
38
|
+
* - `concurrent-link` — the payment or its ledger rows kept changing while the reversal ran; retry.
|
|
39
|
+
*/
|
|
40
|
+
exports.PAYMENT_CREDIT_REVERSAL_LOCK_REASONS = [
|
|
41
|
+
'credit-reversed',
|
|
42
|
+
'not-credited',
|
|
43
|
+
'refund-obligation',
|
|
44
|
+
'edited-ledger-pointer',
|
|
45
|
+
'concurrent-link',
|
|
46
|
+
];
|
package/dist/cjs/serviceOrder.js
CHANGED
|
@@ -301,6 +301,11 @@ exports.SERVICE_ORDER_ERROR_CODES = [
|
|
|
301
301
|
'WARRANTY_BELOW_LEGAL_MINIMUM',
|
|
302
302
|
'TECHNICIAN_NOT_FOUND',
|
|
303
303
|
'TECHNICIAN_NOT_ELIGIBLE',
|
|
304
|
+
// The customer tracking link (`mode: 'trackingLink'`) — see `SERVICE_TRACKING_OPERATOR_ERROR_CODES`
|
|
305
|
+
'SERVICE_TRACKING_DISABLED',
|
|
306
|
+
'SERVICE_TRACKING_NO_ADDRESS',
|
|
307
|
+
'SERVICE_TRACKING_LINK_LAPSED',
|
|
308
|
+
'ADMIN_REQUIRED',
|
|
304
309
|
'CUSTODY_ILLEGAL_TRANSITION',
|
|
305
310
|
'SERVICE_ORDER_BILLABLE_FROZEN',
|
|
306
311
|
'SERVICE_ORDER_EDIT_CHANGED',
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Customer service-order tracking — the page a customer opens from a link a
|
|
4
|
+
* shop sends (`/servicio/{handle}`), and the same view for a signed-in customer
|
|
5
|
+
* on their account.
|
|
6
|
+
*
|
|
7
|
+
* The HANDLE is 16 base62 characters (~95 bits), stored server-side and a
|
|
8
|
+
* bearer credential over exactly ONE service order of one store. It authorises
|
|
9
|
+
* the read (`GET /services/tracking?handle=`) and the current presupuesto PDF
|
|
10
|
+
* (`GET /services/tracking?handle=&mode=quote-pdf`), and nothing else. Every
|
|
11
|
+
* earlier link of an order dies when staff revoke it. A link stops reading
|
|
12
|
+
* `serviceTrackingLinkDaysAfterClose` days (platform global, default 90) after
|
|
13
|
+
* the order reached a terminal status.
|
|
14
|
+
*
|
|
15
|
+
* The gate is `ecommerce.config.serviceTracking` AND the `workshop` module on.
|
|
16
|
+
* With the gate off, staff cannot mint and the signed-in reads refuse, but a
|
|
17
|
+
* link minted earlier keeps reading (STORE_MODULES rule 4).
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.SERVICE_TRACKING_HANDLE_RE = exports.SERVICE_TRACKING_PATH_PREFIX = exports.SERVICE_TRACKING_LINK_ACTIONS = exports.SERVICE_TRACKING_OPERATOR_ERROR_CODES = exports.SERVICE_TRACKING_ACCOUNT_ERROR_CODES = exports.SERVICE_TRACKING_ERROR_CODES = void 0;
|
|
21
|
+
/**
|
|
22
|
+
* The refusals the PUBLIC tracking routes answer themselves.
|
|
23
|
+
*
|
|
24
|
+
* - SERVICE_TRACKING_TOKEN_INVALID (401): every reason a handle cannot be
|
|
25
|
+
* honoured — missing, malformed, unknown, revoked, past the lifetime,
|
|
26
|
+
* presented on another store's host or with no store host at all, or naming
|
|
27
|
+
* an order that is gone or disabled — with one identical body, so a handle is
|
|
28
|
+
* never an oracle.
|
|
29
|
+
* - SERVICE_QUOTE_NOT_FOUND (404, `mode=quote-pdf` only): the order has no
|
|
30
|
+
* current presupuesto a customer was sent.
|
|
31
|
+
*/
|
|
32
|
+
exports.SERVICE_TRACKING_ERROR_CODES = ['SERVICE_TRACKING_TOKEN_INVALID', 'SERVICE_QUOTE_NOT_FOUND'];
|
|
33
|
+
/**
|
|
34
|
+
* The refusals of the signed-in reads (`GET /user?mode=service-orders` and
|
|
35
|
+
* `?mode=service-order&serviceOrderId=`).
|
|
36
|
+
*
|
|
37
|
+
* - SERVICE_TRACKING_DISABLED (403): the store's gate is off.
|
|
38
|
+
* - SERVICE_ORDER_NOT_FOUND (404): no such order, a disabled one, or one that
|
|
39
|
+
* belongs to another customer — one answer for all three.
|
|
40
|
+
*/
|
|
41
|
+
exports.SERVICE_TRACKING_ACCOUNT_ERROR_CODES = ['SERVICE_TRACKING_DISABLED', 'SERVICE_ORDER_NOT_FOUND'];
|
|
42
|
+
/**
|
|
43
|
+
* The refusals of the staff `POST /services { mode: 'trackingLink' }`, beside
|
|
44
|
+
* the router's own (`ROLE_FORBIDDEN`, `VALIDATION_FAILED`, `SERVICE_ORDER_NOT_FOUND`).
|
|
45
|
+
*
|
|
46
|
+
* - SERVICE_TRACKING_DISABLED (403, mint only): the store's gate is off.
|
|
47
|
+
* - SERVICE_TRACKING_NO_ADDRESS (409, mint only): the store has no public
|
|
48
|
+
* storefront address to build the link on (no slug, or public links off).
|
|
49
|
+
* - SERVICE_TRACKING_LINK_LAPSED (409, mint only): the order closed more than
|
|
50
|
+
* `serviceTrackingLinkDaysAfterClose` days ago, so a link would not read.
|
|
51
|
+
* - ADMIN_REQUIRED (403, revoke only): revoking takes ADMIN, SUPERVISOR or MANAGER.
|
|
52
|
+
*/
|
|
53
|
+
exports.SERVICE_TRACKING_OPERATOR_ERROR_CODES = [
|
|
54
|
+
'SERVICE_TRACKING_DISABLED',
|
|
55
|
+
'SERVICE_TRACKING_NO_ADDRESS',
|
|
56
|
+
'SERVICE_TRACKING_LINK_LAPSED',
|
|
57
|
+
'ADMIN_REQUIRED',
|
|
58
|
+
];
|
|
59
|
+
/** `POST /services { mode: 'trackingLink' }` actions. */
|
|
60
|
+
exports.SERVICE_TRACKING_LINK_ACTIONS = ['mint', 'revoke'];
|
|
61
|
+
/** The storefront path a tracking link opens: `/servicio/{handle}`. */
|
|
62
|
+
exports.SERVICE_TRACKING_PATH_PREFIX = '/servicio/';
|
|
63
|
+
/** The handle's exact shape: 16 base62 characters. */
|
|
64
|
+
exports.SERVICE_TRACKING_HANDLE_RE = /^[0-9A-Za-z]{16}$/;
|