@tallyui/pos 3.2.1 → 3.3.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, 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)) {
@@ -272,7 +353,7 @@ function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, children
272
353
  ...rounding ? { rounding } : {}
273
354
  };
274
355
  },
275
- [ratesPpm, pricesIncludeTax, rounding, rateCodes]
356
+ [ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates]
276
357
  );
277
358
  return /* @__PURE__ */ jsx2(TaxCtx.Provider, { value, children });
278
359
  }
@@ -319,7 +400,16 @@ function withPricingContext(context, settings) {
319
400
  }
320
401
  function taxProviderProps(settings) {
321
402
  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 } };
403
+ return {
404
+ ratesPpm: settings.taxRatesPpm,
405
+ pricesIncludeTax: settings.pricesIncludeTax,
406
+ ...rounding && { rounding },
407
+ ...settings.taxRateCodes && { rateCodes: settings.taxRateCodes },
408
+ ...settings.taxRates && { taxRates: settings.taxRates },
409
+ ...settings.taxRoundAtSubtotal !== void 0 && { taxRoundAtSubtotal: settings.taxRoundAtSubtotal },
410
+ ...settings.shippingTaxClass !== void 0 && { shippingTaxClass: settings.shippingTaxClass },
411
+ ...settings.taxClassSlugs && { taxClassSlugs: settings.taxClassSlugs }
412
+ };
323
413
  }
324
414
 
325
415
  // src/store-settings/use-store-settings.ts
