@tallyui/pos 3.5.2 → 3.6.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
@@ -534,6 +534,28 @@ declare function useParkedSales(drafts: RxCollection | undefined): {
534
534
  discard: (id: string) => Promise<void>;
535
535
  };
536
536
 
537
+ /** WCPOS receipt data schema version these blocks follow (wiki: fiscal groundwork, receipt identity and QR §2). */
538
+ declare const RECEIPT_SCHEMA_VERSION = "1.4.0";
539
+ /** 1.4 `software`: which software produced the receipt. */
540
+ interface ReceiptSoftware {
541
+ name: string;
542
+ plugin_version: string;
543
+ app_version: string;
544
+ app_build: string;
545
+ platform: string;
546
+ }
547
+ /** 1.4 `register`: `{ id: '', name: '' }` when there is none. */
548
+ interface ReceiptRegister {
549
+ id: string;
550
+ name: string;
551
+ }
552
+ /** 1.4 `fiscal` identity fields a till can know. Sale time, zone and counter come with the per-register sale counter (#475). */
553
+ interface ReceiptFiscal {
554
+ document_type: 'sale' | 'refund' | 'void' | 'cancellation' | 'closure' | 'x_report';
555
+ is_reprint: boolean;
556
+ reprint_count: number;
557
+ qr_payload: string;
558
+ }
537
559
  interface ReceiptLineItem {
538
560
  name: string;
539
561
  sku: string;
@@ -549,6 +571,10 @@ interface ReceiptLineItem {
549
571
  }[];
550
572
  }
