@tallyui/pos 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +42 -8
- package/dist/index.js +325 -53
- package/package.json +6 -6
- package/src/index.ts +1 -1
- package/src/order/order-builder.ts +62 -20
- package/src/order/order-drafts.ts +1 -0
- package/src/order/types.ts +11 -3
- package/src/order/woocommerce-tax.ts +68 -0
- package/src/outbox/index.ts +1 -1
- package/src/outbox/order-outbox.ts +5 -3
- package/src/outbox/types.ts +8 -1
- package/src/pos-order/command.ts +21 -6
- package/src/pos-order/finalize.ts +37 -6
- package/src/pos-order/schema.ts +41 -5
- package/src/pos-order/types.ts +9 -4
- package/src/receipt/build-receipt-data.ts +3 -1
- package/src/register/closure-document.ts +1 -1
- package/src/register/session-store.ts +5 -3
- package/src/sale/cart.ts +1 -0
- package/src/sale/use-sale.ts +3 -0
- package/src/store-settings/map-store-settings.ts +5 -1
- package/src/tax/exact.ts +3 -1
- package/src/tax/index.ts +1 -1
- package/src/tax/tax-provider.tsx +9 -4
- package/src/tax/types.ts +6 -1
package/dist/index.js
CHANGED
|
@@ -109,6 +109,84 @@ function useCurrencyCode() {
|
|
|
109
109
|
return ctx.currencyCode;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// src/order/woocommerce-tax.ts
|
|
113
|
+
import { minorUnitDigits, woocommerceTax } from "@tallyui/core";
|
|
114
|
+
function woocommerceLine(amountMinor, rates, inclusive, currency, roundAtSubtotal, pricesIncludeTax = inclusive) {
|
|
115
|
+
const dp = minorUnitDigits(currency), factor = 10 ** dp;
|
|
116
|
+
const precision = woocommerceTax.getRoundingPrecision(dp);
|
|
117
|
+
const input = {
|
|
118
|
+
amount: amountMinor / factor,
|
|
119
|
+
rates: rates.map((rate) => ({ ...rate, order: rate.priority })),
|
|
120
|
+
amountIncludesTax: inclusive,
|
|
121
|
+
dp,
|
|
122
|
+
perRatePrecision: precision
|
|
123
|
+
};
|
|
124
|
+
let result = woocommerceTax.calculateTaxes(input);
|
|
125
|
+
const net = woocommerceTax.roundHalfUp(input.amount - (inclusive ? result.total : 0), precision);
|
|
126
|
+
if (inclusive && rates.length) result = woocommerceTax.calculateTaxes({ ...input, amount: net, amountIncludesTax: false });
|
|
127
|
+
const taxLines = result.taxes.map((tax2) => {
|
|
128
|
+
const rate = rates.find((rate2) => rate2.id === tax2.id);
|
|
129
|
+
return {
|
|
130
|
+
code: rate.code,
|
|
131
|
+
rateId: rate.id,
|
|
132
|
+
compound: rate.compound,
|
|
133
|
+
ratePpm: Math.round(Number(rate.rate) * 1e4),
|
|
134
|
+
taxMicros: String(Math.round(tax2.total * factor * 1e6))
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
const tax = result.taxes.reduce((sum, rate) => sum + (roundAtSubtotal ? rate.total : woocommerceTax.roundTaxTotal(rate.total, dp, pricesIncludeTax)), 0);
|
|
138
|
+
return {
|
|
139
|
+
netMinor: Math.round(woocommerceTax.roundHalfUp(net, dp) * factor),
|
|
140
|
+
netMicros: String(Math.round(net * factor * 1e6)),
|
|
141
|
+
totalMinor: Math.round(woocommerceTax.roundHalfUp(net + tax, dp) * factor),
|
|
142
|
+
taxLines,
|
|
143
|
+
taxMicros: taxLines.reduce((sum, rate) => sum + BigInt(rate.taxMicros), 0n).toString()
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function woocommerceTotals(lines, fees, shipping, currency, roundAtSubtotal, inclusive) {
|
|
147
|
+
const dp = minorUnitDigits(currency), factor = 10 ** dp;
|
|
148
|
+
const net = (line) => Number(line.netMicros) / (factor * 1e6);
|
|
149
|
+
const productNet = lines.reduce((sum, line) => sum + (roundAtSubtotal ? net(line) : woocommerceTax.roundHalfUp(net(line), dp)), 0);
|
|
150
|
+
const amount = productNet + fees.reduce((sum, line) => sum + net(line), 0) + shipping.reduce((sum, line) => sum + woocommerceTax.roundHalfUp(net(line), dp), 0);
|
|
151
|
+
const taxes = (items) => woocommerceTax.roundHalfUp(items.reduce((sum, line) => line.taxLines.reduce((sum2, tax2) => sum2 + (roundAtSubtotal ? Number(tax2.taxMicros) / (factor * 1e6) : woocommerceTax.roundTaxTotal(Number(tax2.taxMicros) / (factor * 1e6), dp, inclusive)), sum), 0), woocommerceTax.getRoundingPrecision(dp));
|
|
152
|
+
const tax = woocommerceTax.roundHalfUp(taxes([...lines, ...fees]) + taxes(shipping), 6);
|
|
153
|
+
return {
|
|
154
|
+
subtotalMinor: Math.round(woocommerceTax.roundHalfUp(productNet, dp) * factor),
|
|
155
|
+
taxMinor: Math.round((roundAtSubtotal ? woocommerceTax.roundHalfUp(tax, dp) : woocommerceTax.roundTaxTotal(tax, dp, inclusive)) * factor),
|
|
156
|
+
totalMinor: Math.round(woocommerceTax.roundHalfUp(woocommerceTax.roundHalfUp(amount + tax, 6), dp) * factor)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function woocommerceTaxByRate(lines, roundAtSubtotal, taxLabels, orderTaxMinor) {
|
|
160
|
+
const groups = /* @__PURE__ */ new Map();
|
|
161
|
+
for (const line of lines) for (const tax of line.taxLines) {
|
|
162
|
+
const key = JSON.stringify([tax.rateId, tax.code, tax.ratePpm]);
|
|
163
|
+
const row = groups.get(key) ?? {
|
|
164
|
+
code: tax.code,
|
|
165
|
+
ratePpm: tax.ratePpm,
|
|
166
|
+
label: taxLabels?.[tax.ratePpm] ?? `Tax ${tax.ratePpm / 1e4}%`,
|
|
167
|
+
netMinor: 0,
|
|
168
|
+
amountMinor: 0,
|
|
169
|
+
amount: 0,
|
|
170
|
+
inclusive: line.taxInclusive
|
|
171
|
+
};
|
|
172
|
+
const amount = Number(tax.taxMicros) / 1e6;
|
|
173
|
+
row.amount += roundAtSubtotal ? amount : woocommerceTax.roundTaxTotal(amount, 0, line.taxInclusive);
|
|
174
|
+
row.netMinor += line.netMinor;
|
|
175
|
+
groups.set(key, row);
|
|
176
|
+
}
|
|
177
|
+
const rows = [...groups.values()].map(({ amount, inclusive, ...row }) => ({
|
|
178
|
+
...row,
|
|
179
|
+
amountMinor: woocommerceTax.roundTaxTotal(woocommerceTax.roundHalfUp(amount, 6), 0, inclusive)
|
|
180
|
+
}));
|
|
181
|
+
if (orderTaxMinor !== void 0 && rows.length) {
|
|
182
|
+
const residue = orderTaxMinor - rows.reduce((sum, row) => sum + row.amountMinor, 0);
|
|
183
|
+
const bound = Math.floor(rows.length / 2) + 1;
|
|
184
|
+
if (Math.abs(residue) > bound) throw new Error(`woocommerce tax by rate: residue ${residue} exceeds its bound ${bound} (ADR-076)`);
|
|
185
|
+
rows[rows.length - 1].amountMinor += residue;
|
|
186
|
+
}
|
|
187
|
+
return rows;
|
|
188
|
+
}
|
|
189
|
+
|
|
112
190
|
// src/tax/exact.ts
|
|
113
191
|
var MICROS_PER_MINOR = 1000000n;
|
|
114
192
|
var DEFAULT_TAX_ROUNDING = { granularity: "per_order", mode: "half_away_from_zero" };
|
|
@@ -211,6 +289,7 @@ function roundedTaxByRate(lines, rounding) {
|
|
|
211
289
|
return { baseMinor: Number(baseMinor), taxMinor: rows.reduce((sum, row) => sum + row.amountMinor, 0), rates: rows };
|
|
212
290
|
}
|
|
213
291
|
function taxLinesByRate(lines, orderTaxMinor, taxLabels, rounding) {
|
|
292
|
+
if (rounding?.granularity === "woocommerce" && lines.every((line) => line.netMicros !== void 0)) return woocommerceTaxByRate(lines, rounding.roundAtSubtotal, taxLabels, orderTaxMinor);
|
|
214
293
|
const label = (ratePpm) => taxLabels?.[ratePpm] ?? `Tax ${ratePpm / 1e4}%`;
|
|
215
294
|
const rounded = roundedTaxByRate(lines, rounding);
|
|
216
295
|
if (rounded) return rounded.rates.map(({ code, ratePpm, netMinor, amountMinor }) => ({ label: label(ratePpm), code, ratePpm, netMinor, amountMinor }));
|
|
@@ -244,10 +323,11 @@ function taxLinesByRate(lines, orderTaxMinor, taxLabels, rounding) {
|
|
|
244
323
|
|
|
245
324
|
// src/tax/tax-provider.tsx
|
|
246
325
|
import { createContext as createContext2, useContext as useContext2, useMemo as useMemo2 } from "react";
|
|
326
|
+
import { woocommerceTax as woocommerceTax2 } from "@tallyui/core";
|
|
247
327
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
248
328
|
var taxLogger = createLogger("tax");
|
|
249
329
|
var TaxCtx = createContext2(null);
|
|
250
|
-
function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, children }) {
|
|
330
|
+
function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs, children }) {
|
|
251
331
|
const value = useMemo2(
|
|
252
332
|
() => {
|
|
253
333
|
if (Object.values(ratesPpm).some((rate) => !Number.isSafeInteger(rate) || rate < 0)) {
|
|
@@ -255,6 +335,7 @@ function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, children
|
|
|
255
335
|
}
|
|
256
336
|
const unknown = /* @__PURE__ */ new Set();
|
|
257
337
|
return {
|
|
338
|
+
...taxRates ? { getTaxRates: (taxClass, options) => (taxRates[woocommerceTax2.normalizeTaxClass(taxClass)] ?? []).filter((rate) => !options?.shipping || rate.shipping) } : {},
|
|
258
339
|
getTaxRatePpm: (taxClass) => {
|
|
259
340
|
if (taxClass !== void 0 && Object.hasOwn(ratesPpm, taxClass)) return ratesPpm[taxClass];
|
|
260
341
|
if (taxClass !== void 0 && !unknown.has(taxClass)) {
|
|
@@ -269,10 +350,12 @@ function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, children
|
|
|
269
350
|
return rateCodes && Object.hasOwn(rateCodes, key) ? rateCodes[key] : void 0;
|
|
270
351
|
},
|
|
271
352
|
pricesIncludeTax,
|
|
272
|
-
...rounding ? { rounding } : {}
|
|
353
|
+
...rounding ? { rounding } : {},
|
|
354
|
+
...shippingTaxClass !== void 0 ? { shippingTaxClass } : {},
|
|
355
|
+
...taxClassSlugs !== void 0 ? { taxClassSlugs } : {}
|
|
273
356
|
};
|
|
274
357
|
},
|
|
275
|
-
[ratesPpm, pricesIncludeTax, rounding, rateCodes]
|
|
358
|
+
[ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs]
|
|
276
359
|
);
|
|
277
360
|
return /* @__PURE__ */ jsx2(TaxCtx.Provider, { value, children });
|
|
278
361
|
}
|
|
@@ -319,7 +402,16 @@ function withPricingContext(context, settings) {
|
|
|
319
402
|
}
|
|
320
403
|
function taxProviderProps(settings) {
|
|
321
404
|
const rounding = settings.taxRounding?.granularity === "custom" ? void 0 : settings.taxRounding;
|
|
322
|
-
return {
|
|
405
|
+
return {
|
|
406
|
+
ratesPpm: settings.taxRatesPpm,
|
|
407
|
+
pricesIncludeTax: settings.pricesIncludeTax,
|
|
408
|
+
...rounding && { rounding },
|
|
409
|
+
...settings.taxRateCodes && { rateCodes: settings.taxRateCodes },
|
|
410
|
+
...settings.taxRates && { taxRates: settings.taxRates },
|
|
411
|
+
...settings.taxRoundAtSubtotal !== void 0 && { taxRoundAtSubtotal: settings.taxRoundAtSubtotal },
|
|
412
|
+
...settings.shippingTaxClass !== void 0 && { shippingTaxClass: settings.shippingTaxClass },
|
|
413
|
+
...settings.taxClassSlugs && { taxClassSlugs: settings.taxClassSlugs }
|
|
414
|
+
};
|
|
323
415
|
}
|
|
324
416
|
|
|
325
417
|
// src/store-settings/use-store-settings.ts
|
|
@@ -464,7 +556,7 @@ function createRepository(collection) {
|
|
|
464
556
|
|
|
465
557
|
// src/order/order-builder.ts
|
|
466
558
|
import { BehaviorSubject } from "rxjs";
|
|
467
|
-
import { resolvePrice } from "@tallyui/core";
|
|
559
|
+
import { resolvePrice, woocommerceTax as woocommerceTax3 } from "@tallyui/core";
|
|
468
560
|
|
|
469
561
|
// src/order/allocate-order-discount.ts
|
|
470
562
|
function allocateOrderDiscount(lineAmountsMinor, totalMinor) {
|
|
@@ -522,6 +614,13 @@ function createOrderBuilder(options) {
|
|
|
522
614
|
const { taxContext } = options;
|
|
523
615
|
const currency = options.currency.toUpperCase();
|
|
524
616
|
const orderId = options.id ?? uid();
|
|
617
|
+
const woo = taxContext.rounding?.granularity === "woocommerce" && taxContext.getTaxRates ? taxContext.rounding : void 0;
|
|
618
|
+
const wooLine = (amount, taxClass, inclusive, untaxed = false, shipping2 = false) => woocommerceLine(amount, untaxed ? [] : taxContext.getTaxRates(taxClass, { shipping: shipping2 }), inclusive, currency, woo.roundAtSubtotal, taxContext.pricesIncludeTax);
|
|
619
|
+
const displayAmount = (line, amount) => {
|
|
620
|
+
if (!woo || line.taxInclusive === taxContext.pricesIncludeTax) return toDisplayMode(line, amount, taxContext.pricesIncludeTax);
|
|
621
|
+
const item = wooLine(amount, line.taxClass, line.taxInclusive, line.taxStatus === "none");
|
|
622
|
+
return taxContext.pricesIncludeTax ? item.totalMinor : item.netMinor;
|
|
623
|
+
};
|
|
525
624
|
let lineItems = [];
|
|
526
625
|
let fees = [];
|
|
527
626
|
let shipping = [];
|
|
@@ -542,6 +641,13 @@ function createOrderBuilder(options) {
|
|
|
542
641
|
const lineDiscountMinor = Math.min(grossMinor, recalcedDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
|
|
543
642
|
const discountMinor = lineDiscountMinor + orderDiscountMinor;
|
|
544
643
|
const netMinor = grossMinor - discountMinor;
|
|
644
|
+
if (woo) return {
|
|
645
|
+
...line,
|
|
646
|
+
discounts: recalcedDiscounts,
|
|
647
|
+
discountMinor,
|
|
648
|
+
orderDiscountMinor,
|
|
649
|
+
...wooLine(netMinor, line.taxClass, line.taxInclusive, line.taxStatus === "none")
|
|
650
|
+
};
|
|
545
651
|
const combinedRate = line.taxLines.reduce((sum, tax) => sum + tax.ratePpm, 0);
|
|
546
652
|
const inclusiveTax = line.taxInclusive ? taxMicros(netMinor, combinedRate, true) : 0n;
|
|
547
653
|
let allocatedTax = 0n;
|
|
@@ -568,7 +674,7 @@ function createOrderBuilder(options) {
|
|
|
568
674
|
return Math.max(0, Math.min(discount.value, base));
|
|
569
675
|
}
|
|
570
676
|
function buildOrder() {
|
|
571
|
-
const lineAmounts = lineItems.map((li) => li.netMinor);
|
|
677
|
+
const lineAmounts = lineItems.map((li) => woo ? li.unitPriceMinor * li.quantity - li.discountMinor : li.netMinor);
|
|
572
678
|
const base = lineAmounts.reduce((sum, amount) => sum + Math.max(0, amount), 0);
|
|
573
679
|
let remaining = base;
|
|
574
680
|
const recalcedOrderDiscounts = orderDiscounts.map((d) => {
|
|
@@ -578,14 +684,16 @@ function createOrderBuilder(options) {
|
|
|
578
684
|
});
|
|
579
685
|
const shares = allocateOrderDiscount(lineAmounts, recalcedOrderDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
|
|
580
686
|
const lines = lineItems.map((li, index) => recalculateLine(li, shares[index]));
|
|
581
|
-
|
|
687
|
+
if (woo) shipping = shipping.map((charge) => ({ ...charge, ...recalculateCharge(charge.id, charge, true) }));
|
|
688
|
+
const wooTotals = woo ? woocommerceTotals(lines, fees, shipping, currency, woo.roundAtSubtotal, taxContext.pricesIncludeTax) : void 0;
|
|
689
|
+
const { subtotalMinor } = wooTotals ?? sumLines(lines, taxContext.rounding);
|
|
582
690
|
const charges = [...fees, ...shipping];
|
|
583
691
|
const taxedLines = [...lines, ...charges.map((charge) => ({
|
|
584
692
|
...charge,
|
|
585
693
|
taxInclusive: taxContext.pricesIncludeTax,
|
|
586
694
|
taxMicros: charge.taxLines.reduce((sum, tax) => sum + BigInt(tax.taxMicros), 0n).toString()
|
|
587
695
|
}))];
|
|
588
|
-
const { taxMinor, totalMinor } = sumLines(taxedLines, taxContext.rounding);
|
|
696
|
+
const { taxMinor, totalMinor } = wooTotals ?? sumLines(taxedLines, taxContext.rounding);
|
|
589
697
|
if (taxContext.rounding?.granularity === "per_rate_group_items" && taxedLines.some((li) => li.taxInclusive) && !perRateGroupWarned.has(taxContext)) {
|
|
590
698
|
perRateGroupWarned.add(taxContext);
|
|
591
699
|
taxLogger.warn("per_rate_group_items with inclusive lines: using per_order figures until the display has a rounding row (#287, #310)");
|
|
@@ -597,11 +705,11 @@ function createOrderBuilder(options) {
|
|
|
597
705
|
const discounts = returned ? [] : li.discounts.map((d) => ({
|
|
598
706
|
discountId: d.id,
|
|
599
707
|
...d.label !== void 0 ? { label: d.label } : {},
|
|
600
|
-
amountMinor:
|
|
708
|
+
amountMinor: displayAmount(li, d.amountMinor)
|
|
601
709
|
}));
|
|
602
|
-
const shareMinor =
|
|
603
|
-
const remainingMinor = toDisplayMode(li, li.netMinor, taxInclusive);
|
|
604
|
-
const amountMinor = li.taxInclusive === taxInclusive && !returned ? li.unitPriceMinor * li.quantity : discounts.reduce((sum, d) => sum + d.amountMinor, remainingMinor + shareMinor);
|
|
710
|
+
const shareMinor = displayAmount(li, li.orderDiscountMinor);
|
|
711
|
+
const remainingMinor = woo ? taxInclusive ? li.totalMinor : li.netMinor : toDisplayMode(li, li.netMinor, taxInclusive);
|
|
712
|
+
const amountMinor = !woo && li.taxInclusive === taxInclusive && !returned ? li.unitPriceMinor * li.quantity : discounts.reduce((sum, d) => sum + d.amountMinor, remainingMinor + shareMinor);
|
|
605
713
|
return { line: { lineId: li.id, amountMinor, discounts }, shareMinor, remainingMinor, returned };
|
|
606
714
|
});
|
|
607
715
|
const displayLines = figures.map((f) => f.line);
|
|
@@ -610,8 +718,17 @@ function createOrderBuilder(options) {
|
|
|
610
718
|
(sum, line) => line.discounts.reduce((lineSum, d) => lineSum + d.amountMinor, sum),
|
|
611
719
|
orderDiscountDisplay
|
|
612
720
|
);
|
|
613
|
-
const
|
|
614
|
-
const
|
|
721
|
+
const chargeRows = (items) => items.map(({ id: id2, name, amountMinor, netMinor, totalMinor: totalMinor2 }) => ({ id: id2, name, amountMinor: woo ? taxInclusive ? totalMinor2 : netMinor : amountMinor }));
|
|
722
|
+
const displayFees = chargeRows(fees), displayShipping = chargeRows(shipping);
|
|
723
|
+
let displaySubtotal = (taxInclusive ? totalMinor : totalMinor - taxMinor) + displayDiscount - [...displayFees, ...displayShipping].reduce((sum, charge) => sum + charge.amountMinor, 0);
|
|
724
|
+
let residue = displaySubtotal - displayLines.reduce((sum, line) => sum + line.amountMinor, 0);
|
|
725
|
+
if (woo) {
|
|
726
|
+
const charge = displayShipping.at(-1) ?? displayFees.at(-1);
|
|
727
|
+
const last = charge ?? displayLines.at(-1);
|
|
728
|
+
if (last) last.amountMinor += residue;
|
|
729
|
+
if (charge) displaySubtotal -= residue;
|
|
730
|
+
residue = displaySubtotal - displayLines.reduce((sum, line) => sum + line.amountMinor, 0);
|
|
731
|
+
}
|
|
615
732
|
const converted = lines.flatMap((li, index) => li.taxInclusive === taxInclusive ? [] : [index]);
|
|
616
733
|
const conversions = converted.reduce((count, index) => count + [
|
|
617
734
|
lines[index].netMinor,
|
|
@@ -631,7 +748,7 @@ function createOrderBuilder(options) {
|
|
|
631
748
|
if (displayDiscount < 0) throw new Error(`Display discount ${displayDiscount} is negative (ADR-063)`);
|
|
632
749
|
for (const { line, shareMinor, returned } of figures) {
|
|
633
750
|
if (returned && line.discounts.length > 0) throw new Error(`Return line ${line.lineId} shows discount rows (ADR-063)`);
|
|
634
|
-
if (returned) continue;
|
|
751
|
+
if (returned || woo) continue;
|
|
635
752
|
const rows = line.discounts.reduce((sum, d) => sum + d.amountMinor, 0);
|
|
636
753
|
if (shareMinor < 0 || line.discounts.some((d) => d.amountMinor < 0) || line.amountMinor < rows + shareMinor) {
|
|
637
754
|
throw new Error(`Display line ${line.lineId} shows ${line.amountMinor}, less than its rows ${rows} and share ${shareMinor} (ADR-063)`);
|
|
@@ -645,8 +762,8 @@ function createOrderBuilder(options) {
|
|
|
645
762
|
totalMinor,
|
|
646
763
|
lines: displayLines,
|
|
647
764
|
orderDiscountMinor: orderDiscountDisplay,
|
|
648
|
-
...fees.length ? { fees:
|
|
649
|
-
...shipping.length ? { shipping:
|
|
765
|
+
...fees.length ? { fees: displayFees } : {},
|
|
766
|
+
...shipping.length ? { shipping: displayShipping } : {}
|
|
650
767
|
};
|
|
651
768
|
const paidMinor = payments.reduce((sum, p) => sum + p.amountMinor, 0);
|
|
652
769
|
const balanceDueMinor = Math.max(0, totalMinor - paidMinor);
|
|
@@ -679,11 +796,33 @@ function createOrderBuilder(options) {
|
|
|
679
796
|
function emit() {
|
|
680
797
|
subject.next(buildOrder());
|
|
681
798
|
}
|
|
682
|
-
function recalculateCharge(id2, input) {
|
|
799
|
+
function recalculateCharge(id2, input, shipping2 = false) {
|
|
683
800
|
if (!Number.isInteger(input.amountMinor)) throw new RangeError("Amount must be integer minor units");
|
|
684
801
|
if (input.amountMinor < 0) throw new RangeError("Amount must be >= 0");
|
|
685
802
|
if (!input.name.trim()) throw new RangeError("Name must not be empty");
|
|
686
803
|
const taxStatus = input.taxStatus ?? "taxable";
|
|
804
|
+
if (woo) {
|
|
805
|
+
const taxClass = shipping2 ? taxContext.shippingTaxClass === void 0 || taxContext.shippingTaxClass === "inherit" ? woocommerceTax3.resolveInheritedShippingTaxClass(lineItems.map((line3) => ({
|
|
806
|
+
tax_class: line3.taxClass,
|
|
807
|
+
taxable: line3.taxStatus !== "none"
|
|
808
|
+
})), taxContext.taxClassSlugs ?? []) : taxContext.shippingTaxClass : input.taxClass;
|
|
809
|
+
const untaxed = shipping2 ? taxClass === woocommerceTax3.NO_SHIPPING_TAX : taxStatus === "none";
|
|
810
|
+
const line2 = wooLine(
|
|
811
|
+
input.amountMinor,
|
|
812
|
+
taxClass === woocommerceTax3.NO_SHIPPING_TAX ? void 0 : taxClass,
|
|
813
|
+
taxContext.pricesIncludeTax,
|
|
814
|
+
untaxed,
|
|
815
|
+
shipping2
|
|
816
|
+
);
|
|
817
|
+
return {
|
|
818
|
+
id: id2,
|
|
819
|
+
name: input.name,
|
|
820
|
+
amountMinor: input.amountMinor,
|
|
821
|
+
taxClass: input.taxClass,
|
|
822
|
+
taxStatus,
|
|
823
|
+
...line2
|
|
824
|
+
};
|
|
825
|
+
}
|
|
687
826
|
const code = taxContext.getTaxRateCode?.(input.taxClass);
|
|
688
827
|
const line = recalculateLine({
|
|
689
828
|
id: id2,
|
|
@@ -712,7 +851,7 @@ function createOrderBuilder(options) {
|
|
|
712
851
|
taxStatus,
|
|
713
852
|
taxLines: line.taxLines,
|
|
714
853
|
netMinor: line.netMinor,
|
|
715
|
-
taxMicros:
|
|
854
|
+
taxMicros: line.taxMicros
|
|
716
855
|
};
|
|
717
856
|
}
|
|
718
857
|
function addLine(input) {
|
|
@@ -721,11 +860,11 @@ function createOrderBuilder(options) {
|
|
|
721
860
|
if (unitPrice.currency !== currency) throw new RangeError("Line currency must match " + currency);
|
|
722
861
|
if (!Number.isInteger(unitPrice.amount)) throw new RangeError("Price must be integer minor units");
|
|
723
862
|
if (!Number.isInteger(quantity) || quantity < 1) throw new RangeError("Quantity must be an integer >= 1");
|
|
724
|
-
const code = input.taxRates ? void 0 : taxContext.getTaxRateCode?.(input.taxClass);
|
|
725
|
-
const taxRates = input.taxStatus === "none" ? [{ ...code !== void 0 ? { code } : {}, ratePpm: 0 }] : input.taxRates ?? [{ ...code !== void 0 ? { code } : {}, ratePpm: taxContext.getTaxRatePpm(input.taxClass) }];
|
|
863
|
+
const code = woo || input.taxRates ? void 0 : taxContext.getTaxRateCode?.(input.taxClass);
|
|
864
|
+
const taxRates = woo ? wooLine(unitPrice.amount * quantity, input.taxClass, unitPrice.taxInclusive ?? taxContext.pricesIncludeTax, input.taxStatus === "none").taxLines : input.taxStatus === "none" ? [{ ...code !== void 0 ? { code } : {}, ratePpm: 0 }] : input.taxRates ?? [{ ...code !== void 0 ? { code } : {}, ratePpm: taxContext.getTaxRatePpm(input.taxClass) }];
|
|
726
865
|
const taxInclusive = unitPrice.taxInclusive ?? taxContext.pricesIncludeTax;
|
|
727
866
|
const existing = lineItems.find(
|
|
728
|
-
(li) => !input.custom && !li.custom && li.productId === productId && li.variantId === variantId && li.taxInclusive === taxInclusive && li.unitPriceMinor === unitPrice.amount && li.taxLines.length === taxRates.length && li.taxLines.every((tax, index) => tax.code === taxRates[index].code && tax.ratePpm === taxRates[index].ratePpm)
|
|
867
|
+
(li) => !input.custom && !li.custom && li.productId === productId && li.variantId === variantId && li.taxInclusive === taxInclusive && (!woo || li.taxClass === input.taxClass && li.taxStatus === "none" === (input.taxStatus === "none")) && li.unitPriceMinor === unitPrice.amount && li.taxLines.length === taxRates.length && li.taxLines.every((tax, index) => tax.code === taxRates[index].code && tax.ratePpm === taxRates[index].ratePpm)
|
|
729
868
|
);
|
|
730
869
|
if (existing) {
|
|
731
870
|
lineItems = lineItems.map(
|
|
@@ -737,6 +876,7 @@ function createOrderBuilder(options) {
|
|
|
737
876
|
const lineId = uid();
|
|
738
877
|
const line = {
|
|
739
878
|
id: lineId,
|
|
879
|
+
...(input.custom ? input.taxClass !== void 0 : woo) ? { taxClass: input.taxClass } : {},
|
|
740
880
|
productId,
|
|
741
881
|
...input.custom ? { custom: true } : {},
|
|
742
882
|
...input.taxStatus === "none" ? { taxStatus: "none" } : {},
|
|
@@ -786,7 +926,7 @@ function createOrderBuilder(options) {
|
|
|
786
926
|
emit();
|
|
787
927
|
},
|
|
788
928
|
addShipping(input) {
|
|
789
|
-
const charge = { ...recalculateCharge(uid(), input), methodId: input.methodId };
|
|
929
|
+
const charge = { ...recalculateCharge(uid(), input, true), methodId: input.methodId };
|
|
790
930
|
shipping = [...shipping, charge];
|
|
791
931
|
emit();
|
|
792
932
|
return charge.id;
|
|
@@ -795,7 +935,7 @@ function createOrderBuilder(options) {
|
|
|
795
935
|
const charge = shipping.find((entry) => entry.id === id2);
|
|
796
936
|
if (!charge) throw new Error(`Unknown shipping ${id2}`);
|
|
797
937
|
const input = { ...charge, ...patch };
|
|
798
|
-
const updated = { ...recalculateCharge(id2, input), methodId: input.methodId };
|
|
938
|
+
const updated = { ...recalculateCharge(id2, input, true), methodId: input.methodId };
|
|
799
939
|
shipping = shipping.map((entry) => entry.id === id2 ? updated : entry);
|
|
800
940
|
emit();
|
|
801
941
|
},
|
|
@@ -820,7 +960,8 @@ function createOrderBuilder(options) {
|
|
|
820
960
|
imageUrl: traits.getImageUrl(doc),
|
|
821
961
|
unitPrice,
|
|
822
962
|
quantity: opts?.quantity,
|
|
823
|
-
taxClass: traits.getTaxClass?.(doc, opts?.variantId)
|
|
963
|
+
taxClass: traits.getTaxClass?.(doc, opts?.variantId),
|
|
964
|
+
...traits.getTaxStatus?.(doc, opts?.variantId) === "none" ? { taxStatus: "none" } : {}
|
|
824
965
|
});
|
|
825
966
|
},
|
|
826
967
|
updateQuantity(lineId, quantity) {
|
|
@@ -967,20 +1108,23 @@ function netDiscountMinor(line, pricesIncludeTax) {
|
|
|
967
1108
|
return roundMicrosToMinor(BigInt(line.discountMinor) * MICROS_PER_MINOR - taxBefore + taxAfter);
|
|
968
1109
|
}
|
|
969
1110
|
function contentVersion(order) {
|
|
1111
|
+
if (order.fees?.length || order.shipping?.length || order.lines.some((line) => line.custom)) return 5;
|
|
970
1112
|
return order.display && order.taxByRate ? 3 : order.lines.reduce((sum, line) => sum + line.discountMinor, 0) > 0 ? 2 : 1;
|
|
971
1113
|
}
|
|
972
1114
|
function toOrderCreateEnvelope(order, deviceId, attempt2 = 1, options) {
|
|
973
1115
|
const grossMinor = order.lines.reduce((sum, line) => sum + line.discountMinor, 0);
|
|
974
1116
|
const content = contentVersion(order);
|
|
975
1117
|
const cap = options?.maxVersion === void 0 ? order.sentVersion : Math.min(options.maxVersion, order.sentVersion ?? options.maxVersion);
|
|
1118
|
+
if (content === 5 && cap !== void 0 && cap < 5) throw new UnsupportedOrderVersionError(5, cap);
|
|
976
1119
|
if (grossMinor > 0 && cap !== void 0 && cap < 2) throw new UnsupportedOrderVersionError(2, cap);
|
|
977
1120
|
const version = content === 3 && cap !== void 0 && cap >= 4 ? 4 : Math.min(content, cap ?? content);
|
|
978
|
-
const lineDiscounts = order.lines.map((line) => version
|
|
1121
|
+
const lineDiscounts = order.lines.map((line) => version >= 4 ? netDiscountMinor(line, order.pricesIncludeTax) : line.discountMinor);
|
|
979
1122
|
const discountMinor = lineDiscounts.reduce((sum, discount) => sum + discount, 0);
|
|
980
1123
|
const email = order.customer?.email;
|
|
981
1124
|
const id2 = order.customer?.id;
|
|
982
1125
|
const customerId = typeof id2 === "string" && id2.length > 0 && id2.length <= 64 ? id2 : void 0;
|
|
983
1126
|
const sessionId = order.sessionId ?? order.lateSessionId;
|
|
1127
|
+
const { fees: _fees, shipping: _shipping, ...storedDisplay } = { ...order.display };
|
|
984
1128
|
return {
|
|
985
1129
|
id: order.commandId,
|
|
986
1130
|
type: "order.create",
|
|
@@ -995,13 +1139,30 @@ function toOrderCreateEnvelope(order, deviceId, attempt2 = 1, options) {
|
|
|
995
1139
|
pricesIncludeTax: order.pricesIncludeTax,
|
|
996
1140
|
lines: order.lines.map((line, i) => ({
|
|
997
1141
|
clientLineId: line.id,
|
|
998
|
-
variantId: line.variantId ?? line.productId,
|
|
1142
|
+
...line.custom ? { custom: { ...line.custom } } : { variantId: line.variantId ?? line.productId },
|
|
999
1143
|
title: line.name,
|
|
1000
1144
|
quantity: line.quantity,
|
|
1001
1145
|
unitPriceMinor: line.unitPriceMinor,
|
|
1002
1146
|
...line.taxInclusive !== void 0 ? { taxInclusive: line.taxInclusive } : {},
|
|
1003
1147
|
...lineDiscounts[i] > 0 ? { discountMinor: lineDiscounts[i] } : {}
|
|
1004
1148
|
})),
|
|
1149
|
+
...version === 5 && order.fees?.length ? { fees: order.fees.map((fee) => ({
|
|
1150
|
+
clientFeeId: fee.id,
|
|
1151
|
+
name: fee.name,
|
|
1152
|
+
amountMinor: fee.amountMinor,
|
|
1153
|
+
taxStatus: fee.taxStatus,
|
|
1154
|
+
...fee.taxClass !== void 0 ? { taxClass: fee.taxClass } : {},
|
|
1155
|
+
taxMinor: roundMicrosToMinor(BigInt(fee.taxMicros))
|
|
1156
|
+
})) } : {},
|
|
1157
|
+
...version === 5 && order.shipping?.length ? { shipping: order.shipping.map((charge) => ({
|
|
1158
|
+
clientShippingId: charge.id,
|
|
1159
|
+
name: charge.name,
|
|
1160
|
+
...charge.methodId !== void 0 ? { methodId: charge.methodId } : {},
|
|
1161
|
+
amountMinor: charge.amountMinor,
|
|
1162
|
+
taxStatus: charge.taxStatus,
|
|
1163
|
+
...charge.taxClass !== void 0 ? { taxClass: charge.taxClass } : {},
|
|
1164
|
+
taxMinor: roundMicrosToMinor(BigInt(charge.taxMicros))
|
|
1165
|
+
})) } : {},
|
|
1005
1166
|
payments: order.payments.map((payment) => ({
|
|
1006
1167
|
clientPaymentId: payment.id,
|
|
1007
1168
|
method: payment.method,
|
|
@@ -1015,11 +1176,16 @@ function toOrderCreateEnvelope(order, deviceId, attempt2 = 1, options) {
|
|
|
1015
1176
|
taxMinor: order.taxMinor,
|
|
1016
1177
|
totalMinor: order.totalMinor,
|
|
1017
1178
|
...version >= 3 ? {
|
|
1018
|
-
display: {
|
|
1019
|
-
|
|
1020
|
-
amountMinor,
|
|
1021
|
-
|
|
1022
|
-
|
|
1179
|
+
display: {
|
|
1180
|
+
...storedDisplay,
|
|
1181
|
+
lines: order.display.lines.map(({ lineId, amountMinor, discounts }) => ({
|
|
1182
|
+
clientLineId: lineId,
|
|
1183
|
+
amountMinor,
|
|
1184
|
+
discounts
|
|
1185
|
+
})),
|
|
1186
|
+
...version === 5 && order.display.fees ? { fees: order.display.fees.map(({ id: id3, amountMinor }) => ({ clientFeeId: id3, amountMinor })) } : {},
|
|
1187
|
+
...version === 5 && order.display.shipping ? { shipping: order.display.shipping.map(({ id: id3, amountMinor }) => ({ clientShippingId: id3, amountMinor })) } : {}
|
|
1188
|
+
},
|
|
1023
1189
|
taxByRate: order.taxByRate.map(({ ratePpm, code, netMinor, amountMinor, grossMinor: grossMinor2 }) => ({
|
|
1024
1190
|
ratePpm,
|
|
1025
1191
|
...code !== void 0 ? { code } : {},
|
|
@@ -1080,6 +1246,7 @@ function restoreOrderDraft(saved, options) {
|
|
|
1080
1246
|
productId: line.productId,
|
|
1081
1247
|
custom: line.custom,
|
|
1082
1248
|
taxStatus: line.taxStatus,
|
|
1249
|
+
...taxContext.rounding?.granularity === "woocommerce" ? { taxClass: line.taxClass } : {},
|
|
1083
1250
|
variantId: line.variantId,
|
|
1084
1251
|
name: line.name,
|
|
1085
1252
|
sku: line.sku,
|
|
@@ -1199,7 +1366,7 @@ function useParkedSales(drafts) {
|
|
|
1199
1366
|
|
|
1200
1367
|
// src/receipt/build-receipt-data.ts
|
|
1201
1368
|
function buildReceiptData(order, config) {
|
|
1202
|
-
const taxedLines = [...order.lineItems, ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({
|
|
1369
|
+
const taxedLines = [...order.lineItems.map((line) => order.taxRounding?.granularity === "woocommerce" && line.netMicros !== void 0 ? { ...line, taxInclusive: order.pricesIncludeTax } : line), ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({
|
|
1203
1370
|
...charge,
|
|
1204
1371
|
taxInclusive: order.pricesIncludeTax
|
|
1205
1372
|
}))];
|
|
@@ -1211,6 +1378,7 @@ function buildReceiptData(order, config) {
|
|
|
1211
1378
|
0n
|
|
1212
1379
|
);
|
|
1213
1380
|
const lineTotals = order.lineItems.map((li) => {
|
|
1381
|
+
if (order.taxRounding?.granularity === "woocommerce" && li.netMicros !== void 0) return order.pricesIncludeTax ? li.totalMinor : li.netMinor;
|
|
1214
1382
|
if (!li.priceTaxModeConverted) return li.netMinor;
|
|
1215
1383
|
const share = roundMicrosToMinor(taxSoFar + BigInt(li.taxMicros)) - roundMicrosToMinor(taxSoFar);
|
|
1216
1384
|
taxSoFar += BigInt(li.taxMicros);
|
|
@@ -1354,7 +1522,7 @@ function uuidv7(now, randomBytes) {
|
|
|
1354
1522
|
}
|
|
1355
1523
|
|
|
1356
1524
|
// src/pos-order/finalize.ts
|
|
1357
|
-
import { minorUnitDigits } from "@tallyui/core";
|
|
1525
|
+
import { minorUnitDigits as minorUnitDigits2 } from "@tallyui/core";
|
|
1358
1526
|
|
|
1359
1527
|
// src/outbox/logger.ts
|
|
1360
1528
|
var outboxLogger = createLogger("outbox");
|
|
@@ -1369,8 +1537,12 @@ function withSentForm(order, posOrder) {
|
|
|
1369
1537
|
...order,
|
|
1370
1538
|
customer,
|
|
1371
1539
|
lineItems: order.lineItems.map((line, i) => ({ ...line, name: posOrder.lines[i].name })),
|
|
1540
|
+
...order.fees ? { fees: order.fees.map((fee, i) => ({ ...fee, name: posOrder.fees[i].name })) } : {},
|
|
1541
|
+
...order.shipping ? { shipping: order.shipping.map((charge, i) => ({ ...charge, name: posOrder.shipping[i].name })) } : {},
|
|
1372
1542
|
display: posOrder.display ? {
|
|
1373
1543
|
...order.display,
|
|
1544
|
+
...order.display.fees ? { fees: order.display.fees.map((row, i) => ({ ...row, name: posOrder.display.fees[i].name })) } : {},
|
|
1545
|
+
...order.display.shipping ? { shipping: order.display.shipping.map((row, i) => ({ ...row, name: posOrder.display.shipping[i].name })) } : {},
|
|
1374
1546
|
lines: order.display.lines.map((line, i) => ({
|
|
1375
1547
|
...line,
|
|
1376
1548
|
discounts: line.discounts.map((discount, j) => ({ ...discount, label: posOrder.display.lines[i].discounts[j].label }))
|
|
@@ -1383,7 +1555,15 @@ function freezeSentForm(order) {
|
|
|
1383
1555
|
const lines = order.lines.map((line) => {
|
|
1384
1556
|
const name = cutText(line.name, PAYLOAD_STRING_MAX);
|
|
1385
1557
|
changed ||= name !== line.name;
|
|
1386
|
-
|
|
1558
|
+
const custom = line.custom && { ...line.custom, name: cutText(line.custom.name, PAYLOAD_STRING_MAX) };
|
|
1559
|
+
changed ||= custom !== void 0 && custom.name !== line.custom.name;
|
|
1560
|
+
return { ...line, name, ...custom ? { custom } : {} };
|
|
1561
|
+
});
|
|
1562
|
+
const charges = { fees: order.fees, shipping: order.shipping };
|
|
1563
|
+
for (const field of ["fees", "shipping"]) if (charges[field]) charges[field] = charges[field].map((charge) => {
|
|
1564
|
+
const name = cutText(charge.name, PAYLOAD_STRING_MAX);
|
|
1565
|
+
changed ||= name !== charge.name;
|
|
1566
|
+
return { ...charge, name };
|
|
1387
1567
|
});
|
|
1388
1568
|
const display = order.display && { ...order.display, lines: order.display.lines.map((line) => ({
|
|
1389
1569
|
...line,
|
|
@@ -1394,6 +1574,11 @@ function freezeSentForm(order) {
|
|
|
1394
1574
|
return { ...discount, label };
|
|
1395
1575
|
})
|
|
1396
1576
|
})) };
|
|
1577
|
+
for (const field of ["fees", "shipping"]) if (display?.[field]) display[field] = display[field].map((row) => {
|
|
1578
|
+
const name = cutText(row.name, PAYLOAD_STRING_MAX);
|
|
1579
|
+
changed ||= name !== row.name;
|
|
1580
|
+
return { ...row, name };
|
|
1581
|
+
});
|
|
1397
1582
|
const payments = order.payments.map((payment) => {
|
|
1398
1583
|
if (payment.reference === void 0) return payment;
|
|
1399
1584
|
const reference = cutText(payment.reference, PAYLOAD_STRING_MAX);
|
|
@@ -1409,7 +1594,16 @@ function freezeSentForm(order) {
|
|
|
1409
1594
|
if (!localWarnings.some((warning) => warning.code === "customer_omitted" && warning.field === field)) localWarnings.push({ code: "customer_omitted", field });
|
|
1410
1595
|
}
|
|
1411
1596
|
}
|
|
1412
|
-
return changed ? {
|
|
1597
|
+
return changed ? {
|
|
1598
|
+
...order,
|
|
1599
|
+
lines,
|
|
1600
|
+
payments,
|
|
1601
|
+
customer,
|
|
1602
|
+
...charges.fees ? { fees: charges.fees } : {},
|
|
1603
|
+
...charges.shipping ? { shipping: charges.shipping } : {},
|
|
1604
|
+
...display ? { display } : {},
|
|
1605
|
+
...localWarnings.length ? { localWarnings } : {}
|
|
1606
|
+
} : order;
|
|
1413
1607
|
}
|
|
1414
1608
|
function referenceError(label, value) {
|
|
1415
1609
|
const reason = referenceReason(value);
|
|
@@ -1421,8 +1615,8 @@ function referenceReason(value) {
|
|
|
1421
1615
|
return value.includes("\0") ? "nul" : null;
|
|
1422
1616
|
}
|
|
1423
1617
|
function finalizeOrder(order, options = {}) {
|
|
1424
|
-
if (order.fees?.length || order.shipping?.length || order.lineItems.some((line) => line.custom)) {
|
|
1425
|
-
throw new Error("finalize: fees, shipping and custom lines need order.create version 5
|
|
1618
|
+
if ((order.fees?.length || order.shipping?.length || order.lineItems.some((line) => line.custom)) && (options.capabilities?.orderCreate ?? 1) < 5) {
|
|
1619
|
+
throw new Error("finalize: fees, shipping and custom lines need the store to accept order.create version 5; update the store's TallyUI plugin");
|
|
1426
1620
|
}
|
|
1427
1621
|
if (!order.lineItems.length) throw new Error("finalize: no lines");
|
|
1428
1622
|
if (order.discountMinor < 0 || order.lineItems.some((line) => line.discountMinor < 0) || order.discounts.some((d) => d.amountMinor < 0)) {
|
|
@@ -1465,15 +1659,25 @@ function finalizeOrder(order, options = {}) {
|
|
|
1465
1659
|
id: newId(),
|
|
1466
1660
|
productId: line.productId,
|
|
1467
1661
|
...line.variantId !== void 0 ? { variantId: line.variantId } : {},
|
|
1662
|
+
...line.custom ? { custom: {
|
|
1663
|
+
name: line.name,
|
|
1664
|
+
...line.sku ? { sku: line.sku } : {},
|
|
1665
|
+
...line.taxClass !== void 0 ? { taxClass: line.taxClass } : {},
|
|
1666
|
+
taxStatus: line.taxStatus ?? "taxable"
|
|
1667
|
+
} } : {},
|
|
1668
|
+
...line.taxStatus !== void 0 ? { taxStatus: line.taxStatus } : {},
|
|
1468
1669
|
name: line.name,
|
|
1469
1670
|
sku: line.sku,
|
|
1470
1671
|
quantity: line.quantity,
|
|
1471
1672
|
unitPriceMinor: line.unitPriceMinor,
|
|
1472
1673
|
discountMinor: line.discountMinor,
|
|
1473
1674
|
netMinor: line.netMinor,
|
|
1675
|
+
...order.taxRounding?.granularity === "woocommerce" ? { netMicros: line.netMicros } : {},
|
|
1474
1676
|
taxLines: line.taxLines.map((tax) => ({ ...tax })),
|
|
1475
1677
|
...line.priceTaxModeConverted ? { taxInclusive: line.taxInclusive } : {}
|
|
1476
1678
|
}));
|
|
1679
|
+
const fees = order.fees?.map((charge) => ({ ...charge, id: newId(), taxLines: charge.taxLines.map((tax) => ({ ...tax })) }));
|
|
1680
|
+
const shipping = order.shipping?.map((charge) => ({ ...charge, id: newId(), taxLines: charge.taxLines.map((tax) => ({ ...tax })) }));
|
|
1477
1681
|
const payments = order.payments.map((payment) => ({
|
|
1478
1682
|
id: newId(),
|
|
1479
1683
|
method: payment.method,
|
|
@@ -1511,8 +1715,10 @@ function finalizeOrder(order, options = {}) {
|
|
|
1511
1715
|
}
|
|
1512
1716
|
display = {
|
|
1513
1717
|
currency: order.currency,
|
|
1514
|
-
exponent:
|
|
1718
|
+
exponent: minorUnitDigits2(order.currency),
|
|
1515
1719
|
...order.display,
|
|
1720
|
+
...fees?.length ? { fees: order.display.fees.map((row, i) => ({ ...row, id: fees[i].id })) } : {},
|
|
1721
|
+
...shipping?.length ? { shipping: order.display.shipping.map((row, i) => ({ ...row, id: shipping[i].id })) } : {},
|
|
1516
1722
|
lines: order.display.lines.map((line, i) => ({
|
|
1517
1723
|
lineId: lines[i].id,
|
|
1518
1724
|
amountMinor: line.amountMinor,
|
|
@@ -1523,7 +1729,8 @@ function finalizeOrder(order, options = {}) {
|
|
|
1523
1729
|
}))
|
|
1524
1730
|
}))
|
|
1525
1731
|
};
|
|
1526
|
-
|
|
1732
|
+
const taxedLines = [...order.lineItems, ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({ ...charge, taxInclusive: order.pricesIncludeTax }))];
|
|
1733
|
+
taxByRate = taxLinesByRate(taxedLines, order.taxMinor, void 0, order.taxRounding).map(({ ratePpm, code, netMinor, amountMinor }) => ({
|
|
1527
1734
|
ratePpm,
|
|
1528
1735
|
...code !== void 0 ? { code } : {},
|
|
1529
1736
|
netMinor,
|
|
@@ -1538,7 +1745,7 @@ function finalizeOrder(order, options = {}) {
|
|
|
1538
1745
|
}
|
|
1539
1746
|
}
|
|
1540
1747
|
const rounding = order.taxRounding ?? DEFAULT_TAX_ROUNDING;
|
|
1541
|
-
const taxRounding = rounding.granularity === "custom" ? { granularity: "custom" } : { granularity: rounding.granularity, mode: rounding.mode };
|
|
1748
|
+
const taxRounding = rounding.granularity === "custom" ? { granularity: "custom" } : rounding.granularity === "woocommerce" ? { granularity: "woocommerce", roundAtSubtotal: rounding.roundAtSubtotal } : { granularity: rounding.granularity, mode: rounding.mode };
|
|
1542
1749
|
const now = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1543
1750
|
return freezeSentForm({
|
|
1544
1751
|
id: id2,
|
|
@@ -1551,6 +1758,8 @@ function finalizeOrder(order, options = {}) {
|
|
|
1551
1758
|
pricesIncludeTax: order.pricesIncludeTax,
|
|
1552
1759
|
lines,
|
|
1553
1760
|
payments,
|
|
1761
|
+
...fees?.length ? { fees } : {},
|
|
1762
|
+
...shipping?.length ? { shipping } : {},
|
|
1554
1763
|
subtotalMinor: order.subtotalMinor,
|
|
1555
1764
|
discountMinor: order.discountMinor,
|
|
1556
1765
|
taxMinor: order.taxMinor,
|
|
@@ -1573,7 +1782,7 @@ function finalizeOrder(order, options = {}) {
|
|
|
1573
1782
|
import { addRxPlugin } from "rxdb";
|
|
1574
1783
|
import { RxDBMigrationSchemaPlugin } from "rxdb/plugins/migration-schema";
|
|
1575
1784
|
var posOrderSchema = {
|
|
1576
|
-
version:
|
|
1785
|
+
version: 8,
|
|
1577
1786
|
primaryKey: "id",
|
|
1578
1787
|
type: "object",
|
|
1579
1788
|
additionalProperties: false,
|
|
@@ -1603,9 +1812,17 @@ var posOrderSchema = {
|
|
|
1603
1812
|
name: { type: "string" },
|
|
1604
1813
|
sku: { type: "string" },
|
|
1605
1814
|
quantity: { type: "integer" },
|
|
1815
|
+
taxStatus: { type: "string", enum: ["taxable", "none"] },
|
|
1816
|
+
custom: { type: "object", additionalProperties: false, properties: {
|
|
1817
|
+
name: { type: "string", minLength: 1, maxLength: 255 },
|
|
1818
|
+
sku: { type: "string", maxLength: 64 },
|
|
1819
|
+
taxClass: { type: "string", maxLength: 64 },
|
|
1820
|
+
taxStatus: { type: "string", enum: ["taxable", "none"] }
|
|
1821
|
+
}, required: ["name", "taxStatus"] },
|
|
1606
1822
|
unitPriceMinor: { type: "integer" },
|
|
1607
1823
|
discountMinor: { type: "integer" },
|
|
1608
1824
|
netMinor: { type: "integer" },
|
|
1825
|
+
netMicros: { type: "string", pattern: "^-?[0-9]{1,24}$", maxLength: 25 },
|
|
1609
1826
|
taxLines: { type: "array", items: {
|
|
1610
1827
|
type: "object",
|
|
1611
1828
|
properties: { code: { type: "string" }, ratePpm: { type: "integer" }, taxMicros: { type: "string" } },
|
|
@@ -1614,6 +1831,43 @@ var posOrderSchema = {
|
|
|
1614
1831
|
},
|
|
1615
1832
|
required: ["id", "productId", "name", "sku", "quantity", "unitPriceMinor", "discountMinor", "netMinor", "taxLines"]
|
|
1616
1833
|
} },
|
|
1834
|
+
fees: { type: "array", items: { type: "object", additionalProperties: false, properties: {
|
|
1835
|
+
id: { type: "string", maxLength: 36 },
|
|
1836
|
+
name: { type: "string", minLength: 1, maxLength: 255 },
|
|
1837
|
+
amountMinor: { type: "integer", minimum: 0 },
|
|
1838
|
+
taxClass: { type: "string", maxLength: 64 },
|
|
1839
|
+
taxStatus: { type: "string", enum: ["taxable", "none"] },
|
|
1840
|
+
netMinor: { type: "integer" },
|
|
1841
|
+
taxMicros: { type: "string" },
|
|
1842
|
+
netMicros: { type: "string", pattern: "^-?[0-9]{1,24}$", maxLength: 25 },
|
|
1843
|
+
totalMinor: { type: "integer" },
|
|
1844
|
+
taxLines: { type: "array", items: { type: "object", additionalProperties: false, properties: {
|
|
1845
|
+
code: { type: "string", maxLength: 255 },
|
|
1846
|
+
ratePpm: { type: "integer" },
|
|
1847
|
+
taxMicros: { type: "string" },
|
|
1848
|
+
rateId: { type: "integer" },
|
|
1849
|
+
compound: { type: "boolean" }
|
|
1850
|
+
}, required: ["ratePpm", "taxMicros"] } }
|
|
1851
|
+
}, required: ["id", "name", "amountMinor", "taxStatus", "taxLines", "netMinor", "taxMicros"] } },
|
|
1852
|
+
shipping: { type: "array", items: { type: "object", additionalProperties: false, properties: {
|
|
1853
|
+
id: { type: "string", maxLength: 36 },
|
|
1854
|
+
name: { type: "string", minLength: 1, maxLength: 255 },
|
|
1855
|
+
methodId: { type: "string", maxLength: 64 },
|
|
1856
|
+
amountMinor: { type: "integer", minimum: 0 },
|
|
1857
|
+
taxClass: { type: "string", maxLength: 64 },
|
|
1858
|
+
taxStatus: { type: "string", enum: ["taxable", "none"] },
|
|
1859
|
+
netMinor: { type: "integer" },
|
|
1860
|
+
taxMicros: { type: "string" },
|
|
1861
|
+
netMicros: { type: "string", pattern: "^-?[0-9]{1,24}$", maxLength: 25 },
|
|
1862
|
+
totalMinor: { type: "integer" },
|
|
1863
|
+
taxLines: { type: "array", items: { type: "object", additionalProperties: false, properties: {
|
|
1864
|
+
code: { type: "string", maxLength: 255 },
|
|
1865
|
+
ratePpm: { type: "integer" },
|
|
1866
|
+
taxMicros: { type: "string" },
|
|
1867
|
+
rateId: { type: "integer" },
|
|
1868
|
+
compound: { type: "boolean" }
|
|
1869
|
+
}, required: ["ratePpm", "taxMicros"] } }
|
|
1870
|
+
}, required: ["id", "name", "amountMinor", "taxStatus", "taxLines", "netMinor", "taxMicros"] } },
|
|
1617
1871
|
payments: { type: "array", items: {
|
|
1618
1872
|
type: "object",
|
|
1619
1873
|
properties: {
|
|
@@ -1651,11 +1905,12 @@ var posOrderSchema = {
|
|
|
1651
1905
|
}, required: ["since", "reason", "isolated"] },
|
|
1652
1906
|
lateSessionId: { type: "string" },
|
|
1653
1907
|
taxRounding: { type: "object", additionalProperties: false, properties: {
|
|
1654
|
-
granularity: { type: "string", enum: ["per_order", "per_line_items", "per_rate_group_items", "custom"] },
|
|
1908
|
+
granularity: { type: "string", enum: ["per_order", "per_line_items", "per_rate_group_items", "custom", "woocommerce"] },
|
|
1909
|
+
roundAtSubtotal: { type: "boolean" },
|
|
1655
1910
|
mode: { type: "string", enum: ["half_away_from_zero", "half_up"] }
|
|
1656
1911
|
}, required: ["granularity"] },
|
|
1657
|
-
sentVersion: { type: "integer", minimum: 1, maximum:
|
|
1658
|
-
downgradedFrom: { type: "integer", minimum: 1, maximum:
|
|
1912
|
+
sentVersion: { type: "integer", minimum: 1, maximum: 5 },
|
|
1913
|
+
downgradedFrom: { type: "integer", minimum: 1, maximum: 5 },
|
|
1659
1914
|
// The nested objects are closed too: loosening a schema later is free, tightening one costs a migration.
|
|
1660
1915
|
display: { type: "object", additionalProperties: false, properties: {
|
|
1661
1916
|
currency: { type: "string" },
|
|
@@ -1666,6 +1921,16 @@ var posOrderSchema = {
|
|
|
1666
1921
|
taxMinor: { type: "integer" },
|
|
1667
1922
|
totalMinor: { type: "integer" },
|
|
1668
1923
|
orderDiscountMinor: { type: "integer" },
|
|
1924
|
+
fees: { type: "array", items: { type: "object", additionalProperties: false, properties: {
|
|
1925
|
+
id: { type: "string", maxLength: 36 },
|
|
1926
|
+
name: { type: "string", minLength: 1, maxLength: 255 },
|
|
1927
|
+
amountMinor: { type: "integer" }
|
|
1928
|
+
}, required: ["id", "name", "amountMinor"] } },
|
|
1929
|
+
shipping: { type: "array", items: { type: "object", additionalProperties: false, properties: {
|
|
1930
|
+
id: { type: "string", maxLength: 36 },
|
|
1931
|
+
name: { type: "string", minLength: 1, maxLength: 255 },
|
|
1932
|
+
amountMinor: { type: "integer" }
|
|
1933
|
+
}, required: ["id", "name", "amountMinor"] } },
|
|
1669
1934
|
lines: { type: "array", items: {
|
|
1670
1935
|
type: "object",
|
|
1671
1936
|
additionalProperties: false,
|
|
@@ -1728,7 +1993,7 @@ function posOrderCollection() {
|
|
|
1728
1993
|
};
|
|
1729
1994
|
return {
|
|
1730
1995
|
schema: posOrderSchema,
|
|
1731
|
-
migrationStrategies: { 1: identity2, 2: identity2, 3: identity2, 4: identity2, 5: recordSent, 6: recordRounding, 7: identity2 }
|
|
1996
|
+
migrationStrategies: { 1: identity2, 2: identity2, 3: identity2, 4: identity2, 5: recordSent, 6: recordRounding, 7: identity2, 8: identity2 }
|
|
1732
1997
|
};
|
|
1733
1998
|
}
|
|
1734
1999
|
|
|
@@ -2628,10 +2893,11 @@ async function writeClosure({
|
|
|
2628
2893
|
const roundings = /* @__PURE__ */ new Set();
|
|
2629
2894
|
for (const order of bound) {
|
|
2630
2895
|
const recorded = order.taxRounding ?? DEFAULT_TAX_ROUNDING;
|
|
2631
|
-
const
|
|
2632
|
-
roundings.add(`${granularity} ${mode}`);
|
|
2896
|
+
const rounding = recorded.granularity === "custom" ? DEFAULT_TAX_ROUNDING : recorded;
|
|
2897
|
+
roundings.add(rounding.granularity === "woocommerce" ? `woocommerce ${rounding.roundAtSubtotal}` : `${rounding.granularity} ${rounding.mode}`);
|
|
2633
2898
|
const lines = order.lines.map((line) => ({ ...line, taxInclusive: line.taxInclusive ?? order.pricesIncludeTax }));
|
|
2634
|
-
|
|
2899
|
+
const taxedLines = [...lines, ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({ ...charge, taxInclusive: order.pricesIncludeTax }))];
|
|
2900
|
+
for (const { ratePpm, netMinor, amountMinor } of taxLinesByRate(taxedLines, order.taxMinor, void 0, order.taxRounding)) {
|
|
2635
2901
|
const existing2 = taxRates.get(String(ratePpm));
|
|
2636
2902
|
taxRates.set(String(ratePpm), {
|
|
2637
2903
|
ratePpm,
|
|
@@ -2998,7 +3264,7 @@ function envelope(row, context, xreport = false) {
|
|
|
2998
3264
|
});
|
|
2999
3265
|
return {
|
|
3000
3266
|
closure: {
|
|
3001
|
-
has_sales:
|
|
3267
|
+
has_sales: Number(breakdowns.transaction_count ?? 0) > 0 || Number(row.period_sales_total || 0) !== 0 || Number(row.period_refunds_total || 0) !== 0,
|
|
3002
3268
|
has_perpetual: row.perpetual_sales_total != null || row.perpetual_refunds_total != null,
|
|
3003
3269
|
...Object.fromEntries(
|
|
3004
3270
|
["payment_methods", "tax_rates", "movements"].map((key) => [`has_${key}`, Object.keys(breakdowns[key] ?? {}).length > 0])
|
|
@@ -3659,7 +3925,8 @@ function addEntryToCart(builder, entry, traits, currency) {
|
|
|
3659
3925
|
sku: variant.sku ?? "",
|
|
3660
3926
|
imageUrl: traits.getImageUrl?.(product),
|
|
3661
3927
|
unitPrice,
|
|
3662
|
-
taxClass: traits.getTaxClass?.(product, variant.id)
|
|
3928
|
+
taxClass: traits.getTaxClass?.(product, variant.id),
|
|
3929
|
+
...traits.getTaxStatus?.(product, variant.id) === "none" ? { taxStatus: "none" } : {}
|
|
3663
3930
|
});
|
|
3664
3931
|
}
|
|
3665
3932
|
|
|
@@ -3827,6 +4094,9 @@ function useSale(settings, opts) {
|
|
|
3827
4094
|
stage,
|
|
3828
4095
|
error,
|
|
3829
4096
|
idle: idle3,
|
|
4097
|
+
/** The store's capabilities as passed in: a component offers fees, shipping and custom lines
|
|
4098
|
+
* only when `orderCreate >= 5`, and their tax status and class only as `lineTax` allows. */
|
|
4099
|
+
capabilities: opts.capabilities,
|
|
3830
4100
|
/** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
|
|
3831
4101
|
saving,
|
|
3832
4102
|
/** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
|
|
@@ -4449,6 +4719,7 @@ function createOrderOutbox(options) {
|
|
|
4449
4719
|
const serverMax = firstSend && validMax(await Promise.resolve().then(() => options.getMaxOrderCreateVersion?.()).catch((cause) => {
|
|
4450
4720
|
outboxLogger.warn("Failed to read capabilities", { cause });
|
|
4451
4721
|
})) || 3;
|
|
4722
|
+
const localOrders = /* @__PURE__ */ new Map();
|
|
4452
4723
|
const batch = await Promise.all(orders.map(async (order) => {
|
|
4453
4724
|
const frozen = freezeSentForm(order);
|
|
4454
4725
|
const attempt2 = (attempts.get(order.commandId) ?? 0) + 1;
|
|
@@ -4458,14 +4729,15 @@ function createOrderOutbox(options) {
|
|
|
4458
4729
|
envelope2 = toOrderCreateEnvelope(frozen, deviceId, attempt2, { maxVersion: order.sentVersion ?? serverMax });
|
|
4459
4730
|
} catch (cause) {
|
|
4460
4731
|
if (!(cause instanceof UnsupportedOrderVersionError)) throw cause;
|
|
4461
|
-
envelope2 = toOrderCreateEnvelope(frozen, deviceId, attempt2, { maxVersion: 3 });
|
|
4732
|
+
envelope2 = toOrderCreateEnvelope(frozen, deviceId, attempt2, { maxVersion: cause.needed === 5 ? 5 : 3 });
|
|
4462
4733
|
}
|
|
4463
4734
|
if (frozen !== order || order.sentVersion === void 0) {
|
|
4464
4735
|
await (await collection.findOne(order.id).exec())?.incrementalModify((data) => ({ ...freezeSentForm(data), sentVersion: data.sentVersion ?? envelope2.version }));
|
|
4465
4736
|
}
|
|
4737
|
+
localOrders.set(envelope2.id, frozen);
|
|
4466
4738
|
return envelope2;
|
|
4467
4739
|
}));
|
|
4468
|
-
let outcome = await transport.send(batch);
|
|
4740
|
+
let outcome = await transport.send(batch, { local: { orders: localOrders } });
|
|
4469
4741
|
backendNotFound.record(outcome, now());
|
|
4470
4742
|
if (outcome.kind !== "retry" || outcome.reason !== "network") {
|
|
4471
4743
|
const at2 = now();
|