@vibedout/commerce 0.2.3 → 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/README.md CHANGED
@@ -27,7 +27,7 @@ in-memory slots, a fake payments port, and the suites a stock, a holding source
27
27
  | `@vibedout/commerce/money` | `isMinorUnits`, `isCurrencyCode`, `lineTotalMinor`, `sumMinor`, `subtractMinor`, `basisPointsOf`, `taxIncludedIn`, `allocateMinor`, `MAX_RATE_BP`, `ROUNDINGS` |
28
28
  | `@vibedout/commerce/payments` | `checkStartPayment`, `paymentRequestFrom` |
29
29
  | `@vibedout/commerce/ports` | `PORT_METHODS`, `PORT_NAMES`, `checkPortProvider` |
30
- | `@vibedout/commerce/discounts` | `spreadDiscount`, `lineDiscountsFrom`, `checkLineDiscounts` |
30
+ | `@vibedout/commerce/discounts` | `spreadDiscount`, `lineDiscountsFrom`, `checkLineDiscounts`, `attributeDiscounts`, `lineDiscountsOf` |
31
31
  | `@vibedout/commerce/snapshot` | `SNAPSHOT_KEYS`, `snapshotText` |
32
32
  | `@vibedout/commerce/errors` | `CommerceError`, `CommerceProblem`, `PROBLEM_CODES` |
33
33
  | `@vibedout/commerce/testing` | `runLineSourceConformance`, `checkLineSource`, `runStockConformance`, `createMemoryStock`, `runLineHoldConformance`, `createMemorySlots`, `runPaymentsConformance`, `createFakePayments`, `runTaxConformance`, `createFlatTax`, `runDiscountConformance`, `createFlatDiscount`, `testContext` |
@@ -238,6 +238,12 @@ payments port runs `checkStartPayment(input)` first and answers `invalid_request
238
238
  `free_order`: the order is paid with no provider. The discounts come off the items only: together they may not be more than the lines, even
239
239
  when the shipping and the tax would pay for them (a provider shows a discount on the item lines, and no line goes below zero).
240
240
 
241
+ **Each discount on its own lines (0.2.4).** A `discount` adjustment carries `lines` (`{ index, amountMinor }`, the position in `request.lines`), filled in by
242
+ `paymentRequestFrom` from the lines `computeTotals` worked out once (below), so the provider's page shows every line discounted by exactly what the order and
243
+ the invoice state for it: a 10% pack discount that came off the keychains does not touch the 0% card or the exempt booklet next to them. `checkStartPayment`
244
+ refuses `lines` that are not lines of the request, that repeat, that do not add up to the discount, or that take more off a line than it costs (together with the other
245
+ discounts); a discount that leaves them out is spread by the payments feature over what the others left.
246
+
241
247
  `cancel` is optional (0.2.0): the order behind a reference will not be paid (staff cancelled it, or its time ran out), so the provider's page
242
248
  stops taking money and a voucher stops being payable.
243
249
 
@@ -276,10 +282,27 @@ const perLine = lineDiscountsFrom(lines, applied) ?? spreadDiscount(lines, disco
276
282
  await tax.compute(ctx, { currency, lines, discountMinor, lineDiscountsMinor: perLine, shippingMinor, destination });
277
283
  ```
278
284
 
