@tallyui/pos 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -125,6 +125,8 @@ type TaxRateMap = {
125
125
  [taxClass: string]: number;
126
126
  };
127
127
  interface TaxContext {
128
+ shippingTaxClass?: string;
129
+ taxClassSlugs?: readonly string[];
128
130
  getTaxRates?(taxClass?: string, options?: {
129
131
  shipping?: boolean;
130
132
  }): readonly WooRate[];
@@ -148,7 +150,7 @@ interface TaxProviderProps extends Pick<StoreSettings, 'taxRoundAtSubtotal' | 's
148
150
  rateCodes?: Record<string, string>;
149
151
  children: ReactNode;
150
152
  }
151
- declare function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, children }: TaxProviderProps): react_jsx_runtime.JSX.Element;
153
+ declare function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs, children }: TaxProviderProps): react_jsx_runtime.JSX.Element;
152
154
  declare function useTax(): TaxContext;
153
155
 
154
156
  type StoreSettingsResolution = {
@@ -1957,6 +1959,9 @@ declare function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
1957
1959
  stage: SaleStage;
1958
1960
  error: string | null;
1959
1961
  idle: boolean;
1962
+ /** The store's capabilities as passed in: a component offers fees, shipping and custom lines
1963
+ * only when `orderCreate >= 5`, and their tax status and class only as `lineTax` allows. */
1964
+ capabilities: ServerCapabilities | undefined;
1960
1965
  /** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
1961
1966
  saving: boolean;
1962
1967
  /** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
package/dist/index.js CHANGED
@@ -327,7 +327,7 @@ import { woocommerceTax as woocommerceTax2 } from "@tallyui/core";
327
327
  import { jsx as jsx2 } from "react/jsx-runtime";
328
328
  var taxLogger = createLogger("tax");
329
329
  var TaxCtx = createContext2(null);
330
- function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, children }) {
330
+ function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs, children }) {
331
331
  const value = useMemo2(
332
332
  () => {
333
333
  if (Object.values(ratesPpm).some((rate) => !Number.isSafeInteger(rate) || rate < 0)) {
@@ -350,10 +350,12 @@ function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates
350
350
  return rateCodes && Object.hasOwn(rateCodes, key) ? rateCodes[key] : void 0;
351
351
  },
352
352
  pricesIncludeTax,
353
- ...rounding ? { rounding } : {}
353
+ ...rounding ? { rounding } : {},
354
+ ...shippingTaxClass !== void 0 ? { shippingTaxClass } : {},
355
+ ...taxClassSlugs !== void 0 ? { taxClassSlugs } : {}
354
356
  };
355
357
  },
356
- [ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates]
358
+ [ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs]
357
359
  );
358
360
  return /* @__PURE__ */ jsx2(TaxCtx.Provider, { value, children });
359
361
  }
