@tallyui/pos 3.3.0 → 3.5.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
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { Money, TaxRounding, StoreSettings, TallyConnector, SyncContext, StoreSettingsChoice, StoreSettingsChoices, ProductTraits, OrderCreateLine, PaymentMethodKind, CommandServerRefs, CommandWarning, CommandError, ServerCapabilities, OrderCreateEnvelope, RegisterCommandType, RegisterCommandResult, VariantSummary, TraitContext, CommandResult, AnyCommandEnvelope, CommandEnvelope, OrderCreatePayload, RegisterCommandEnvelope } from '@tallyui/core';
3
+ import { Money, TaxRounding, StoreSettings, TallyConnector, SyncContext, StoreSettingsChoice, StoreSettingsChoices, ProductTraits, ProductCategory, TraitContext, OrderCreateLine, PaymentMethodKind, CommandServerRefs, CommandWarning, CommandError, ServerCapabilities, OrderCreateEnvelope, RegisterCommandType, RegisterCommandResult, VariantSummary, CommandResult, AnyCommandEnvelope, CommandEnvelope, OrderCreatePayload, RegisterCommandEnvelope } from '@tallyui/core';
4
4
  export { getProductStock, withStockOverlay } from '@tallyui/core';
5
5
  export { countFresh, readFresh, watchFresh } from '@tallyui/core/rxdb';
6
6
  import * as rxdb from 'rxdb';