285
+ **One discount per line for everyone (0.2.4).** The two lines above answer nothing when only some of the discounts say their lines (a pack discount that does and a code
286
+ for the whole order that does not): the total is spread and the pack discount is lost on the line it belongs to. `lineDiscountsOf(lines, applied)` always answers: the
287
+ lines a discount names are kept, and a discount that names none (or whose lines `checkLineDiscounts` refuses) takes its share of what the others left, to the centavo
288
+ (`allocateMinor`); when the named lines together would ask more of a line than it costs, none is trusted and every discount is spread. `attributeDiscounts(lines,
289
+ applied)` is the same per discount, each with its `lines` filled in. `computeTotals` uses it once: `PricedLine.discountMinor` is what the discounts took off that line,
290
+ `Totals.discounts` carry their lines, and everything downstream states the same amounts: the tax port (`lineDiscountsOf` as `TaxInput.lineDiscountsMinor`),
291
+ `paymentRequestFrom` (each discount's `lines`), the order a checkout keeps on each of its lines, and the invoice (a CFDI states a discount on each concept).
292
+
293
+ ```ts
294
+ import { computeTotals, lineDiscountsOf } from "@vibedout/commerce";
295
+
296
+ const perLine = lineDiscountsOf(lines, applied); // [1500, 0, 0]: the pack discount came off the keychains only
297
+ const priced = computeTotals({ lines, discounts: applied });
298
+ priced.ok && priced.totals.lines.map((line) => line.discountMinor); // [1500, 0, 0], the same amounts
299
+ ```
300
+
279
301
  **What a source writes for the features that price and tax its lines (0.2.2).** The snapshot is free-form, but three keys are read by other
280
302
  features: `sku` (a discount rule names a SKU), `productId` (a rule covers every variant of a product) and `taxClass` (`standard`, `zero`, `exempt`:
281
303
  a tax provider maps it to a rate; not the SAT product key, which is `ResolvedLine.taxCode`). A source that sells goods writes them; a reader uses
282
304
  `snapshotText(line, "sku")`, which answers nothing for a missing or malformed value, and falls back to its default.
305
+ A fourth, for a source of goods that ship, is a number and not text: `weightG` (`SNAPSHOT_KEYS.weightG`), the grams of one unit, which the shipping feature adds up (with its box) to weigh the parcel a cart's rates are quoted for. Read it with `snapshotWeightG(line)`, which answers 0 for a missing, negative or non-numeric value, so a source that does not know the weight never makes a parcel heavier.
283
306
 
284
307
  A title is checked by `isDisplayText(value, max)` (0.2.2): one to `max` characters, no space at either end, no control character, line break or invisible override.
285
308
  `checkResolvedLine` and `computeTotals` refuse a line or a discount whose title fails it, so a feature that lets a person type such a title validates with it
@@ -386,6 +409,12 @@ line; two asks for one ref staying two lines. `checkLineSource(source, fixtures)
386
409
 
387
410
  ## Changelog
388
411
 
412
+ - 0.2.4: one discount per line for everyone who needs it. `attributeDiscounts` gives every discount the lines it came off (the ones it named, else its share of
413
+ what the others left) and `lineDiscountsOf` adds them up; `computeTotals` works them once (`PricedLine.discountMinor`, and `Totals.discounts` now carry their
414
+ `lines`); `PaymentAdjustment.lines` and `LineDiscountPart` (a discount adjustment says which of the request's lines it came off), set by `paymentRequestFrom` and
415
+ checked by `checkStartPayment`. Additive for a provider and a caller written for 0.2.3, with two things to know: `PricedLine` has a new field, so code that builds
416
+ one by hand adds `discountMinor`, and `Totals.discounts` are the given discounts with `lines` filled in (a discount of 0 has none). Also `SNAPSHOT_KEYS.weightG` and `snapshotWeightG`
417
+ (the grams of one unit a line source writes for the shipping feature's parcel); `snapshotText` takes the text keys only (`SnapshotTextKey`).
389
418
  - 0.2.3: `validateCartAsks`, the check a cart's `complete` runs on its lines before any price is asked and any place is held (`{ source, ref,
390
419
  quantity, options? }` per line: the source is a key, with `find` a registered one, the same source, ref and options twice are refused, the rest
391
420
  as `validateAsks`), with the problem codes `invalid_source` and `unknown_source`. Additive: `validateAsks` and everything else are as they were.
@@ -1,5 +1,5 @@
1
1
  import { n as CommerceProblem } from "./errors-DGzIIflf.mjs";
2
- import { U as ResolvedLine, r as AppliedDiscount } from "./types-BzlVbRp1.mjs";
2
+ import { W as ResolvedLine, r as AppliedDiscount } from "./types-BUyLunnB.mjs";
3
3
  //#region src/discounts.d.ts
4
4
  /** What a discount needs of a line: its price and quantity. A `ResolvedLine`, a `PricedLine` and a cart line all fit. */
5
5
  type DiscountableLine = Pick<ResolvedLine, "unitAmountMinor" | "quantity">;
@@ -29,5 +29,35 @@ declare function checkLineDiscounts(lines: readonly DiscountableLine[], discount
29
29
  * ```
30
30
  */
31
31
  declare function lineDiscountsFrom(lines: readonly DiscountableLine[], discounts: readonly AppliedDiscount[]): number[] | undefined;
32
+ /**
33
+ * The discounts with the lines each came off, every one that takes something off (0.2.4):
34
+ *
35
+ * - a discount that says its lines ({@link AppliedDiscount.lines}, valid by {@link checkLineDiscounts}) keeps them, as long as together no line
36
+ * is discounted by more than it costs;
37
+ * - 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
38
+ * proportion to it, to the centavo (`allocateMinor`), in the order the discounts are given;
39
+ * - 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.
40
+ *
41
+ * 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
42
+ * 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
43
+ * {@link CommerceError}: `discount_exceeds_subtotal` when the discounts are more than the lines cost, `out_of_range` for an amount that is not whole.
44
+ *
45
+ * ```ts
46
+ * const lines = [{ unitAmountMinor: 3000, quantity: 5 }, { unitAmountMinor: 1000, quantity: 1 }];
47
+ * attributeDiscounts(lines, [{ title: "Pack", amountMinor: 1500, currency: "MXN", lines: [{ index: 0, amountMinor: 1500 }] }]);
48
+ * // the pack discount stays on the first line; the exempt second line is not discounted
49
+ * ```
50
+ */
51
+ declare function attributeDiscounts(lines: readonly DiscountableLine[], discounts: readonly AppliedDiscount[]): AppliedDiscount[];
52
+ /**
53
+ * What the discounts took off each line, in the order of `lines`: always an answer ({@link attributeDiscounts} added up per line), where
54
+ * {@link lineDiscountsFrom} answers nothing for a discount of the whole order. Pass it as `TaxInput.lineDiscountsMinor`; it is what an order
55
+ * keeps on each line (`PricedLine.discountMinor`) and what a provider's page and an invoice state.
56
+ *
57
+ * ```ts
58
+ * lineDiscountsOf(lines, applied); // [1500, 0]: the pack discount came off the keychain only
59
+ * ```
60
+ */
61
+ declare function lineDiscountsOf(lines: readonly DiscountableLine[], discounts: readonly AppliedDiscount[]): number[];
32
62
  //#endregion
33
- export { DiscountableLine, checkLineDiscounts, lineDiscountsFrom, spreadDiscount };
63
+ export { DiscountableLine, attributeDiscounts, checkLineDiscounts, lineDiscountsFrom, lineDiscountsOf, spreadDiscount };
@@ -6,6 +6,11 @@ import { o as isMinorUnits, r as allocateMinor, s as lineTotalMinor } from "./mo
6
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
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
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.
9
14
  */
10
15
  /**
11
16
  * `discountMinor` spread over the lines in proportion to their totals, in whole minor units that add up to it exactly (`allocateMinor`): what a
@@ -93,5 +98,88 @@ function lineDiscountsFrom(lines, discounts) {
93
98
  }
94
99
  return taken;
95
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
+ }
96
184
  //#endregion
97
- export { checkLineDiscounts, lineDiscountsFrom, spreadDiscount };
185
+ export { attributeDiscounts, checkLineDiscounts, lineDiscountsFrom, lineDiscountsOf, spreadDiscount };
package/dist/index.d.mts CHANGED
@@ -1,14 +1,14 @@
1
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 StartPaymentResult, A as LineHoldResult, B as PortContext, C as HoldStatus, D as LineAvailability, E as LineAsk, F as PAYMENT_METHODS, G as SettleResult, H as PortProviders, I as PaymentAdjustment, J as ShippingRatesPort, K as ShippingRate, L as PaymentLine, M as LineSnapshot, N as LineSource, O as LineHold, P as MAX_HOLD_MS, Q as StartPaymentInput, R as PaymentMethod, S as HoldResult, T as LINE_AVAILABILITIES, U as ResolvedLine, V as PortName, W as STOCK_TRACKINGS, X as ShortLine, Y as ShortAsk, Z as SnapshotValue, _ as Fulfilment, a as CancelPaymentResult, at as TaxInput, b as HoldLine, c as CommerceLocale, ct as Money, d as DiscountInput, et as StockLevel, f as DiscountPort, g as FindLineSource, h as FULFILMENTS, i as CancelPaymentFailure, it as TaxBreakdown, j as LineSettleResult, k as LineHoldRequest, l as DISCOUNT_REJECTIONS, m as DiscountResult, n as AdjustmentKind, nt as StockSubject, o as CartAsk, ot as TaxPort, p as DiscountRejection, q as ShippingRatesInput, r as AppliedDiscount, rt as StockTracking, s as CartLine, st as TaxResult, t as ADJUSTMENT_KINDS, tt as StockPort, u as Destination, v as HOLD_STATUSES, w as HoldingLineSource, x as HoldRequest, y as Hold, z as PaymentsPort } from "./types-BzlVbRp1.mjs";
3
- import { DiscountableLine, checkLineDiscounts, lineDiscountsFrom, spreadDiscount } from "./discounts.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
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
5
  import { MAX_RATE_BP, ROUNDINGS, Rounding, allocateMinor, basisPointsOf, isCurrencyCode, isMinorUnits, lineTotalMinor, subtractMinor, sumMinor, taxIncludedIn } from "./money.mjs";
6
6
  import { ChosenShipping, PricedLine, Totals, TotalsInput, TotalsResult, assertTotals, computeTotals } from "./totals.mjs";
7
7
  import { PaymentRequestParts, checkStartPayment, paymentRequestFrom } from "./payments.mjs";
8
8
  import { OPTIONAL_PORT_METHODS, PORT_METHODS, PORT_NAMES, UnlistedPortMethods, checkPortProvider, isPortName } from "./ports.mjs";
9
- import { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, SnapshotKey, snapshotText } from "./snapshot.mjs";
9
+ import { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, SnapshotKey, SnapshotTextKey, snapshotText, snapshotWeightG } from "./snapshot.mjs";
10
10
  //#region src/internal.d.ts
11
11
  /** A non-empty title for a person, at most `max` long, with no space at either end. */
12
12
  declare const isDisplayText: (value: unknown, max: number) => value is string;
13
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, 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, SnapshotValue, StartPaymentInput, StartPaymentResult, StockLevel, StockPort, StockSubject, StockTracking, TaxBreakdown, TaxInput, TaxPort, TaxResult, Totals, TotalsInput, TotalsResult, UnlistedPortMethods, allocateMinor, assertTotals, basisPointsOf, blockedProblem, blockedReason, checkLineDiscounts, checkPortProvider, checkResolvedLine, checkResolvedLines, checkStartPayment, computeTotals, holdLinesFrom, holdsCapacity, isCurrencyCode, isDisplayText, isMinorUnits, isPortName, lineDiscountsFrom, lineTotalMinor, partitionLines, paymentRequestFrom, problem, resolveCart, resolveLines, snapshotText, spreadDiscount, subtractMinor, sumMinor, taxIncludedIn, validateAsks, validateCartAsks };
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 CHANGED
@@ -1,11 +1,11 @@
1
1
  import { CommerceError, PROBLEM_CODES, problem } from "./errors.mjs";
