@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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +2031 -43
  3. package/dist/index.js +4629 -205
  4. package/package.json +23 -12
  5. package/src/currency/currency-provider.tsx +12 -4
  6. package/src/currency/index.ts +1 -2
  7. package/src/index.ts +53 -4
  8. package/src/order/allocate-order-discount.ts +34 -0
  9. package/src/order/index.ts +8 -0
  10. package/src/order/order-builder.ts +256 -95
  11. package/src/order/order-manager.ts +26 -43
  12. package/src/order/tax-figures.ts +23 -0
  13. package/src/order/types.ts +73 -15
  14. package/src/outbox/backend-not-found.ts +27 -0
  15. package/src/outbox/http-transport.ts +67 -0
  16. package/src/outbox/index.ts +9 -0
  17. package/src/outbox/logger.ts +3 -0
  18. package/src/outbox/order-outbox.ts +502 -0
  19. package/src/outbox/register-outbox.ts +250 -0
  20. package/src/outbox/types.test-d.ts +22 -0
  21. package/src/outbox/types.ts +36 -0
  22. package/src/outbox/use-order-outbox.ts +175 -0
  23. package/src/pos-order/__fixtures__/order-create-v3.json +97 -0
  24. package/src/pos-order/command.ts +123 -0
  25. package/src/pos-order/device-id.ts +23 -0
  26. package/src/pos-order/finalize.ts +219 -0
  27. package/src/pos-order/index.ts +10 -0
  28. package/src/pos-order/needs-attention.ts +16 -0
  29. package/src/pos-order/open.ts +269 -0
  30. package/src/pos-order/same-sale.ts +26 -0
  31. package/src/pos-order/schema.ts +130 -0
  32. package/src/pos-order/types.ts +93 -0
  33. package/src/pos-order/uuidv7.ts +54 -0
  34. package/src/product/index.ts +2 -0
  35. package/src/product/search-products.ts +32 -0
  36. package/src/product/stock.ts +25 -0
  37. package/src/receipt/build-receipt-data.ts +39 -23
  38. package/src/receipt/types.ts +17 -10
  39. package/src/register/__fixtures__/closure-local-row.json +61 -0
  40. package/src/register/__fixtures__/closure.json +197 -0
  41. package/src/register/__fixtures__/corrections-dst.json +63 -0
  42. package/src/register/closure-document.ts +277 -0
  43. package/src/register/closure-rows.ts +65 -0
  44. package/src/register/document-labels.ts +46 -0
  45. package/src/register/expected.ts +66 -0
  46. package/src/register/export-csv.ts +74 -0
  47. package/src/register/facts.ts +109 -0
  48. package/src/register/index.ts +32 -0
  49. package/src/register/money.ts +18 -0
  50. package/src/register/movement-input.ts +59 -0
  51. package/src/register/register-commands.ts +147 -0
  52. package/src/register/register-count.denominations.ts +11 -0
  53. package/src/register/register-count.helpers.ts +64 -0
  54. package/src/register/register-document.ts +268 -0
  55. package/src/register/schemas.ts +175 -0
  56. package/src/register/session-store.ts +570 -0
  57. package/src/register/settled-figures.ts +98 -0
  58. package/src/register/use-register-session.ts +469 -0
  59. package/src/rxdb/index.ts +2 -0
  60. package/src/sale/cart.ts +23 -0
  61. package/src/sale/catalogue.ts +21 -0
  62. package/src/sale/index.ts +5 -0
  63. package/src/sale/use-sale.ts +394 -0
  64. package/src/store-settings/index.ts +5 -0
  65. package/src/store-settings/map-store-settings.ts +18 -0
  66. package/src/store-settings/resolve-store-settings.ts +65 -0
  67. package/src/store-settings/use-store-settings.ts +84 -0
  68. package/src/tax/exact.ts +218 -0
  69. package/src/tax/index.ts +4 -3
  70. package/src/tax/tax-provider.tsx +35 -12
  71. package/src/tax/types.ts +8 -6
  72. package/src/tender/index.ts +2 -0
  73. package/src/tender/tender-state.ts +326 -0
  74. package/src/currency/currency-provider.test.tsx +0 -36
  75. package/src/currency/format-currency.test.ts +0 -36
  76. package/src/currency/format-currency.ts +0 -18
  77. package/src/logging/logger.test.ts +0 -154
  78. package/src/logging/sinks.test.ts +0 -60
  79. package/src/order/discount-engine.test.ts +0 -152
  80. package/src/order/order-builder.test.ts +0 -246
  81. package/src/order/order-manager.test.ts +0 -209
  82. package/src/order/payment.test.ts +0 -91
  83. package/src/receipt/build-receipt-data.test.ts +0 -166
  84. package/src/repository/create-repository.test.ts +0 -166
  85. package/src/tax/calculate.test.ts +0 -59
  86. package/src/tax/calculate.ts +0 -32
  87. package/src/tax/tax-provider.test.tsx +0 -51
@@ -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
- price: number;
28
- quantity: number;
29
- taxRate: number;
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
- discountAmount: number;
33
- lineTotal: number;
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
- amount: number;
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: number;
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';
@@ -0,0 +1,3 @@
1
+ import { createLogger } from '../logging';
2
+
3
+ export const outboxLogger = createLogger('outbox');