@@ -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 = {
@@ -616,6 +618,53 @@ declare function buildReceiptData(order: SentOrder, config: ReceiptConfig): Rece
616
618
  */
617
619
  declare function searchProducts<Doc>(docs: Doc[], term: string, traits: ProductTraits<Doc>): Doc[];
618
620
 
621
+ /** The product's categories: `getCategories`, else `getCategoryNames` with each name as its id. */
622
+ declare function productCategories<Doc>(doc: Doc, traits: ProductTraits<Doc>): ProductCategory[];
623
+ /** Every category across `docs`, once per id (the first name seen wins), sorted by name (natural, case-insensitive), ties by id. */
624
+ declare function listCategories<Doc>(docs: readonly Doc[], traits: ProductTraits<Doc>): ProductCategory[];
625
+ /** Whether `doc` is in the category with this id. */
626
+ declare function inCategory<Doc>(doc: Doc, categoryId: string, traits: ProductTraits<Doc>): boolean;
627
+
628
+ /** A sort on one field: ascending or descending. */
629
+ interface ProductSort {
630
+ field: string;
631
+ dir: 'asc' | 'desc';
632
+ }
633
+ type ProductSortValue = string | number | undefined;
634
+ /** The fields productSortValue knows, in the default column order. */
635
+ declare const PRODUCT_SORT_FIELDS: readonly ["name", "sku", "barcode", "price", "stock", "category"];
636
+ declare function productSortValue<Doc>(doc: Doc, field: string, traits: ProductTraits<Doc>, context?: TraitContext): ProductSortValue;
637
+ declare function sortProducts<Doc>(docs: Doc[], sort: ProductSort | null | undefined, valueOf: (doc: Doc, field: string) => ProductSortValue): Doc[];
638
+
639
+ type CatalogueView = 'grid' | 'table';
640
+ type CatalogueGridColumns = 'auto' | 2 | 3 | 4 | 5 | 6 | 7 | 8;
641
+ interface CatalogueViewState {
642
+ view: CatalogueView;
643
+ gridColumns: CatalogueGridColumns;
644
+ sort: ProductSort | null;
645
+ categoryId: string | null;
646
+ }
647
+ declare const DEFAULT_CATALOGUE_VIEW_STATE: CatalogueViewState;
648
+ type CatalogueViewAction = {
649
+ type: 'setView';
650
+ view: CatalogueView;
651
+ } | {
652
+ type: 'setGridColumns';
653
+ gridColumns: CatalogueGridColumns;
654
+ } | {
655
+ type: 'setSort';
656
+ sort: ProductSort | null;
657
+ } | {
658
+ type: 'setCategory';
659
+ categoryId: string | null;
660
+ } | {
661
+ type: 'replace';
662
+ state: CatalogueViewState;
663
+ };
664
+ declare function catalogueViewReducer(state: CatalogueViewState, action: CatalogueViewAction): CatalogueViewState;
665
+ declare function normalizeCatalogueViewState(raw: unknown, defaults?: CatalogueViewState): CatalogueViewState;
666
+ declare function resolveGridColumns(gridColumns: CatalogueGridColumns, containerWidth: number): number;
667
+
619
668
  /** The `stock_levels` collection as a live map of key to stock value. */
620
669
  declare function stockOverlay$(collection: RxCollection): Observable<Map<string, unknown>>;
621
670
  /**
@@ -1957,6 +2006,9 @@ declare function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
1957
2006
  stage: SaleStage;
1958
2007
  error: string | null;
1959
2008
  idle: boolean;
2009
+ /** The store's capabilities as passed in: a component offers fees, shipping and custom lines
2010
+ * only when `orderCreate >= 5`, and their tax status and class only as `lineTax` allows. */
2011
+ capabilities: ServerCapabilities | undefined;
1960
2012
  /** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
1961
2013
  saving: boolean;
1962
2014
  /** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
@@ -2444,4 +2496,4 @@ declare function planLegs(plan: TenderPlan, rowsSinceFrom: {
2444
2496
  };
2445
2497
  };
2446
2498
 
2447
- export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type ChargeInput, type ChargeLine, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FeeLine, type FinalizeOptions, type HttpTransportOptions, type LedgerRow, type LineItem, type LineTaxLine, type LogEntry, type LogLevel, type LogSink, type Logger, MAX_TENDER_MINOR, MICROS_PER_MINOR, type Movement$1 as Movement, type MovementType, type Order, type OrderBuilder, type OrderBuilderOptions, OrderContentMismatchError, type OrderManager, type OrderManagerOptions, type OrderOutbox, type OrderOutboxOptions, type OrderTaxTotals, type OrderTransportContext, type OutboxState, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type ShippingLine, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterOutboxOptions, type UseRegisterOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, initTenderState, initialTenderState, isServerDecimal, labelKeys, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, observeRegister$, openSession, openSessionSelector, orderDraftSchema, outboxLogger, overThreshold, parkedOrderSummaries$, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveStoreSettings, restoreOrderDraft, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useParkedSales, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure, writeOrderDraft };
2499
+ export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type CatalogueGridColumns, type CatalogueView, type CatalogueViewAction, type CatalogueViewState, type ChargeInput, type ChargeLine, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DEFAULT_CATALOGUE_VIEW_STATE, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FeeLine, type FinalizeOptions, type HttpTransportOptions, type LedgerRow, type LineItem, type LineTaxLine, type LogEntry, type LogLevel, type LogSink, type Logger, MAX_TENDER_MINOR, MICROS_PER_MINOR, type Movement$1 as Movement, type MovementType, type Order, type OrderBuilder, type OrderBuilderOptions, OrderContentMismatchError, type OrderManager, type OrderManagerOptions, type OrderOutbox, type OrderOutboxOptions, type OrderTaxTotals, type OrderTransportContext, type OutboxState, PRODUCT_SORT_FIELDS, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type ProductSort, type ProductSortValue, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type ShippingLine, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterOutboxOptions, type UseRegisterOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, catalogueViewReducer, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, inCategory, initTenderState, initialTenderState, isServerDecimal, labelKeys, listCategories, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, normalizeCatalogueViewState, observeRegister$, openSession, openSessionSelector, orderDraftSchema, outboxLogger, overThreshold, parkedOrderSummaries$, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, productCategories, productSortValue, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveGridColumns, resolveStoreSettings, restoreOrderDraft, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, sortProducts, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useParkedSales, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure, writeOrderDraft };
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,
@@ -1440,6 +1454,110 @@ function searchProducts(docs, term, traits) {
1440
1454
  });
1441
1455
  }
1442
1456
 
1457
+ // src/product/categories.ts
1458
+ function productCategories(doc, traits) {
1459
+ return traits.getCategories ? traits.getCategories(doc) : traits.getCategoryNames(doc).map((name) => ({ id: name, name }));
1460
+ }
1461
+ function listCategories(docs, traits) {
1462
+ const categories = /* @__PURE__ */ new Map();
1463
+ for (const doc of docs) {
1464
+ for (const category of productCategories(doc, traits)) {
1465
+ if (!categories.has(category.id)) categories.set(category.id, category);
1466
+ }
1467
+ }
1468
+ return [...categories.values()].sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true, sensitivity: "base" }) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
1469
+ }
1470
+ function inCategory(doc, categoryId, traits) {
1471
+ return productCategories(doc, traits).some((category) => category.id === categoryId);
1472
+ }
1473
+
1474
+ // src/product/catalogue-view-state.ts
1475
+ var DEFAULT_CATALOGUE_VIEW_STATE = Object.freeze({
1476
+ view: "grid",
1477
+ gridColumns: "auto",
1478
+ sort: null,
1479
+ categoryId: null
1480
+ });
1481
+ function catalogueViewReducer(state, action) {
1482
+ switch (action.type) {
1483
+ case "setView":
1484
+ return state.view === action.view ? state : { ...state, view: action.view };
1485
+ case "setGridColumns":
1486
+ return state.gridColumns === action.gridColumns ? state : { ...state, gridColumns: action.gridColumns };
1487
+ case "setCategory":
1488
+ return state.categoryId === action.categoryId ? state : { ...state, categoryId: action.categoryId };
1489
+ case "setSort":
1490
+ return state.sort?.field === action.sort?.field && state.sort?.dir === action.sort?.dir ? state : { ...state, sort: action.sort };
1491
+ case "replace":
1492
+ return action.state;
1493
+ }
1494
+ }
1495
+ function normalizeCatalogueViewState(raw, defaults = DEFAULT_CATALOGUE_VIEW_STATE) {
1496
+ if (typeof raw !== "object" || raw === null) return defaults;
1497
+ try {
1498
+ const { view, gridColumns, sort, categoryId } = raw;
1499
+ const candidate = sort;
1500
+ return {
1501
+ categoryId: categoryId === null || typeof categoryId === "string" && categoryId.length > 0 ? categoryId : defaults.categoryId,
1502
+ view: view === "grid" || view === "table" ? view : defaults.view,
1503
+ gridColumns: gridColumns === "auto" || typeof gridColumns === "number" && Number.isInteger(gridColumns) && gridColumns >= 2 && gridColumns <= 8 ? gridColumns : defaults.gridColumns,
1504
+ sort: sort === null ? null : typeof candidate === "object" && candidate && typeof candidate.field === "string" && candidate.field.length > 0 && (candidate.dir === "asc" || candidate.dir === "desc") ? { field: candidate.field, dir: candidate.dir } : defaults.sort
1505
+ };
1506
+ } catch {
1507
+ return defaults;
1508
+ }
1509
+ }
1510
+ var MIN_TILE_WIDTH = 160;
1511
+ function resolveGridColumns(gridColumns, containerWidth) {
1512
+ return gridColumns === "auto" ? Math.max(2, Math.min(6, Math.floor((containerWidth - 8) / (MIN_TILE_WIDTH + 8)))) : gridColumns;
1513
+ }
1514
+
1515
+ // src/product/sort-products.ts
1516
+ import { resolvePrice as resolvePrice2, resolvePriceRange } from "@tallyui/core";
1517
+ var PRODUCT_SORT_FIELDS = ["name", "sku", "barcode", "price", "stock", "category"];
1518
+ function productSortValue(doc, field, traits, context) {
1519
+ let value;
1520
+ switch (field) {
1521
+ case "name":
1522
+ value = traits.getName(doc);
1523
+ break;
1524
+ case "sku":
1525
+ value = traits.getSku(doc);
1526
+ break;
1527
+ case "barcode":
1528
+ value = traits.getBarcode(doc);
1529
+ break;
1530
+ case "price": {
1531
+ const variants = traits.getVariants?.(doc, context);
1532
+ value = variants && variants.length >= 2 ? resolvePriceRange(variants, context?.currency)?.min.amount : resolvePrice2(traits.getPrices(doc, context), context?.currency)?.current.amount;
1533
+ break;
1534
+ }
1535
+ case "stock":
1536
+ value = traits.getStock(doc).quantity;
1537
+ break;
1538
+ case "category":
1539
+ value = traits.getCategoryNames(doc).join(", ");
1540
+ break;
1541
+ default:
1542
+ return void 0;
1543
+ }
1544
+ return value === "" ? void 0 : value;
1545
+ }
1546
+ function sortProducts(docs, sort, valueOf) {
1547
+ if (!sort) return docs;
1548
+ return docs.map((doc, index) => ({ doc, index, value: valueOf(doc, sort.field) })).sort((a, b) => {
1549
+ const aMissing = a.value === void 0 || a.value === "";
1550
+ const bMissing = b.value === void 0 || b.value === "";
1551
+ if (aMissing || bMissing) {
1552
+ return aMissing === bMissing ? a.index - b.index : aMissing ? 1 : -1;
1553
+ }
1554
+ const av = a.value;
1555
+ const bv = b.value;
1556
+ const comparison = typeof av === "number" ? typeof bv === "number" ? av - bv : -1 : typeof bv === "number" ? 1 : av.localeCompare(bv, void 0, { numeric: true, sensitivity: "base" });
1557
+ return (sort.dir === "desc" ? -comparison : comparison) || a.index - b.index;
1558
+ }).map(({ doc }) => doc);
1559
+ }
1560
+
1443
1561
  // src/product/stock.ts
