@tallyui/pos 0.1.0 → 3.0.0-next.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/LICENSE +21 -0
- package/dist/index.d.ts +2031 -43
- package/dist/index.js +4629 -205
- package/package.json +23 -12
- package/src/currency/currency-provider.tsx +12 -4
- package/src/currency/index.ts +1 -2
- package/src/index.ts +53 -4
- package/src/order/allocate-order-discount.ts +34 -0
- package/src/order/index.ts +8 -0
- package/src/order/order-builder.ts +256 -95
- package/src/order/order-manager.ts +26 -43
- package/src/order/tax-figures.ts +23 -0
- package/src/order/types.ts +73 -15
- package/src/outbox/backend-not-found.ts +27 -0
- package/src/outbox/http-transport.ts +67 -0
- package/src/outbox/index.ts +9 -0
- package/src/outbox/logger.ts +3 -0
- package/src/outbox/order-outbox.ts +502 -0
- package/src/outbox/register-outbox.ts +250 -0
- package/src/outbox/types.test-d.ts +22 -0
- package/src/outbox/types.ts +36 -0
- package/src/outbox/use-order-outbox.ts +175 -0
- package/src/pos-order/__fixtures__/order-create-v3.json +97 -0
- package/src/pos-order/command.ts +123 -0
- package/src/pos-order/device-id.ts +23 -0
- package/src/pos-order/finalize.ts +219 -0
- package/src/pos-order/index.ts +10 -0
- package/src/pos-order/needs-attention.ts +16 -0
- package/src/pos-order/open.ts +269 -0
- package/src/pos-order/same-sale.ts +26 -0
- package/src/pos-order/schema.ts +130 -0
- package/src/pos-order/types.ts +93 -0
- package/src/pos-order/uuidv7.ts +54 -0
- package/src/product/index.ts +2 -0
- package/src/product/search-products.ts +32 -0
- package/src/product/stock.ts +25 -0
- package/src/receipt/build-receipt-data.ts +39 -23
- package/src/receipt/types.ts +17 -10
- package/src/register/__fixtures__/closure-local-row.json +61 -0
- package/src/register/__fixtures__/closure.json +197 -0
- package/src/register/__fixtures__/corrections-dst.json +63 -0
- package/src/register/closure-document.ts +277 -0
- package/src/register/closure-rows.ts +65 -0
- package/src/register/document-labels.ts +46 -0
- package/src/register/expected.ts +66 -0
- package/src/register/export-csv.ts +74 -0
- package/src/register/facts.ts +109 -0
- package/src/register/index.ts +32 -0
- package/src/register/money.ts +18 -0
- package/src/register/movement-input.ts +59 -0
- package/src/register/register-commands.ts +147 -0
- package/src/register/register-count.denominations.ts +11 -0
- package/src/register/register-count.helpers.ts +64 -0
- package/src/register/register-document.ts +268 -0
- package/src/register/schemas.ts +175 -0
- package/src/register/session-store.ts +570 -0
- package/src/register/settled-figures.ts +98 -0
- package/src/register/use-register-session.ts +469 -0
- package/src/rxdb/index.ts +2 -0
- package/src/sale/cart.ts +23 -0
- package/src/sale/catalogue.ts +21 -0
- package/src/sale/index.ts +5 -0
- package/src/sale/use-sale.ts +394 -0
- package/src/store-settings/index.ts +5 -0
- package/src/store-settings/map-store-settings.ts +18 -0
- package/src/store-settings/resolve-store-settings.ts +65 -0
- package/src/store-settings/use-store-settings.ts +84 -0
- package/src/tax/exact.ts +218 -0
- package/src/tax/index.ts +4 -3
- package/src/tax/tax-provider.tsx +35 -12
- package/src/tax/types.ts +8 -6
- package/src/tender/index.ts +2 -0
- package/src/tender/tender-state.ts +326 -0
- package/src/currency/currency-provider.test.tsx +0 -36
- package/src/currency/format-currency.test.ts +0 -36
- package/src/currency/format-currency.ts +0 -18
- package/src/logging/logger.test.ts +0 -154
- package/src/logging/sinks.test.ts +0 -60
- package/src/order/discount-engine.test.ts +0 -152
- package/src/order/order-builder.test.ts +0 -246
- package/src/order/order-manager.test.ts +0 -209
- package/src/order/payment.test.ts +0 -91
- package/src/receipt/build-receipt-data.test.ts +0 -166
- package/src/repository/create-repository.test.ts +0 -166
- package/src/tax/calculate.test.ts +0 -59
- package/src/tax/calculate.ts +0 -32
- package/src/tax/tax-provider.test.tsx +0 -51
package/src/order/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Money, TaxRounding } from '@tallyui/core';
|
|
2
|
+
|
|
1
3
|
export interface Order {
|
|
2
4
|
id: string;
|
|
3
5
|
status: 'draft' | 'parked' | 'saved' | 'completed';
|
|
@@ -6,17 +8,47 @@ export interface Order {
|
|
|
6
8
|
payments: Payment[];
|
|
7
9
|
customer: CustomerSummary | null;
|
|
8
10
|
note: string;
|
|
9
|
-
subtotal: number;
|
|
10
|
-
discountTotal: number;
|
|
11
|
-
taxTotal: number;
|
|
12
|
-
total: number;
|
|
13
|
-
balanceDue: number;
|
|
14
|
-
changeDue: number;
|
|
15
11
|
currency: string;
|
|
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;
|
|
15
|
+
// Settlement figures, sent in `order.create` (ADR-038, ADR-062). Show `display` instead (ADR-063).
|
|
16
|
+
subtotalMinor: number; // settlement: excl. tax, after line and order discounts (both pre-tax, ADR-062)
|
|
17
|
+
discountMinor: number; // settlement: Σ lineItems[].discountMinor, each in its line's own mode, so it mixes modes; not for display
|
|
18
|
+
taxMinor: number; // settlement: rounded once per order, unless taxRounding says otherwise (#287)
|
|
19
|
+
totalMinor: number; // settlement: what the customer pays
|
|
20
|
+
display: DisplayTotals;
|
|
21
|
+
paidMinor: number;
|
|
22
|
+
balanceDueMinor: number; // max(0, total − paid)
|
|
23
|
+
changeDueMinor: number; // max(0, paid − total)
|
|
16
24
|
createdAt: string;
|
|
17
25
|
updatedAt: string;
|
|
18
26
|
}
|
|
19
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
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Figures for showing the cart or a receipt in the store's display mode (ADR-063); never sent to the server.
|
|
33
|
+
* A parked order's saved draft may carry them; they are recomputed from the lines on resume, so never authoritative.
|
|
34
|
+
*/
|
|
35
|
+
export interface DisplayTotals {
|
|
36
|
+
taxInclusive: boolean; // = order.pricesIncludeTax
|
|
37
|
+
subtotalMinor: number; // before discounts: total − tax + discount when exclusive, total + discount when inclusive
|
|
38
|
+
discountMinor: number; // Σ lines' discount rows + orderDiscountMinor, in the display mode; >= 0
|
|
39
|
+
taxMinor: number; // = order.taxMinor (added when exclusive, included when inclusive)
|
|
40
|
+
totalMinor: number; // = order.totalMinor
|
|
41
|
+
lines: DisplayLine[]; // in lineItems order; Σ amountMinor = subtotalMinor
|
|
42
|
+
orderDiscountMinor: number; // the order discounts as one row, not allocated: Σ each line's share, converted on its own
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One line in the display mode (ADR-063): before any discount, with its own discounts as sub-rows. */
|
|
46
|
+
export interface DisplayLine {
|
|
47
|
+
lineId: string;
|
|
48
|
+
amountMinor: number; // quantity × unit price in the display mode; the last converted line also carries the rounding residue
|
|
49
|
+
discounts: { discountId: string; label?: string; amountMinor: number }[]; // this line's own discounts, each converted on its own
|
|
50
|
+
}
|
|
51
|
+
|
|
20
52
|
export interface LineItem {
|
|
21
53
|
id: string;
|
|
22
54
|
productId: string;
|
|
@@ -24,31 +56,45 @@ export interface LineItem {
|
|
|
24
56
|
name: string;
|
|
25
57
|
sku: string;
|
|
26
58
|
imageUrl?: string;
|
|
27
|
-
|
|
28
|
-
quantity: number;
|
|
29
|
-
|
|
30
|
-
taxAmount: number;
|
|
59
|
+
unitPriceMinor: number; // as sold, integer
|
|
60
|
+
quantity: number; // integer >= 1
|
|
61
|
+
taxLines: LineTaxLine[]; // stacked rates on the same net base (ADR-040)
|
|
31
62
|
discounts: AppliedDiscount[];
|
|
32
|
-
|
|
33
|
-
|
|
63
|
+
discountMinor: number; // line discounts + orderDiscountMinor, in the line's own mode
|
|
64
|
+
orderDiscountMinor: number; // this line's allocated share of the order discounts (ADR-062)
|
|
65
|
+
netMinor: number; // unitPriceMinor × quantity − discountMinor; the taxed base
|
|
66
|
+
taxMicros: string; // Σ taxLines[].taxMicros, decimal string of a bigint
|
|
67
|
+
taxInclusive: boolean; // the price's own tax mode; the order's when the price has none
|
|
68
|
+
/** Set only when the price's tax mode differs from the order's; named price mode → order mode. */
|
|
69
|
+
priceTaxModeConverted?: 'inclusive-to-exclusive' | 'exclusive-to-inclusive';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LineTaxLine {
|
|
73
|
+
code?: string;
|
|
74
|
+
ratePpm: number;
|
|
75
|
+
taxMicros: string; // exact, decimal string of a bigint
|
|
34
76
|
}
|
|
35
77
|
|
|
36
78
|
export interface Discount {
|
|
37
79
|
type: 'percentage' | 'fixed';
|
|
38
|
-
value: number;
|
|
80
|
+
value: number; // percentage: percent (e.g. 10); fixed: integer minor units
|
|
39
81
|
label?: string;
|
|
40
82
|
couponCode?: string;
|
|
41
83
|
}
|
|
42
84
|
|
|
43
85
|
export interface AppliedDiscount extends Discount {
|
|
44
86
|
id: string;
|
|
45
|
-
|
|
87
|
+
amountMinor: number; // an order discount: its own-mode amount, allocated across the lines
|
|
46
88
|
}
|
|
47
89
|
|
|
48
90
|
export interface Payment {
|
|
49
91
|
id: string;
|
|
50
92
|
method: string;
|
|
51
|
-
amount:
|
|
93
|
+
// For cash, record the full amount tendered here, not the capped amount: `finalizeOrder`
|
|
94
|
+
// caps it at the balance due and writes out `tenderedMinor`/`changeMinor` itself.
|
|
95
|
+
amountMinor: number;
|
|
96
|
+
tenderedMinor?: number;
|
|
97
|
+
changeMinor?: number;
|
|
52
98
|
reference?: string;
|
|
53
99
|
}
|
|
54
100
|
|
|
@@ -64,3 +110,15 @@ export interface PaymentMethod {
|
|
|
64
110
|
icon?: string;
|
|
65
111
|
requiresReference?: boolean;
|
|
66
112
|
}
|
|
113
|
+
|
|
114
|
+
export interface AddLineInput {
|
|
115
|
+
productId: string;
|
|
116
|
+
variantId?: string;
|
|
117
|
+
name: string;
|
|
118
|
+
sku?: string;
|
|
119
|
+
imageUrl?: string;
|
|
120
|
+
unitPrice: Money & { taxInclusive?: boolean }; // currency must equal the order currency; taxInclusive wins over the order's pricesIncludeTax
|
|
121
|
+
quantity?: number; // default 1
|
|
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
|
|
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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { COMMANDS_PATH, PROTOCOL_HEADER, PROTOCOL_VERSION, isCommandBatchResponse, type AnyCommandEnvelope } from '@tallyui/core';
|
|
2
|
+
import type { CommandTransport } from './types';
|
|
3
|
+
|
|
4
|
+
// These HTTP statuses permanently refuse the entire command batch.
|
|
5
|
+
const PERMANENT_STATUSES = new Set([400, 403, 413, 415, 422]);
|
|
6
|
+
|
|
7
|
+
export interface HttpTransportOptions {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
getHeaders: () => Record<string, string> | Promise<Record<string, string>>;
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createHttpCommandTransport(options: HttpTransportOptions): CommandTransport<AnyCommandEnvelope> {
|
|
15
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
16
|
+
let end = options.baseUrl.length;
|
|
17
|
+
while (end > 0 && options.baseUrl[end - 1] === '/') {
|
|
18
|
+
end--;
|
|
19
|
+
}
|
|
20
|
+
const baseUrl = options.baseUrl.slice(0, end);
|
|
21
|
+
return {
|
|
22
|
+
async send(batch) {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 30000);
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`${baseUrl}${COMMANDS_PATH}`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
31
|
+
...await options.getHeaders(),
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ commands: batch }),
|
|
34
|
+
signal: controller.signal,
|
|
35
|
+
});
|
|
36
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
37
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter);
|
|
38
|
+
const retry = (reason: string) => ({ kind: 'retry' as const, reason,
|
|
39
|
+
...(Number.isFinite(seconds) && seconds >= 0 ? { retryAfterMs: seconds * 1000 } : {}),
|
|
40
|
+
});
|
|
41
|
+
if (response.status === 401) return { kind: 'unauthorized' };
|
|
42
|
+
if (PERMANENT_STATUSES.has(response.status)) {
|
|
43
|
+
let reason = `status_${response.status}`;
|
|
44
|
+
try {
|
|
45
|
+
const body = await response.json();
|
|
46
|
+
if (typeof body?.message === 'string') reason = body.message;
|
|
47
|
+
else if (typeof body?.code === 'string') reason = body.code;
|
|
48
|
+
} catch {}
|
|
49
|
+
return { kind: 'refused', status: response.status, reason };
|
|
50
|
+
}
|
|
51
|
+
if (response.status !== 200) return retry(`status_${response.status}`);
|
|
52
|
+
let body: unknown;
|
|
53
|
+
try {
|
|
54
|
+
body = await response.json();
|
|
55
|
+
} catch {
|
|
56
|
+
return retry(controller.signal.aborted ? 'timeout' : 'bad_body');
|
|
57
|
+
}
|
|
58
|
+
return isCommandBatchResponse(body) ? { kind: 'results', results: body.results } : retry('bad_body');
|
|
59
|
+
} catch {
|
|
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' };
|
|
62
|
+
} finally {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createHttpCommandTransport } from './http-transport';
|
|
2
|
+
export type { HttpTransportOptions } from './http-transport';
|
|
3
|
+
export { createOrderOutbox } from './order-outbox';
|
|
4
|
+
export type { OrderOutboxOptions, OrderOutbox } from './order-outbox';
|
|
5
|
+
export { createRegisterOutbox } from './register-outbox';
|
|
6
|
+
export type { RegisterOutboxOptions, RegisterOutbox } from './register-outbox';
|
|
7
|
+
export type { TransportOutcome, CommandTransport, OutboxState } from './types';
|
|
8
|
+
export { useOrderOutbox, outboxLogger } from './use-order-outbox';
|
|
9
|
+
export type { UseOrderOutboxOptions, UseOrderOutboxResult } from './use-order-outbox';
|