2
2
  import { r as isDisplayText } from "./internal-p2FS_A4L.mjs";
3
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 { checkLineDiscounts, lineDiscountsFrom, spreadDiscount } from "./discounts.mjs";
4
+ import { attributeDiscounts, checkLineDiscounts, lineDiscountsFrom, lineDiscountsOf, spreadDiscount } from "./discounts.mjs";
5
5
  import { ADJUSTMENT_KINDS, DISCOUNT_REJECTIONS, FULFILMENTS, HOLD_STATUSES, LINE_AVAILABILITIES, MAX_HOLD_MS, PAYMENT_METHODS, STOCK_TRACKINGS } from "./types.mjs";
6
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
7
  import { checkStartPayment, paymentRequestFrom } from "./payments.mjs";
8
8
  import { OPTIONAL_PORT_METHODS, PORT_METHODS, PORT_NAMES, checkPortProvider, isPortName } from "./ports.mjs";
9
- import { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, snapshotText } from "./snapshot.mjs";
9
+ import { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, snapshotText, snapshotWeightG } from "./snapshot.mjs";
10
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, basisPointsOf, blockedProblem, blockedReason, checkLineDiscounts, checkPortProvider, checkResolvedLine, checkResolvedLines, checkStartPayment, computeTotals, holdLinesFrom, holdsCapacity, isCurrencyCode, isDisplayText, isMinorUnits, isPortName, lineDiscountsFrom, lineTotalMinor, partitionLines, paymentRequestFrom, problem, resolveCart, resolveLines, snapshotText, spreadDiscount, subtractMinor, sumMinor, taxIncludedIn, validateAsks, validateCartAsks };
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 };
package/dist/lines.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as CommerceProblem } from "./errors-DGzIIflf.mjs";
2
- import { B as PortContext, E as LineAsk, N as LineSource, U as ResolvedLine, b as HoldLine, g as FindLineSource, nt as StockSubject, o as CartAsk, s as CartLine, w as HoldingLineSource } from "./types-BzlVbRp1.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
3
  //#region src/lines.d.ts