@@ -522,6 +612,13 @@ function createOrderBuilder(options) {
522
612
  const { taxContext } = options;
523
613
  const currency = options.currency.toUpperCase();
524
614
  const orderId = options.id ?? uid();
615
+ const woo = taxContext.rounding?.granularity === "woocommerce" && taxContext.getTaxRates ? taxContext.rounding : void 0;
616
+ const wooLine = (amount, taxClass, inclusive, untaxed = false, shipping2 = false) => woocommerceLine(amount, untaxed ? [] : taxContext.getTaxRates(taxClass, { shipping: shipping2 }), inclusive, currency, woo.roundAtSubtotal, taxContext.pricesIncludeTax);
617
+ const displayAmount = (line, amount) => {
618
+ if (!woo || line.taxInclusive === taxContext.pricesIncludeTax) return toDisplayMode(line, amount, taxContext.pricesIncludeTax);
619
+ const item = wooLine(amount, line.taxClass, line.taxInclusive, line.taxStatus === "none");
620
+ return taxContext.pricesIncludeTax ? item.totalMinor : item.netMinor;
621
+ };
525
622
  let lineItems = [];
526
623
  let fees = [];
527
624
  let shipping = [];
@@ -542,6 +639,13 @@ function createOrderBuilder(options) {
542
639
  const lineDiscountMinor = Math.min(grossMinor, recalcedDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
543
640
  const discountMinor = lineDiscountMinor + orderDiscountMinor;
544
641
  const netMinor = grossMinor - discountMinor;
642
+ if (woo) return {
643
+ ...line,
644
+ discounts: recalcedDiscounts,
645
+ discountMinor,
646
+ orderDiscountMinor,
647
+ ...wooLine(netMinor, line.taxClass, line.taxInclusive, line.taxStatus === "none")
648
+ };
545
649
  const combinedRate = line.taxLines.reduce((sum, tax) => sum + tax.ratePpm, 0);
546
650
  const inclusiveTax = line.taxInclusive ? taxMicros(netMinor, combinedRate, true) : 0n;
547
651
  let allocatedTax = 0n;
@@ -568,7 +672,7 @@ function createOrderBuilder(options) {
568
672
  return Math.max(0, Math.min(discount.value, base));
569
673
  }
570
674
  function buildOrder() {
571
- const lineAmounts = lineItems.map((li) => li.netMinor);
675
+ const lineAmounts = lineItems.map((li) => woo ? li.unitPriceMinor * li.quantity - li.discountMinor : li.netMinor);
572
676
  const base = lineAmounts.reduce((sum, amount) => sum + Math.max(0, amount), 0);
573
677
  let remaining = base;
574
678
  const recalcedOrderDiscounts = orderDiscounts.map((d) => {
@@ -578,14 +682,15 @@ function createOrderBuilder(options) {
578
682
  });
579
683
  const shares = allocateOrderDiscount(lineAmounts, recalcedOrderDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
580
684
  const lines = lineItems.map((li, index) => recalculateLine(li, shares[index]));
581
- const { subtotalMinor } = sumLines(lines, taxContext.rounding);
685
+ const wooTotals = woo ? woocommerceTotals(lines, fees, shipping, currency, woo.roundAtSubtotal, taxContext.pricesIncludeTax) : void 0;
686
+ const { subtotalMinor } = wooTotals ?? sumLines(lines, taxContext.rounding);
582
687
  const charges = [...fees, ...shipping];
583
688
  const taxedLines = [...lines, ...charges.map((charge) => ({
584
689
  ...charge,
585
690
  taxInclusive: taxContext.pricesIncludeTax,
586
691
  taxMicros: charge.taxLines.reduce((sum, tax) => sum + BigInt(tax.taxMicros), 0n).toString()
587
692
  }))];
588
- const { taxMinor, totalMinor } = sumLines(taxedLines, taxContext.rounding);
693
+ const { taxMinor, totalMinor } = wooTotals ?? sumLines(taxedLines, taxContext.rounding);
589
694
  if (taxContext.rounding?.granularity === "per_rate_group_items" && taxedLines.some((li) => li.taxInclusive) && !perRateGroupWarned.has(taxContext)) {
590
695
  perRateGroupWarned.add(taxContext);
591
696
  taxLogger.warn("per_rate_group_items with inclusive lines: using per_order figures until the display has a rounding row (#287, #310)");
@@ -597,11 +702,11 @@ function createOrderBuilder(options) {
597
702
  const discounts = returned ? [] : li.discounts.map((d) => ({
598
703
  discountId: d.id,
599
704
  ...d.label !== void 0 ? { label: d.label } : {},
600
- amountMinor: toDisplayMode(li, d.amountMinor, taxInclusive)
705
+ amountMinor: displayAmount(li, d.amountMinor)
601
706
  }));
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);
707
+ const shareMinor = displayAmount(li, li.orderDiscountMinor);
708
+ const remainingMinor = woo ? taxInclusive ? li.totalMinor : li.netMinor : toDisplayMode(li, li.netMinor, taxInclusive);
709
+ const amountMinor = !woo && li.taxInclusive === taxInclusive && !returned ? li.unitPriceMinor * li.quantity : discounts.reduce((sum, d) => sum + d.amountMinor, remainingMinor + shareMinor);
605
710
  return { line: { lineId: li.id, amountMinor, discounts }, shareMinor, remainingMinor, returned };
606
711
  });
607
712
  const displayLines = figures.map((f) => f.line);
@@ -610,8 +715,17 @@ function createOrderBuilder(options) {
610
715
  (sum, line) => line.discounts.reduce((lineSum, d) => lineSum + d.amountMinor, sum),
611
716
  orderDiscountDisplay
612
717
  );
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);
718
+ const chargeRows = (items) => items.map(({ id: id2, name, amountMinor, netMinor, totalMinor: totalMinor2 }) => ({ id: id2, name, amountMinor: woo ? taxInclusive ? totalMinor2 : netMinor : amountMinor }));
719
+ const displayFees = chargeRows(fees), displayShipping = chargeRows(shipping);
720
+ let displaySubtotal = (taxInclusive ? totalMinor : totalMinor - taxMinor) + displayDiscount - [...displayFees, ...displayShipping].reduce((sum, charge) => sum + charge.amountMinor, 0);
721
+ let residue = displaySubtotal - displayLines.reduce((sum, line) => sum + line.amountMinor, 0);
722
+ if (woo) {
723
+ const charge = displayShipping.at(-1) ?? displayFees.at(-1);
724
+ const last = charge ?? displayLines.at(-1);
725
+ if (last) last.amountMinor += residue;
726
+ if (charge) displaySubtotal -= residue;
727
+ residue = displaySubtotal - displayLines.reduce((sum, line) => sum + line.amountMinor, 0);
728
+ }
615
729
  const converted = lines.flatMap((li, index) => li.taxInclusive === taxInclusive ? [] : [index]);
616
730
  const conversions = converted.reduce((count, index) => count + [
617
731
  lines[index].netMinor,
@@ -631,7 +745,7 @@ function createOrderBuilder(options) {
631
745
  if (displayDiscount < 0) throw new Error(`Display discount ${displayDiscount} is negative (ADR-063)`);
632
746
  for (const { line, shareMinor, returned } of figures) {
633
747
  if (returned && line.discounts.length > 0) throw new Error(`Return line ${line.lineId} shows discount rows (ADR-063)`);
634
- if (returned) continue;
748
+ if (returned || woo) continue;
635
749
  const rows = line.discounts.reduce((sum, d) => sum + d.amountMinor, 0);
636
750
  if (shareMinor < 0 || line.discounts.some((d) => d.amountMinor < 0) || line.amountMinor < rows + shareMinor) {
637
751
  throw new Error(`Display line ${line.lineId} shows ${line.amountMinor}, less than its rows ${rows} and share ${shareMinor} (ADR-063)`);
@@ -645,8 +759,8 @@ function createOrderBuilder(options) {
645
759
  totalMinor,
646
760
  lines: displayLines,
647
761
  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 })) } : {}
762
+ ...fees.length ? { fees: displayFees } : {},
763
+ ...shipping.length ? { shipping: displayShipping } : {}
650
764
  };
651
765
  const paidMinor = payments.reduce((sum, p) => sum + p.amountMinor, 0);
652
766
  const balanceDueMinor = Math.max(0, totalMinor - paidMinor);
@@ -679,11 +793,22 @@ function createOrderBuilder(options) {
679
793
  function emit() {
680
794
  subject.next(buildOrder());
681
795
  }
682
- function recalculateCharge(id2, input) {
796
+ function recalculateCharge(id2, input, shipping2 = false) {
683
797
  if (!Number.isInteger(input.amountMinor)) throw new RangeError("Amount must be integer minor units");
684
798
  if (input.amountMinor < 0) throw new RangeError("Amount must be >= 0");
685
799
  if (!input.name.trim()) throw new RangeError("Name must not be empty");
686
800
  const taxStatus = input.taxStatus ?? "taxable";
801
+ if (woo) {
802
+ const line2 = wooLine(input.amountMinor, input.taxClass, taxContext.pricesIncludeTax, taxStatus === "none", shipping2);
803
+ return {
804
+ id: id2,
805
+ name: input.name,
806
+ amountMinor: input.amountMinor,
807
+ taxClass: input.taxClass,
808
+ taxStatus,
809
+ ...line2
810
+ };
811
+ }
687
812
  const code = taxContext.getTaxRateCode?.(input.taxClass);
688
813
  const line = recalculateLine({
689
814
  id: id2,
@@ -712,7 +837,7 @@ function createOrderBuilder(options) {
712
837
  taxStatus,
713
838
  taxLines: line.taxLines,
714
839
  netMinor: line.netMinor,
715
- taxMicros: Number(line.taxMicros)
840
+ taxMicros: line.taxMicros
716
841
  };
717
842
  }
718
843
  function addLine(input) {
@@ -721,11 +846,11 @@ function createOrderBuilder(options) {
721
846
  if (unitPrice.currency !== currency) throw new RangeError("Line currency must match " + currency);
722
847
  if (!Number.isInteger(unitPrice.amount)) throw new RangeError("Price must be integer minor units");
723
848
  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) }];
849
+ const code = woo || input.taxRates ? void 0 : taxContext.getTaxRateCode?.(input.taxClass);
850
+ 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
851
  const taxInclusive = unitPrice.taxInclusive ?? taxContext.pricesIncludeTax;
727
852
  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)
853
+ (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
854
  );
730
855
  if (existing) {
731
856
  lineItems = lineItems.map(
@@ -737,6 +862,7 @@ function createOrderBuilder(options) {
737
862
  const lineId = uid();
738
863
  const line = {
739
864
  id: lineId,
865
+ ...(input.custom ? input.taxClass !== void 0 : woo) ? { taxClass: input.taxClass } : {},
740
866
  productId,
741
867
  ...input.custom ? { custom: true } : {},
742
868
  ...input.taxStatus === "none" ? { taxStatus: "none" } : {},
@@ -786,7 +912,7 @@ function createOrderBuilder(options) {
786
912
  emit();
787
913
  },
788
914
  addShipping(input) {
789
- const charge = { ...recalculateCharge(uid(), input), methodId: input.methodId };
915
+ const charge = { ...recalculateCharge(uid(), input, true), methodId: input.methodId };
790
916
  shipping = [...shipping, charge];
791
917
  emit();
792
918
  return charge.id;
@@ -795,7 +921,7 @@ function createOrderBuilder(options) {
795
921
  const charge = shipping.find((entry) => entry.id === id2);
796
922
  if (!charge) throw new Error(`Unknown shipping ${id2}`);
797
923
  const input = { ...charge, ...patch };
798
- const updated = { ...recalculateCharge(id2, input), methodId: input.methodId };
924
+ const updated = { ...recalculateCharge(id2, input, true), methodId: input.methodId };
799
925
  shipping = shipping.map((entry) => entry.id === id2 ? updated : entry);
800
926
  emit();
801
927
  },
@@ -820,7 +946,8 @@ function createOrderBuilder(options) {
820
946
  imageUrl: traits.getImageUrl(doc),
821
947
  unitPrice,
822
948
  quantity: opts?.quantity,
823
- taxClass: traits.getTaxClass?.(doc, opts?.variantId)
949
+ taxClass: traits.getTaxClass?.(doc, opts?.variantId),
950
+ ...traits.getTaxStatus?.(doc, opts?.variantId) === "none" ? { taxStatus: "none" } : {}
824
951
  });
825
952
  },
826
953
  updateQuantity(lineId, quantity) {
@@ -967,20 +1094,23 @@ function netDiscountMinor(line, pricesIncludeTax) {
967
1094
  return roundMicrosToMinor(BigInt(line.discountMinor) * MICROS_PER_MINOR - taxBefore + taxAfter);
968
1095
  }
969
1096
  function contentVersion(order) {
1097
+ if (order.fees?.length || order.shipping?.length || order.lines.some((line) => line.custom)) return 5;
970
1098
  return order.display && order.taxByRate ? 3 : order.lines.reduce((sum, line) => sum + line.discountMinor, 0) > 0 ? 2 : 1;
971
1099
  }
972
1100
  function toOrderCreateEnvelope(order, deviceId, attempt2 = 1, options) {
973
1101
  const grossMinor = order.lines.reduce((sum, line) => sum + line.discountMinor, 0);
974
1102
  const content = contentVersion(order);
975
1103
  const cap = options?.maxVersion === void 0 ? order.sentVersion : Math.min(options.maxVersion, order.sentVersion ?? options.maxVersion);
1104
+ if (content === 5 && cap !== void 0 && cap < 5) throw new UnsupportedOrderVersionError(5, cap);
976
1105
  if (grossMinor > 0 && cap !== void 0 && cap < 2) throw new UnsupportedOrderVersionError(2, cap);
977
1106
  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);
1107
+ const lineDiscounts = order.lines.map((line) => version >= 4 ? netDiscountMinor(line, order.pricesIncludeTax) : line.discountMinor);
979
1108
  const discountMinor = lineDiscounts.reduce((sum, discount) => sum + discount, 0);
980
1109
  const email = order.customer?.email;
981
1110
  const id2 = order.customer?.id;
982
1111
  const customerId = typeof id2 === "string" && id2.length > 0 && id2.length <= 64 ? id2 : void 0;
983
1112
  const sessionId = order.sessionId ?? order.lateSessionId;
1113
+ const { fees: _fees, shipping: _shipping, ...storedDisplay } = { ...order.display };
984
1114
  return {
985
1115
  id: order.commandId,
986
1116
  type: "order.create",
@@ -995,13 +1125,30 @@ function toOrderCreateEnvelope(order, deviceId, attempt2 = 1, options) {
995
1125
  pricesIncludeTax: order.pricesIncludeTax,
996
1126
  lines: order.lines.map((line, i) => ({
997
1127
  clientLineId: line.id,
998
- variantId: line.variantId ?? line.productId,
1128
+ ...line.custom ? { custom: { ...line.custom } } : { variantId: line.variantId ?? line.productId },
999
1129
  title: line.name,
1000
1130
  quantity: line.quantity,
1001
1131
  unitPriceMinor: line.unitPriceMinor,
1002
1132
  ...line.taxInclusive !== void 0 ? { taxInclusive: line.taxInclusive } : {},
1003
1133
  ...lineDiscounts[i] > 0 ? { discountMinor: lineDiscounts[i] } : {}
1004
1134
  })),
1135
+ ...version === 5 && order.fees?.length ? { fees: order.fees.map((fee) => ({
1136
+ clientFeeId: fee.id,
1137
+ name: fee.name,
1138
+ amountMinor: fee.amountMinor,
1139
+ taxStatus: fee.taxStatus,
1140
+ ...fee.taxClass !== void 0 ? { taxClass: fee.taxClass } : {},
1141
+ taxMinor: roundMicrosToMinor(BigInt(fee.taxMicros))
1142
+ })) } : {},
1143
+ ...version === 5 && order.shipping?.length ? { shipping: order.shipping.map((charge) => ({
1144
+ clientShippingId: charge.id,
1145
+ name: charge.name,
1146
+ ...charge.methodId !== void 0 ? { methodId: charge.methodId } : {},
1147
+ amountMinor: charge.amountMinor,
1148
+ taxStatus: charge.taxStatus,
1149
+ ...charge.taxClass !== void 0 ? { taxClass: charge.taxClass } : {},
1150
+ taxMinor: roundMicrosToMinor(BigInt(charge.taxMicros))
1151
+ })) } : {},
1005
1152
  payments: order.payments.map((payment) => ({
1006
1153
  clientPaymentId: payment.id,
1007
1154
  method: payment.method,
@@ -1015,11 +1162,16 @@ function toOrderCreateEnvelope(order, deviceId, attempt2 = 1, options) {
1015
1162
  taxMinor: order.taxMinor,
1016
1163
  totalMinor: order.totalMinor,
1017
1164
  ...version >= 3 ? {
1018
- display: { ...order.display, lines: order.display.lines.map(({ lineId, amountMinor, discounts }) => ({
1019
- clientLineId: lineId,
1020
- amountMinor,
1021
- discounts
1022
- })) },
1165
+ display: {
1166
+ ...storedDisplay,
1167
+ lines: order.display.lines.map(({ lineId, amountMinor, discounts }) => ({
1168
+ clientLineId: lineId,
1169
+ amountMinor,
1170
+ discounts
1171
+ })),
1172
+ ...version === 5 && order.display.fees ? { fees: order.display.fees.map(({ id: id3, amountMinor }) => ({ clientFeeId: id3, amountMinor })) } : {},
1173
+ ...version === 5 && order.display.shipping ? { shipping: order.display.shipping.map(({ id: id3, amountMinor }) => ({ clientShippingId: id3, amountMinor })) } : {}
1174
+ },
1023
1175
  taxByRate: order.taxByRate.map(({ ratePpm, code, netMinor, amountMinor, grossMinor: grossMinor2 }) => ({
1024
1176
  ratePpm,
1025
1177
  ...code !== void 0 ? { code } : {},
@@ -1080,6 +1232,7 @@ function restoreOrderDraft(saved, options) {
1080
1232
  productId: line.productId,
1081
1233
  custom: line.custom,
1082
1234
  taxStatus: line.taxStatus,
1235
+ ...taxContext.rounding?.granularity === "woocommerce" ? { taxClass: line.taxClass } : {},
1083
1236
  variantId: line.variantId,
1084
1237
  name: line.name,
1085
1238
  sku: line.sku,
@@ -1199,7 +1352,7 @@ function useParkedSales(drafts) {
1199
1352
 
1200
1353
  // src/receipt/build-receipt-data.ts
1201
1354
  function buildReceiptData(order, config) {
1202
- const taxedLines = [...order.lineItems, ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({
1355
+ 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
1356
  ...charge,
1204
1357
  taxInclusive: order.pricesIncludeTax
1205
1358
  }))];
@@ -1211,6 +1364,7 @@ function buildReceiptData(order, config) {
1211
1364
  0n
1212
1365
  );
1213
1366
  const lineTotals = order.lineItems.map((li) => {
1367
+ if (order.taxRounding?.granularity === "woocommerce" && li.netMicros !== void 0) return order.pricesIncludeTax ? li.totalMinor : li.netMinor;
1214
1368
  if (!li.priceTaxModeConverted) return li.netMinor;
1215
1369
  const share = roundMicrosToMinor(taxSoFar + BigInt(li.taxMicros)) - roundMicrosToMinor(taxSoFar);
1216
1370
  taxSoFar += BigInt(li.taxMicros);
@@ -1354,7 +1508,7 @@ function uuidv7(now, randomBytes) {
1354
1508
  }
1355
1509
 
1356
1510
  // src/pos-order/finalize.ts
1357
- import { minorUnitDigits } from "@tallyui/core";
1511
+ import { minorUnitDigits as minorUnitDigits2 } from "@tallyui/core";
1358
1512
 
1359
1513
  // src/outbox/logger.ts
1360
1514
  var outboxLogger = createLogger("outbox");
@@ -1369,8 +1523,12 @@ function withSentForm(order, posOrder) {
1369
1523
  ...order,
1370
1524
  customer,
1371
1525
  lineItems: order.lineItems.map((line, i) => ({ ...line, name: posOrder.lines[i].name })),
1526
+ ...order.fees ? { fees: order.fees.map((fee, i) => ({ ...fee, name: posOrder.fees[i].name })) } : {},
1527
+ ...order.shipping ? { shipping: order.shipping.map((charge, i) => ({ ...charge, name: posOrder.shipping[i].name })) } : {},
1372
1528
  display: posOrder.display ? {
1373
1529
  ...order.display,
1530
+ ...order.display.fees ? { fees: order.display.fees.map((row, i) => ({ ...row, name: posOrder.display.fees[i].name })) } : {},
1531
+ ...order.display.shipping ? { shipping: order.display.shipping.map((row, i) => ({ ...row, name: posOrder.display.shipping[i].name })) } : {},
1374
1532
  lines: order.display.lines.map((line, i) => ({
1375
1533
  ...line,
1376
1534
  discounts: line.discounts.map((discount, j) => ({ ...discount, label: posOrder.display.lines[i].discounts[j].label }))
@@ -1383,7 +1541,15 @@ function freezeSentForm(order) {
1383
1541
  const lines = order.lines.map((line) => {
1384
1542
  const name = cutText(line.name, PAYLOAD_STRING_MAX);
1385
1543
  changed ||= name !== line.name;
1386
- return { ...line, name };
1544
+ const custom = line.custom && { ...line.custom, name: cutText(line.custom.name, PAYLOAD_STRING_MAX) };
1545
+ changed ||= custom !== void 0 && custom.name !== line.custom.name;
1546
+ return { ...line, name, ...custom ? { custom } : {} };
1547
+ });
1548
+ const charges = { fees: order.fees, shipping: order.shipping };
1549
+ for (const field of ["fees", "shipping"]) if (charges[field]) charges[field] = charges[field].map((charge) => {
1550
+ const name = cutText(charge.name, PAYLOAD_STRING_MAX);
1551
+ changed ||= name !== charge.name;
1552
+ return { ...charge, name };
1387
1553
  });
1388
1554
  const display = order.display && { ...order.display, lines: order.display.lines.map((line) => ({
1389
1555
  ...line,
@@ -1394,6 +1560,11 @@ function freezeSentForm(order) {
1394
1560
  return { ...discount, label };
1395
1561
  })
1396
1562
  })) };
1563
+ for (const field of ["fees", "shipping"]) if (display?.[field]) display[field] = display[field].map((row) => {
1564
+ const name = cutText(row.name, PAYLOAD_STRING_MAX);
1565
+ changed ||= name !== row.name;
1566
+ return { ...row, name };
1567
+ });
1397
1568
  const payments = order.payments.map((payment) => {
1398
1569
  if (payment.reference === void 0) return payment;
1399
1570
  const reference = cutText(payment.reference, PAYLOAD_STRING_MAX);
@@ -1409,7 +1580,16 @@ function freezeSentForm(order) {
1409
1580
  if (!localWarnings.some((warning) => warning.code === "customer_omitted" && warning.field === field)) localWarnings.push({ code: "customer_omitted", field });
1410
1581
  }
1411
1582
  }
1412
- return changed ? { ...order, lines, payments, customer, ...display ? { display } : {}, ...localWarnings.length ? { localWarnings } : {} } : order;
1583
+ return changed ? {
1584
+ ...order,
1585
+ lines,
1586
+ payments,
1587
+ customer,
1588
+ ...charges.fees ? { fees: charges.fees } : {},
1589
+ ...charges.shipping ? { shipping: charges.shipping } : {},
1590
+ ...display ? { display } : {},
1591
+ ...localWarnings.length ? { localWarnings } : {}
1592
+ } : order;
1413
1593
  }
1414
1594
  function referenceError(label, value) {
1415
1595
  const reason = referenceReason(value);
@@ -1421,8 +1601,8 @@ function referenceReason(value) {
1421
1601
  return value.includes("\0") ? "nul" : null;
1422
1602
  }
1423
1603
  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");
1604
+ if ((order.fees?.length || order.shipping?.length || order.lineItems.some((line) => line.custom)) && (options.capabilities?.orderCreate ?? 1) < 5) {
1605
+ 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
1606
  }
1427
1607
  if (!order.lineItems.length) throw new Error("finalize: no lines");
1428
1608
  if (order.discountMinor < 0 || order.lineItems.some((line) => line.discountMinor < 0) || order.discounts.some((d) => d.amountMinor < 0)) {
@@ -1465,15 +1645,25 @@ function finalizeOrder(order, options = {}) {
1465
1645
  id: newId(),
1466
1646
  productId: line.productId,
1467
1647
  ...line.variantId !== void 0 ? { variantId: line.variantId } : {},
1648
+ ...line.custom ? { custom: {
1649
+ name: line.name,
1650
+ ...line.sku ? { sku: line.sku } : {},
1651
+ ...line.taxClass !== void 0 ? { taxClass: line.taxClass } : {},
1652
+ taxStatus: line.taxStatus ?? "taxable"
1653
+ } } : {},
1654
+ ...line.taxStatus !== void 0 ? { taxStatus: line.taxStatus } : {},
1468
1655
  name: line.name,
1469
1656
  sku: line.sku,
1470
1657
  quantity: line.quantity,
1471
1658
  unitPriceMinor: line.unitPriceMinor,
1472
1659
  discountMinor: line.discountMinor,
1473
1660
  netMinor: line.netMinor,
1661
+ ...order.taxRounding?.granularity === "woocommerce" ? { netMicros: line.netMicros } : {},
1474
1662
  taxLines: line.taxLines.map((tax) => ({ ...tax })),
1475
1663
  ...line.priceTaxModeConverted ? { taxInclusive: line.taxInclusive } : {}
1476
1664
  }));
1665
+ const fees = order.fees?.map((charge) => ({ ...charge, id: newId(), taxLines: charge.taxLines.map((tax) => ({ ...tax })) }));
1666
+ const shipping = order.shipping?.map((charge) => ({ ...charge, id: newId(), taxLines: charge.taxLines.map((tax) => ({ ...tax })) }));
1477
1667
  const payments = order.payments.map((payment) => ({
1478
1668
  id: newId(),
1479
1669
  method: payment.method,
@@ -1511,8 +1701,10 @@ function finalizeOrder(order, options = {}) {
1511
1701
  }
1512
1702
  display = {
1513
1703
  currency: order.currency,
1514
- exponent: minorUnitDigits(order.currency),
1704
+ exponent: minorUnitDigits2(order.currency),
1515
1705
  ...order.display,
1706
+ ...fees?.length ? { fees: order.display.fees.map((row, i) => ({ ...row, id: fees[i].id })) } : {},
1707
+ ...shipping?.length ? { shipping: order.display.shipping.map((row, i) => ({ ...row, id: shipping[i].id })) } : {},
1516
1708
  lines: order.display.lines.map((line, i) => ({
1517
1709
  lineId: lines[i].id,
1518
1710
  amountMinor: line.amountMinor,
@@ -1523,7 +1715,8 @@ function finalizeOrder(order, options = {}) {
1523
1715
  }))
1524
1716
  }))
1525
1717
  };
1526
- taxByRate = taxLinesByRate(order.lineItems, order.taxMinor, void 0, order.taxRounding).map(({ ratePpm, code, netMinor, amountMinor }) => ({
1718
+ const taxedLines = [...order.lineItems, ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({ ...charge, taxInclusive: order.pricesIncludeTax }))];
1719
+ taxByRate = taxLinesByRate(taxedLines, order.taxMinor, void 0, order.taxRounding).map(({ ratePpm, code, netMinor, amountMinor }) => ({
1527
1720
  ratePpm,
1528
1721
  ...code !== void 0 ? { code } : {},
1529
1722
  netMinor,
@@ -1538,7 +1731,7 @@ function finalizeOrder(order, options = {}) {
1538
1731
  }
1539
1732
  }
1540
1733
  const rounding = order.taxRounding ?? DEFAULT_TAX_ROUNDING;
1541
- const taxRounding = rounding.granularity === "custom" ? { granularity: "custom" } : { granularity: rounding.granularity, mode: rounding.mode };
1734
+ const taxRounding = rounding.granularity === "custom" ? { granularity: "custom" } : rounding.granularity === "woocommerce" ? { granularity: "woocommerce", roundAtSubtotal: rounding.roundAtSubtotal } : { granularity: rounding.granularity, mode: rounding.mode };
1542
1735
  const now = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
1543
1736
  return freezeSentForm({
1544
1737
  id: id2,
@@ -1551,6 +1744,8 @@ function finalizeOrder(order, options = {}) {
1551
1744
  pricesIncludeTax: order.pricesIncludeTax,
1552
1745
  lines,
1553
1746
  payments,
1747
+ ...fees?.length ? { fees } : {},
1748
+ ...shipping?.length ? { shipping } : {},
1554
1749
  subtotalMinor: order.subtotalMinor,
1555
1750
  discountMinor: order.discountMinor,
1556
1751
  taxMinor: order.taxMinor,
@@ -1573,7 +1768,7 @@ function finalizeOrder(order, options = {}) {
1573
1768
  import { addRxPlugin } from "rxdb";
1574
1769
  import { RxDBMigrationSchemaPlugin } from "rxdb/plugins/migration-schema";
1575
1770
  var posOrderSchema = {
1576
- version: 7,
1771
+ version: 8,
1577
1772
  primaryKey: "id",
1578
1773
  type: "object",
1579
1774
  additionalProperties: false,
@@ -1603,9 +1798,17 @@ var posOrderSchema = {
1603
1798
  name: { type: "string" },
1604
1799
  sku: { type: "string" },
1605
1800
  quantity: { type: "integer" },
1801
+ taxStatus: { type: "string", enum: ["taxable", "none"] },
1802
+ custom: { type: "object", additionalProperties: false, properties: {
1803
+ name: { type: "string", minLength: 1, maxLength: 255 },
1804
+ sku: { type: "string", maxLength: 64 },
1805
+ taxClass: { type: "string", maxLength: 64 },
1806
+ taxStatus: { type: "string", enum: ["taxable", "none"] }
1807
+ }, required: ["name", "taxStatus"] },
1606
1808
  unitPriceMinor: { type: "integer" },
1607
1809
  discountMinor: { type: "integer" },
1608
1810
  netMinor: { type: "integer" },
1811
+ netMicros: { type: "string", pattern: "^-?[0-9]{1,24}$", maxLength: 25 },
1609
1812
  taxLines: { type: "array", items: {
1610
1813
  type: "object",
1611
1814
  properties: { code: { type: "string" }, ratePpm: { type: "integer" }, taxMicros: { type: "string" } },
@@ -1614,6 +1817,43 @@ var posOrderSchema = {
1614
1817
  },
1615
1818
  required: ["id", "productId", "name", "sku", "quantity", "unitPriceMinor", "discountMinor", "netMinor", "taxLines"]
1616
1819
  } },
1820
+ fees: { type: "array", items: { type: "object", additionalProperties: false, properties: {
1821
+ id: { type: "string", maxLength: 36 },
1822
+ name: { type: "string", minLength: 1, maxLength: 255 },
1823
+ amountMinor: { type: "integer", minimum: 0 },
1824
+ taxClass: { type: "string", maxLength: 64 },
1825
+ taxStatus: { type: "string", enum: ["taxable", "none"] },
1826
+ netMinor: { type: "integer" },
1827
+ taxMicros: { type: "string" },
1828
+ netMicros: { type: "string", pattern: "^-?[0-9]{1,24}$", maxLength: 25 },
1829
+ totalMinor: { type: "integer" },
1830
+ taxLines: { type: "array", items: { type: "object", additionalProperties: false, properties: {
1831
+ code: { type: "string", maxLength: 255 },
1832
+ ratePpm: { type: "integer" },
1833
+ taxMicros: { type: "string" },
1834
+ rateId: { type: "integer" },
1835
+ compound: { type: "boolean" }
1836
+ }, required: ["ratePpm", "taxMicros"] } }
1837
+ }, required: ["id", "name", "amountMinor", "taxStatus", "taxLines", "netMinor", "taxMicros"] } },
1838
+ shipping: { type: "array", items: { type: "object", additionalProperties: false, properties: {
1839
+ id: { type: "string", maxLength: 36 },
1840
+ name: { type: "string", minLength: 1, maxLength: 255 },
1841
+ methodId: { type: "string", maxLength: 64 },
1842
+ amountMinor: { type: "integer", minimum: 0 },
1843
+ taxClass: { type: "string", maxLength: 64 },
1844
+ taxStatus: { type: "string", enum: ["taxable", "none"] },
1845
+ netMinor: { type: "integer" },
1846
+ taxMicros: { type: "string" },
1847
+ netMicros: { type: "string", pattern: "^-?[0-9]{1,24}$", maxLength: 25 },
1848
+ totalMinor: { type: "integer" },
1849
+ taxLines: { type: "array", items: { type: "object", additionalProperties: false, properties: {
1850
+ code: { type: "string", maxLength: 255 },
1851
+ ratePpm: { type: "integer" },
1852
+ taxMicros: { type: "string" },
1853
+ rateId: { type: "integer" },
1854
+ compound: { type: "boolean" }
1855
+ }, required: ["ratePpm", "taxMicros"] } }
1856
+ }, required: ["id", "name", "amountMinor", "taxStatus", "taxLines", "netMinor", "taxMicros"] } },
1617
1857
  payments: { type: "array", items: {
1618
1858
  type: "object",
1619
1859
  properties: {
@@ -1651,11 +1891,12 @@ var posOrderSchema = {
1651
1891
  }, required: ["since", "reason", "isolated"] },
1652
1892
  lateSessionId: { type: "string" },
1653
1893
  taxRounding: { type: "object", additionalProperties: false, properties: {
1654
- granularity: { type: "string", enum: ["per_order", "per_line_items", "per_rate_group_items", "custom"] },
1894
+ granularity: { type: "string", enum: ["per_order", "per_line_items", "per_rate_group_items", "custom", "woocommerce"] },
1895
+ roundAtSubtotal: { type: "boolean" },
1655
1896
  mode: { type: "string", enum: ["half_away_from_zero", "half_up"] }
1656
1897
  }, required: ["granularity"] },
1657
- sentVersion: { type: "integer", minimum: 1, maximum: 4 },
1658
- downgradedFrom: { type: "integer", minimum: 1, maximum: 4 },
1898
+ sentVersion: { type: "integer", minimum: 1, maximum: 5 },
1899
+ downgradedFrom: { type: "integer", minimum: 1, maximum: 5 },
1659
1900
  // The nested objects are closed too: loosening a schema later is free, tightening one costs a migration.
1660
1901
  display: { type: "object", additionalProperties: false, properties: {
1661
1902
  currency: { type: "string" },
@@ -1666,6 +1907,16 @@ var posOrderSchema = {
1666
1907
  taxMinor: { type: "integer" },
1667
1908
  totalMinor: { type: "integer" },
1668
1909
  orderDiscountMinor: { type: "integer" },
1910
+ fees: { type: "array", items: { type: "object", additionalProperties: false, properties: {
1911
+ id: { type: "string", maxLength: 36 },
1912
+ name: { type: "string", minLength: 1, maxLength: 255 },
1913
+ amountMinor: { type: "integer" }
1914
+ }, required: ["id", "name", "amountMinor"] } },
1915
+ shipping: { type: "array", items: { type: "object", additionalProperties: false, properties: {
1916
+ id: { type: "string", maxLength: 36 },
1917
+ name: { type: "string", minLength: 1, maxLength: 255 },
1918
+ amountMinor: { type: "integer" }
1919
+ }, required: ["id", "name", "amountMinor"] } },
1669
1920
  lines: { type: "array", items: {
1670
1921
  type: "object",
1671
1922
  additionalProperties: false,
@@ -1728,7 +1979,7 @@ function posOrderCollection() {
1728
1979
  };
1729
1980
  return {
1730
1981
  schema: posOrderSchema,
1731
- migrationStrategies: { 1: identity2, 2: identity2, 3: identity2, 4: identity2, 5: recordSent, 6: recordRounding, 7: identity2 }
1982
+ migrationStrategies: { 1: identity2, 2: identity2, 3: identity2, 4: identity2, 5: recordSent, 6: recordRounding, 7: identity2, 8: identity2 }
1732
1983
  };
1733
1984
  }
1734
1985
 
@@ -2628,8 +2879,8 @@ async function writeClosure({
2628
2879
  const roundings = /* @__PURE__ */ new Set();
2629
2880
  for (const order of bound) {
2630
2881
  const recorded = order.taxRounding ?? DEFAULT_TAX_ROUNDING;
2631
- const { granularity, mode } = recorded.granularity === "custom" ? DEFAULT_TAX_ROUNDING : recorded;
2632
- roundings.add(`${granularity} ${mode}`);
2882
+ const rounding = recorded.granularity === "custom" ? DEFAULT_TAX_ROUNDING : recorded;
2883
+ roundings.add(rounding.granularity === "woocommerce" ? `woocommerce ${rounding.roundAtSubtotal}` : `${rounding.granularity} ${rounding.mode}`);
2633
2884
  const lines = order.lines.map((line) => ({ ...line, taxInclusive: line.taxInclusive ?? order.pricesIncludeTax }));
2634
2885
  for (const { ratePpm, netMinor, amountMinor } of taxLinesByRate(lines, order.taxMinor, void 0, order.taxRounding)) {
2635
2886
  const existing2 = taxRates.get(String(ratePpm));
@@ -2998,7 +3249,7 @@ function envelope(row, context, xreport = false) {
2998
3249
  });
2999
3250
  return {
3000
3251
  closure: {
3001
- has_sales: row.period_sales_total != null || row.period_refunds_total != null || Number(breakdowns.transaction_count) > 0,
3252
+ has_sales: Number(breakdowns.transaction_count ?? 0) > 0 || Number(row.period_sales_total || 0) !== 0 || Number(row.period_refunds_total || 0) !== 0,
3002
3253
  has_perpetual: row.perpetual_sales_total != null || row.perpetual_refunds_total != null,
3003
3254
  ...Object.fromEntries(
3004
3255
  ["payment_methods", "tax_rates", "movements"].map((key) => [`has_${key}`, Object.keys(breakdowns[key] ?? {}).length > 0])
@@ -3659,7 +3910,8 @@ function addEntryToCart(builder, entry, traits, currency) {
3659
3910
  sku: variant.sku ?? "",
3660
3911
  imageUrl: traits.getImageUrl?.(product),
3661
3912
  unitPrice,
3662
- taxClass: traits.getTaxClass?.(product, variant.id)
3913
+ taxClass: traits.getTaxClass?.(product, variant.id),
3914
+ ...traits.getTaxStatus?.(product, variant.id) === "none" ? { taxStatus: "none" } : {}
3663
3915
  });
3664
3916
  }
3665
3917
 
@@ -4449,6 +4701,7 @@ function createOrderOutbox(options) {
4449
4701
  const serverMax = firstSend && validMax(await Promise.resolve().then(() => options.getMaxOrderCreateVersion?.()).catch((cause) => {
4450
4702
  outboxLogger.warn("Failed to read capabilities", { cause });
4451
4703
  })) || 3;
4704
+ const localOrders = /* @__PURE__ */ new Map();
4452
4705
  const batch = await Promise.all(orders.map(async (order) => {
4453
4706
  const frozen = freezeSentForm(order);
4454
4707
  const attempt2 = (attempts.get(order.commandId) ?? 0) + 1;
@@ -4458,14 +4711,15 @@ function createOrderOutbox(options) {
4458
4711
  envelope2 = toOrderCreateEnvelope(frozen, deviceId, attempt2, { maxVersion: order.sentVersion ?? serverMax });
4459
4712
  } catch (cause) {
4460
4713
  if (!(cause instanceof UnsupportedOrderVersionError)) throw cause;
4461
- envelope2 = toOrderCreateEnvelope(frozen, deviceId, attempt2, { maxVersion: 3 });
4714
+ envelope2 = toOrderCreateEnvelope(frozen, deviceId, attempt2, { maxVersion: cause.needed === 5 ? 5 : 3 });
4462
4715
  }
4463
4716
  if (frozen !== order || order.sentVersion === void 0) {
4464
4717
  await (await collection.findOne(order.id).exec())?.incrementalModify((data) => ({ ...freezeSentForm(data), sentVersion: data.sentVersion ?? envelope2.version }));
4465
4718
  }
4719
+ localOrders.set(envelope2.id, frozen);
4466
4720
  return envelope2;
4467
4721
  }));
4468
- let outcome = await transport.send(batch);
4722
+ let outcome = await transport.send(batch, { local: { orders: localOrders } });
4469
4723
  backendNotFound.record(outcome, now());
4470
4724
  if (outcome.kind !== "retry" || outcome.reason !== "network") {
4471
4725
  const at2 = now();