1444
1562
  import { map as map3 } from "rxjs";
1445
1563
  import { STOCK_LEVELS_LAST_PASS } from "@tallyui/core";
@@ -2882,7 +3000,8 @@ async function writeClosure({
2882
3000
  const rounding = recorded.granularity === "custom" ? DEFAULT_TAX_ROUNDING : recorded;
2883
3001
  roundings.add(rounding.granularity === "woocommerce" ? `woocommerce ${rounding.roundAtSubtotal}` : `${rounding.granularity} ${rounding.mode}`);
2884
3002
  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)) {
3003
+ const taxedLines = [...lines, ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({ ...charge, taxInclusive: order.pricesIncludeTax }))];
3004
+ for (const { ratePpm, netMinor, amountMinor } of taxLinesByRate(taxedLines, order.taxMinor, void 0, order.taxRounding)) {
2886
3005
  const existing2 = taxRates.get(String(ratePpm));
2887
3006
  taxRates.set(String(ratePpm), {
2888
3007
  ratePpm,
@@ -3890,7 +4009,7 @@ function useRegisterSession(options) {
3890
4009
  }
3891
4010
 
3892
4011
  // src/sale/cart.ts
3893
- import { resolvePrice as resolvePrice2 } from "@tallyui/core";
4012
+ import { resolvePrice as resolvePrice3 } from "@tallyui/core";
3894
4013
  var CartError = class extends Error {
3895
4014
  };
3896
4015
  function addEntryToCart(builder, entry, traits, currency) {
@@ -3901,7 +4020,7 @@ function addEntryToCart(builder, entry, traits, currency) {
3901
4020
  return existing.id;
3902
4021
  }
3903
4022
  const name = traits.getName(product) + (traits.getVariantCount(product) > 1 ? ` \xB7 ${variant.title}` : "");
3904
- const unitPrice = resolvePrice2(variant.prices, currency)?.current;
4023
+ const unitPrice = resolvePrice3(variant.prices, currency)?.current;
3905
4024
  if (!unitPrice) throw new CartError(`No ${currency} price for ${name}`);
3906
4025
  return builder.addLine({
3907
4026
  productId: traits.getId(product),
@@ -4079,6 +4198,9 @@ function useSale(settings, opts) {
4079
4198
  stage,
4080
4199
  error,
4081
4200
  idle: idle3,
4201
+ /** The store's capabilities as passed in: a component offers fees, shipping and custom lines
4202
+ * only when `orderCreate >= 5`, and their tax status and class only as `lineTax` allows. */
4203
+ capabilities: opts.capabilities,
4082
4204
  /** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
4083
4205
  saving,
4084
4206
  /** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
@@ -4406,7 +4528,7 @@ function useSale(settings, opts) {
4406
4528
  }
4407
4529
 
4408
4530
  // src/sale/catalogue.ts
4409
- import { findVariantByCode, formatMoney as formatMoney2, resolvePrice as resolvePrice3 } from "@tallyui/core";
4531
+ import { findVariantByCode, formatMoney as formatMoney2, resolvePrice as resolvePrice4 } from "@tallyui/core";
4410
4532
  function catalogueEntries(products, traits, context) {
4411
4533
  return products.flatMap((product) => {
4412
4534
  const variants = traits.getVariants ? traits.getVariants(product, context) : traits.hasVariants(product) ? [] : [{
@@ -4424,7 +4546,7 @@ function findEntryByCode(entries, code) {
4424
4546
  return variant ? entries.find((entry) => entry.variant === variant) : void 0;
4425
4547
  }
4426
4548
  function variantPriceLabel(variant, currency, locale) {
4427
- const price = resolvePrice3(variant.prices, currency);
4549
+ const price = resolvePrice4(variant.prices, currency);
4428
4550
  return price ? formatMoney2(price.current, locale) : void 0;
4429
4551
  }
4430
4552
 
@@ -5588,10 +5710,12 @@ function planLegs(plan, rowsSinceFrom, balanceMinor) {
5588
5710
  export {
5589
5711
  CartError,
5590
5712
  CurrencyProvider,
5713
+ DEFAULT_CATALOGUE_VIEW_STATE,
5591
5714
  DISCOUNTS_UNSUPPORTED,
5592
5715
  MAX_TENDER_MINOR,
5593
5716
  MICROS_PER_MINOR,
5594
5717
  OrderContentMismatchError,
5718
+ PRODUCT_SORT_FIELDS,
5595
5719
  PosOrderOpenClosedError,
5596
5720
  RegisterApprovalRequiredError,
5597
5721
  RegisterCloseIncompleteError,
@@ -5621,6 +5745,7 @@ export {
5621
5745
  callbackSink,
5622
5746
  cashMovementSchema,
5623
5747
  catalogueEntries,
5748
+ catalogueViewReducer,
5624
5749
  changeMinor,
5625
5750
  clampClosureScope,
5626
5751
  closeNeedsApproval,
@@ -5652,10 +5777,12 @@ export {
5652
5777
  getBoundRegisterId,
5653
5778
  getDeviceId,
5654
5779
  getProductStock,
5780
+ inCategory,
5655
5781
  initTenderState,
5656
5782
  initialTenderState,
5657
5783
  isServerDecimal,
5658
5784
  labelKeys,
5785
+ listCategories,
5659
5786
  minorToDecimal,
5660
5787
  mintClosureNumber,
5661
5788
  mintUuid,
@@ -5664,6 +5791,7 @@ export {
5664
5791
  needsAttention,
5665
5792
  nextSaleCounter,
5666
5793
  normalizeAmount,
5794
+ normalizeCatalogueViewState,
5667
5795
  observeRegister$,
5668
5796
  openSession,
5669
5797
  openSessionSelector,
@@ -5676,6 +5804,8 @@ export {
5676
5804
  posOrderCollection,
5677
5805
  posOrderSchema,
5678
5806
  posOrdersLogger,
5807
+ productCategories,
5808
+ productSortValue,
5679
5809
  quickTenderedAmounts,
5680
5810
  ratePpmFromPercent,
5681
5811
  readBoundRegister,
@@ -5691,6 +5821,7 @@ export {
5691
5821
  registerSessionCollection,
5692
5822
  registerSessionSchema,
5693
5823
  requireOpenSession,
5824
+ resolveGridColumns,
5694
5825
  resolveStoreSettings,
5695
5826
  restoreOrderDraft,
5696
5827
  roundMicrosToMinor,
@@ -5700,6 +5831,7 @@ export {
5700
5831
  selectClosureRows,
5701
5832
  sessionOpenCommand,
5702
5833
  sessionTransitionCommand,
5834
+ sortProducts,
5703
5835
  stampSession,
5704
5836
  startCounting,
5705
5837
  stockOverlay$,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/pos",
3
- "version": "3.3.0",
3
+ "version": "3.5.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.5.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.5.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.5.0",
42
+ "@tallyui/connector-woocommerce": "3.5.0",
43
+ "@tallyui/database": "3.5.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",
package/src/index.ts CHANGED
@@ -55,7 +55,7 @@ export { buildReceiptData } from './receipt';
55
55
  export type { ReceiptData, ReceiptLineItem, ReceiptConfig } from './receipt';
56
56
 
57
57
  // Product
58
- export { searchProducts, withStockOverlay, getProductStock, stockOverlay$, stockOverlayAsOf$ } from './product';
58
+ export { searchProducts, sortProducts, productSortValue, PRODUCT_SORT_FIELDS, type ProductSort, type ProductSortValue, catalogueViewReducer, normalizeCatalogueViewState, resolveGridColumns, DEFAULT_CATALOGUE_VIEW_STATE, type CatalogueView, type CatalogueGridColumns, type CatalogueViewState, type CatalogueViewAction, withStockOverlay, getProductStock, stockOverlay$, stockOverlayAsOf$, productCategories, listCategories, inCategory } from './product';
59
59
 
60
60
  // Sale
61
61
  export { useSale, DISCOUNTS_UNSUPPORTED, SALE_SAVING, saleLogger, addEntryToCart, CartError, catalogueEntries, findEntryByCode, variantPriceLabel } from './sale';
@@ -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
  }
@@ -0,0 +1,60 @@
1
+ import type { ProductSort } from './sort-products';
2
+
3
+ export type CatalogueView = 'grid' | 'table';
4
+ export type CatalogueGridColumns = 'auto' | 2 | 3 | 4 | 5 | 6 | 7 | 8;
5
+ export interface CatalogueViewState {
6
+ view: CatalogueView;
7
+ gridColumns: CatalogueGridColumns;
8
+ sort: ProductSort | null;
9
+ categoryId: string | null;
10
+ }
11
+ export const DEFAULT_CATALOGUE_VIEW_STATE: CatalogueViewState = Object.freeze({
12
+ view: 'grid', gridColumns: 'auto', sort: null, categoryId: null,
13
+ });
14
+
15
+ export type CatalogueViewAction =
16
+ | { type: 'setView'; view: CatalogueView }
17
+ | { type: 'setGridColumns'; gridColumns: CatalogueGridColumns }
18
+ | { type: 'setSort'; sort: ProductSort | null }
19
+ | { type: 'setCategory'; categoryId: string | null }
20
+ | { type: 'replace'; state: CatalogueViewState };
21
+
22
+ export function catalogueViewReducer(state: CatalogueViewState, action: CatalogueViewAction): CatalogueViewState {
23
+ switch (action.type) {
24
+ case 'setView': return state.view === action.view ? state : { ...state, view: action.view };
25
+ case 'setGridColumns': return state.gridColumns === action.gridColumns ? state : { ...state, gridColumns: action.gridColumns };
26
+ case 'setCategory': return state.categoryId === action.categoryId ? state : { ...state, categoryId: action.categoryId };
27
+ case 'setSort': return state.sort?.field === action.sort?.field && state.sort?.dir === action.sort?.dir
28
+ ? state : { ...state, sort: action.sort };
29
+ case 'replace': return action.state;
30
+ }
31
+ }
32
+
33
+ export function normalizeCatalogueViewState(raw: unknown, defaults: CatalogueViewState = DEFAULT_CATALOGUE_VIEW_STATE): CatalogueViewState {
34
+ if (typeof raw !== 'object' || raw === null) return defaults;
35
+ try {
36
+ const { view, gridColumns, sort, categoryId } = raw as Record<string, unknown>;
37
+ const candidate = sort as Partial<ProductSort> | null;
38
+ return {
39
+ categoryId: categoryId === null || (typeof categoryId === 'string' && categoryId.length > 0)
40
+ ? categoryId : defaults.categoryId,
41
+ view: view === 'grid' || view === 'table' ? view : defaults.view,
42
+ gridColumns: gridColumns === 'auto' || (typeof gridColumns === 'number' && Number.isInteger(gridColumns)
43
+ && gridColumns >= 2 && gridColumns <= 8) ? gridColumns as CatalogueGridColumns : defaults.gridColumns,
44
+ sort: sort === null ? null : typeof candidate === 'object' && candidate
45
+ && typeof candidate.field === 'string' && candidate.field.length > 0
46
+ && (candidate.dir === 'asc' || candidate.dir === 'desc')
47
+ ? { field: candidate.field, dir: candidate.dir } : defaults.sort,
48
+ };
49
+ } catch {
50
+ return defaults;
51
+ }
52
+ }
53
+
54
+ // Keep product names readable and touch targets at least 160 px wide where two columns fit.
55
+ const MIN_TILE_WIDTH = 160;
56
+ export function resolveGridColumns(gridColumns: CatalogueGridColumns, containerWidth: number): number {
57
+ // ProductGrid has 4 px padding on each side of the content and each cell: 160 px tile + 8.
58
+ return gridColumns === 'auto'
59
+ ? Math.max(2, Math.min(6, Math.floor((containerWidth - 8) / (MIN_TILE_WIDTH + 8)))) : gridColumns;
60
+ }
@@ -0,0 +1,26 @@
1
+ import type { ProductCategory, ProductTraits } from '@tallyui/core';
2
+
3
+ /** The product's categories: `getCategories`, else `getCategoryNames` with each name as its id. */
4
+ export function productCategories<Doc>(doc: Doc, traits: ProductTraits<Doc>): ProductCategory[] {
5
+ return traits.getCategories
6
+ ? traits.getCategories(doc)
7
+ : traits.getCategoryNames(doc).map((name) => ({ id: name, name }));
8
+ }
9
+
10
+ /** Every category across `docs`, once per id (the first name seen wins), sorted by name (natural, case-insensitive), ties by id. */
11
+ export function listCategories<Doc>(docs: readonly Doc[], traits: ProductTraits<Doc>): ProductCategory[] {
12
+ const categories = new Map<string, ProductCategory>();
13
+ for (const doc of docs) {
14
+ for (const category of productCategories(doc, traits)) {
15
+ if (!categories.has(category.id)) categories.set(category.id, category);
16
+ }
17
+ }
18
+ return [...categories.values()].sort((a, b) =>
19
+ a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' })
20
+ || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
21
+ }
22
+
23
+ /** Whether `doc` is in the category with this id. */
24
+ export function inCategory<Doc>(doc: Doc, categoryId: string, traits: ProductTraits<Doc>): boolean {
25
+ return productCategories(doc, traits).some((category) => category.id === categoryId);
26
+ }
@@ -1,2 +1,5 @@
1
1
  export { searchProducts } from './search-products';
2
+ export { productCategories, listCategories, inCategory } from './categories';
3
+ export { catalogueViewReducer, normalizeCatalogueViewState, resolveGridColumns, DEFAULT_CATALOGUE_VIEW_STATE, type CatalogueView, type CatalogueGridColumns, type CatalogueViewState, type CatalogueViewAction } from './catalogue-view-state';
4
+ export { sortProducts, productSortValue, PRODUCT_SORT_FIELDS, type ProductSort, type ProductSortValue } from './sort-products';
2
5
  export { withStockOverlay, getProductStock, stockOverlay$, stockOverlayAsOf$ } from './stock';
@@ -0,0 +1,51 @@
1
+ import { resolvePrice, resolvePriceRange, type ProductTraits, type TraitContext } from '@tallyui/core';
2
+
3
+ /** A sort on one field: ascending or descending. */
4
+ export interface ProductSort { field: string; dir: 'asc' | 'desc' }
5
+ export type ProductSortValue = string | number | undefined;
6
+ /** The fields productSortValue knows, in the default column order. */
7
+ export const PRODUCT_SORT_FIELDS = ['name', 'sku', 'barcode', 'price', 'stock', 'category'] as const;
8
+
9
+ export function productSortValue<Doc>(
10
+ doc: Doc, field: string, traits: ProductTraits<Doc>, context?: TraitContext,
11
+ ): ProductSortValue {
12
+ let value: ProductSortValue;
13
+ switch (field) {
14
+ case 'name': value = traits.getName(doc); break;
15
+ case 'sku': value = traits.getSku(doc); break;
16
+ case 'barcode': value = traits.getBarcode(doc); break;
17
+ case 'price': {
18
+ const variants = traits.getVariants?.(doc, context);
19
+ value = variants && variants.length >= 2
20
+ ? resolvePriceRange(variants, context?.currency)?.min.amount
21
+ : resolvePrice(traits.getPrices(doc, context), context?.currency)?.current.amount;
22
+ break;
23
+ }
24
+ case 'stock': value = traits.getStock(doc).quantity; break;
25
+ case 'category': value = traits.getCategoryNames(doc).join(', '); break;
26
+ default: return undefined;
27
+ }
28
+ return value === '' ? undefined : value;
29
+ }
30
+
31
+ export function sortProducts<Doc>(
32
+ docs: Doc[], sort: ProductSort | null | undefined,
33
+ valueOf: (doc: Doc, field: string) => ProductSortValue,
34
+ ): Doc[] {
35
+ if (!sort) return docs;
36
+ return docs.map((doc, index) => ({ doc, index, value: valueOf(doc, sort.field) }))
37
+ .sort((a, b) => {
38
+ const aMissing = a.value === undefined || a.value === '';
39
+ const bMissing = b.value === undefined || b.value === '';
40
+ if (aMissing || bMissing) {
41
+ return aMissing === bMissing ? a.index - b.index : aMissing ? 1 : -1;
42
+ }
43
+ const av = a.value!;
44
+ const bv = b.value!;
45
+ const comparison = typeof av === 'number'
46
+ ? (typeof bv === 'number' ? av - bv : -1)
47
+ : (typeof bv === 'number' ? 1 : av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' }));
48
+ return (sort.dir === 'desc' ? -comparison : comparison) || a.index - b.index;
49
+ })
50
+ .map(({ doc }) => doc);
51
+ }
@@ -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;