4
4
  interface AskLimits {
5
5
  /** Lines in one request. */
@@ -1,5 +1,5 @@
1
1
  import { n as CommerceProblem } from "./errors-DGzIIflf.mjs";
2
- import { Q as StartPaymentInput, R as PaymentMethod, c as CommerceLocale, n as AdjustmentKind } from "./types-BzlVbRp1.mjs";
2
+ import { $ as StartPaymentInput, c as CommerceLocale, n as AdjustmentKind, z as PaymentMethod } from "./types-BUyLunnB.mjs";
3
3
  import { Totals } from "./totals.mjs";
4
4
  //#region src/payments.d.ts
5
5
  /**
@@ -23,8 +23,9 @@ interface PaymentRequestParts {
23
23
  locale?: CommerceLocale;
24
24
  }
25
25
  /**
26
- * The request for `totals`: its lines, and its shipping, added tax and discounts as adjustments, so the provider's page and the order agree
27
- * to the centavo. It does not check anything; {@link checkStartPayment} does, and a total of zero is a free order that starts no payment.
26
+ * The request for `totals`: its lines, and its shipping, added tax and discounts as adjustments (each discount with the lines it came off,
27
+ * 0.2.4), so the provider's page and the order agree to the centavo, line by line. It does not check anything; {@link checkStartPayment} does, and a total of
28
+ * zero is a free order that starts no payment.
28
29
  */
29
30
  declare function paymentRequestFrom(totals: Totals, parts: PaymentRequestParts): StartPaymentInput;
30
31
  //#endregion
package/dist/payments.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { problem } from "./errors.mjs";
2
2
  import { a as isText, i as isRecord, r as isDisplayText } from "./internal-p2FS_A4L.mjs";
3
3
  import { a as isCurrencyCode, l as sumMinor, o as isMinorUnits, s as lineTotalMinor } from "./money-aFFtGd0E.mjs";
4
+ import { checkLineDiscounts } from "./discounts.mjs";
4
5
  import { ADJUSTMENT_KINDS, PAYMENT_METHODS } from "./types.mjs";
5
6
  //#region src/payments.ts
6
7
  /**
@@ -80,6 +81,27 @@ function checkStartPayment(input) {
80
81
  }
81
82
  if (!isMinorUnits(input.totalMinor)) add("invalid_payment", "totalMinor", "totalMinor is a whole number of minor units");
82
83
  if (out.length > 0) return out;
84
+ if (Array.isArray(input.adjustments) && Array.isArray(input.lines)) {
85
+ const lines = input.lines;
86
+ const named = input.adjustments.map((adjustment, index) => {
87
+ if (adjustment.lines !== void 0 && adjustment.kind !== "discount") add("invalid_payment", `adjustments[${index}].lines`, "only a discount says which lines it came off");
88
+ return adjustment.kind === "discount" && adjustment.lines !== void 0 ? {
89
+ title: adjustment.title,
90
+ currency: adjustment.currency,
91
+ amountMinor: adjustment.amountMinor,
92
+ lines: adjustment.lines
93
+ } : {
94
+ title: adjustment.title,
95
+ currency: adjustment.currency,
96
+ amountMinor: 0
97
+ };
98
+ });
99
+ for (const found of checkLineDiscounts(lines, named, "adjustments")) out.push(found.code === "discount_exceeds_subtotal" ? {
100
+ ...found,
101
+ path: found.path.replace(/^adjustments\./, "")
102
+ } : found);
103
+ if (out.length > 0) return out;
104
+ }
83
105
  const code = currency;
84
106
  try {
85
107
  const items = sumMinor(code, lineTotals);
@@ -110,8 +132,9 @@ const DEFAULT_LABELS = {
110
132
  }
111
133
  };
112
134
  /**
113
- * The request for `totals`: its lines, and its shipping, added tax and discounts as adjustments, so the provider's page and the order agree
114
- * to the centavo. It does not check anything; {@link checkStartPayment} does, and a total of zero is a free order that starts no payment.
135
+ * The request for `totals`: its lines, and its shipping, added tax and discounts as adjustments (each discount with the lines it came off,
136
+ * 0.2.4), so the provider's page and the order agree to the centavo, line by line. It does not check anything; {@link checkStartPayment} does, and a total of
137
+ * zero is a free order that starts no payment.
115
138
  */
116
139
  function paymentRequestFrom(totals, parts) {
117
140
  const labels = {
@@ -139,7 +162,11 @@ function paymentRequestFrom(totals, parts) {
139
162
  kind: "discount",
140
163
  title: discount.title || labels.discount,
141
164
  amountMinor: discount.amountMinor,
142
- currency
165
+ currency,
166
+ ...discount.lines ? { lines: discount.lines.map((part) => ({
167
+ index: part.index,
168
+ amountMinor: part.amountMinor
169
+ })) } : {}
143
170
  });
144
171
  return {
145
172
  reference: parts.reference,
package/dist/ports.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as CommerceProblem } from "./errors-DGzIIflf.mjs";
2
- import { H as PortProviders, V as PortName } from "./types-BzlVbRp1.mjs";
2
+ import { H as PortName, U as PortProviders } from "./types-BUyLunnB.mjs";
3
3
  //#region src/ports.d.ts
4
4
  type MethodsOf<K extends PortName> = Extract<keyof PortProviders[K], string>;
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { M as LineSnapshot } from "./types-BzlVbRp1.mjs";
1
+ import { N as LineSnapshot } from "./types-BUyLunnB.mjs";
2
2
  //#region src/snapshot.d.ts
3
3
  declare const SNAPSHOT_KEYS: {
4
4
  /** The stock-keeping unit: the code a discount rule names and a person reads on a packing slip. */
@@ -7,8 +7,12 @@ declare const SNAPSHOT_KEYS: {
7
7
  readonly productId: "productId";
8
8
  /** The tax class the goods are in (`standard`, `zero`, `exempt`), which a tax provider maps to a rate. Not the SAT product key (`ResolvedLine.taxCode`). */
9
9
  readonly taxClass: "taxClass";
10
+ /** The weight of one unit in grams (a number, not text: read it with {@link snapshotWeightG}). The shipping feature adds it up to weigh the parcel a cart's rates are quoted for. */
11
+ readonly weightG: "weightG";
10
12
  };
11
13
  type SnapshotKey = keyof typeof SNAPSHOT_KEYS;
14
+ /** The keys that hold text; `weightG` holds a number. */
15
+ type SnapshotTextKey = Exclude<SnapshotKey, "weightG">;
12
16
  /** The longest value of any of these keys. */
13
17
  declare const MAX_SNAPSHOT_TEXT = 120;
14
18
  /**
@@ -21,6 +25,17 @@ declare const MAX_SNAPSHOT_TEXT = 120;
21
25
  */
22
26
  declare function snapshotText(line: {
23
27
  snapshot?: LineSnapshot;
24
- }, key: SnapshotKey): string | undefined;
28
+ }, key: SnapshotTextKey): string | undefined;
29
+ /**
30
+ * The grams one unit of the line weighs, when its source wrote them under `weightG` (0.2.4), else 0: a missing, negative or non-numeric value weighs nothing, so a
31
+ * source that does not know the weight never makes a parcel heavier by accident.
32
+ *
33
+ * ```ts
34
+ * const grams = lines.reduce((sum, line) => sum + snapshotWeightG(line) * line.quantity, 0);
35
+ * ```
36
+ */
37
+ declare function snapshotWeightG(line: {
38
+ snapshot?: LineSnapshot;
39
+ }): number;
25
40
  //#endregion
26
- export { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, SnapshotKey, snapshotText };
41
+ export { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, SnapshotKey, SnapshotTextKey, snapshotText, snapshotWeightG };
package/dist/snapshot.mjs CHANGED
@@ -12,7 +12,9 @@ const SNAPSHOT_KEYS = {
12
12
  /** The id of the product the line is a variant of, in the source's own ids: a discount rule can cover every variant of a product. */
13
13
  productId: "productId",
14
14
  /** The tax class the goods are in (`standard`, `zero`, `exempt`), which a tax provider maps to a rate. Not the SAT product key (`ResolvedLine.taxCode`). */
15
- taxClass: "taxClass"
15
+ taxClass: "taxClass",
16
+ /** The weight of one unit in grams (a number, not text: read it with {@link snapshotWeightG}). The shipping feature adds it up to weigh the parcel a cart's rates are quoted for. */
17
+ weightG: "weightG"
16
18
  };
17
19
  /** The longest value of any of these keys. */
18
20
  const MAX_SNAPSHOT_TEXT = 120;
@@ -28,5 +30,17 @@ function snapshotText(line, key) {
28
30
  const value = line.snapshot?.[SNAPSHOT_KEYS[key]];
29
31
  return isText(value, 120) ? value : void 0;
30
32
  }
33
+ /**
34
+ * The grams one unit of the line weighs, when its source wrote them under `weightG` (0.2.4), else 0: a missing, negative or non-numeric value weighs nothing, so a
35
+ * source that does not know the weight never makes a parcel heavier by accident.
36
+ *
37
+ * ```ts
38
+ * const grams = lines.reduce((sum, line) => sum + snapshotWeightG(line) * line.quantity, 0);
39
+ * ```
40
+ */
41
+ function snapshotWeightG(line) {
42
+ const value = line.snapshot?.[SNAPSHOT_KEYS.weightG];
43
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
44
+ }
31
45
  //#endregion
32
- export { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, snapshotText };
46
+ export { MAX_SNAPSHOT_TEXT, SNAPSHOT_KEYS, snapshotText, snapshotWeightG };
@@ -1,5 +1,5 @@
1
1
  import { n as CommerceProblem } from "./errors-DGzIIflf.mjs";
2
- import { $ as StartPaymentResult, B as PortContext, D as LineAvailability, N as LineSource, Q as StartPaymentInput, d as DiscountInput, f as DiscountPort, nt as StockSubject, ot as TaxPort, tt as StockPort, u as Destination, w as HoldingLineSource, z as PaymentsPort } from "./types-BzlVbRp1.mjs";
2
+ import { $ as StartPaymentInput, B as PaymentsPort, D as LineAvailability, P as LineSource, V as PortContext, d as DiscountInput, et as StartPaymentResult, f as DiscountPort, nt as StockPort, rt as StockSubject, st as TaxPort, u as Destination, w as HoldingLineSource } from "./types-BUyLunnB.mjs";
3
3
  //#region src/testing/context.d.ts
4
4
  /** 2026-10-08 12:00 UTC: a fixed "now" for tests, so a hold that expires does so when the test says. */
5
5
  declare const TEST_NOW: number;
package/dist/totals.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as CommerceProblem } from "./errors-DGzIIflf.mjs";
2
- import { M as LineSnapshot, U as ResolvedLine, _ as Fulfilment, it as TaxBreakdown, nt as StockSubject, r as AppliedDiscount, st as TaxResult } from "./types-BzlVbRp1.mjs";
2
+ import { N as LineSnapshot, W as ResolvedLine, _ as Fulfilment, at as TaxBreakdown, ct as TaxResult, r as AppliedDiscount, rt as StockSubject } from "./types-BUyLunnB.mjs";
3
3
  //#region src/totals.d.ts
