@tallyui/pos 2.0.0 → 3.0.0-next.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.
Files changed (44) hide show
  1. package/dist/index.d.ts +323 -61
  2. package/dist/index.js +1423 -186
  3. package/package.json +8 -8
  4. package/src/index.ts +15 -7
  5. package/src/order/index.ts +3 -0
  6. package/src/order/order-builder.ts +23 -7
  7. package/src/order/order-manager.ts +3 -1
  8. package/src/order/tax-figures.ts +23 -0
  9. package/src/order/types.ts +9 -3
  10. package/src/outbox/backend-not-found.ts +27 -0
  11. package/src/outbox/http-transport.ts +5 -4
  12. package/src/outbox/index.ts +2 -0
  13. package/src/outbox/logger.ts +3 -0
  14. package/src/outbox/order-outbox.ts +336 -27
  15. package/src/outbox/register-outbox.ts +250 -0
  16. package/src/outbox/types.test-d.ts +22 -0
  17. package/src/outbox/types.ts +17 -3
  18. package/src/outbox/use-order-outbox.ts +20 -6
  19. package/src/pos-order/__fixtures__/order-create-v3.json +97 -0
  20. package/src/pos-order/command.ts +99 -11
  21. package/src/pos-order/finalize.ts +145 -7
  22. package/src/pos-order/index.ts +2 -2
  23. package/src/pos-order/needs-attention.ts +9 -4
  24. package/src/pos-order/open.ts +100 -37
  25. package/src/pos-order/schema.ts +35 -5
  26. package/src/pos-order/types.ts +31 -5
  27. package/src/product/search-products.ts +5 -2
  28. package/src/receipt/build-receipt-data.ts +5 -3
  29. package/src/receipt/types.ts +1 -0
  30. package/src/register/closure-document.ts +7 -0
  31. package/src/register/index.ts +5 -3
  32. package/src/register/movement-input.ts +1 -1
  33. package/src/register/register-commands.ts +147 -0
  34. package/src/register/register-document.ts +26 -1
  35. package/src/register/session-store.ts +41 -12
  36. package/src/register/use-register-session.ts +46 -2
  37. package/src/sale/cart.ts +1 -0
  38. package/src/sale/use-sale.ts +97 -40
  39. package/src/store-settings/map-store-settings.ts +7 -2
  40. package/src/store-settings/use-store-settings.ts +55 -9
  41. package/src/tax/exact.ts +74 -11
  42. package/src/tax/index.ts +1 -1
  43. package/src/tax/tax-provider.tsx +26 -4
  44. package/src/tax/types.ts +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/pos",
3
- "version": "2.0.0",
3
+ "version": "3.0.0-next.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": "2.0.0",
30
+ "@tallyui/core": "^3.0.0-next.1",
31
31
  "react": ">=18",
32
- "rxdb": ">=16",
32
+ "rxdb": "~17.5.0",
33
33
  "rxjs": ">=7"
34
34
  },
