@vibedout/commerce 0.0.0-stage → 0.2.4
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/LICENSE +21 -0
- package/README.md +439 -2
- package/dist/discounts.d.mts +63 -0
- package/dist/discounts.mjs +185 -0
- package/dist/errors-DGzIIflf.d.mts +24 -0
- package/dist/errors.d.mts +2 -0
- package/dist/errors.mjs +64 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +11 -0
- package/dist/internal-p2FS_A4L.mjs +22 -0
- package/dist/lines.d.mts +142 -0
- package/dist/lines.mjs +385 -0
- package/dist/money-aFFtGd0E.mjs +166 -0
- package/dist/money.d.mts +50 -0
- package/dist/money.mjs +2 -0
- package/dist/payments.d.mts +32 -0
- package/dist/payments.mjs +193 -0
- package/dist/ports.d.mts +40 -0
- package/dist/ports.mjs +73 -0
- package/dist/snapshot.d.mts +41 -0
- package/dist/snapshot.mjs +46 -0
- package/dist/testing.d.mts +237 -0
- package/dist/testing.mjs +2318 -0
- package/dist/totals.d.mts +82 -0
- package/dist/totals.mjs +130 -0
- package/dist/types-BUyLunnB.d.mts +503 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +46 -0
- package/package.json +91 -3
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { CommerceError, problem } from "./errors.mjs";
|
|
2
|
+
import { i as isRecord } from "./internal-p2FS_A4L.mjs";
|
|
3
|
+
import { o as isMinorUnits, r as allocateMinor, s as lineTotalMinor } from "./money-aFFtGd0E.mjs";
|
|
4
|
+
//#region src/discounts.ts
|
|
5
|
+
/**
|
|
6
|
+
* What a discount did to each line (0.2.2). A discount port answers amounts for the order; a tax is worked on what each line costs after its
|
|
7
|
+
* discount, and an invoice (a CFDI) states a discount per line. These are the two ways to know that amount: the discount says which lines it
|
|
8
|
+
* came off ({@link AppliedDiscount.lines}), or it is spread over the lines in proportion to their totals.
|
|
9
|
+
*
|
|
10
|
+
* 0.2.4 makes that one notion for everyone who needs it: {@link attributeDiscounts} gives every discount its lines (the ones it said, else its
|
|
11
|
+
* share of what the others left), and {@link lineDiscountsOf} adds them up per line. The tax port is asked with that, `computeTotals` keeps it on
|
|
12
|
+
* each `PricedLine` and in `Totals.discounts`, `paymentRequestFrom` sends it to the payments port, and an order keeps it on its lines, so the
|
|
13
|
+
* tax, the provider's page and the invoice all state the same discount on the same line.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* `discountMinor` spread over the lines in proportion to their totals, in whole minor units that add up to it exactly (`allocateMinor`): what a
|
|
17
|
+
* tax provider does when the discounts do not say which lines they came off. No line gives more than its total. Throws {@link CommerceError}
|
|
18
|
+
* (`discount_exceeds_subtotal`) when the discount is more than the lines cost together.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* spreadDiscount([{ unitAmountMinor: 3000, quantity: 1 }, { unitAmountMinor: 1000, quantity: 1 }], 1000); // [750, 250]
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function spreadDiscount(lines, discountMinor) {
|
|
25
|
+
const totals = lines.map((line) => lineTotalMinor(line.unitAmountMinor, line.quantity));
|
|
26
|
+
if (!isMinorUnits(discountMinor)) throw new CommerceError("out_of_range", "a discount is a whole number of minor units, zero or more", "discountMinor");
|
|
27
|
+
if (discountMinor === 0) return totals.map(() => 0);
|
|
28
|
+
if (totals.length === 0) throw new CommerceError("discount_exceeds_subtotal", "there are no lines to take a discount from", "lines");
|
|
29
|
+
const sum = totals.reduce((total, amount) => total + amount, 0);
|
|
30
|
+
if (discountMinor > sum) throw new CommerceError("discount_exceeds_subtotal", `a discount of ${discountMinor} is more than the lines cost (${sum})`, "discountMinor");
|
|
31
|
+
return allocateMinor(discountMinor, totals);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The problems with the attributions of `discounts` over `lines`: every discount that takes something off says which lines it came off, each
|
|
35
|
+
* `index` is a line, the amounts are whole and add up to the discount's `amountMinor`, and no line is discounted by more than it costs. A
|
|
36
|
+
* discount of 0 needs nothing. An empty list means {@link lineDiscountsFrom} can answer.
|
|
37
|
+
*/
|
|
38
|
+
function checkLineDiscounts(lines, discounts, path = "discounts") {
|
|
39
|
+
const out = [];
|
|
40
|
+
const taken = lines.map(() => 0);
|
|
41
|
+
for (const [at, discount] of discounts.entries()) {
|
|
42
|
+
const where = `${path}[${at}]`;
|
|
43
|
+
if (!isRecord(discount) || !isMinorUnits(discount.amountMinor)) {
|
|
44
|
+
out.push(problem("invalid_adjustment", where, "a discount has a whole amount of minor units, zero or more"));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (discount.amountMinor === 0) continue;
|
|
48
|
+
if (!Array.isArray(discount.lines)) {
|
|
49
|
+
out.push(problem("invalid_adjustment", `${where}.lines`, "a discount that takes something off says which lines it came off"));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let sum = 0;
|
|
53
|
+
const seen = /* @__PURE__ */ new Set();
|
|
54
|
+
const parts = discount.lines;
|
|
55
|
+
for (const [position, part] of parts.entries()) {
|
|
56
|
+
const partPath = `${where}.lines[${position}]`;
|
|
57
|
+
const index = isRecord(part) ? part.index : void 0;
|
|
58
|
+
const amountMinor = isRecord(part) ? part.amountMinor : void 0;
|
|
59
|
+
if (typeof index !== "number" || !Number.isSafeInteger(index) || index < 0 || index >= lines.length) {
|
|
60
|
+
out.push(problem("invalid_adjustment", partPath, "index is the position of a line"));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!isMinorUnits(amountMinor)) {
|
|
64
|
+
out.push(problem("invalid_adjustment", partPath, "amountMinor is a whole number of minor units, zero or more"));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (seen.has(index)) {
|
|
68
|
+
out.push(problem("invalid_adjustment", partPath, "a line appears once in a discount"));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
seen.add(index);
|
|
72
|
+
sum += amountMinor;
|
|
73
|
+
taken[index] = taken[index] + amountMinor;
|
|
74
|
+
}
|
|
75
|
+
if (sum !== discount.amountMinor) out.push(problem("invalid_adjustment", `${where}.lines`, `the lines add up to ${sum}, the discount is ${discount.amountMinor}`));
|
|
76
|
+
}
|
|
77
|
+
for (const [index, line] of lines.entries()) if (isMinorUnits(line.unitAmountMinor) && Number.isSafeInteger(line.quantity) && line.quantity >= 1) {
|
|
78
|
+
const total = lineTotalMinor(line.unitAmountMinor, line.quantity);
|
|
79
|
+
if (taken[index] > total) out.push(problem("discount_exceeds_subtotal", `${path}.lines[${index}]`, `line ${index} is discounted by ${taken[index]}, it costs ${total}`));
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* What the discounts took off each line, in the order of `lines`, or `undefined` when they do not say (a discount of the whole order, or
|
|
85
|
+
* attributions that {@link checkLineDiscounts} refuses): then spread the total with {@link spreadDiscount}. Pass the result as
|
|
86
|
+
* `TaxInput.lineDiscountsMinor`.
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* const perLine = lineDiscountsFrom(lines, applied) ?? spreadDiscount(lines, discountMinor);
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
function lineDiscountsFrom(lines, discounts) {
|
|
93
|
+
if (checkLineDiscounts(lines, discounts).length > 0) return void 0;
|
|
94
|
+
const taken = lines.map(() => 0);
|
|
95
|
+
for (const discount of discounts) {
|
|
96
|
+
if (discount.amountMinor === 0) continue;
|
|
97
|
+
for (const part of discount.lines ?? []) taken[part.index] = taken[part.index] + part.amountMinor;
|
|
98
|
+
}
|
|
99
|
+
return taken;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The discounts with the lines each came off, every one that takes something off (0.2.4):
|
|
103
|
+
*
|
|
104
|
+
* - a discount that says its lines ({@link AppliedDiscount.lines}, valid by {@link checkLineDiscounts}) keeps them, as long as together no line
|
|
105
|
+
* is discounted by more than it costs;
|
|
106
|
+
* - a discount that does not (a code for the whole order), or whose lines are not valid, is spread over what the others left of the lines in
|
|
107
|
+
* proportion to it, to the centavo (`allocateMinor`), in the order the discounts are given;
|
|
108
|
+
* - when the attributions that were given would take more off a line than it costs, none of them is trusted and every discount is spread.
|
|
109
|
+
*
|
|
110
|
+
* With no discount that names its lines this is {@link spreadDiscount} of each, so a project that did not know about `lines` states what it did
|
|
111
|
+
* before. A discount of 0 is left as given, without lines. The amounts are the discounts' own; the answer adds up to them exactly. Throws
|
|
112
|
+
* {@link CommerceError}: `discount_exceeds_subtotal` when the discounts are more than the lines cost, `out_of_range` for an amount that is not whole.
|
|
113
|
+
*
|
|
114
|
+
* ```ts
|
|
115
|
+
* const lines = [{ unitAmountMinor: 3000, quantity: 5 }, { unitAmountMinor: 1000, quantity: 1 }];
|
|
116
|
+
* attributeDiscounts(lines, [{ title: "Pack", amountMinor: 1500, currency: "MXN", lines: [{ index: 0, amountMinor: 1500 }] }]);
|
|
117
|
+
* // the pack discount stays on the first line; the exempt second line is not discounted
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
function attributeDiscounts(lines, discounts) {
|
|
121
|
+
const totals = lines.map((line) => lineTotalMinor(line.unitAmountMinor, line.quantity));
|
|
122
|
+
const stands = discounts.map((discount) => discount.amountMinor > 0 && discount.lines !== void 0 && checkLineDiscounts(lines, [discount]).length === 0);
|
|
123
|
+
const claimed = lines.map(() => 0);
|
|
124
|
+
for (const [at, discount] of discounts.entries()) {
|
|
125
|
+
if (!stands[at]) continue;
|
|
126
|
+
for (const part of discount.lines ?? []) claimed[part.index] = claimed[part.index] + part.amountMinor;
|
|
127
|
+
}
|
|
128
|
+
const consistent = claimed.every((amount, index) => amount <= totals[index]);
|
|
129
|
+
const taken = lines.map(() => 0);
|
|
130
|
+
if (consistent) for (let index = 0; index < lines.length; index++) taken[index] = claimed[index];
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const [at, discount] of discounts.entries()) {
|
|
133
|
+
if (discount.amountMinor === 0) {
|
|
134
|
+
const { lines: _ignored, ...rest } = discount;
|
|
135
|
+
out.push(rest);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (stands[at] && consistent) {
|
|
139
|
+
const parts = (discount.lines ?? []).filter((part) => part.amountMinor > 0).map((part) => ({
|
|
140
|
+
index: part.index,
|
|
141
|
+
amountMinor: part.amountMinor
|
|
142
|
+
})).sort((a, b) => a.index - b.index);
|
|
143
|
+
out.push({
|
|
144
|
+
...discount,
|
|
145
|
+
lines: parts
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const room = totals.map((total, index) => total - taken[index]);
|
|
150
|
+
const left = room.reduce((sum, amount) => sum + amount, 0);
|
|
151
|
+
if (!isMinorUnits(discount.amountMinor)) throw new CommerceError("out_of_range", "a discount is a whole number of minor units, zero or more", "discountMinor");
|
|
152
|
+
if (discount.amountMinor > left) throw new CommerceError("discount_exceeds_subtotal", `a discount of ${discount.amountMinor} is more than the lines have left (${left})`, "discountMinor");
|
|
153
|
+
const shares = allocateMinor(discount.amountMinor, room);
|
|
154
|
+
const parts = [];
|
|
155
|
+
for (const [index, share] of shares.entries()) {
|
|
156
|
+
if (share === 0) continue;
|
|
157
|
+
parts.push({
|
|
158
|
+
index,
|
|
159
|
+
amountMinor: share
|
|
160
|
+
});
|
|
161
|
+
taken[index] = taken[index] + share;
|
|
162
|
+
}
|
|
163
|
+
out.push({
|
|
164
|
+
...discount,
|
|
165
|
+
lines: parts
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* What the discounts took off each line, in the order of `lines`: always an answer ({@link attributeDiscounts} added up per line), where
|
|
172
|
+
* {@link lineDiscountsFrom} answers nothing for a discount of the whole order. Pass it as `TaxInput.lineDiscountsMinor`; it is what an order
|
|
173
|
+
* keeps on each line (`PricedLine.discountMinor`) and what a provider's page and an invoice state.
|
|
174
|
+
*
|
|
175
|
+
* ```ts
|
|
176
|
+
* lineDiscountsOf(lines, applied); // [1500, 0]: the pack discount came off the keychain only
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
function lineDiscountsOf(lines, discounts) {
|
|
180
|
+
const perLine = lines.map(() => 0);
|
|
181
|
+
for (const discount of attributeDiscounts(lines, discounts)) for (const part of discount.lines ?? []) perLine[part.index] = perLine[part.index] + part.amountMinor;
|
|
182
|
+
return perLine;
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
export { attributeDiscounts, checkLineDiscounts, lineDiscountsFrom, lineDiscountsOf, spreadDiscount };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What can be wrong, as data. A check of untrusted input (a buyer's asks, the lines a source returns, a payment request) returns a list of
|
|
4
|
+
* {@link CommerceProblem} and never throws; a route maps `code` to its own error. Only the low-level helpers of `./money` throw, with a
|
|
5
|
+
* {@link CommerceError} that carries the same codes.
|
|
6
|
+
*/
|
|
7
|
+
declare const PROBLEM_CODES: readonly ["invalid_asks", "invalid_ref", "invalid_quantity", "invalid_options", "duplicate_ref", "invalid_source", "unknown_source", "invalid_lines", "line_count_mismatch", "line_ref_mismatch", "line_quantity_mismatch", "invalid_line", "invalid_title", "invalid_price", "invalid_currency", "invalid_availability", "invalid_max_quantity", "invalid_fulfilment", "invalid_stock_subject", "invalid_snapshot", "sold_out", "not_orderable", "not_found", "over_max_quantity", "empty_order", "currency_mismatch", "out_of_range", "invalid_adjustment", "discount_exceeds_subtotal", "tax_breakdown_mismatch", "invalid_payment", "payment_not_reconciled", "free_order", "invalid_port"];
|
|
8
|
+
type ProblemCode = (typeof PROBLEM_CODES)[number];
|
|
9
|
+
interface CommerceProblem {
|
|
10
|
+
code: ProblemCode;
|
|
11
|
+
/** Where, in the shape of the input: `asks[1].quantity`, `lines[0].unitAmountMinor`, `total`. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** For a log or a developer, in English. A person-facing text is the caller's, chosen by `code`. */
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
/** Thrown by the low-level money helpers and by `assertTotals`. `code` is the first problem's; `problems` has at least one entry. */
|
|
17
|
+
declare class CommerceError extends Error {
|
|
18
|
+
readonly code: ProblemCode;
|
|
19
|
+
readonly problems: readonly CommerceProblem[];
|
|
20
|
+
constructor(code: ProblemCode, message: string, path?: string, problems?: readonly CommerceProblem[]);
|
|
21
|
+
}
|
|
22
|
+
declare const problem: (code: ProblemCode, path: string, message: string) => CommerceProblem;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { problem as a, ProblemCode as i, CommerceProblem as n, PROBLEM_CODES as r, CommerceError as t };
|
package/dist/errors.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* What can be wrong, as data. A check of untrusted input (a buyer's asks, the lines a source returns, a payment request) returns a list of
|
|
4
|
+
* {@link CommerceProblem} and never throws; a route maps `code` to its own error. Only the low-level helpers of `./money` throw, with a
|
|
5
|
+
* {@link CommerceError} that carries the same codes.
|
|
6
|
+
*/
|
|
7
|
+
const PROBLEM_CODES = [
|
|
8
|
+
"invalid_asks",
|
|
9
|
+
"invalid_ref",
|
|
10
|
+
"invalid_quantity",
|
|
11
|
+
"invalid_options",
|
|
12
|
+
"duplicate_ref",
|
|
13
|
+
"invalid_source",
|
|
14
|
+
"unknown_source",
|
|
15
|
+
"invalid_lines",
|
|
16
|
+
"line_count_mismatch",
|
|
17
|
+
"line_ref_mismatch",
|
|
18
|
+
"line_quantity_mismatch",
|
|
19
|
+
"invalid_line",
|
|
20
|
+
"invalid_title",
|
|
21
|
+
"invalid_price",
|
|
22
|
+
"invalid_currency",
|
|
23
|
+
"invalid_availability",
|
|
24
|
+
"invalid_max_quantity",
|
|
25
|
+
"invalid_fulfilment",
|
|
26
|
+
"invalid_stock_subject",
|
|
27
|
+
"invalid_snapshot",
|
|
28
|
+
"sold_out",
|
|
29
|
+
"not_orderable",
|
|
30
|
+
"not_found",
|
|
31
|
+
"over_max_quantity",
|
|
32
|
+
"empty_order",
|
|
33
|
+
"currency_mismatch",
|
|
34
|
+
"out_of_range",
|
|
35
|
+
"invalid_adjustment",
|
|
36
|
+
"discount_exceeds_subtotal",
|
|
37
|
+
"tax_breakdown_mismatch",
|
|
38
|
+
"invalid_payment",
|
|
39
|
+
"payment_not_reconciled",
|
|
40
|
+
"free_order",
|
|
41
|
+
"invalid_port"
|
|
42
|
+
];
|
|
43
|
+
/** Thrown by the low-level money helpers and by `assertTotals`. `code` is the first problem's; `problems` has at least one entry. */
|
|
44
|
+
var CommerceError = class extends Error {
|
|
45
|
+
code;
|
|
46
|
+
problems;
|
|
47
|
+
constructor(code, message, path = "", problems) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "CommerceError";
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.problems = problems ?? [{
|
|
52
|
+
code,
|
|
53
|
+
path,
|
|
54
|
+
message
|
|
55
|
+
}];
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const problem = (code, path, message) => ({
|
|
59
|
+
code,
|
|
60
|
+
path,
|
|
61
|
+
message
|
|
62
|
+
});
|
|
63
|
+
//#endregion
|
|
64
|
+
export { CommerceError, PROBLEM_CODES, problem };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { a as problem, i as ProblemCode, n as CommerceProblem, r as PROBLEM_CODES, t as CommerceError } from "./errors-DGzIIflf.mjs";
|
|
2
|
+
import { $ as StartPaymentInput, A as LineHoldRequest, B as PaymentsPort, C as HoldStatus, D as LineAvailability, E as LineAsk, F as MAX_HOLD_MS, G as STOCK_TRACKINGS, H as PortName, I as PAYMENT_METHODS, J as ShippingRatesInput, K as SettleResult, L as PaymentAdjustment, M as LineSettleResult, N as LineSnapshot, O as LineDiscountPart, P as LineSource, Q as SnapshotValue, R as PaymentLine, S as HoldResult, T as LINE_AVAILABILITIES, U as PortProviders, V as PortContext, W as ResolvedLine, X as ShortAsk, Y as ShippingRatesPort, Z as ShortLine, _ as Fulfilment, a as CancelPaymentResult, at as TaxBreakdown, b as HoldLine, c as CommerceLocale, ct as TaxResult, d as DiscountInput, et as StartPaymentResult, f as DiscountPort, g as FindLineSource, h as FULFILMENTS, i as CancelPaymentFailure, it as StockTracking, j as LineHoldResult, k as LineHold, l as DISCOUNT_REJECTIONS, lt as Money, m as DiscountResult, n as AdjustmentKind, nt as StockPort, o as CartAsk, ot as TaxInput, p as DiscountRejection, q as ShippingRate, r as AppliedDiscount, rt as StockSubject, s as CartLine, st as TaxPort, t as ADJUSTMENT_KINDS, tt as StockLevel, u as Destination, v as HOLD_STATUSES, w as HoldingLineSource, x as HoldRequest, y as Hold, z as PaymentMethod } from "./types-BUyLunnB.mjs";
|
|
3
|
+
import { DiscountableLine, attributeDiscounts, checkLineDiscounts, lineDiscountsFrom, lineDiscountsOf, spreadDiscount } from "./discounts.mjs";
|
|
4
|
+
import { AskLimits, AsksResult, BlockedLine, BlockedReason, CartAskOptions, CartAsksResult, CartResult, DEFAULT_ASK_LIMITS, MAX_REF_LENGTH, MAX_SNAPSHOT_BYTES, ResolveResult, blockedProblem, blockedReason, checkResolvedLine, checkResolvedLines, holdLinesFrom, holdsCapacity, partitionLines, resolveCart, resolveLines, validateAsks, validateCartAsks } from "./lines.mjs";
|
|
5
|
+
import { MAX_RATE_BP, ROUNDINGS, Rounding, allocateMinor, basisPointsOf, isCurrencyCode, isMinorUnits, lineTotalMinor, subtractMinor, sumMinor, taxIncludedIn } from "./money.mjs";
|
|
6
|
+
import { ChosenShipping, PricedLine, Totals, TotalsInput, TotalsResult, assertTotals, computeTotals } from "./totals.mjs";
|
|
7
|
+
import { PaymentRequestParts, checkStartPayment, paymentRequestFrom } from "./payments.mjs";
|
|
8
|
+
import { OPTIONAL_PORT_METHODS, PORT_METHODS, PORT_NAMES, UnlistedPortMethods, checkPortProvider, isPortName } from "./ports.mjs";
|
|
9
|
+
import { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, SnapshotKey, SnapshotTextKey, snapshotText, snapshotWeightG } from "./snapshot.mjs";
|
|
10
|
+
//#region src/internal.d.ts
|
|
11
|
+
/** A non-empty title for a person, at most `max` long, with no space at either end. */
|
|
12
|
+
declare const isDisplayText: (value: unknown, max: number) => value is string;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { ADJUSTMENT_KINDS, AdjustmentKind, AppliedDiscount, AskLimits, AsksResult, BlockedLine, BlockedReason, CancelPaymentFailure, CancelPaymentResult, CartAsk, CartAskOptions, CartAsksResult, CartLine, CartResult, ChosenShipping, CommerceError, CommerceLocale, CommerceProblem, DEFAULT_ASK_LIMITS, DISCOUNT_REJECTIONS, Destination, DiscountInput, DiscountPort, DiscountRejection, DiscountResult, DiscountableLine, FULFILMENTS, FindLineSource, Fulfilment, HOLD_STATUSES, Hold, HoldLine, HoldRequest, HoldResult, HoldStatus, HoldingLineSource, LINE_AVAILABILITIES, LineAsk, LineAvailability, LineDiscountPart, LineHold, LineHoldRequest, LineHoldResult, LineSettleResult, LineSnapshot, LineSource, MAX_HOLD_MS, MAX_RATE_BP, MAX_REF_LENGTH, MAX_SNAPSHOT_BYTES, MAX_SNAPSHOT_TEXT, type Money, OPTIONAL_PORT_METHODS, PAYMENT_METHODS, PORT_METHODS, PORT_NAMES, PROBLEM_CODES, PaymentAdjustment, PaymentLine, PaymentMethod, PaymentRequestParts, PaymentsPort, PortContext, PortName, PortProviders, PricedLine, ProblemCode, ROUNDINGS, ResolveResult, ResolvedLine, Rounding, SNAPSHOT_KEYS, STOCK_TRACKINGS, SettleResult, ShippingRate, ShippingRatesInput, ShippingRatesPort, ShortAsk, ShortLine, SnapshotKey, SnapshotTextKey, SnapshotValue, StartPaymentInput, StartPaymentResult, StockLevel, StockPort, StockSubject, StockTracking, TaxBreakdown, TaxInput, TaxPort, TaxResult, Totals, TotalsInput, TotalsResult, UnlistedPortMethods, allocateMinor, assertTotals, attributeDiscounts, basisPointsOf, blockedProblem, blockedReason, checkLineDiscounts, checkPortProvider, checkResolvedLine, checkResolvedLines, checkStartPayment, computeTotals, holdLinesFrom, holdsCapacity, isCurrencyCode, isDisplayText, isMinorUnits, isPortName, lineDiscountsFrom, lineDiscountsOf, lineTotalMinor, partitionLines, paymentRequestFrom, problem, resolveCart, resolveLines, snapshotText, snapshotWeightG, spreadDiscount, subtractMinor, sumMinor, taxIncludedIn, validateAsks, validateCartAsks };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CommerceError, PROBLEM_CODES, problem } from "./errors.mjs";
|
|
2
|
+
import { r as isDisplayText } from "./internal-p2FS_A4L.mjs";
|
|
3
|
+
import { a as isCurrencyCode, c as subtractMinor, i as basisPointsOf, l as sumMinor, n as ROUNDINGS, o as isMinorUnits, r as allocateMinor, s as lineTotalMinor, t as MAX_RATE_BP, u as taxIncludedIn } from "./money-aFFtGd0E.mjs";
|
|
4
|
+
import { attributeDiscounts, checkLineDiscounts, lineDiscountsFrom, lineDiscountsOf, spreadDiscount } from "./discounts.mjs";
|
|
5
|
+
import { ADJUSTMENT_KINDS, DISCOUNT_REJECTIONS, FULFILMENTS, HOLD_STATUSES, LINE_AVAILABILITIES, MAX_HOLD_MS, PAYMENT_METHODS, STOCK_TRACKINGS } from "./types.mjs";
|
|
6
|
+
import { DEFAULT_ASK_LIMITS, MAX_REF_LENGTH, MAX_SNAPSHOT_BYTES, blockedProblem, blockedReason, checkResolvedLine, checkResolvedLines, holdLinesFrom, holdsCapacity, partitionLines, resolveCart, resolveLines, validateAsks, validateCartAsks } from "./lines.mjs";
|
|
7
|
+
import { checkStartPayment, paymentRequestFrom } from "./payments.mjs";
|
|
8
|
+
import { OPTIONAL_PORT_METHODS, PORT_METHODS, PORT_NAMES, checkPortProvider, isPortName } from "./ports.mjs";
|
|
9
|
+
import { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, snapshotText, snapshotWeightG } from "./snapshot.mjs";
|
|
10
|
+
import { assertTotals, computeTotals } from "./totals.mjs";
|
|
11
|
+
export { ADJUSTMENT_KINDS, CommerceError, DEFAULT_ASK_LIMITS, DISCOUNT_REJECTIONS, FULFILMENTS, HOLD_STATUSES, LINE_AVAILABILITIES, MAX_HOLD_MS, MAX_RATE_BP, MAX_REF_LENGTH, MAX_SNAPSHOT_BYTES, MAX_SNAPSHOT_TEXT, OPTIONAL_PORT_METHODS, PAYMENT_METHODS, PORT_METHODS, PORT_NAMES, PROBLEM_CODES, ROUNDINGS, SNAPSHOT_KEYS, STOCK_TRACKINGS, allocateMinor, assertTotals, attributeDiscounts, basisPointsOf, blockedProblem, blockedReason, checkLineDiscounts, checkPortProvider, checkResolvedLine, checkResolvedLines, checkStartPayment, computeTotals, holdLinesFrom, holdsCapacity, isCurrencyCode, isDisplayText, isMinorUnits, isPortName, lineDiscountsFrom, lineDiscountsOf, lineTotalMinor, partitionLines, paymentRequestFrom, problem, resolveCart, resolveLines, snapshotText, snapshotWeightG, spreadDiscount, subtractMinor, sumMinor, taxIncludedIn, validateAsks, validateCartAsks };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/internal.ts
|
|
2
|
+
/** Small checks the modules share. Not a public subpath. */
|
|
3
|
+
/**
|
|
4
|
+
* What an id, a ref, a key or an option value may not contain: control characters (C0 and C1), the invisible format characters (zero-width,
|
|
5
|
+
* bidirectional overrides, the soft hyphen, the byte order mark), the line and paragraph separators and lone surrogates.
|
|
6
|
+
*/
|
|
7
|
+
const UNSAFE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cs}]/u;
|
|
8
|
+
/**
|
|
9
|
+
* What a title shown to a person may not contain: the same, except the format characters a script or an emoji needs (the zero-width joiner of
|
|
10
|
+
* a family emoji, the non-joiner of Persian, the directional marks of Arabic and Hebrew), which stay; the bidirectional overrides and isolates,
|
|
11
|
+
* which can make a title read as something else, and the byte order mark go.
|
|
12
|
+
*/
|
|
13
|
+
const UNSAFE_DISPLAY = /[\p{Cc}\p{Zl}\p{Zp}\p{Cs}\u202a-\u202e\u2066-\u2069\ufeff]/u;
|
|
14
|
+
/** A subject type of the stock port: dotted lowercase words (`catalog.variant`, `menu.item`), so `type:id` can be split back. */
|
|
15
|
+
const SUBJECT_TYPE = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$/;
|
|
16
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
/** A non-empty string of printable characters (an id, a ref, a key), at most `max` long. */
|
|
18
|
+
const isText = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max && !UNSAFE.test(value);
|
|
19
|
+
/** A non-empty title for a person, at most `max` long, with no space at either end. */
|
|
20
|
+
const isDisplayText = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max && value === value.trim() && !UNSAFE_DISPLAY.test(value);
|
|
21
|
+
//#endregion
|
|
22
|
+
export { isText as a, isRecord as i, UNSAFE as n, isDisplayText as r, SUBJECT_TYPE as t };
|
package/dist/lines.d.mts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { n as CommerceProblem } from "./errors-DGzIIflf.mjs";
|
|
2
|
+
import { E as LineAsk, P as LineSource, V as PortContext, W as ResolvedLine, b as HoldLine, g as FindLineSource, o as CartAsk, rt as StockSubject, s as CartLine, w as HoldingLineSource } from "./types-BUyLunnB.mjs";
|
|
3
|
+
//#region src/lines.d.ts
|
|
4
|
+
interface AskLimits {
|
|
5
|
+
/** Lines in one request. */
|
|
6
|
+
maxLines: number;
|
|
7
|
+
/** One line's quantity. */
|
|
8
|
+
maxQuantity: number;
|
|
9
|
+
maxRefLength: number;
|
|
10
|
+
/** Options on one line. */
|
|
11
|
+
maxOptions: number;
|
|
12
|
+
}
|
|
13
|
+
declare const DEFAULT_ASK_LIMITS: AskLimits;
|
|
14
|
+
/** The longest ref or stock subject id there is: `maxRefLength` of {@link AskLimits} is cut to this, so a line can always be checked. */
|
|
15
|
+
declare const MAX_REF_LENGTH = 1024;
|
|
16
|
+
/** What a line's snapshot may weigh as JSON, in bytes: it is copied into every order line. */
|
|
17
|
+
declare const MAX_SNAPSHOT_BYTES = 2048;
|
|
18
|
+
type AsksResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
asks: LineAsk[];
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
problems: CommerceProblem[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Reads the `asks` of a request body. What comes out has exactly `ref`, `quantity` and (when given) `options`: anything else the browser sent,
|
|
27
|
+
* a `price` above all, is dropped, never read. A repeated ref is refused (the client sums its own quantities).
|
|
28
|
+
*/
|
|
29
|
+
declare function validateAsks(input: unknown, limits?: Partial<AskLimits>): AsksResult;
|
|
30
|
+
type CartAsksResult = {
|
|
31
|
+
ok: true;
|
|
32
|
+
asks: CartAsk[];
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
problems: CommerceProblem[];
|
|
36
|
+
};
|
|
37
|
+
interface CartAskOptions extends Partial<AskLimits> {
|
|
38
|
+
/**
|
|
39
|
+
* The registry's lookup (`(key) => linePort(key)...`, as {@link resolveCart} takes it). With it, an ask whose source nobody registered is
|
|
40
|
+
* refused as `unknown_source`, so a cart that names a feature that was removed stops before anything is held; without it only the key's
|
|
41
|
+
* shape is checked.
|
|
42
|
+
*/
|
|
43
|
+
find?: FindLineSource;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reads the lines of a cart: a list of `{ source, ref, quantity, options? }` (the same limits as {@link validateAsks}: 1 to 50 lines,
|
|
47
|
+
* quantities 1 to 99, refs of 1 to 120 printable characters) and gives back exactly those fields, in the order sent. It is the check a cart's
|
|
48
|
+
* `complete` runs on its lines BEFORE any price is asked and any place is held, so a hostile or stale cart costs nothing and holds nothing.
|
|
49
|
+
*
|
|
50
|
+
* - `invalid_source`: the source is not a key (dotted lowercase words, at most 80 characters), so it is never looked up;
|
|
51
|
+
* - `unknown_source`: with {@link CartAskOptions.find}, nothing is registered under the key;
|
|
52
|
+
* - `duplicate_ref`: the same source, ref and options twice (the same ref under two sources, or with other options, is two lines);
|
|
53
|
+
* - what {@link validateAsks} refuses for a ref, a quantity or the options, at the same paths (`asks[2].quantity`).
|
|
54
|
+
*
|
|
55
|
+
* Anything else in an ask, a price above all, is dropped and never read. Whether the thing exists and can be bought is the source's answer,
|
|
56
|
+
* not this check's: price the result with {@link resolveCart}.
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* const read = validateCartAsks(cart.lines, { find: (key) => { const found = linePort<LineSource>(key); return found.ok ? found.port : undefined; } });
|
|
60
|
+
* if (!read.ok) return refuse(read.problems);
|
|
61
|
+
* const priced = await resolveCart(find, ctx, read.asks);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
declare function validateCartAsks(input: unknown, options?: CartAskOptions): CartAsksResult;
|
|
65
|
+
/** The problems with one line, judged on its own: its shape and the values it may hold. Does not look at the ask it answers. */
|
|
66
|
+
declare function checkResolvedLine(line: unknown, path?: string): CommerceProblem[];
|
|
67
|
+
/** The problems with a source's whole answer to `asks`: one line per ask, in order, each valid and echoing its ask's ref and quantity. */
|
|
68
|
+
declare function checkResolvedLines(asks: readonly LineAsk[], lines: unknown, path?: string): CommerceProblem[];
|
|
69
|
+
type ResolveResult = {
|
|
70
|
+
ok: true;
|
|
71
|
+
lines: ResolvedLine[];
|
|
72
|
+
} | {
|
|
73
|
+
ok: false;
|
|
74
|
+
problems: CommerceProblem[];
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Asks a source for the lines and checks the answer, so nothing a source gets wrong reaches a total. The source is given frozen copies, so
|
|
78
|
+
* one that sorts or edits its input in place fails loudly instead of corrupting the caller's order. A source that throws is not caught: that
|
|
79
|
+
* is a fault, not a problem with the answer.
|
|
80
|
+
*/
|
|
81
|
+
declare function resolveLines(source: LineSource, ctx: PortContext, asks: readonly LineAsk[]): Promise<ResolveResult>;
|
|
82
|
+
type CartResult = {
|
|
83
|
+
ok: true;
|
|
84
|
+
lines: CartLine[];
|
|
85
|
+
} | {
|
|
86
|
+
ok: false;
|
|
87
|
+
problems: CommerceProblem[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Prices a cart whose lines name different sources: each source is asked once, for its own asks in the cart's order (through
|
|
91
|
+
* {@link resolveLines}, so its answer is checked and it is given frozen copies without the `source` key), the sources at the same time, and
|
|
92
|
+
* the lines come back one per ask in the cart's order, each with its `source`.
|
|
93
|
+
*
|
|
94
|
+
* - An ask whose source `find` does not have (a feature that was removed, a key nobody registered, an empty key) is a `not_found` line with a
|
|
95
|
+
* price of 0 in no currency (`XXX`), as a ref no source has: the cart shows it as no longer sold and totals the rest (`partitionLines`).
|
|
96
|
+
* - A source that answers wrong makes the whole answer `{ ok: false, problems }`, listed source by source; a problem with one line has the cart's
|
|
97
|
+
* line as its path (`lines[3].unitAmountMinor`), one with a source's whole answer (`line_count_mismatch`) the path `lines`, and every message
|
|
98
|
+
* starts with the source's key. A source that throws is not caught.
|
|
99
|
+
* - The asks are not checked here: check them where they come in (`validateAsks` for the ref, quantity and options; the registry's lookup
|
|
100
|
+
* finds only a key that was registered). A line not sold here shows its ref as its title.
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* const cart = await resolveCart((key) => { const found = linePort<LineSource>(key); return found.ok ? found.port : undefined; }, ctx, asks);
|
|
104
|
+
* if (!cart.ok) throw new Error(cart.problems[0]?.message);
|
|
105
|
+
* const { chargeable, blocked } = partitionLines(cart.lines);
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
declare function resolveCart(find: FindLineSource, ctx: PortContext, asks: readonly CartAsk[]): Promise<CartResult>;
|
|
109
|
+
/**
|
|
110
|
+
* Whether a source holds its own places (`hold`, `commit` and `release` are all there), so a checkout holds its asks beside the stock. A source
|
|
111
|
+
* with only some of the three is `false`, and a checkout would not hold its places: `checkPortProvider` and the line-source harness (its `holds`
|
|
112
|
+
* check) report such a source.
|
|
113
|
+
*/
|
|
114
|
+
declare function holdsCapacity(source: LineSource): source is HoldingLineSource;
|
|
115
|
+
type BlockedReason = "sold_out" | "not_orderable" | "not_found" | "over_max_quantity";
|
|
116
|
+
/** Why a (valid) line cannot be charged now, or `undefined` when it can. */
|
|
117
|
+
declare function blockedReason(line: ResolvedLine): BlockedReason | undefined;
|
|
118
|
+
declare function blockedProblem(line: ResolvedLine, path: string): CommerceProblem | undefined;
|
|
119
|
+
interface BlockedLine<Line extends ResolvedLine = ResolvedLine> {
|
|
120
|
+
/** The position in the list that was partitioned, which is the position of the ask. */
|
|
121
|
+
index: number;
|
|
122
|
+
line: Line;
|
|
123
|
+
reason: BlockedReason;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Splits valid lines into those that can be charged and those that cannot, with the reason: a cart shows the second group ("sold out", "only 2
|
|
127
|
+
* left") and totals the first. The lines keep their type, so a cart's lines keep their `source`.
|
|
128
|
+
*/
|
|
129
|
+
declare function partitionLines<Line extends ResolvedLine>(lines: readonly Line[]): {
|
|
130
|
+
chargeable: Line[];
|
|
131
|
+
blocked: BlockedLine<Line>[];
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* The lines to hold for these (resolved or priced) lines: each line that names a stock subject, with its quantity. Hold with this and not with
|
|
135
|
+
* the `ref`: a hold on a subject inventory does not know is taken as untracked, always available, with no error.
|
|
136
|
+
*/
|
|
137
|
+
declare function holdLinesFrom(lines: readonly {
|
|
138
|
+
stock?: StockSubject;
|
|
139
|
+
quantity: number;
|
|
140
|
+
}[]): HoldLine[];
|
|
141
|
+
//#endregion
|
|
142
|
+
export { AskLimits, AsksResult, BlockedLine, BlockedReason, CartAskOptions, CartAsksResult, CartResult, DEFAULT_ASK_LIMITS, MAX_REF_LENGTH, MAX_SNAPSHOT_BYTES, ResolveResult, blockedProblem, blockedReason, checkResolvedLine, checkResolvedLines, holdLinesFrom, holdsCapacity, partitionLines, resolveCart, resolveLines, validateAsks, validateCartAsks };
|