@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.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 { ratesPpm: settings.taxRatesPpm, pricesIncludeTax: settings.pricesIncludeTax, ...rounding && { rounding }, ...settings.taxRateCodes && { rateCodes: settings.taxRateCodes } };
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
- const { subtotalMinor } = sumLines(lines, taxContext.rounding);
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: toDisplayMode(li, d.amountMinor, taxInclusive)
708
+ amountMinor: displayAmount(li, d.amountMinor)
601
709
  }));
602
- const shareMinor = toDisplayMode(li, li.orderDiscountMinor, taxInclusive);
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 displaySubtotal = (taxInclusive ? totalMinor : totalMinor - taxMinor) + displayDiscount - charges.reduce((sum, charge) => sum + charge.amountMinor, 0);
614
- const residue = displaySubtotal - displayLines.reduce((sum, line) => sum + line.amountMinor, 0);
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: fees.map(({ id: id2, name, amountMinor }) => ({ id: id2, name, amountMinor })) } : {},
649
- ...shipping.length ? { shipping: shipping.map(({ id: id2, name, amountMinor }) => ({ id: id2, name, amountMinor })) } : {}
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: Number(line.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 === 4 ? netDiscountMinor(line, order.pricesIncludeTax) : line.discountMinor);
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: { ...order.display, lines: order.display.lines.map(({ lineId, amountMinor, discounts }) => ({
1019
- clientLineId: lineId,
1020
- amountMinor,
1021
- discounts
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
- return { ...line, name };
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 ? { ...order, lines, payments, customer, ...display ? { display } : {}, ...localWarnings.length ? { localWarnings } : {} } : order;
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, which this release does not send yet");
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: minorUnitDigits(order.currency),
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
- taxByRate = taxLinesByRate(order.lineItems, order.taxMinor, void 0, order.taxRounding).map(({ ratePpm, code, netMinor, amountMinor }) => ({
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: 7,
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: 4 },
1658
- downgradedFrom: { type: "integer", minimum: 1, maximum: 4 },
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 { granularity, mode } = recorded.granularity === "custom" ? DEFAULT_TAX_ROUNDING : recorded;
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
- for (const { ratePpm, netMinor, amountMinor } of taxLinesByRate(lines, order.taxMinor, void 0, order.taxRounding)) {
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: row.period_sales_total != null || row.period_refunds_total != null || Number(breakdowns.transaction_count) > 0,
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();