35
35
  "devDependencies": {
36
- "@tallyui/core": "2.0.0",
36
+ "@tallyui/core": "3.0.0-next.1",
37
37
  "@types/react": ">=18",
38
38
  "react": "^19.2.4",
39
- "rxdb": "16.21.1",
39
+ "rxdb": "17.5.0",
40
40
  "rxjs": "7.8.2",
41
- "@tallyui/connector-medusa": "2.0.0",
42
- "@tallyui/connector-woocommerce": "2.0.0",
43
- "@tallyui/database": "2.0.0"
41
+ "@tallyui/connector-medusa": "3.0.0-next.1",
42
+ "@tallyui/connector-woocommerce": "3.0.0-next.1",
43
+ "@tallyui/database": "3.0.0-next.1"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ export { CurrencyProvider, useCurrencyFormatter, useCurrencyCode } from './curre
7
7
  export type { CurrencyProviderProps } from './currency';
8
8
 
9
9
  // Tax
10
- export { TaxProvider, useTax } from './tax';
10
+ export { TaxProvider, useTax, taxLogger } from './tax';
11
11
  export { MICROS_PER_MINOR, ratePpmFromPercent, taxMicros, roundMicrosToMinor, computeOrderTax, taxLinesByRate } from './tax';
12
12
  export type { TaxLineInput, OrderTaxTotals, RateTaxLine } from './tax';
13
13
  export type { TaxProviderProps } from './tax';
@@ -25,11 +25,13 @@ export { createRepository } from './repository';
25
25
  export type { Repository } from './repository';
26
26
 
27
27
  // Order
28
- export { createOrderBuilder, createOrderManager, allocateOrderDiscount } from './order';
28
+ export { createOrderBuilder, createOrderManager, allocateOrderDiscount, taxFiguresForBasket } from './order';
29
+ export type { BasketLine } from './order';
29
30
  export type { OrderBuilder, OrderBuilderOptions } from './order';
30
31
  export type { OrderManager, OrderManagerOptions, ParkedOrderSummary } from './order';
31
32
  export type {
32
33
  Order,
34
+ SentOrder,
33
35
  LineItem,
34
36
  LineTaxLine,
35
37
  AddLineInput,
@@ -53,9 +55,13 @@ export { searchProducts, withStockOverlay, getProductStock, stockOverlay$, stock
53
55
  export { useSale, DISCOUNTS_UNSUPPORTED, SALE_SAVING, saleLogger, addEntryToCart, CartError, catalogueEntries, findEntryByCode, variantPriceLabel } from './sale';
54
56
  export type { SaleStage, CatalogueEntry } from './sale';
55
57
 
56
- export { uuidv7, finalizeOrder, toOrderCreateEnvelope, posOrderSchema, posOrderCollection, addPosOrderCollection, PosOrderOpenClosedError, posOrdersLogger, getDeviceId, needsAttention, sameSale, OrderContentMismatchError } from './pos-order';
57
- export type { PosOrderSyncStatus, PosOrderLine, PosOrderPayment, PosOrder, FinalizeOptions } from './pos-order';
58
+ export { uuidv7, finalizeOrder, toOrderCreateEnvelope, UnsupportedOrderVersionError, posOrderSchema, posOrderCollection, addPosOrderCollection, PosOrderOpenClosedError, posOrdersLogger, getDeviceId, needsAttention, sameSale, OrderContentMismatchError } from './pos-order';
59
+ export type { PosOrderSyncStatus, PosOrderLine, PosOrderPayment, PosOrderLocalWarning, PosOrderServerFailures, PosOrder, FinalizeOptions } from './pos-order';
58
60
  export { createHttpCommandTransport, createOrderOutbox, useOrderOutbox, outboxLogger } from './outbox';
61
+ export { createRegisterOutbox } from './outbox';
62
+ export type { RegisterOutboxOptions, RegisterOutbox } from './outbox';
63
+ export { createBackendNotFound } from './outbox/backend-not-found';
64
+ export type { BackendNotFound } from './outbox/backend-not-found';
59
65
  export type { HttpTransportOptions, OrderOutboxOptions, OrderOutbox, TransportOutcome, CommandTransport, OutboxState, UseOrderOutboxOptions, UseOrderOutboxResult } from './outbox';
60
66
  export { tenderReducer, initTenderState, initialTenderState, appliedMinor, changeMinor, quickTenderedAmounts, evenSplitShareMinor, activePlan, planLegs, MAX_TENDER_MINOR } from './tender';
61
67
  export type { PaymentTransport, TenderView, TenderLineId, TenderPlan, SplitTab, TenderState, TenderKey, TenderAction, PlanLeg } from './tender';
@@ -65,11 +71,13 @@ export { normalizeAmount, isServerDecimal, movementFieldError, deriveExpected, p
65
71
  export type { MovementType, LedgerRow, Movement } from './register';
66
72
  export { registerSessionSchema, registerSessionCollection, cashMovementSchema, closureSchema } from './register';
67
73
  export type { RegisterSession, CashMovement, Closure } from './register';
68
- export { mintUuid, readRegister, ensureRegister, observeRegister$, getBoundRegisterId, readBoundRegister, bindRegister, unbindRegister, nextSaleCounter, mintClosureNumber, advancePerpetual } from './register';
74
+ export { registerCommandSchema, registerCommandCollection, registerCommandsLogger, sessionOpenCommand, sessionTransitionCommand, movementCommand, closureCommand, reconcileRegisterCommands } from './register';
75
+ export type { RegisterCommand, RegisterCommandCollection } from './register';
76
+ export { mintUuid, readRegister, ensureRegister, observeRegister$, getBoundRegisterId, readBoundRegister, bindRegister, unbindRegister, nextSaleCounter, mintClosureNumber, advancePerpetual, RegisterIdInvalidError } from './register';
69
77
  export type { RegisterHost, RegisterCounters, RegisterBucket, RegisterStore, RegisterDocument } from './register';
70
- export { RegisterSessionRequiredError, RegisterSessionClosedError, RegisterMovementStrandedError, openSessionSelector, requireOpenSession, stampSession, openSession, startCounting, backToSelling, closeSession, recordMovement, voidMovement, writeClosure } from './register';
78
+ export { RegisterSessionRequiredError, RegisterSessionClosedError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, openSessionSelector, requireOpenSession, stampSession, openSession, startCounting, backToSelling, closeSession, recordMovement, voidMovement, writeClosure } from './register';
71
79
  export type { RegisterSessionCollection, CashMovementCollection, ClosureCollection } from './register';
72
80
  export { deriveSettled, exportCsv, labelKeys, clampClosureScope, selectClosureRows } from './register';
73
81
  export type { Correction, RecordedFigures, ClosureScope } from './register';
74
- export { minorToDecimal, formatClosureDate, buildClosureDocument, buildXReportDocument, registerFactsLogger, recordRegisterFact, type ClosureContext, type Actor, type RegisterFact } from './register';
82
+ export { minorToDecimal, formatClosureDate, buildClosureDocument, buildXReportDocument, TAX_ROUNDING_MIXED_NOTE, registerFactsLogger, recordRegisterFact, type ClosureContext, type Actor, type RegisterFact } from './register';
75
83
  export { useRegisterSession, RegisterTenderInProgressError, RegisterSessionAlreadyOpenError, RegisterCloseIncompleteError, RegisterApprovalRequiredError, type UseRegisterSessionOptions } from './register';
@@ -1,10 +1,13 @@
1
1
  export { createOrderBuilder } from './order-builder';
2
+ export { taxFiguresForBasket } from './tax-figures';
3
+ export type { BasketLine } from './tax-figures';
2
4
  export { allocateOrderDiscount } from './allocate-order-discount';
3
5
  export type { OrderBuilder, OrderBuilderOptions } from './order-builder';
4
6
  export { createOrderManager } from './order-manager';
5
7
  export type { OrderManager, OrderManagerOptions, ParkedOrderSummary } from './order-manager';
6
8
  export type {
7
9
  Order,
10
+ SentOrder,
8
11
  LineItem,
9
12
  LineTaxLine,
10
13
  AddLineInput,
@@ -1,7 +1,8 @@
1
1
  import { BehaviorSubject, type Observable } from 'rxjs';
2
- import { resolvePrice, type ProductTraits } from '@tallyui/core';
2
+ import { resolvePrice, type ProductTraits, type TaxRounding } from '@tallyui/core';
3
3
  import type { TaxContext } from '../tax/types';
4
- import { taxMicros, roundMicrosToMinor } from '../tax/exact';
4
+ import { taxMicros, roundMicrosToMinor, roundedTaxByRate } from '../tax/exact';
5
+ import { taxLogger } from '../tax/tax-provider';
5
6
  import { allocateOrderDiscount } from './allocate-order-discount';
6
7
  import type {
7
8
  Order,
@@ -13,6 +14,9 @@ import type {
13
14
  AddLineInput,
14
15
  } from './types';
15
16
 
17
+ /** Tax contexts already warned that per_rate_group_items fell back to per_order for inclusive lines (#287). */
18
+ const perRateGroupWarned = new WeakSet<TaxContext>();
19
+
16
20
  let nextId = 0;
17
21
  function uid(): string {
18
22
  return `${Date.now()}-${++nextId}`;
@@ -23,14 +27,18 @@ function roundHalfAway(n: number): number {
23
27
  }
24
28
 
25
29
  /** The settlement totals of recalculated lines; the display figures derive from them (ADR-063). */
26
- function sumLines(lines: LineItem[]): { subtotalMinor: number; taxMinor: number; totalMinor: number } {
30
+ function sumLines(lines: LineItem[], rounding?: TaxRounding): { subtotalMinor: number; taxMinor: number; totalMinor: number } {
31
+ // per_line_items, per_rate_group_items (#287): total = Σ rounded nets + tax (order-level-tax-calculation-strategy.js:44-49, order.entity.js:88-89).
32
+ const rounded = roundedTaxByRate(lines, rounding);
33
+ if (rounded) return { subtotalMinor: rounded.baseMinor, taxMinor: rounded.taxMinor, totalMinor: Math.max(0, rounded.baseMinor + rounded.taxMinor) };
34
+ const mode = rounding?.granularity === 'per_order' ? rounding.mode : undefined;
27
35
  const netMinor = lines.reduce((sum, li) => sum + li.netMinor, 0);
28
36
  const lineTaxMicros = lines.reduce((sum, li) => sum + BigInt(li.taxMicros), 0n);
29
37
  const exclusiveTaxMicros = lines.reduce((sum, li) => li.taxInclusive ? sum : sum + BigInt(li.taxMicros), 0n);
30
- const taxMinor = roundMicrosToMinor(lineTaxMicros);
38
+ const taxMinor = roundMicrosToMinor(lineTaxMicros, mode);
31
39
  // Each line pays in its own mode: an inclusive line its gross, an exclusive line its net plus tax.
32
40
  // The totals sum the lines as recalculated; nothing is subtracted after tax.
33
- const linesTotal = netMinor + roundMicrosToMinor(exclusiveTaxMicros);
41
+ const linesTotal = netMinor + roundMicrosToMinor(exclusiveTaxMicros, mode);
34
42
  return { subtotalMinor: linesTotal - taxMinor, taxMinor, totalMinor: Math.max(0, linesTotal) };
35
43
  }
36
44
 
@@ -158,7 +166,11 @@ export function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder {
158
166
  const shares = allocateOrderDiscount(lineAmounts, recalcedOrderDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
159
167
  const lines = lineItems.map((li, index) => recalculateLine(li, shares[index]));
160
168
 
161
- const { subtotalMinor, taxMinor, totalMinor } = sumLines(lines);
169
+ const { subtotalMinor, taxMinor, totalMinor } = sumLines(lines, taxContext.rounding);
170
+ if (taxContext.rounding?.granularity === 'per_rate_group_items' && lines.some((li) => li.taxInclusive) && !perRateGroupWarned.has(taxContext)) {
171
+ perRateGroupWarned.add(taxContext);
172
+ taxLogger.warn('per_rate_group_items with inclusive lines: using per_order figures until the display has a rounding row (#287, #310)');
173
+ }
162
174
  const discountMinor = lines.reduce((sum, li) => sum + li.discountMinor, 0);
163
175
  // Display figures (ADR-063): every discount the cashier entered, and each line's order share, converted on its own
164
176
  // into the display mode. A line in the display mode shows quantity × unit price; a converted line shows its
@@ -239,6 +251,7 @@ export function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder {
239
251
  changeDueMinor,
240
252
  currency,
241
253
  pricesIncludeTax: taxContext.pricesIncludeTax,
254
+ ...(taxContext.rounding ? { taxRounding: taxContext.rounding } : {}),
242
255
  createdAt: now,
243
256
  updatedAt: new Date().toISOString(),
244
257
  };
@@ -254,7 +267,9 @@ export function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder {
254
267
  if (unitPrice.currency !== currency) throw new RangeError('Line currency must match ' + currency);
255
268
  if (!Number.isInteger(unitPrice.amount)) throw new RangeError('Price must be integer minor units');
256
269
  if (!Number.isInteger(quantity) || quantity < 1) throw new RangeError('Quantity must be an integer >= 1');
257
- const taxRates = input.taxRates ?? [{ ratePpm: taxContext.getTaxRatePpm() }];
270
+ // A rate from the tax class carries the backend's name for it when one is mapped (#287, #288).
271
+ const code = input.taxRates ? undefined : taxContext.getTaxRateCode?.(input.taxClass);
272
+ const taxRates = input.taxRates ?? [{ ...(code !== undefined ? { code } : {}), ratePpm: taxContext.getTaxRatePpm(input.taxClass) }];
258
273
  const taxInclusive = unitPrice.taxInclusive ?? taxContext.pricesIncludeTax;
259
274
 
260
275
  const existing = lineItems.find(
@@ -324,6 +339,7 @@ export function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder {
324
339
  imageUrl: traits.getImageUrl(doc),
325
340
  unitPrice,
326
341
  quantity: opts?.quantity,
342
+ taxClass: traits.getTaxClass?.(doc, opts?.variantId),
327
343
  });
328
344
  },
329
345
 
@@ -1,6 +1,7 @@
1
1
  import { BehaviorSubject, map, type Observable } from 'rxjs';
2
2
  import type { RxCollection } from 'rxdb';
3
3
  import type { TaxContext } from '../tax/types';
4
+ import { customerRefusal } from '../pos-order/command';
4
5
  import { createOrderBuilder, type OrderBuilder } from './order-builder';
5
6
  import type { Order } from './types';
6
7
 
@@ -91,7 +92,8 @@ export function createOrderManager(options: OrderManagerOptions): OrderManager {
91
92
  });
92
93
 
93
94
  // Restore state from saved order
94
- if (savedOrder.customer) {
95
+ // A customer whose email or id order.create would refuse (see useSale's setCustomer) isn't restored.
96
+ if (savedOrder.customer && !customerRefusal(savedOrder.customer)) {
95
97
  builder.setCustomer(savedOrder.customer);
96
98
  }
97
99
  if (savedOrder.note) {
@@ -0,0 +1,23 @@
1
+ import type { TaxRounding } from '@tallyui/core';
2
+ import { createOrderBuilder } from './order-builder';
3
+ import { taxLinesByRate } from '../tax/exact';
4
+
5
+ /** A sent line; `discountMinor` is its `lines[].discountMinor` (line discounts + its order share), in its own mode. */
6
+ export type BasketLine = { unitPriceMinor: number; quantity: number; discountMinor?: number; taxInclusive: boolean; taxLines: Array<{ code?: string; ratePpm: number }> };
7
+
8
+ /**
9
+ * The till's settlement figures for a sent basket under a store's strategy (#287), computed exactly as a sale computes
10
+ * them, so a backend can score its own orders against the till (vendurepos/app#38). Each discountMinor is applied as a
11
+ * fixed line discount in the line's own mode.
12
+ */
13
+ export function taxFiguresForBasket(currency: string, pricesIncludeTax: boolean, lines: readonly BasketLine[], rounding?: TaxRounding) {
14
+ const code = currency.toUpperCase();
15
+ const builder = createOrderBuilder({ currency: code, taxContext: { getTaxRatePpm: () => 0, pricesIncludeTax, ...(rounding ? { rounding } : {}) } });
16
+ lines.forEach((line, index) => {
17
+ const lineId = builder.addLine({ productId: `line-${index}`, name: `Line ${index}`, quantity: line.quantity,
18
+ unitPrice: { amount: line.unitPriceMinor, currency: code, taxInclusive: line.taxInclusive }, taxRates: line.taxLines });
19
+ if (line.discountMinor) builder.applyLineDiscount(lineId, { type: 'fixed', value: line.discountMinor });
20
+ });
21
+ const { subtotalMinor, taxMinor, totalMinor, lineItems, taxRounding } = builder.getSnapshot();
22
+ return { subtotalMinor, taxMinor, totalMinor, taxByRate: taxLinesByRate(lineItems, taxMinor, undefined, taxRounding) };
23
+ }
@@ -1,4 +1,4 @@
1
- import type { Money } from '@tallyui/core';
1
+ import type { Money, TaxRounding } from '@tallyui/core';
2
2
 
3
3
  export interface Order {
4
4
  id: string;
@@ -10,10 +10,12 @@ export interface Order {
10
10
  note: string;
11
11
  currency: string;
12
12
  pricesIncludeTax: boolean;
13
+ /** The store's tax rounding strategy the figures were computed with (#287), from the sale's tax context; absent is per_order. */
14
+ taxRounding?: TaxRounding;
13
15
  // Settlement figures, sent in `order.create` (ADR-038, ADR-062). Show `display` instead (ADR-063).
14
16
  subtotalMinor: number; // settlement: excl. tax, after line and order discounts (both pre-tax, ADR-062)
15
17
  discountMinor: number; // settlement: Σ lineItems[].discountMinor, each in its line's own mode, so it mixes modes; not for display
16
- taxMinor: number; // settlement: rounded once per order
18
+ taxMinor: number; // settlement: rounded once per order, unless taxRounding says otherwise (#287)
17
19
  totalMinor: number; // settlement: what the customer pays
18
20
  display: DisplayTotals;
19
21
  paidMinor: number;
@@ -23,6 +25,9 @@ export interface Order {
23
25
  updatedAt: string;
24
26
  }
25
27
 
28
+ /** An Order as it was stored and sent, whose customer id may have been left out. */
29
+ export type SentOrder = Omit<Order, 'customer'> & { customer: (Omit<CustomerSummary, 'id'> & { id?: string }) | null };
30
+
26
31
  /**
27
32
  * Figures for showing the cart or a receipt in the store's display mode (ADR-063); never sent to the server.
28
33
  * A parked order's saved draft may carry them; they are recomputed from the lines on resume, so never authoritative.
@@ -114,5 +119,6 @@ export interface AddLineInput {
114
119
  imageUrl?: string;
115
120
  unitPrice: Money & { taxInclusive?: boolean }; // currency must equal the order currency; taxInclusive wins over the order's pricesIncludeTax
116
121
  quantity?: number; // default 1
117
- taxRates?: Array<{ code?: string; ratePpm: number }>; // default: [{ ratePpm: from taxContext }]
122
+ taxRates?: Array<{ code?: string; ratePpm: number }>; // default: [{ ratePpm: taxContext's rate for taxClass }]
123
+ taxClass?: string; // the product's tax class (ProductTraits.getTaxClass); ignored when taxRates is given
118
124
  }
@@ -0,0 +1,27 @@
1
+ import { BehaviorSubject, type Observable } from 'rxjs';
2
+ import type { OutboxState, TransportOutcome } from './types';
3
+
4
+ // Consecutive 404 answers before backendMissing tells the cashier; fewer may be a deploy blip.
5
+ const NOT_FOUND_BEFORE_NOTICE = 3;
6
+
7
+ /** Counts consecutive 404s across every outbox given it, so a missing plugin shows once, whichever outbox meets it. */
8
+ export interface BackendNotFound {
9
+ /** A 404 counts; any other answer from the store clears; offline (`network`) changes nothing. */
10
+ record(outcome: TransportOutcome, at: number): void;
11
+ backendMissing$: Observable<OutboxState['backendMissing']>;
12
+ }
13
+
14
+ export function createBackendNotFound(): BackendNotFound {
15
+ let count = 0;
16
+ let since = 0; // when the first of the consecutive 404s arrived
17
+ const backendMissing$ = new BehaviorSubject<OutboxState['backendMissing']>(undefined);
18
+ return {
19
+ backendMissing$,
20
+ record(outcome, at) {
21
+ if (outcome.kind === 'retry' && outcome.reason === 'network') return;
22
+ if (outcome.kind === 'retry' && outcome.reason === 'status_404') { if (count++ === 0) since = at; } else count = 0;
23
+ const missing = count >= NOT_FOUND_BEFORE_NOTICE;
24
+ if (missing !== !!backendMissing$.value) backendMissing$.next(missing ? { since } : undefined);
25
+ },
26
+ };
27
+ }
@@ -1,4 +1,4 @@
1
- import { COMMANDS_PATH, PROTOCOL_HEADER, PROTOCOL_VERSION, isCommandBatchResponse } from '@tallyui/core';
1
+ import { COMMANDS_PATH, PROTOCOL_HEADER, PROTOCOL_VERSION, isCommandBatchResponse, type AnyCommandEnvelope } from '@tallyui/core';
2
2
  import type { CommandTransport } from './types';
3
3
 
4
4
  // These HTTP statuses permanently refuse the entire command batch.
@@ -11,7 +11,7 @@ export interface HttpTransportOptions {
11
11
  timeoutMs?: number;
12
12
  }
13
13
 
14
- export function createHttpCommandTransport(options: HttpTransportOptions): CommandTransport {
14
+ export function createHttpCommandTransport(options: HttpTransportOptions): CommandTransport<AnyCommandEnvelope> {
15
15
  const fetch = options.fetch ?? globalThis.fetch;
16
16
  let end = options.baseUrl.length;
17
17
  while (end > 0 && options.baseUrl[end - 1] === '/') {
@@ -53,11 +53,12 @@ export function createHttpCommandTransport(options: HttpTransportOptions): Comma
53
53
  try {
54
54
  body = await response.json();
55
55
  } catch {
56
- return retry(controller.signal.aborted ? 'network' : 'bad_body');
56
+ return retry(controller.signal.aborted ? 'timeout' : 'bad_body');
57
57
  }
58
58
  return isCommandBatchResponse(body) ? { kind: 'results', results: body.results } : retry('bad_body');
59
59
  } catch {
60
- return { kind: 'retry', reason: 'network' };
60
+ // `timeout`: sent, but no answer within the deadline. `network`: the store could not be reached.
61
+ return { kind: 'retry', reason: controller.signal.aborted ? 'timeout' : 'network' };
61
62
  } finally {
62
63
  clearTimeout(timeout);
63
64
  }
@@ -2,6 +2,8 @@ export { createHttpCommandTransport } from './http-transport';
2
2
  export type { HttpTransportOptions } from './http-transport';
3
3
  export { createOrderOutbox } from './order-outbox';
4
4
  export type { OrderOutboxOptions, OrderOutbox } from './order-outbox';
5
+ export { createRegisterOutbox } from './register-outbox';
6
+ export type { RegisterOutboxOptions, RegisterOutbox } from './register-outbox';
5
7
  export type { TransportOutcome, CommandTransport, OutboxState } from './types';
6
8
  export { useOrderOutbox, outboxLogger } from './use-order-outbox';
7
9
  export type { UseOrderOutboxOptions, UseOrderOutboxResult } from './use-order-outbox';
@@ -0,0 +1,3 @@
1
+ import { createLogger } from '../logging';
2
+
3
+ export const outboxLogger = createLogger('outbox');