@@ -554,7 +556,7 @@ function createRepository(collection) {
554
556
 
555
557
  // src/order/order-builder.ts
556
558
  import { BehaviorSubject } from "rxjs";
557
- import { resolvePrice } from "@tallyui/core";
559
+ import { resolvePrice, woocommerceTax as woocommerceTax3 } from "@tallyui/core";
558
560
 
559
561
  // src/order/allocate-order-discount.ts
560
562
  function allocateOrderDiscount(lineAmountsMinor, totalMinor) {
@@ -682,6 +684,7 @@ function createOrderBuilder(options) {
682
684
  });
683
685
  const shares = allocateOrderDiscount(lineAmounts, recalcedOrderDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
684
686
  const lines = lineItems.map((li, index) => recalculateLine(li, shares[index]));
687
+ if (woo) shipping = shipping.map((charge) => ({ ...charge, ...recalculateCharge(charge.id, charge, true) }));
685
688
  const wooTotals = woo ? woocommerceTotals(lines, fees, shipping, currency, woo.roundAtSubtotal, taxContext.pricesIncludeTax) : void 0;
686
689
  const { subtotalMinor } = wooTotals ?? sumLines(lines, taxContext.rounding);
687
690
  const charges = [...fees, ...shipping];
@@ -799,7 +802,18 @@ function createOrderBuilder(options) {
799
802
  if (!input.name.trim()) throw new RangeError("Name must not be empty");
800
803
  const taxStatus = input.taxStatus ?? "taxable";
801
804
  if (woo) {
802
- const line2 = wooLine(input.amountMinor, input.taxClass, taxContext.pricesIncludeTax, taxStatus === "none", shipping2);
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
+ );
803
817
  return {
804
818
  id: id2,
805
819
  name: input.name,
@@ -2882,7 +2896,8 @@ async function writeClosure({
2882
2896
  const rounding = recorded.granularity === "custom" ? DEFAULT_TAX_ROUNDING : recorded;
2883
2897
  roundings.add(rounding.granularity === "woocommerce" ? `woocommerce ${rounding.roundAtSubtotal}` : `${rounding.granularity} ${rounding.mode}`);
2884
2898
  const lines = order.lines.map((line) => ({ ...line, taxInclusive: line.taxInclusive ?? order.pricesIncludeTax }));
2885
- 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)) {
2886
2901
  const existing2 = taxRates.get(String(ratePpm));
2887
2902
  taxRates.set(String(ratePpm), {
2888
2903
  ratePpm,
@@ -4079,6 +4094,9 @@ function useSale(settings, opts) {
4079
4094
  stage,
4080
4095
  error,
4081
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,
4082
4100
  /** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
4083
4101
  saving,
4084
4102
  /** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/pos",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "type": "module",
5
5
  "description": "Shared POS business logic for Tally UI — tax, orders, payments, discounts, receipts",
6
6
  "main": "./dist/index.js",
@@ -27,20 +27,20 @@
27
27
  "directory": "packages/pos"
28
28
  },
29
29
  "peerDependencies": {
30
- "@tallyui/core": "^3.3.0",
30
+ "@tallyui/core": "^3.4.0",
31
31
  "react": ">=18",
32
32
  "rxdb": "~17.5.0",
33
33
  "rxjs": ">=7"
34
34
  },
35
35
  "devDependencies": {
36
- "@tallyui/core": "3.3.0",
36
+ "@tallyui/core": "3.4.0",
37
37
  "@types/react": ">=18",
38
38
  "react": "^19.2.4",
39
39
  "rxdb": "17.5.0",
40
40
  "rxjs": "7.8.2",
41
- "@tallyui/connector-medusa": "3.3.0",
42
- "@tallyui/connector-woocommerce": "3.3.0",
43
- "@tallyui/database": "3.3.0"
41
+ "@tallyui/connector-medusa": "3.4.0",
42
+ "@tallyui/connector-woocommerce": "3.4.0",
43
+ "@tallyui/database": "3.4.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",
@@ -1,5 +1,5 @@
1
1
  import { BehaviorSubject, type Observable } from 'rxjs';
2
- import { resolvePrice, type ProductTraits, type TaxRounding } from '@tallyui/core';
2
+ import { resolvePrice, woocommerceTax, type ProductTraits, type TaxRounding } from '@tallyui/core';
3
3
  import type { TaxContext } from '../tax/types';
4
4
  import { taxMicros, roundMicrosToMinor, roundedTaxByRate } from '../tax/exact';
5
5
  import { taxLogger } from '../tax/tax-provider';
@@ -188,6 +188,7 @@ export function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder {
188
188
  const shares = allocateOrderDiscount(lineAmounts, recalcedOrderDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
189
189
  const lines = lineItems.map((li, index) => recalculateLine(li, shares[index]));
190
190
 
191
+ if (woo) shipping = shipping.map((charge) => ({ ...charge, ...recalculateCharge(charge.id, charge, true) }));
191
192
  const wooTotals = woo ? woocommerceTotals(lines, fees, shipping, currency, woo.roundAtSubtotal, taxContext.pricesIncludeTax) : undefined;
192
193
  const { subtotalMinor } = wooTotals ?? sumLines(lines, taxContext.rounding);
193
194
  const charges = [...fees, ...shipping];
@@ -310,7 +311,16 @@ export function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder {
310
311
  if (!input.name.trim()) throw new RangeError('Name must not be empty');
311
312
  const taxStatus = input.taxStatus ?? 'taxable';
312
313
  if (woo) {
313
- const line = wooLine(input.amountMinor, input.taxClass, taxContext.pricesIncludeTax, taxStatus === 'none', shipping);
314
+ const taxClass = shipping
315
+ ? taxContext.shippingTaxClass === undefined || taxContext.shippingTaxClass === 'inherit'
316
+ ? woocommerceTax.resolveInheritedShippingTaxClass(lineItems.map((line) => ({
317
+ tax_class: line.taxClass, taxable: line.taxStatus !== 'none',
318
+ })), taxContext.taxClassSlugs ?? [])
319
+ : taxContext.shippingTaxClass
320
+ : input.taxClass;
321
+ const untaxed = shipping ? taxClass === woocommerceTax.NO_SHIPPING_TAX : taxStatus === 'none';
322
+ const line = wooLine(input.amountMinor, taxClass === woocommerceTax.NO_SHIPPING_TAX ? undefined : taxClass,
323
+ taxContext.pricesIncludeTax, untaxed, shipping);
314
324
  return { id, name: input.name, amountMinor: input.amountMinor, taxClass: input.taxClass, taxStatus,
315
325
  ...line };
316
326
  }
@@ -484,7 +484,9 @@ export async function writeClosure({
484
484
  const rounding = recorded.granularity === 'custom' ? DEFAULT_TAX_ROUNDING : recorded;
485
485
  roundings.add(rounding.granularity === 'woocommerce' ? `woocommerce ${rounding.roundAtSubtotal}` : `${rounding.granularity} ${rounding.mode}`);
486
486
  const lines = order.lines.map((line) => ({ ...line, taxInclusive: line.taxInclusive ?? order.pricesIncludeTax }));
487
- for (const { ratePpm, netMinor, amountMinor } of taxLinesByRate(lines, order.taxMinor, undefined, order.taxRounding)) {
487
+ const taxedLines = [...lines, ...[...(order.fees ?? []), ...(order.shipping ?? [])]
488
+ .map((charge) => ({ ...charge, taxInclusive: order.pricesIncludeTax }))];
489
+ for (const { ratePpm, netMinor, amountMinor } of taxLinesByRate(taxedLines, order.taxMinor, undefined, order.taxRounding)) {
488
490
  const existing = taxRates.get(String(ratePpm));
489
491
  taxRates.set(String(ratePpm), {
490
492
  ratePpm,
@@ -249,6 +249,9 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
249
249
 
250
250
  const result = {
251
251
  order, stage, error, idle,
252
+ /** The store's capabilities as passed in: a component offers fees, shipping and custom lines
253
+ * only when `orderCreate >= 5`, and their tax status and class only as `lineTax` allows. */
254
+ capabilities: opts.capabilities,
252
255
  /** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
253
256
  saving,
254
257
  /** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
@@ -19,7 +19,7 @@ export interface TaxProviderProps extends Pick<StoreSettings, 'taxRoundAtSubtota
19
19
  children: ReactNode;
20
20
  }
21
21
 
22
- export function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, children }: TaxProviderProps) {
22
+ export function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs, children }: TaxProviderProps) {
23
23
  const value = useMemo<TaxContext>(
24
24
  () => {
25
25
  if (Object.values(ratesPpm).some((rate) => !Number.isSafeInteger(rate) || rate < 0)) {
@@ -45,9 +45,11 @@ export function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, t
45
45
  },
46
46
  pricesIncludeTax,
47
47
  ...(rounding ? { rounding } : {}),
48
+ ...(shippingTaxClass !== undefined ? { shippingTaxClass } : {}),
49
+ ...(taxClassSlugs !== undefined ? { taxClassSlugs } : {}),
48
50
  };
49
51
  },
50
- [ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates],
52
+ [ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs],
51
53
  );
52
54
 
53
55
  return <TaxCtx.Provider value={value}>{children}</TaxCtx.Provider>;
package/src/tax/types.ts CHANGED
@@ -9,6 +9,8 @@ export type TaxRateMap = {
9
9
  };
10
10
 
11
11
  export interface TaxContext {
12
+ shippingTaxClass?: string;
13
+ taxClassSlugs?: readonly string[];
12
14
  getTaxRates?(taxClass?: string, options?: { shipping?: boolean }): readonly WooRate[];
13
15
  /** Tax rate for a tax class as integer parts per million (19% = 190000); the default class when omitted or unknown. */
14
16
  getTaxRatePpm(taxClass?: string): number;