@tallyui/pos 3.0.4 → 3.1.1

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
@@ -465,6 +465,12 @@ interface OrderManager {
465
465
  }
466
466
  declare function createOrderManager(options: OrderManagerOptions): OrderManager;
467
467
 
468
+ /** The parked sales in `drafts`, newest first, live; and a discard that removes one. */
469
+ declare function useParkedSales(drafts: RxCollection | undefined): {
470
+ parked: ParkedOrderSummary[];
471
+ discard: (id: string) => Promise<void>;
472
+ };
473
+
468
474
  interface ReceiptLineItem {
469
475
  name: string;
470
476
  sku: string;
@@ -596,6 +602,8 @@ interface PosOrderPayment {
596
602
  }
597
603
  interface PosOrder {
598
604
  id: string;
605
+ /** The sale order's id (the builder's), set by finalizeOrder (pos_orders v7, ADR-072): support traceability from a draft or preview to this stored order. Never sent, never printed. */
606
+ saleId?: string;
599
607
  createdAt: string;
600
608
  currency: string;
601
609
  pricesIncludeTax: boolean;
@@ -711,6 +719,8 @@ declare function toOrderCreateEnvelope(order: PosOrder, deviceId: string, attemp
711
719
  * Like version 4 (ADR-069), it is one-way: an older build shows no orders.
712
720
  * Version 6 adds `taxRounding`, the strategy the sale's figures were computed with (#287; never sent), and changes
713
721
  * nothing else. Its migration sets it on every older row. It is one-way like version 5.
722
+ * Version 7 adds `saleId`, the sale order's id (ADR-072; never sent), and changes nothing else. Its migration is the
723
+ * identity: older orders have no `saleId`. It is one-way like version 6.
714
724
  */
715
725
  declare const posOrderSchema: RxJsonSchema<PosOrder>;
716
726
  /**
@@ -1895,6 +1905,14 @@ declare function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
1895
1905
  amountMinor: number;
1896
1906
  reference?: string;
1897
1907
  } | null) => void;
1908
+ /** ADR-072: external is capped at the balance due; change comes only from cash. */
1909
+ addTender(tender: {
1910
+ method: "cash" | "external";
1911
+ amountMinor: number;
1912
+ reference?: string;
1913
+ }): string | null;
1914
+ /** ADR-072: remove one tender and its dropped-reference warning. An id not among the payments changes nothing. */
1915
+ removeTender(paymentId: string): void;
1898
1916
  cancelTender(): void;
1899
1917
  /**
1900
1918
  * Finalizes the tender, stamps the session (see `session`), hands the order to `onSaleCompleted`,
@@ -2304,4 +2322,4 @@ declare function planLegs(plan: TenderPlan, rowsSinceFrom: {
2304
2322
  };
2305
2323
  };
2306
2324
 
2307
- export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, 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 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 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 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, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure, writeOrderDraft };
2325
+ export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, 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 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 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 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 };
package/dist/index.js CHANGED
@@ -1082,6 +1082,27 @@ function createOrderManager(options) {
1082
1082
  };
1083
1083
  }
1084
1084
 
1085
+ // src/order/use-parked-sales.ts
1086
+ import { useEffect as useEffect2, useState as useState2 } from "react";
1087
+ function useParkedSales(drafts) {
1088
+ const [parked, setParked] = useState2([]);
1089
+ useEffect2(() => {
1090
+ if (!drafts) {
1091
+ setParked([]);
1092
+ return;
1093
+ }
1094
+ const subscription = parkedOrderSummaries$(drafts).subscribe((summaries) => {
1095
+ setParked([...summaries].sort((a, b) => b.parkedAt.localeCompare(a.parkedAt)));
1096
+ });
1097
+ return () => subscription.unsubscribe();
1098
+ }, [drafts]);
1099
+ const discard = async (id2) => {
1100
+ if (!drafts) throw new Error("useParkedSales: discard() needs the drafts collection");
1101
+ await (await drafts.findOne(id2).exec())?.remove();
1102
+ };
1103
+ return { parked, discard };
1104
+ }
1105
+
1085
1106
  // src/receipt/build-receipt-data.ts
1086
1107
  function buildReceiptData(order, config) {
1087
1108
  const taxLines = taxLinesByRate(order.lineItems, order.taxMinor, config.taxLabels, order.taxRounding).map(
@@ -1181,7 +1202,7 @@ function stockOverlayAsOf$(collection) {
1181
1202
  }
1182
1203
 
1183
1204
  // src/sale/use-sale.ts
1184
- import { useEffect as useEffect3, useLayoutEffect, useRef as useRef3, useState as useState3 } from "react";
1205
+ import { useEffect as useEffect4, useLayoutEffect, useRef as useRef3, useState as useState4 } from "react";
1185
1206
 
1186
1207
  // src/pos-order/uuidv7.ts
1187
1208
  function format(ms, bytes) {
@@ -1418,6 +1439,7 @@ function finalizeOrder(order, options = {}) {
1418
1439
  const now = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
1419
1440
  return freezeSentForm({
1420
1441
  id: id2,
1442
+ saleId: order.id,
1421
1443
  createdAt: now,
1422
1444
  updatedAt: now,
1423
1445
  commandId: newId(),
@@ -1448,12 +1470,13 @@ function finalizeOrder(order, options = {}) {
1448
1470
  import { addRxPlugin } from "rxdb";
1449
1471
  import { RxDBMigrationSchemaPlugin } from "rxdb/plugins/migration-schema";
1450
1472
  var posOrderSchema = {
1451
- version: 6,
1473
+ version: 7,
1452
1474
  primaryKey: "id",
1453
1475
  type: "object",
1454
1476
  additionalProperties: false,
1455
1477
  properties: {
1456
1478
  id: { type: "string", maxLength: 36 },
1479
+ saleId: { type: "string", maxLength: 36 },
1457
1480
  commandId: { type: "string", maxLength: 36 },
1458
1481
  createdAt: { type: "string", maxLength: 40 },
1459
1482
  updatedAt: { type: "string" },
@@ -1602,7 +1625,7 @@ function posOrderCollection() {
1602
1625
  };
1603
1626
  return {
1604
1627
  schema: posOrderSchema,
1605
- migrationStrategies: { 1: identity2, 2: identity2, 3: identity2, 4: identity2, 5: recordSent, 6: recordRounding }
1628
+ migrationStrategies: { 1: identity2, 2: identity2, 3: identity2, 4: identity2, 5: recordSent, 6: recordRounding, 7: identity2 }
1606
1629
  };
1607
1630
  }
1608
1631
 
@@ -2994,7 +3017,7 @@ function buildXReportDocument(session, context) {
2994
3017
  }
2995
3018
 
2996
3019
  // src/register/use-register-session.ts
2997
- import { useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
3020
+ import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3, useSyncExternalStore } from "react";
2998
3021
  import { combineLatest, map as map5, of, switchMap } from "rxjs";
2999
3022
 
3000
3023
  // src/register/register-commands.ts
@@ -3214,7 +3237,7 @@ function useRegisterSession(options) {
3214
3237
  }
3215
3238
  });
3216
3239
  };
3217
- useEffect2(reconcile, [commandTarget]);
3240
+ useEffect3(reconcile, [commandTarget]);
3218
3241
  const source = useMemo3(() => {
3219
3242
  if (!enabled || !sessions || !movements || !closures || !orders || !registerId || !register) return null;
3220
3243
  return combineLatest([
@@ -3233,14 +3256,14 @@ function useRegisterSession(options) {
3233
3256
  const tender = useRef2(tenderInProgress);
3234
3257
  tender.current = tenderInProgress;
3235
3258
  const closing = useSyncExternalStore(subscribeClosing, () => closingByRegister.has(registerId ?? ""));
3236
- const [observed, setObserved] = useState2(null);
3237
- useEffect2(() => {
3259
+ const [observed, setObserved] = useState3(null);
3260
+ useEffect3(() => {
3238
3261
  if (!source) return;
3239
3262
  const subscription = source.subscribe((snapshot) => setObserved({ source, snapshot }));
3240
3263
  return () => subscription.unsubscribe();
3241
3264
  }, [source]);
3242
- const [, setMinute] = useState2(0);
3243
- useEffect2(() => {
3265
+ const [, setMinute] = useState3(0);
3266
+ useEffect3(() => {
3244
3267
  if (!enabled || !expectedCloseTime) return;
3245
3268
  const timer = setInterval(() => setMinute((value) => value + 1), 6e4);
3246
3269
  return () => clearInterval(timer);
@@ -3272,7 +3295,7 @@ function useRegisterSession(options) {
3272
3295
  return state.running = run;
3273
3296
  };
3274
3297
  const seen = useRef2(null);
3275
- useEffect2(() => {
3298
+ useEffect3(() => {
3276
3299
  if (!data) return;
3277
3300
  const ids = data.closureRows.map((row) => row.id);
3278
3301
  if (seen.current?.source !== source) {
@@ -3545,19 +3568,19 @@ var HUNG_SAVE_CHECK_MS = 5e3;
3545
3568
  function useSale(settings, opts) {
3546
3569
  const taxContext = useTax();
3547
3570
  const madeWith = useRef3({ taxContext, currency: settings.currency });
3548
- const [rendered, setBuilder] = useState3(() => createOrderBuilder({ currency: settings.currency, taxContext }));
3571
+ const [rendered, setBuilder] = useState4(() => createOrderBuilder({ currency: settings.currency, taxContext }));
3549
3572
  const builderNow = useRef3(rendered);
3550
- const [order, setOrder] = useState3(() => rendered.getSnapshot());
3551
- const [stage, setStage] = useState3({ kind: "cart" });
3552
- const [saleError, setError] = useState3(null);
3573
+ const [order, setOrder] = useState4(() => rendered.getSnapshot());
3574
+ const [stage, setStage] = useState4({ kind: "cart" });
3575
+ const [saleError, setError] = useState4(null);
3553
3576
  const configError = referenceError("cashierRef", opts.cashierRef) ?? referenceError("registerId", opts.registerId);
3554
3577
  const pending = useRef3(null);
3555
- const [saving, setSaving] = useState3(false);
3578
+ const [saving, setSaving] = useState4(false);
3556
3579
  const error = saving && saleError ? saleError : configError ?? saleError;
3557
3580
  const tenderSession = useRef3(null);
3558
- const droppedReference = useRef3(null);
3581
+ const droppedReferences = useRef3(/* @__PURE__ */ new Set());
3559
3582
  const confirmed = useRef3(null);
3560
- const [canContinue, setCanContinue] = useState3(false);
3583
+ const [canContinue, setCanContinue] = useState4(false);
3561
3584
  const attempts = useRef3(0);
3562
3585
  const hungSaveTimer = useRef3(null);
3563
3586
  const checking = useRef3(null);
@@ -3590,7 +3613,7 @@ function useSale(settings, opts) {
3590
3613
  pending.current = null;
3591
3614
  inFlight.current = null;
3592
3615
  tenderSession.current = null;
3593
- droppedReference.current = null;
3616
+ droppedReferences.current.clear();
3594
3617
  setSaving(false);
3595
3618
  madeWith.current = { taxContext, currency: settings.currency };
3596
3619
  const next = restored ?? createOrderBuilder({ currency: settings.currency, taxContext });
@@ -3601,7 +3624,7 @@ function useSale(settings, opts) {
3601
3624
  setStage({ kind: "cart" });
3602
3625
  setError(null);
3603
3626
  }
3604
- useEffect3(() => {
3627
+ useEffect4(() => {
3605
3628
  const subscription = rendered.order$.subscribe(setOrder);
3606
3629
  return () => subscription.unsubscribe();
3607
3630
  }, [rendered]);
@@ -3611,7 +3634,7 @@ function useSale(settings, opts) {
3611
3634
  const liveIdle = stageNow.current.kind === "cart" && !live.lineItems.length && !inFlight.current && !pending.current;
3612
3635
  if (liveIdle && (madeWith.current.taxContext !== taxContext || madeWith.current.currency !== settings.currency)) resetSale(live.customer ?? null);
3613
3636
  });
3614
- useEffect3(() => () => {
3637
+ useEffect4(() => () => {
3615
3638
  clearHungSaveTimer();
3616
3639
  if (pending.current || inFlight.current) {
3617
3640
  saleLogger.error(
@@ -3620,21 +3643,29 @@ function useSale(settings, opts) {
3620
3643
  );
3621
3644
  }
3622
3645
  }, []);
3623
- function setTender(tender) {
3624
- if (locked()) return;
3625
- const reason = referenceReason(tender?.reference);
3646
+ function addOne(tender) {
3647
+ const reason = referenceReason(tender.reference);
3626
3648
  const dropped = reason && (reason === "nul" ? "it contains a NUL character" : `it is over ${PAYLOAD_STRING_MAX} characters`);
3627
- const kept = tender && dropped ? { method: tender.method, amountMinor: tender.amountMinor } : tender;
3649
+ const kept = dropped ? { method: tender.method, amountMinor: tender.amountMinor } : tender;
3628
3650
  const builder = builderNow.current;
3629
- const previous = builder.getSnapshot().payments[0];
3630
- if (previous) builder.removePayment(previous.id);
3631
- const paymentId = kept ? builder.addPayment(kept) : null;
3632
- droppedReference.current = dropped ? paymentId : null;
3633
- setError(dropped && `The terminal's payment reference couldn't be kept (${dropped}); the payment is recorded without it.`);
3651
+ const paymentId = builder.addPayment(kept);
3652
+ if (dropped) {
3653
+ droppedReferences.current.add(paymentId);
3654
+ setError(`The terminal's payment reference couldn't be kept (${dropped}); the payment is recorded without it.`);
3655
+ }
3634
3656
  try {
3635
3657
  if (dropped) saleLogger.warn("the terminal's payment reference was dropped", { reason: dropped, method: tender.method });
3636
3658
  } catch {
3637
3659
  }
3660
+ return paymentId;
3661
+ }
3662
+ function setTender(tender) {
3663
+ if (locked()) return;
3664
+ const builder = builderNow.current;
3665
+ for (const payment of builder.getSnapshot().payments) builder.removePayment(payment.id);
3666
+ droppedReferences.current.clear();
3667
+ setError(null);
3668
+ if (tender) addOne(tender);
3638
3669
  }
3639
3670
  function checkStored(completion) {
3640
3671
  const attempt2 = attempts.current;
@@ -3672,7 +3703,7 @@ function useSale(settings, opts) {
3672
3703
  if (hungSaveTimer.current === timer) clearHungSaveTimer();
3673
3704
  if (pending.current !== completion) return;
3674
3705
  pending.current = null;
3675
- droppedReference.current = null;
3706
+ droppedReferences.current.clear();
3676
3707
  confirm(null);
3677
3708
  setSaving(false);
3678
3709
  setError(null);
@@ -3778,6 +3809,23 @@ function useSale(settings, opts) {
3778
3809
  setStage({ kind: "tender", method });
3779
3810
  },
3780
3811
  setTender,
3812
+ /** ADR-072: external is capped at the balance due; change comes only from cash. */
3813
+ addTender(tender) {
3814
+ if (locked() || !Number.isInteger(tender.amountMinor) || tender.amountMinor <= 0) return null;
3815
+ if (tender.method === "external") {
3816
+ const due = builderNow.current.getSnapshot().balanceDueMinor;
3817
+ if (due <= 0) return null;
3818
+ return addOne({ ...tender, amountMinor: Math.min(tender.amountMinor, due) });
3819
+ }
3820
+ return addOne(tender);
3821
+ },
3822
+ /** ADR-072: remove one tender and its dropped-reference warning. An id not among the payments changes nothing. */
3823
+ removeTender(paymentId) {
3824
+ if (locked()) return;
3825
+ if (!builderNow.current.getSnapshot().payments.some((payment) => payment.id === paymentId)) return;
3826
+ builderNow.current.removePayment(paymentId);
3827
+ droppedReferences.current.delete(paymentId);
3828
+ },
3781
3829
  cancelTender() {
3782
3830
  if (locked()) return;
3783
3831
  setTender(null);
@@ -3805,13 +3853,14 @@ function useSale(settings, opts) {
3805
3853
  if (pending.current) return deliver(pending.current);
3806
3854
  setSaving(true);
3807
3855
  const current = builderNow.current.getSnapshot();
3856
+ const localWarnings = current.payments.filter((payment) => droppedReferences.current.has(payment.id)).map((payment) => ({ code: "payment_reference_dropped", paymentId: payment.id }));
3808
3857
  let posOrder;
3809
3858
  try {
3810
3859
  posOrder = finalizeOrder(current, {
3811
3860
  registerId: opts.registerId,
3812
3861
  cashierRef: opts.cashierRef,
3813
3862
  capabilities: opts.capabilities,
3814
- ...droppedReference.current ? { localWarnings: [{ code: "payment_reference_dropped", paymentId: droppedReference.current }] } : {}
3863
+ ...localWarnings.length ? { localWarnings } : {}
3815
3864
  });
3816
3865
  } catch (error2) {
3817
3866
  setSaving(false);
@@ -4735,15 +4784,15 @@ function createRegisterOutbox(options) {
4735
4784
  }
4736
4785
 
4737
4786
  // src/outbox/use-register-outbox.ts
4738
- import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
4787
+ import { useEffect as useEffect5, useRef as useRef4, useState as useState5 } from "react";
4739
4788
  var idle = { pending: 0, sending: false };
4740
4789
  function useRegisterOutbox(options) {
4741
4790
  const { commands, deviceId } = options;
4742
4791
  const latest = useRef4(options);
4743
4792
  latest.current = options;
4744
4793
  const current = useRef4(null);
4745
- const [state, setState] = useState4(idle);
4746
- useEffect4(() => {
4794
+ const [state, setState] = useState5(idle);
4795
+ useEffect5(() => {
4747
4796
  setState(idle);
4748
4797
  if (!commands) return;
4749
4798
  const outbox = createRegisterOutbox({
@@ -4777,7 +4826,7 @@ function useRegisterOutbox(options) {
4777
4826
  }
4778
4827
 
4779
4828
  // src/outbox/use-order-outbox.ts
4780
- import { useEffect as useEffect5, useMemo as useMemo4, useRef as useRef5, useState as useState5 } from "react";
4829
+ import { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef5, useState as useState6 } from "react";
4781
4830
  var idle2 = { pending: 0, sending: false };
4782
4831
  var noneStuck = [];
4783
4832
  function useOrderOutbox(options) {
@@ -4787,12 +4836,12 @@ function useOrderOutbox(options) {
4787
4836
  const current = useRef5(null);
4788
4837
  const openingError = useRef5(null);
4789
4838
  const loggedMismatches = useRef5(/* @__PURE__ */ new Set());
4790
- const [orders, setOrders] = useState5(null);
4791
- const [state, setState] = useState5(idle2);
4792
- const [recent, setRecent] = useState5([]);
4839
+ const [orders, setOrders] = useState6(null);
4840
+ const [state, setState] = useState6(idle2);
4841
+ const [recent, setRecent] = useState6([]);
4793
4842
  const savesInFlight = useRef5(0);
4794
- const [savesInFlightCount, setSavesInFlightCount] = useState5(0);
4795
- useEffect5(() => {
4843
+ const [savesInFlightCount, setSavesInFlightCount] = useState6(0);
4844
+ useEffect6(() => {
4796
4845
  let active = true;
4797
4846
  let dispose;
4798
4847
  openingError.current = null;
@@ -4835,7 +4884,7 @@ function useOrderOutbox(options) {
4835
4884
  dispose?.();
4836
4885
  };
4837
4886
  }, [storeKey, deviceId]);
4838
- useEffect5(() => {
4887
+ useEffect6(() => {
4839
4888
  latest.current.onBusy?.(state.sending);
4840
4889
  return () => latest.current.onBusy?.(false);
4841
4890
  }, [state.sending]);
@@ -5218,6 +5267,7 @@ export {
5218
5267
  useCurrencyCode,
5219
5268
  useCurrencyFormatter,
5220
5269
  useOrderOutbox,
5270
+ useParkedSales,
5221
5271
  useRegisterOutbox,
5222
5272
  useRegisterSession,
5223
5273
  useSale,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/pos",
3
- "version": "3.0.4",
3
+ "version": "3.1.1",
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.0.4",
30
+ "@tallyui/core": "^3.1.1",
31
31
  "react": ">=18",
32
32
  "rxdb": "~17.5.0",
33
33
  "rxjs": ">=7"
34
34
  },
35
35
  "devDependencies": {
36
- "@tallyui/core": "3.0.4",
36
+ "@tallyui/core": "3.1.1",
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.0.4",
42
- "@tallyui/connector-woocommerce": "3.0.4",
43
- "@tallyui/database": "3.0.4"
41
+ "@tallyui/connector-medusa": "3.1.1",
42
+ "@tallyui/connector-woocommerce": "3.1.1",
43
+ "@tallyui/database": "3.1.1"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export type { Repository } from './repository';
27
27
  // Order
28
28
  export { createOrderBuilder, createOrderManager, allocateOrderDiscount, taxFiguresForBasket } from './order';
29
29
  export { orderDraftSchema, writeOrderDraft, restoreOrderDraft, parkedOrderSummaries$ } from './order';
30
+ export { useParkedSales } from './order';
30
31
  export type { BasketLine } from './order';
31
32
  export type { OrderBuilder, OrderBuilderOptions } from './order';
32
33
  export type { OrderManager, OrderManagerOptions, ParkedOrderSummary } from './order';
@@ -5,6 +5,7 @@ export { allocateOrderDiscount } from './allocate-order-discount';
5
5
  export type { OrderBuilder, OrderBuilderOptions } from './order-builder';
6
6
  export { createOrderManager } from './order-manager';
7
7
  export { orderDraftSchema, writeOrderDraft, restoreOrderDraft, parkedOrderSummaries$ } from './order-drafts';
8
+ export { useParkedSales } from './use-parked-sales';
8
9
  export type { OrderManager, OrderManagerOptions, ParkedOrderSummary } from './order-manager';
9
10
  export type {
10
11
  Order,
@@ -0,0 +1,24 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { RxCollection } from 'rxdb';
3
+ import { parkedOrderSummaries$, type ParkedOrderSummary } from './order-drafts';
4
+
5
+ /** The parked sales in `drafts`, newest first, live; and a discard that removes one. */
6
+ export function useParkedSales(drafts: RxCollection | undefined): {
7
+ parked: ParkedOrderSummary[];
8
+ discard: (id: string) => Promise<void>;
9
+ } {
10
+ const [parked, setParked] = useState<ParkedOrderSummary[]>([]);
11
+ useEffect(() => {
12
+ if (!drafts) { setParked([]); return; }
13
+ const subscription = parkedOrderSummaries$(drafts).subscribe((summaries) => {
14
+ setParked([...summaries].sort((a, b) => b.parkedAt.localeCompare(a.parkedAt)));
15
+ });
16
+ return () => subscription.unsubscribe();
17
+ }, [drafts]);
18
+
19
+ const discard = async (id: string): Promise<void> => {
20
+ if (!drafts) throw new Error('useParkedSales: discard() needs the drafts collection');
21
+ await (await drafts.findOne(id).exec())?.remove();
22
+ };
23
+ return { parked, discard };
24
+ }
@@ -204,7 +204,7 @@ export function finalizeOrder(order: Order, options: FinalizeOptions = {}): PosO
204
204
  : { granularity: rounding.granularity, mode: rounding.mode };
205
205
  const now = (options.now ?? new Date()).toISOString();
206
206
  return freezeSentForm({
207
- id, createdAt: now, updatedAt: now, commandId: newId(), syncStatus: 'pending',
207
+ id, saleId: order.id, createdAt: now, updatedAt: now, commandId: newId(), syncStatus: 'pending',
208
208
  currency: order.currency, pricesIncludeTax: order.pricesIncludeTax, lines, payments,
209
209
  subtotalMinor: order.subtotalMinor, discountMinor: order.discountMinor,
210
210
  taxMinor: order.taxMinor, totalMinor: order.totalMinor, taxRounding,
@@ -18,11 +18,14 @@ import type { PosOrder } from './types';
18
18
  * Like version 4 (ADR-069), it is one-way: an older build shows no orders.
19
19
  * Version 6 adds `taxRounding`, the strategy the sale's figures were computed with (#287; never sent), and changes
20
20
  * nothing else. Its migration sets it on every older row. It is one-way like version 5.
21
+ * Version 7 adds `saleId`, the sale order's id (ADR-072; never sent), and changes nothing else. Its migration is the
22
+ * identity: older orders have no `saleId`. It is one-way like version 6.
21
23
  */
22
24
  export const posOrderSchema: RxJsonSchema<PosOrder> = {
23
- version: 6, primaryKey: 'id', type: 'object', additionalProperties: false,
25
+ version: 7, primaryKey: 'id', type: 'object', additionalProperties: false,
24
26
  properties: {
25
27
  id: { type: 'string', maxLength: 36 },
28
+ saleId: { type: 'string', maxLength: 36 },
26
29
  commandId: { type: 'string', maxLength: 36 },
27
30
  createdAt: { type: 'string', maxLength: 40 },
28
31
  updatedAt: { type: 'string' },
@@ -126,5 +129,5 @@ export function posOrderCollection(): { schema: RxJsonSchema<PosOrder>; migratio
126
129
  // it means no older sale is ever re-rounded (#287).
127
130
  const recordRounding = (doc: PosOrder) => { doc.taxRounding ??= { ...DEFAULT_TAX_ROUNDING }; return doc; };
128
131
  return { schema: posOrderSchema,
129
- migrationStrategies: { 1: identity, 2: identity, 3: identity, 4: identity, 5: recordSent, 6: recordRounding } };
132
+ migrationStrategies: { 1: identity, 2: identity, 3: identity, 4: identity, 5: recordSent, 6: recordRounding, 7: identity } };
130
133
  }
@@ -42,6 +42,8 @@ export interface PosOrderPayment {
42
42
 
43
43
  export interface PosOrder {
44
44
  id: string;
45
+ /** The sale order's id (the builder's), set by finalizeOrder (pos_orders v7, ADR-072): support traceability from a draft or preview to this stored order. Never sent, never printed. */
46
+ saleId?: string;
45
47
  createdAt: string;
46
48
  currency: string;
47
49
  pricesIncludeTax: boolean;
@@ -72,8 +72,8 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
72
72
  const error = saving && saleError ? saleError : configError ?? saleError;
73
73
  // The session pinned by startTender for this tender (`undefined` inside: none); null until a tender starts.
74
74
  const tenderSession = useRef<{ session: typeof opts.session } | null>(null);
75
- // The id, at the tender, of the payment whose terminal reference setTender dropped; null when none was dropped.
76
- const droppedReference = useRef<string | null>(null);
75
+ // The ids, at the tender, of payments whose terminal references were dropped.
76
+ const droppedReferences = useRef<Set<string>>(new Set());
77
77
  // The pending completion isStored confirmed stored after its save failed; `canContinue` mirrors it for
78
78
  // rendering. `attempts` counts complete() attempts, so a confirmation that lands after a new one is dropped.
79
79
  const confirmed = useRef<{ order: Order; posOrder: PosOrder } | null>(null);
@@ -115,7 +115,7 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
115
115
  confirm(null); pending.current = null;
116
116
  inFlight.current = null;
117
117
  tenderSession.current = null;
118
- droppedReference.current = null;
118
+ droppedReferences.current.clear();
119
119
  setSaving(false);
120
120
  madeWith.current = { taxContext, currency: settings.currency };
121
121
  const next = restored ?? createOrderBuilder({ currency: settings.currency, taxContext });
@@ -149,23 +149,32 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
149
149
  }
150
150
  }, []);
151
151
 
152
- function setTender(tender: { method: 'cash' | 'external'; amountMinor: number; reference?: string } | null) {
153
- if (locked()) return;
152
+ function addOne(tender: { method: 'cash' | 'external'; amountMinor: number; reference?: string }): string {
154
153
  // A terminal reference finalize would refuse is dropped as it's entered, never the payment (the money is
155
154
  // taken): the tender applies without it, with a message that doesn't block complete() and a
156
155
  // localWarnings entry on the stored order naming the payment whose reference was dropped.
157
- const reason = referenceReason(tender?.reference);
156
+ const reason = referenceReason(tender.reference);
158
157
  const dropped = reason && (reason === 'nul' ? 'it contains a NUL character' : `it is over ${PAYLOAD_STRING_MAX} characters`);
159
- const kept = tender && dropped ? { method: tender.method, amountMinor: tender.amountMinor } : tender;
158
+ const kept = dropped ? { method: tender.method, amountMinor: tender.amountMinor } : tender;
160
159
  const builder = builderNow.current;
161
- const previous = builder.getSnapshot().payments[0];
162
- if (previous) builder.removePayment(previous.id);
163
- const paymentId = kept ? builder.addPayment(kept) : null;
164
- droppedReference.current = dropped ? paymentId : null;
165
- setError(dropped && `The terminal's payment reference couldn't be kept (${dropped}); the payment is recorded without it.`);
160
+ const paymentId = builder.addPayment(kept);
161
+ if (dropped) {
162
+ droppedReferences.current.add(paymentId);
163
+ setError(`The terminal's payment reference couldn't be kept (${dropped}); the payment is recorded without it.`);
164
+ }
166
165
  try {
167
- if (dropped) saleLogger.warn("the terminal's payment reference was dropped", { reason: dropped, method: tender!.method });
166
+ if (dropped) saleLogger.warn("the terminal's payment reference was dropped", { reason: dropped, method: tender.method });
168
167
  } catch { /* a failing sink must never lose the tender */ }
168
+ return paymentId;
169
+ }
170
+
171
+ function setTender(tender: { method: 'cash' | 'external'; amountMinor: number; reference?: string } | null) {
172
+ if (locked()) return;
173
+ const builder = builderNow.current;
174
+ for (const payment of builder.getSnapshot().payments) builder.removePayment(payment.id);
175
+ droppedReferences.current.clear();
176
+ setError(null);
177
+ if (tender) addOne(tender);
169
178
  }
170
179
 
171
180
  /** Asks `isStored`; Continue is offered only once it confirms, for this attempt, with the completion still pending. */
@@ -212,7 +221,7 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
212
221
  if (hungSaveTimer.current === timer) clearHungSaveTimer();
213
222
  if (pending.current !== completion) return;
214
223
  pending.current = null;
215
- droppedReference.current = null;
224
+ droppedReferences.current.clear();
216
225
  confirm(null);
217
226
  setSaving(false);
218
227
  setError(null);
@@ -315,6 +324,23 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
315
324
  setStage({ kind: 'tender', method });
316
325
  },
317
326
  setTender,
327
+ /** ADR-072: external is capped at the balance due; change comes only from cash. */
328
+ addTender(tender: { method: 'cash' | 'external'; amountMinor: number; reference?: string }): string | null {
329
+ if (locked() || !Number.isInteger(tender.amountMinor) || tender.amountMinor <= 0) return null;
330
+ if (tender.method === 'external') {
331
+ const due = builderNow.current.getSnapshot().balanceDueMinor;
332
+ if (due <= 0) return null;
333
+ return addOne({ ...tender, amountMinor: Math.min(tender.amountMinor, due) });
334
+ }
335
+ return addOne(tender);
336
+ },
337
+ /** ADR-072: remove one tender and its dropped-reference warning. An id not among the payments changes nothing. */
338
+ removeTender(paymentId: string): void {
339
+ if (locked()) return;
340
+ if (!builderNow.current.getSnapshot().payments.some((payment) => payment.id === paymentId)) return;
341
+ builderNow.current.removePayment(paymentId);
342
+ droppedReferences.current.delete(paymentId);
343
+ },
318
344
  cancelTender() {
319
345
  if (locked()) return;
320
346
  setTender(null);
@@ -342,10 +368,12 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
342
368
  if (pending.current) return deliver(pending.current);
343
369
  setSaving(true);
344
370
  const current = builderNow.current.getSnapshot();
371
+ const localWarnings = current.payments.filter((payment) => droppedReferences.current.has(payment.id))
372
+ .map((payment) => ({ code: 'payment_reference_dropped' as const, paymentId: payment.id }));
345
373
  let posOrder: PosOrder;
346
374
  try {
347
375
  posOrder = finalizeOrder(current, { registerId: opts.registerId, cashierRef: opts.cashierRef, capabilities: opts.capabilities,
348
- ...(droppedReference.current ? { localWarnings: [{ code: 'payment_reference_dropped', paymentId: droppedReference.current }] } : {}) });
376
+ ...(localWarnings.length ? { localWarnings } : {}) });
349
377
  } catch (error) {
350
378
  setSaving(false);
351
379
  setError((error as Error).message);