551
573
  interface ReceiptData {
574
+ schemaVersion: string;
575
+ software: ReceiptSoftware;
576
+ register: ReceiptRegister;
577
+ fiscal: ReceiptFiscal;
552
578
  header: {
553
579
  storeName: string;
554
580
  storeAddress?: string;
@@ -602,6 +628,9 @@ interface ReceiptConfig {
602
628
  storeAddress?: string;
603
629
  cashier?: string;
604
630
  register?: string;
631
+ registerName?: string;
632
+ software?: Partial<ReceiptSoftware>;
633
+ fiscal?: Partial<ReceiptFiscal>;
605
634
  taxLabels?: Record<number, string>;
606
635
  }
607
636
 
@@ -921,6 +950,7 @@ declare function getDeviceId(storage: {
921
950
  * session closed (`lateSessionId`) or with local warnings (whatever the sync status), or pending with a `commandId` in
922
951
  * `stuckCommandIds` (the outbox's `OutboxState.stuck`: the store keeps failing it), newest first.
923
952
  * A warning code this till version doesn't know is not shown here (`knownWarnings`). Never changes `orders`.
953
+ * `register_session_unknown` is not shown, because the store counts the order once its session arrives (ADR-078).
924
954
  */
925
955
  declare function needsAttention(orders: PosOrder[], options?: {
926
956
  stuckCommandIds?: readonly string[];
@@ -2496,4 +2526,43 @@ declare function planLegs(plan: TenderPlan, rowsSinceFrom: {
2496
2526
  };
2497
2527
  };
2498
2528
 
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 };
2529
+ /**
2530
+ * One record per voided tender leg: its v2 ledger row plus the provenance a v2 `void`
2531
+ * fiscal record carries (TallyUI#479). Write-once: insert only, never update or delete;
2532
+ * a replay of a known `paymentId` is a no-op for the caller to handle.
2533
+ * Local only, never replicated. No device-side number or checksum:
2534
+ * v2 numbers records on the server.
2535
+ */
2536
+
2537
+ interface TenderVoid {
2538
+ /** The voided leg's client-minted id (the `clientPaymentId` order.create would send). Primary key, write-once. */
2539
+ paymentId: string;
2540
+ /** The sale's id (`PosOrder.saleId`). */
2541
+ saleId: string;
2542
+ type: 'void';
2543
+ method: PaymentMethodKind;
2544
+ amountMinor: number;
2545
+ /** Cash only: what the customer handed over. */
2546
+ tenderedMinor?: number;
2547
+ /** An external terminal's reference. */
2548
+ reference?: string;
2549
+ /** ISO 4217 code. */
2550
+ currency: string;
2551
+ /** `removed`: the cashier removed this one leg. `cancelled`: the cashier backed out of tendering. */
2552
+ reason: 'removed' | 'cancelled';
2553
+ /** Device clock at the void, ISO 8601. */
2554
+ deviceTime: string;
2555
+ /** The device's IANA time zone. */
2556
+ deviceTz: string;
2557
+ /** The bound register's id, `''` when no register is bound. */
2558
+ registerId: string;
2559
+ sessionId?: string;
2560
+ /** The cashier, as the backend's user id string. */
2561
+ cashierRef: string;
2562
+ }
2563
+ declare const tenderVoidSchema: RxJsonSchema<TenderVoid>;
2564
+ declare function tenderVoidCollection(): {
2565
+ readonly schema: RxJsonSchema<TenderVoid>;
2566
+ };
2567
+
2568
+ 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, RECEIPT_SCHEMA_VERSION, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptFiscal, type ReceiptLineItem, type ReceiptRegister, type ReceiptSoftware, 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 TenderVoid, 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, tenderVoidCollection, tenderVoidSchema, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useParkedSales, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure, writeOrderDraft };
package/dist/index.js CHANGED
@@ -1364,6 +1364,9 @@ function useParkedSales(drafts) {
1364
1364
  return { parked, discard };
1365
1365
  }
1366
1366
 
1367
+ // src/receipt/types.ts
1368
+ var RECEIPT_SCHEMA_VERSION = "1.4.0";
1369
+
1367
1370
  // src/receipt/build-receipt-data.ts
1368
1371
  function buildReceiptData(order, config) {
1369
1372
  const taxedLines = [...order.lineItems.map((line) => order.taxRounding?.granularity === "woocommerce" && line.netMicros !== void 0 ? { ...line, taxInclusive: order.pricesIncludeTax } : line), ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({
@@ -1387,6 +1390,23 @@ function buildReceiptData(order, config) {
1387
1390
  const label = (d) => d.label ?? d.couponCode ?? `${d.type} discount`;
1388
1391
  const customer = order.customer?.name || order.customer?.email;
1389
1392
  return {
1393
+ schemaVersion: RECEIPT_SCHEMA_VERSION,
1394
+ software: {
1395
+ name: "WCPOS",
1396
+ plugin_version: "",
1397
+ app_version: "",
1398
+ app_build: "",
1399
+ platform: "",
1400
+ ...config.software
1401
+ },
1402
+ register: { id: config.register ?? "", name: config.registerName ?? "" },
1403
+ fiscal: {
1404
+ document_type: "sale",
1405
+ is_reprint: false,
1406
+ reprint_count: 0,
1407
+ qr_payload: "",
1408
+ ...config.fiscal
1409
+ },
1390
1410
  header: {
1391
1411
  storeName: config.storeName,
1392
1412
  storeAddress: config.storeAddress,
@@ -2282,7 +2302,7 @@ function getDeviceId(storage, key) {
2282
2302
  // src/pos-order/needs-attention.ts
2283
2303
  import { knownWarnings } from "@tallyui/core";
2284
2304
  function needsAttention(orders, options = {}) {
2285
- return orders.filter((order) => order.syncStatus === "rejected" || (order.localWarnings?.length ?? 0) > 0 || order.syncStatus === "applied" && knownWarnings(order.warnings).length > 0 || order.lateSessionId !== void 0 || order.syncStatus === "pending" && options.stuckCommandIds?.includes(order.commandId) === true).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
2305
+ return orders.filter((order) => order.syncStatus === "rejected" || (order.localWarnings?.length ?? 0) > 0 || order.syncStatus === "applied" && knownWarnings(order.warnings).some((warning) => warning.code !== "register_session_unknown") || order.lateSessionId !== void 0 || order.syncStatus === "pending" && options.stuckCommandIds?.includes(order.commandId) === true).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
2286
2306
  }
2287
2307
 
2288
2308
  // src/pos-order/same-sale.ts
@@ -5086,6 +5106,7 @@ function createOrderOutbox(options) {
5086
5106
 
5087
5107
  // src/outbox/register-outbox.ts
5088
5108
  import { BehaviorSubject as BehaviorSubject5 } from "rxjs";
5109
+ var NON_BLOCKING_REJECTIONS = /* @__PURE__ */ new Set(["register_session_superseded", "register_session_abandoned"]);
5089
5110
  function plainCommand({
5090
5111
  key,
5091
5112
  registerId,
@@ -5165,8 +5186,9 @@ function createRegisterOutbox(options) {
5165
5186
  selector: { syncStatus: { $in: ["pending", "rejected"] } },
5166
5187
  sort: [{ seq: "asc" }, { key: "asc" }]
5167
5188
  })) {
5168
- if (command.syncStatus === "rejected") blocked.add(command.registerId);
5169
- else if (!blocked.has(command.registerId)) sendable2.add(command.commandId);
5189
+ if (command.syncStatus === "rejected") {
5190
+ if (!NON_BLOCKING_REJECTIONS.has(command.error?.code ?? "")) blocked.add(command.registerId);
5191
+ } else if (!blocked.has(command.registerId)) sendable2.add(command.commandId);
5170
5192
  }
5171
5193
  for (const id2 of clocks.keys()) if (!sendable2.has(id2)) clocks.delete(id2);
5172
5194
  state$.next({ ...state$.value, ...patch, pending, stuck: stuckState() });
@@ -5202,9 +5224,10 @@ function createRegisterOutbox(options) {
5202
5224
  });
5203
5225
  const commands = [];
5204
5226
  for (const command of ledger) {
5227
+ if (command.syncStatus === "rejected" && NON_BLOCKING_REJECTIONS.has(command.error?.code ?? "")) continue;
5205
5228
  if (command.syncStatus === "rejected") break;
5206
5229
  commands.push(command);
5207
- if (commands.length >= batchSize) break;
5230
+ if (command.type === "register.session.open" || commands.length >= batchSize) break;
5208
5231
  }
5209
5232
  if (stopped) {
5210
5233
  stoppedDuringRun = true;
@@ -5707,6 +5730,49 @@ function planLegs(plan, rowsSinceFrom, balanceMinor) {
5707
5730
  const title = plan.kind === "fixed" && taken === 0 ? plan.title : null;
5708
5731
  return { legs, thisPaymentMinor: each, label: { n: taken + 1, ways, rest, title } };
5709
5732
  }
5733
+
5734
+ // src/tender/tender-void.ts
5735
+ var tenderVoidSchema = {
5736
+ title: "Tender voids",
5737
+ version: 0,
5738
+ primaryKey: "paymentId",
5739
+ type: "object",
5740
+ additionalProperties: false,
5741
+ properties: {
5742
+ paymentId: { type: "string", maxLength: 64 },
5743
+ saleId: { type: "string", maxLength: 64 },
5744
+ type: { type: "string", enum: ["void"], maxLength: 4 },
5745
+ // Not an enum: PaymentMethodKind grows, and a stored enum would need a schema migration per new kind.
5746
+ method: { type: "string", maxLength: 32 },
5747
+ amountMinor: { type: "integer" },
5748
+ tenderedMinor: { type: "integer" },
5749
+ reference: { type: "string" },
5750
+ currency: { type: "string", maxLength: 3 },
5751
+ reason: { type: "string", enum: ["removed", "cancelled"] },
5752
+ deviceTime: { type: "string", maxLength: 40 },
5753
+ deviceTz: { type: "string" },
5754
+ registerId: { type: "string" },
5755
+ sessionId: { type: "string" },
5756
+ cashierRef: { type: "string" }
5757
+ },
5758
+ required: [
5759
+ "paymentId",
5760
+ "saleId",
5761
+ "type",
5762
+ "method",
5763
+ "amountMinor",
5764
+ "currency",
5765
+ "reason",
5766
+ "deviceTime",
5767
+ "deviceTz",
5768
+ "registerId",
5769
+ "cashierRef"
5770
+ ],
5771
+ indexes: [["saleId"], ["deviceTime"]]
5772
+ };
5773
+ function tenderVoidCollection() {
5774
+ return { schema: tenderVoidSchema };
5775
+ }
5710
5776
  export {
5711
5777
  CartError,
5712
5778
  CurrencyProvider,
@@ -5717,6 +5783,7 @@ export {
5717
5783
  OrderContentMismatchError,
5718
5784
  PRODUCT_SORT_FIELDS,
5719
5785
  PosOrderOpenClosedError,
5786
+ RECEIPT_SCHEMA_VERSION,
5720
5787
  RegisterApprovalRequiredError,
5721
5788
  RegisterCloseIncompleteError,
5722
5789
  RegisterIdInvalidError,
@@ -5842,6 +5909,8 @@ export {
5842
5909
  taxMicros,
5843
5910
  taxProviderProps,
5844
5911
  tenderReducer,
5912
+ tenderVoidCollection,
5913
+ tenderVoidSchema,
5845
5914
  toOrderCreateEnvelope,
5846
5915
  unbindRegister,
5847
5916
  useCurrencyCode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/pos",
3
- "version": "3.5.2",
3
+ "version": "3.6.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.5.2",
30
+ "@tallyui/core": "^3.6.0",
31
31
  "react": ">=18",
32
32
  "rxdb": "~17.5.0",
33
33
  "rxjs": ">=7"
34
34
  },
35
35
  "devDependencies": {
36
- "@tallyui/core": "3.5.2",
36
+ "@tallyui/core": "3.6.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.5.2",
42
- "@tallyui/connector-woocommerce": "3.5.2",
43
- "@tallyui/database": "3.5.2"
41
+ "@tallyui/connector-medusa": "3.6.0",
42
+ "@tallyui/connector-woocommerce": "3.6.0",
43
+ "@tallyui/database": "3.6.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",
package/src/index.ts CHANGED
@@ -51,8 +51,8 @@ export type {
51
51
  } from './order';
52
52
 
53
53
  // Receipt
54
- export { buildReceiptData } from './receipt';
55
- export type { ReceiptData, ReceiptLineItem, ReceiptConfig } from './receipt';
54
+ export { buildReceiptData, RECEIPT_SCHEMA_VERSION } from './receipt';
55
+ export type { ReceiptData, ReceiptLineItem, ReceiptConfig, ReceiptSoftware, ReceiptRegister, ReceiptFiscal } from './receipt';
56
56
 
57
57
  // Product
58
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';
@@ -69,8 +69,8 @@ export type { RegisterOutboxOptions, RegisterOutbox, UseRegisterOutboxOptions, U
69
69
  export { createBackendNotFound } from './outbox/backend-not-found';
70
70
  export type { BackendNotFound } from './outbox/backend-not-found';
71
71
  export type { HttpTransportOptions, OrderOutboxOptions, OrderOutbox, TransportOutcome, CommandTransport, OrderTransportContext, OutboxState, UseOrderOutboxOptions, UseOrderOutboxResult } from './outbox';
72
- export { tenderReducer, initTenderState, initialTenderState, appliedMinor, changeMinor, quickTenderedAmounts, evenSplitShareMinor, activePlan, planLegs, MAX_TENDER_MINOR } from './tender';
73
- export type { PaymentTransport, TenderView, TenderLineId, TenderPlan, SplitTab, TenderState, TenderKey, TenderAction, PlanLeg } from './tender';
72
+ export { tenderReducer, initTenderState, initialTenderState, appliedMinor, changeMinor, quickTenderedAmounts, evenSplitShareMinor, activePlan, planLegs, MAX_TENDER_MINOR, tenderVoidSchema, tenderVoidCollection } from './tender';
73
+ export type { PaymentTransport, TenderView, TenderLineId, TenderPlan, SplitTab, TenderState, TenderKey, TenderAction, PlanLeg, TenderVoid } from './tender';
74
74
 
75
75
  // Register
76
76
  export { normalizeAmount, isServerDecimal, movementFieldError, deriveExpected, parseMinor, validAmount, countVariance, overThreshold, closeNeedsApproval, denominationTotal, varianceText, denominations } from './register';
@@ -7,6 +7,9 @@ import { createBackendNotFound, type BackendNotFound } from './backend-not-found
7
7
  import { STUCK_AFTER_MS } from './order-outbox';
8
8
  import type { CommandTransport, OutboxState } from './types';
9
9
 
10
+ /** Final rejections that do not stop their register's ledger (ADR-078): the commands behind them are still sent. */
11
+ export const NON_BLOCKING_REJECTIONS: ReadonlySet<string> = new Set(['register_session_superseded', 'register_session_abandoned']);
12
+
10
13
  export interface RegisterOutboxOptions {
11
14
  collection: RxCollection<RegisterCommand>;
12
15
  transport: CommandTransport<RegisterCommandEnvelope>;
@@ -68,7 +71,7 @@ export function createRegisterOutbox(options: RegisterOutboxOptions): RegisterOu
68
71
  // like a 503, or `no_progress`); a running clock keeps its `since` and takes the latest reason. A `network` failure
69
72
  // pauses every clock (`pausedAt`), and the next answer of any kind resumes them, moving `since` on by the offline
70
73
  // gap, so `since` is now minus the answered time. A clock clears when its command is no longer pending under its
71
- // commandId (applied or rejected), or when a rejected command comes ahead of it in its ledger (it is not being
74
+ // commandId (applied or rejected), or when a blocking rejected command comes ahead of it in its ledger (it is not being
72
75
  // sent: Front desk, 2026-09-30); commands never sent have none. Nothing is isolated: a stuck command at the head
73
76
  // of a register's ledger holds up the commands behind it, by design, since register facts apply in `seq` order.
74
77
  // In memory only: a restart starts with no clocks, and the first answered failure after it starts them afresh
@@ -96,8 +99,9 @@ export function createRegisterOutbox(options: RegisterOutboxOptions): RegisterOu
96
99
  const blocked = new Set<string>();
97
100
  if (clocks.size) for (const command of await readFresh(collection, { selector: { syncStatus: { $in: ['pending', 'rejected'] } },
98
101
  sort: [{ seq: 'asc' }, { key: 'asc' }] })) {
99
- if (command.syncStatus === 'rejected') blocked.add(command.registerId);
100
- else if (!blocked.has(command.registerId)) sendable.add(command.commandId);
102
+ if (command.syncStatus === 'rejected') {
103
+ if (!NON_BLOCKING_REJECTIONS.has(command.error?.code ?? '')) blocked.add(command.registerId);
104
+ } else if (!blocked.has(command.registerId)) sendable.add(command.commandId);
101
105
  }
102
106
  for (const id of clocks.keys()) if (!sendable.has(id)) clocks.delete(id);
103
107
  state$.next({ ...state$.value, ...patch, pending, stuck: stuckState() });
@@ -127,9 +131,10 @@ export function createRegisterOutbox(options: RegisterOutboxOptions): RegisterOu
127
131
  });
128
132
  const commands: RegisterCommand[] = [];
129
133
  for (const command of ledger) {
134
+ if (command.syncStatus === 'rejected' && NON_BLOCKING_REJECTIONS.has(command.error?.code ?? '')) continue;
130
135
  if (command.syncStatus === 'rejected') break;
131
136
  commands.push(command);
132
- if (commands.length >= batchSize) break;
137
+ if (command.type === 'register.session.open' || commands.length >= batchSize) break;
133
138
  }
134
139
  if (stopped) { stoppedDuringRun = true; return; }
135
140
  if (!commands.length) continue;
@@ -6,11 +6,12 @@ import type { PosOrder } from './types';
6
6
  * session closed (`lateSessionId`) or with local warnings (whatever the sync status), or pending with a `commandId` in
7
7
  * `stuckCommandIds` (the outbox's `OutboxState.stuck`: the store keeps failing it), newest first.
8
8
  * A warning code this till version doesn't know is not shown here (`knownWarnings`). Never changes `orders`.
9
+ * `register_session_unknown` is not shown, because the store counts the order once its session arrives (ADR-078).
9
10
  */
10
11
  export function needsAttention(orders: PosOrder[], options: { stuckCommandIds?: readonly string[] } = {}): PosOrder[] {
11
12
  return orders.filter((order) => order.syncStatus === 'rejected'
12
13
  || (order.localWarnings?.length ?? 0) > 0
13
- || (order.syncStatus === 'applied' && knownWarnings(order.warnings).length > 0) || order.lateSessionId !== undefined
14
+ || (order.syncStatus === 'applied' && knownWarnings(order.warnings).some((warning) => warning.code !== 'register_session_unknown')) || order.lateSessionId !== undefined
14
15
  || (order.syncStatus === 'pending' && options.stuckCommandIds?.includes(order.commandId) === true))
15
16
  .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
16
17
  }
@@ -1,6 +1,7 @@
1
1
  import type { Discount, SentOrder } from '../order/types';
2
2
  import { roundMicrosToMinor, taxLinesByRate } from '../tax/exact';
3
3
  import type { ReceiptConfig, ReceiptData } from './types';
4
+ import { RECEIPT_SCHEMA_VERSION } from './types';
4
5
 
5
6
  export function buildReceiptData(order: SentOrder, config: ReceiptConfig): ReceiptData {
6
7
  // Drops the shared helper's `netMinor` (the Z report's own use): the receipt's tax summary
@@ -29,6 +30,14 @@ export function buildReceiptData(order: SentOrder, config: ReceiptConfig): Recei
29
30
  const customer = order.customer?.name || order.customer?.email;
30
31
 
31
32
  return {
33
+ schemaVersion: RECEIPT_SCHEMA_VERSION,
34
+ software: {
35
+ name: 'WCPOS', plugin_version: '', app_version: '', app_build: '', platform: '', ...config.software,
36
+ },
37
+ register: { id: config.register ?? '', name: config.registerName ?? '' },
38
+ fiscal: {
39
+ document_type: 'sale', is_reprint: false, reprint_count: 0, qr_payload: '', ...config.fiscal,
40
+ },
32
41
  header: {
33
42
  storeName: config.storeName,
34
43
  storeAddress: config.storeAddress,
@@ -1,2 +1,3 @@
1
1
  export { buildReceiptData } from './build-receipt-data';
2
- export type { ReceiptData, ReceiptLineItem, ReceiptConfig } from './types';
2
+ export { RECEIPT_SCHEMA_VERSION } from './types';
3
+ export type { ReceiptData, ReceiptLineItem, ReceiptConfig, ReceiptSoftware, ReceiptRegister, ReceiptFiscal } from './types';
@@ -1,3 +1,18 @@
1
+ /** WCPOS receipt data schema version these blocks follow (wiki: fiscal groundwork, receipt identity and QR §2). */
2
+ export const RECEIPT_SCHEMA_VERSION = '1.4.0';
3
+
4
+ /** 1.4 `software`: which software produced the receipt. */
5
+ export interface ReceiptSoftware { name: string; plugin_version: string; app_version: string; app_build: string; platform: string }
6
+ /** 1.4 `register`: `{ id: '', name: '' }` when there is none. */
7
+ export interface ReceiptRegister { id: string; name: string }
8
+ /** 1.4 `fiscal` identity fields a till can know. Sale time, zone and counter come with the per-register sale counter (#475). */
9
+ export interface ReceiptFiscal {
10
+ document_type: 'sale' | 'refund' | 'void' | 'cancellation' | 'closure' | 'x_report';
11
+ is_reprint: boolean;
12
+ reprint_count: number;
13
+ qr_payload: string;
14
+ }
15
+
1
16
  export interface ReceiptLineItem {
2
17
  name: string;
3
18
  sku: string;
@@ -11,6 +26,10 @@ export interface ReceiptLineItem {
11
26
  }
12
27
 
13
28
  export interface ReceiptData {
29
+ schemaVersion: string;
30
+ software: ReceiptSoftware;
31
+ register: ReceiptRegister;
32
+ fiscal: ReceiptFiscal;
14
33
  header: {
15
34
  storeName: string;
16
35
  storeAddress?: string;
@@ -47,5 +66,8 @@ export interface ReceiptConfig {
47
66
  storeAddress?: string;
48
67
  cashier?: string;
49
68
  register?: string;
69
+ registerName?: string;
70
+ software?: Partial<ReceiptSoftware>;
71
+ fiscal?: Partial<ReceiptFiscal>;
50
72
  taxLabels?: Record<number, string>;
51
73
  }
@@ -1,2 +1,4 @@
1
1
  export { tenderReducer, initTenderState, initialTenderState, appliedMinor, changeMinor, quickTenderedAmounts, evenSplitShareMinor, activePlan, planLegs, MAX_TENDER_MINOR } from './tender-state';
2
2
  export type { PaymentTransport, TenderView, TenderLineId, TenderPlan, SplitTab, TenderState, TenderKey, TenderAction, PlanLeg } from './tender-state';
3
+ export { tenderVoidSchema, tenderVoidCollection } from './tender-void';
4
+ export type { TenderVoid } from './tender-void';
@@ -0,0 +1,66 @@
1
+ /**
2
+ * One record per voided tender leg: its v2 ledger row plus the provenance a v2 `void`
3
+ * fiscal record carries (TallyUI#479). Write-once: insert only, never update or delete;
4
+ * a replay of a known `paymentId` is a no-op for the caller to handle.
5
+ * Local only, never replicated. No device-side number or checksum:
6
+ * v2 numbers records on the server.
7
+ */
8
+ import type { PaymentMethodKind } from '@tallyui/core';
9
+ import type { RxJsonSchema } from 'rxdb';
10
+
11
+ export interface TenderVoid {
12
+ /** The voided leg's client-minted id (the `clientPaymentId` order.create would send). Primary key, write-once. */
13
+ paymentId: string;
14
+ /** The sale's id (`PosOrder.saleId`). */
15
+ saleId: string;
16
+ type: 'void';
17
+ method: PaymentMethodKind;
18
+ amountMinor: number;
19
+ /** Cash only: what the customer handed over. */
20
+ tenderedMinor?: number;
21
+ /** An external terminal's reference. */
22
+ reference?: string;
23
+ /** ISO 4217 code. */
24
+ currency: string;
25
+ /** `removed`: the cashier removed this one leg. `cancelled`: the cashier backed out of tendering. */
26
+ reason: 'removed' | 'cancelled';
27
+ /** Device clock at the void, ISO 8601. */
28
+ deviceTime: string;
29
+ /** The device's IANA time zone. */
30
+ deviceTz: string;
31
+ /** The bound register's id, `''` when no register is bound. */
32
+ registerId: string;
33
+ sessionId?: string;
34
+ /** The cashier, as the backend's user id string. */
35
+ cashierRef: string;
36
+ }
37
+
38
+ export const tenderVoidSchema: RxJsonSchema<TenderVoid> = {
39
+ title: 'Tender voids', version: 0, primaryKey: 'paymentId', type: 'object', additionalProperties: false,
40
+ properties: {
41
+ paymentId: { type: 'string', maxLength: 64 },
42
+ saleId: { type: 'string', maxLength: 64 },
43
+ type: { type: 'string', enum: ['void'], maxLength: 4 },
44
+ // Not an enum: PaymentMethodKind grows, and a stored enum would need a schema migration per new kind.
45
+ method: { type: 'string', maxLength: 32 },
46
+ amountMinor: { type: 'integer' },
47
+ tenderedMinor: { type: 'integer' },
48
+ reference: { type: 'string' },
49
+ currency: { type: 'string', maxLength: 3 },
50
+ reason: { type: 'string', enum: ['removed', 'cancelled'] },
51
+ deviceTime: { type: 'string', maxLength: 40 },
52
+ deviceTz: { type: 'string' },
53
+ registerId: { type: 'string' },
54
+ sessionId: { type: 'string' },
55
+ cashierRef: { type: 'string' },
56
+ },
57
+ required: [
58
+ 'paymentId', 'saleId', 'type', 'method', 'amountMinor', 'currency', 'reason',
59
+ 'deviceTime', 'deviceTz', 'registerId', 'cashierRef',
60
+ ],
61
+ indexes: [['saleId'], ['deviceTime']],
62
+ };
63
+
64
+ export function tenderVoidCollection(): { readonly schema: RxJsonSchema<TenderVoid> } {
65
+ return { schema: tenderVoidSchema } as const;
66
+ }