4
4
  interface PricedLine {
5
5
  ref: string;
@@ -8,6 +8,12 @@ interface PricedLine {
8
8
  unitAmountMinor: number;
9
9
  /** `unitAmountMinor * quantity`. */
10
10
  lineTotalMinor: number;
11
+ /**
12
+ * What the discounts took off this line (0.2.4), never more than `lineTotalMinor`; the lines' add up to `Totals.discountMinor`. The line's
13
+ * share is what the discount said (`AppliedDiscount.lines`) or its part of the rest, worked once here (`attributeDiscounts`), so the tax, the
14
+ * payment request and the invoice state the same discount on the same line.
15
+ */
16
+ discountMinor: number;
11
17
  currency: string;
12
18
  taxCode?: string;
13
19
  taxIncluded: boolean;
@@ -40,6 +46,7 @@ interface Totals {
40
46
  currency: string;
41
47
  lines: readonly PricedLine[];
42
48
  subtotalMinor: number;
49
+ /** The discounts as given, each with the lines it came off (0.2.4: `attributeDiscounts`; a discount of 0 has none). */
43
50
  discounts: readonly AppliedDiscount[];
44
51
  discountMinor: number;
45
52
  shipping: ChosenShipping | null;
package/dist/totals.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CommerceError, problem } from "./errors.mjs";
2
2
  import { i as isRecord, r as isDisplayText } from "./internal-p2FS_A4L.mjs";
3
3
  import { a as isCurrencyCode, l as sumMinor, o as isMinorUnits, s as lineTotalMinor } from "./money-aFFtGd0E.mjs";
4
+ import { attributeDiscounts } from "./discounts.mjs";
4
5
  import { blockedProblem, checkResolvedLine } from "./lines.mjs";
5
6
  //#region src/totals.ts
6
7
  /**
@@ -70,6 +71,7 @@ function computeTotals(input) {
70
71
  quantity: line.quantity,
71
72
  unitAmountMinor: line.unitAmountMinor,
72
73
  lineTotalMinor: lineTotalMinor(line.unitAmountMinor, line.quantity),
74
+ discountMinor: 0,
73
75
  currency,
74
76
  ...line.taxCode === void 0 ? {} : { taxCode: line.taxCode },
75
77
  taxIncluded: line.taxIncluded,
@@ -80,6 +82,11 @@ function computeTotals(input) {
80
82
  const subtotalMinor = sumMinor(currency, priced.map((line) => line.lineTotalMinor));
81
83
  const discountMinor = sumMinor(currency, discounts.map((discount) => discount.amountMinor));
82
84
  if (discountMinor > subtotalMinor) return refuse(problem("discount_exceeds_subtotal", "discounts", `discounts of ${discountMinor} exceed the subtotal of ${subtotalMinor}`));
85
+ const attributed = attributeDiscounts(lines, discounts);
86
+ for (const discount of attributed) for (const part of discount.lines ?? []) {
87
+ const line = priced[part.index];
88
+ line.discountMinor += part.amountMinor;
89
+ }
83
90
  const shippingMinor = shipping?.amountMinor ?? 0;
84
91
  const taxMinor = tax?.addedMinor ?? 0;
85
92
  const taxIncludedMinor = tax?.includedMinor ?? 0;
@@ -94,7 +101,7 @@ function computeTotals(input) {
94
101
  currency,
95
102
  lines: priced,
96
103
  subtotalMinor,
97
- discounts,
104
+ discounts: attributed,
98
105
  discountMinor,
99
106
  shipping,
100
107
  shippingMinor,
@@ -290,12 +290,24 @@ interface PaymentLine {
290
290
  }
291
291
  declare const ADJUSTMENT_KINDS: readonly ["shipping", "tax", "discount"];
292
292
  type AdjustmentKind = (typeof ADJUSTMENT_KINDS)[number];
293
+ /** How much of a discount came off one line: `index` is the position of the line in the list the discount is about. */
294
+ interface LineDiscountPart {
295
+ index: number;
296
+ amountMinor: number;
297
+ }
293
298
  /** A charge on top of the lines (`shipping`, `tax`) or a reduction (`discount`). `amountMinor` is never negative: the kind says the sign. */
294
299
  interface PaymentAdjustment {
295
300
  kind: AdjustmentKind;
296
301
  title: string;
297
302
  amountMinor: number;
298
303
  currency: string;
304
+ /**
305
+ * Optional (0.2.4), `discount` only: which of the request's `lines` it came off (`index` is the position in `StartPaymentInput.lines`), so
306
+ * the provider's page shows each line discounted by what the invoice states for it. The amounts add up to `amountMinor` and no line gives
307
+ * more than it costs ({@link checkLineDiscounts}). A discount that leaves it out is spread over what the other discounts left of the lines
308
+ * (the payments feature's own rule). `paymentRequestFrom` fills it in from `Totals.discounts`.
309
+ */
310
+ lines?: readonly LineDiscountPart[];
299
311
  }
300
312
  interface StartPaymentInput {
301
313
  /** The order's id, set by the server. The provider's idempotency key derives from it: the same reference is the same payment. */
@@ -449,12 +461,10 @@ interface AppliedDiscount {
449
461
  currency: string;
450
462
  /**
451
463
  * Optional (0.2.2): which lines it came off, so a tax and an invoice can state each line's discount. `index` is the line's position in
452
- * `DiscountInput.lines`; the amounts add up to `amountMinor` and no line gives more than its total. A discount of the whole order leaves it out.
464
+ * `DiscountInput.lines`; the amounts add up to `amountMinor` and no line gives more than its total. A discount of the whole order leaves it out
465
+ * (`attributeDiscounts`, 0.2.4, spreads it over what the others left of the lines).
453
466
  */
454
- lines?: readonly {
455
- index: number;
456
- amountMinor: number;
457
- }[];
467
+ lines?: readonly LineDiscountPart[];
458
468
  }
459
469
  declare const DISCOUNT_REJECTIONS: readonly ["unknown", "expired", "not_applicable", "used_up", "minimum_not_met"];
460
470
  type DiscountRejection = (typeof DISCOUNT_REJECTIONS)[number];
@@ -490,4 +500,4 @@ interface PortProviders {
490
500
  }
491
501
  type PortName = keyof PortProviders;
492
502
  //#endregion
493
- export { StartPaymentResult as $, LineHoldResult as A, PortContext as B, HoldStatus as C, LineAvailability as D, LineAsk as E, PAYMENT_METHODS as F, SettleResult as G, PortProviders as H, PaymentAdjustment as I, ShippingRatesPort as J, ShippingRate as K, PaymentLine as L, LineSnapshot as M, LineSource as N, LineHold as O, MAX_HOLD_MS as P, StartPaymentInput as Q, PaymentMethod as R, HoldResult as S, LINE_AVAILABILITIES as T, ResolvedLine as U, PortName as V, STOCK_TRACKINGS as W, ShortLine as X, ShortAsk as Y, SnapshotValue as Z, Fulfilment as _, CancelPaymentResult as a, TaxInput as at, HoldLine as b, CommerceLocale as c, Money as ct, DiscountInput as d, StockLevel as et, DiscountPort as f, FindLineSource as g, FULFILMENTS as h, CancelPaymentFailure as i, TaxBreakdown as it, LineSettleResult as j, LineHoldRequest as k, DISCOUNT_REJECTIONS as l, DiscountResult as m, AdjustmentKind as n, StockSubject as nt, CartAsk as o, TaxPort as ot, DiscountRejection as p, ShippingRatesInput as q, AppliedDiscount as r, StockTracking as rt, CartLine as s, TaxResult as st, ADJUSTMENT_KINDS as t, StockPort as tt, Destination as u, HOLD_STATUSES as v, HoldingLineSource as w, HoldRequest as x, Hold as y, PaymentsPort as z };
503
+ export { StartPaymentInput as $, LineHoldRequest as A, PaymentsPort as B, HoldStatus as C, LineAvailability as D, LineAsk as E, MAX_HOLD_MS as F, STOCK_TRACKINGS as G, PortName as H, PAYMENT_METHODS as I, ShippingRatesInput as J, SettleResult as K, PaymentAdjustment as L, LineSettleResult as M, LineSnapshot as N, LineDiscountPart as O, LineSource as P, SnapshotValue as Q, PaymentLine as R, HoldResult as S, LINE_AVAILABILITIES as T, PortProviders as U, PortContext as V, ResolvedLine as W, ShortAsk as X, ShippingRatesPort as Y, ShortLine as Z, Fulfilment as _, CancelPaymentResult as a, TaxBreakdown as at, HoldLine as b, CommerceLocale as c, TaxResult as ct, DiscountInput as d, StartPaymentResult as et, DiscountPort as f, FindLineSource as g, FULFILMENTS as h, CancelPaymentFailure as i, StockTracking as it, LineHoldResult as j, LineHold as k, DISCOUNT_REJECTIONS as l, Money as lt, DiscountResult as m, AdjustmentKind as n, StockPort as nt, CartAsk as o, TaxInput as ot, DiscountRejection as p, ShippingRate as q, AppliedDiscount as r, StockSubject as rt, CartLine as s, TaxPort as st, ADJUSTMENT_KINDS as t, StockLevel as tt, Destination as u, HOLD_STATUSES as v, HoldingLineSource as w, HoldRequest as x, Hold as y, PaymentMethod as z };
package/dist/types.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { $ as StartPaymentResult, A as LineHoldResult, B as PortContext, C as HoldStatus, D as LineAvailability, E as LineAsk, F as PAYMENT_METHODS, G as SettleResult, H as PortProviders, I as PaymentAdjustment, J as ShippingRatesPort, K as ShippingRate, L as PaymentLine, M as LineSnapshot, N as LineSource, O as LineHold, P as MAX_HOLD_MS, Q as StartPaymentInput, R as PaymentMethod, S as HoldResult, T as LINE_AVAILABILITIES, U as ResolvedLine, V as PortName, W as STOCK_TRACKINGS, X as ShortLine, Y as ShortAsk, Z as SnapshotValue, _ as Fulfilment, a as CancelPaymentResult, at as TaxInput, b as HoldLine, c as CommerceLocale, ct as Money, d as DiscountInput, et as StockLevel, f as DiscountPort, g as FindLineSource, h as FULFILMENTS, i as CancelPaymentFailure, it as TaxBreakdown, j as LineSettleResult, k as LineHoldRequest, l as DISCOUNT_REJECTIONS, m as DiscountResult, n as AdjustmentKind, nt as StockSubject, o as CartAsk, ot as TaxPort, p as DiscountRejection, q as ShippingRatesInput, r as AppliedDiscount, rt as StockTracking, s as CartLine, st as TaxResult, t as ADJUSTMENT_KINDS, tt as StockPort, u as Destination, v as HOLD_STATUSES, w as HoldingLineSource, x as HoldRequest, y as Hold, z as PaymentsPort } from "./types-BzlVbRp1.mjs";
2
- export { ADJUSTMENT_KINDS, AdjustmentKind, AppliedDiscount, CancelPaymentFailure, CancelPaymentResult, CartAsk, CartLine, CommerceLocale, DISCOUNT_REJECTIONS, Destination, DiscountInput, DiscountPort, DiscountRejection, DiscountResult, FULFILMENTS, FindLineSource, Fulfilment, HOLD_STATUSES, Hold, HoldLine, HoldRequest, HoldResult, HoldStatus, HoldingLineSource, LINE_AVAILABILITIES, LineAsk, LineAvailability, LineHold, LineHoldRequest, LineHoldResult, LineSettleResult, LineSnapshot, LineSource, MAX_HOLD_MS, type Money, PAYMENT_METHODS, PaymentAdjustment, PaymentLine, PaymentMethod, PaymentsPort, PortContext, PortName, PortProviders, ResolvedLine, STOCK_TRACKINGS, SettleResult, ShippingRate, ShippingRatesInput, ShippingRatesPort, ShortAsk, ShortLine, SnapshotValue, StartPaymentInput, StartPaymentResult, StockLevel, StockPort, StockSubject, StockTracking, TaxBreakdown, TaxInput, TaxPort, TaxResult };
1
+ 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";
2
+ export { ADJUSTMENT_KINDS, AdjustmentKind, AppliedDiscount, CancelPaymentFailure, CancelPaymentResult, CartAsk, CartLine, CommerceLocale, DISCOUNT_REJECTIONS, Destination, DiscountInput, DiscountPort, DiscountRejection, DiscountResult, 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, type Money, PAYMENT_METHODS, PaymentAdjustment, PaymentLine, PaymentMethod, PaymentsPort, PortContext, PortName, PortProviders, ResolvedLine, STOCK_TRACKINGS, SettleResult, ShippingRate, ShippingRatesInput, ShippingRatesPort, ShortAsk, ShortLine, SnapshotValue, StartPaymentInput, StartPaymentResult, StockLevel, StockPort, StockSubject, StockTracking, TaxBreakdown, TaxInput, TaxPort, TaxResult };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedout/commerce",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The contracts a store is built from on Cloudflare Workers, as types and pure functions: line sources that price what a buyer asks for on the server (ResolvedLine in integer minor units with its availability, maximum quantity and a snapshot) and may hold places of their own, the stock port (hold with an expiry, commit, release, fulfil, uncommit), the payments port (start, cancel), and the tax, shipping-rates and discount ports; checks for buyer input and for what a source returns; a cart priced across sources; totals in minor units that refuse a mixed currency; a discount split over lines to the centavo; and test helpers (a line-source harness, an in-memory stock and slots, a fake payments port, and the suites a stock, a holding source and a payments provider pass). No floats, no runtime dependencies.",