@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.
- package/dist/index.d.ts +323 -61
- package/dist/index.js +1423 -186
- package/package.json +8 -8
- package/src/index.ts +15 -7
- package/src/order/index.ts +3 -0
- package/src/order/order-builder.ts +23 -7
- package/src/order/order-manager.ts +3 -1
- package/src/order/tax-figures.ts +23 -0
- package/src/order/types.ts +9 -3
- package/src/outbox/backend-not-found.ts +27 -0
- package/src/outbox/http-transport.ts +5 -4
- package/src/outbox/index.ts +2 -0
- package/src/outbox/logger.ts +3 -0
- package/src/outbox/order-outbox.ts +336 -27
- package/src/outbox/register-outbox.ts +250 -0
- package/src/outbox/types.test-d.ts +22 -0
- package/src/outbox/types.ts +17 -3
- package/src/outbox/use-order-outbox.ts +20 -6
- package/src/pos-order/__fixtures__/order-create-v3.json +97 -0
- package/src/pos-order/command.ts +99 -11
- package/src/pos-order/finalize.ts +145 -7
- package/src/pos-order/index.ts +2 -2
- package/src/pos-order/needs-attention.ts +9 -4
- package/src/pos-order/open.ts +100 -37
- package/src/pos-order/schema.ts +35 -5
- package/src/pos-order/types.ts +31 -5
- package/src/product/search-products.ts +5 -2
- package/src/receipt/build-receipt-data.ts +5 -3
- package/src/receipt/types.ts +1 -0
- package/src/register/closure-document.ts +7 -0
- package/src/register/index.ts +5 -3
- package/src/register/movement-input.ts +1 -1
- package/src/register/register-commands.ts +147 -0
- package/src/register/register-document.ts +26 -1
- package/src/register/session-store.ts +41 -12
- package/src/register/use-register-session.ts +46 -2
- package/src/sale/cart.ts +1 -0
- package/src/sale/use-sale.ts +97 -40
- package/src/store-settings/map-store-settings.ts +7 -2
- package/src/store-settings/use-store-settings.ts +55 -9
- package/src/tax/exact.ts +74 -11
- package/src/tax/index.ts +1 -1
- package/src/tax/tax-provider.tsx +26 -4
- package/src/tax/types.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tallyui/pos",
|
|
3
|
-
"version": "
|
|
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": "
|
|
30
|
+
"@tallyui/core": "^3.0.0-next.1",
|
|
31
31
|
"react": ">=18",
|
|
32
|
-
"rxdb": "
|
|
32
|
+
"rxdb": "~17.5.0",
|
|
33
33
|
"rxjs": ">=7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@tallyui/core": "
|
|
36
|
+
"@tallyui/core": "3.0.0-next.1",
|
|
37
37
|
"@types/react": ">=18",
|
|
38
38
|
"react": "^19.2.4",
|
|
39
|
-
"rxdb": "
|
|
39
|
+
"rxdb": "17.5.0",
|
|
40
40
|
"rxjs": "7.8.2",
|
|
41
|
-
"@tallyui/connector-medusa": "
|
|
42
|
-
"@tallyui/connector-woocommerce": "
|
|
43
|
-
"@tallyui/database": "
|
|
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 {
|
|
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';
|
package/src/order/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/order/types.ts
CHANGED
|
@@ -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:
|
|
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 ? '
|
|
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
|
-
|
|
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
|
}
|
package/src/outbox/index.ts
CHANGED
|
@@ -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';
|