platform-types 1.10.853 → 1.10.855
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 +1 -0
- package/dist/cjs/notification.js +19 -0
- package/dist/cjs/payment.js +44 -0
- package/dist/cjs/serviceOrder.js +5 -0
- package/dist/cjs/serviceTracking.js +56 -3
- package/dist/cjs/storefrontWebLiteralKeys.js +64 -9
- package/dist/customer.d.ts +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/notification.d.ts +24 -1
- package/dist/notification.js +19 -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 +33 -12
- package/dist/serviceOrder.js +5 -0
- package/dist/serviceTracking.d.ts +111 -4
- package/dist/serviceTracking.js +55 -2
- 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);
|
package/dist/cjs/notification.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.WEB_NOTIFICATIONS_MAX_LIMIT = exports.CUSTOMER_NOTIFICATION_TYPES = expo
|
|
|
7
7
|
// Addressed ERROR / ML_FACTURADOR_COLLISION / ML_SELLER_SWITCHED /
|
|
8
8
|
// ML_CONNECTION_BROKEN / MENTION / CUSTOMER_PENDING_APPROVAL /
|
|
9
9
|
// PAYMENT_ATTEMPT_RESOLUTION / CUSTOMER_WHOLESALE_REQUEST / STOREFRONT_QUOTE_REQUEST /
|
|
10
|
+
// SERVICE_QUOTE_DECIDED /
|
|
10
11
|
// REFUND_OBLIGATION_OVERDUE / MESSAGE_CREDITS_LOW / MESSAGE_SKIPPED_NO_CREDITS do not select a
|
|
11
12
|
// producer subscriber audience;
|
|
12
13
|
// the consumer still reads the live USER row for each delivery channel.
|
|
@@ -237,6 +238,24 @@ var NotificationTypeEnum;
|
|
|
237
238
|
* socket).
|
|
238
239
|
*/
|
|
239
240
|
NotificationTypeEnum["STOREFRONT_QUOTE_REQUEST"] = "STOREFRONT_QUOTE_REQUEST";
|
|
241
|
+
/**
|
|
242
|
+
* A customer approved or rejected a presupuesto online — by the tracking link
|
|
243
|
+
* (`POST /services/tracking/quote`) or signed in
|
|
244
|
+
* (`POST /user { mode: 'service-quote-decision' }`). The decision does not
|
|
245
|
+
* move the ticket's stage; the bell is the shop's cue to do it. Names the
|
|
246
|
+
* ticket number and the decision only — never the customer.
|
|
247
|
+
*
|
|
248
|
+
* ⚠️ ADMIN-ONLY, and its own preference key. The producer fans out to every
|
|
249
|
+
* non-disabled ADMIN of the store (`listActiveAdmins`), the same recipients
|
|
250
|
+
* as `STOREFRONT_QUOTE_REQUEST`. No legacy opt-in scan: the consumer enforces
|
|
251
|
+
* explicit type/channel preferences
|
|
252
|
+
* (`notificationPreferences.SERVICE_QUOTE_DECIDED`), default-on when absent.
|
|
253
|
+
* Additive — no backfill or consumer migration owed.
|
|
254
|
+
*
|
|
255
|
+
* ⚠️ Carries `serviceOrderRef` and NEVER a top-level `customerId` (an
|
|
256
|
+
* audience — the bell would reach the customer's own storefront socket).
|
|
257
|
+
*/
|
|
258
|
+
NotificationTypeEnum["SERVICE_QUOTE_DECIDED"] = "SERVICE_QUOTE_DECIDED";
|
|
240
259
|
/**
|
|
241
260
|
* A refund obligation went overdue: its due date passed and the hourly
|
|
242
261
|
* sweep, after reconciling the payment, still found the money owed
|
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
|
@@ -305,6 +305,11 @@ exports.SERVICE_ORDER_ERROR_CODES = [
|
|
|
305
305
|
'SERVICE_TRACKING_DISABLED',
|
|
306
306
|
'SERVICE_TRACKING_NO_ADDRESS',
|
|
307
307
|
'SERVICE_TRACKING_LINK_LAPSED',
|
|
308
|
+
// The customer's online quote decision (`_customerDecision.ts`, shared by the
|
|
309
|
+
// web routes) — see `SERVICE_TRACKING_DECISION_ERROR_CODES`
|
|
310
|
+
'SERVICE_TRACKING_VERIFICATION_FAILED',
|
|
311
|
+
// A tracking link revoked between the handle check and the decision's commit.
|
|
312
|
+
'SERVICE_TRACKING_TOKEN_INVALID',
|
|
308
313
|
'ADMIN_REQUIRED',
|
|
309
314
|
'CUSTODY_ILLEGAL_TRANSITION',
|
|
310
315
|
'SERVICE_ORDER_BILLABLE_FROZEN',
|