@tallyui/pos 0.1.0 → 2.0.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +1770 -42
  3. package/dist/index.js +3422 -204
  4. package/package.json +21 -10
  5. package/src/currency/currency-provider.tsx +12 -4
  6. package/src/currency/index.ts +1 -2
  7. package/src/index.ts +45 -4
  8. package/src/order/allocate-order-discount.ts +34 -0
  9. package/src/order/index.ts +5 -0
  10. package/src/order/order-builder.ts +240 -95
  11. package/src/order/order-manager.ts +23 -42
  12. package/src/order/types.ts +67 -15
  13. package/src/outbox/http-transport.ts +66 -0
  14. package/src/outbox/index.ts +7 -0
  15. package/src/outbox/order-outbox.ts +198 -0
  16. package/src/outbox/types.ts +22 -0
  17. package/src/outbox/use-order-outbox.ts +161 -0
  18. package/src/pos-order/command.ts +35 -0
  19. package/src/pos-order/device-id.ts +23 -0
  20. package/src/pos-order/finalize.ts +81 -0
  21. package/src/pos-order/index.ts +10 -0
  22. package/src/pos-order/needs-attention.ts +11 -0
  23. package/src/pos-order/open.ts +206 -0
  24. package/src/pos-order/same-sale.ts +26 -0
  25. package/src/pos-order/schema.ts +100 -0
  26. package/src/pos-order/types.ts +67 -0
  27. package/src/pos-order/uuidv7.ts +54 -0
  28. package/src/product/index.ts +2 -0
  29. package/src/product/search-products.ts +29 -0
  30. package/src/product/stock.ts +25 -0
  31. package/src/receipt/build-receipt-data.ts +36 -22
  32. package/src/receipt/types.ts +16 -10
  33. package/src/register/__fixtures__/closure-local-row.json +61 -0
  34. package/src/register/__fixtures__/closure.json +197 -0
  35. package/src/register/__fixtures__/corrections-dst.json +63 -0
  36. package/src/register/closure-document.ts +270 -0
  37. package/src/register/closure-rows.ts +65 -0
  38. package/src/register/document-labels.ts +46 -0
  39. package/src/register/expected.ts +66 -0
  40. package/src/register/export-csv.ts +74 -0
  41. package/src/register/facts.ts +109 -0
  42. package/src/register/index.ts +30 -0
  43. package/src/register/money.ts +18 -0
  44. package/src/register/movement-input.ts +59 -0
  45. package/src/register/register-count.denominations.ts +11 -0
  46. package/src/register/register-count.helpers.ts +64 -0
  47. package/src/register/register-document.ts +243 -0
  48. package/src/register/schemas.ts +175 -0
  49. package/src/register/session-store.ts +541 -0
  50. package/src/register/settled-figures.ts +98 -0
  51. package/src/register/use-register-session.ts +425 -0
  52. package/src/rxdb/index.ts +2 -0
  53. package/src/sale/cart.ts +22 -0
  54. package/src/sale/catalogue.ts +21 -0
  55. package/src/sale/index.ts +5 -0
  56. package/src/sale/use-sale.ts +337 -0
  57. package/src/store-settings/index.ts +5 -0
  58. package/src/store-settings/map-store-settings.ts +13 -0
  59. package/src/store-settings/resolve-store-settings.ts +65 -0
  60. package/src/store-settings/use-store-settings.ts +69 -0
  61. package/src/tax/exact.ts +155 -0
  62. package/src/tax/index.ts +3 -2
  63. package/src/tax/tax-provider.tsx +13 -12
  64. package/src/tax/types.ts +3 -7
  65. package/src/tender/index.ts +2 -0
  66. package/src/tender/tender-state.ts +326 -0
  67. package/src/currency/currency-provider.test.tsx +0 -36
  68. package/src/currency/format-currency.test.ts +0 -36
  69. package/src/currency/format-currency.ts +0 -18
  70. package/src/logging/logger.test.ts +0 -154
  71. package/src/logging/sinks.test.ts +0 -60
  72. package/src/order/discount-engine.test.ts +0 -152
  73. package/src/order/order-builder.test.ts +0 -246
  74. package/src/order/order-manager.test.ts +0 -209
  75. package/src/order/payment.test.ts +0 -91
  76. package/src/receipt/build-receipt-data.test.ts +0 -166
  77. package/src/repository/create-repository.test.ts +0 -166
  78. package/src/tax/calculate.test.ts +0 -59
  79. package/src/tax/calculate.ts +0 -32
  80. package/src/tax/tax-provider.test.tsx +0 -51
package/dist/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { MangoQuery, RxCollection } from 'rxdb';
3
+ import { Money, TallyConnector, SyncContext, StoreSettingsChoice, StoreSettings, StoreSettingsChoices, ProductTraits, PaymentMethodKind, CommandServerRefs, CommandWarning, CommandError, ServerCapabilities, CommandEnvelope, OrderCreatePayload, VariantSummary, CommandResult } from '@tallyui/core';
4
+ export { getProductStock, withStockOverlay } from '@tallyui/core';
5
+ export { countFresh, readFresh, watchFresh } from '@tallyui/core/rxdb';
6
+ import * as rxdb from 'rxdb';
7
+ import { MangoQuery, RxCollection, RxJsonSchema, MigrationStrategies, RxDatabase, RxLocalDocumentMutation } from 'rxdb';
8
+ import * as rxjs from 'rxjs';
4
9
  import { Observable } from 'rxjs';
5
- import { ProductTraits } from '@tallyui/core';
6
10
 
7
11
  type LogLevel = 'debug' | 'info' | 'warn' | 'error';
8
12
  interface LogEntry {
@@ -44,42 +48,157 @@ interface CallbackSinkOptions {
44
48
  }
45
49
  declare function callbackSink(options: CallbackSinkOptions): LogSink;
46
50
 
47
- declare function formatCurrency(amount: number, currencyCode: string, locale?: string): string;
48
-
49
51
  interface CurrencyProviderProps {
50
52
  currencyCode: string;
51
53
  locale?: string;
52
54
  children: ReactNode;
53
55
  }
54
56
  declare function CurrencyProvider({ currencyCode, locale, children }: CurrencyProviderProps): react_jsx_runtime.JSX.Element;
55
- declare function useCurrencyFormatter(): (amount: number) => string;
57
+ declare function useCurrencyFormatter(): (money: Money) => string;
58
+ declare function useCurrencyCode(): string;
56
59
 
57
- interface TaxResult {
58
- priceExclTax: number;
59
- priceInclTax: number;
60
- taxAmount: number;
60
+ /** Micro-minor-units per minor unit. */
61
+ declare const MICROS_PER_MINOR = 1000000n;
62
+ /** Converts a plain decimal percentage with at most four fractional digits to safe integer ppm. */
63
+ declare function ratePpmFromPercent(percent: number | string): number;
64
+ /**
65
+ * Exact exclusive tax, or inclusive tax rounded half away from zero to a micro-minor-unit.
66
+ * Throws RangeError unless amount and rate are safe integers and rate is non-negative.
67
+ */
68
+ declare function taxMicros(amountMinor: number, ratePpm: number, pricesIncludeTax: boolean): bigint;
69
+ /** Rounds micro-minor-units to integer minor units, half away from zero. */
70
+ declare function roundMicrosToMinor(micros: bigint): number;
71
+ interface TaxLineInput {
72
+ unitPriceMinor: number;
73
+ quantity: number;
74
+ ratePpm: number;
75
+ }
76
+ interface OrderTaxTotals {
77
+ subtotalMinor: number;
78
+ taxMinor: number;
79
+ totalMinor: number;
80
+ lineTaxMicros: bigint[];
61
81
  }
82
+ /**
83
+ * Sums tax on each unit price × quantity and rounds once for the order.
84
+ * Exclusive: total = subtotal + tax. Inclusive: subtotal = total − tax.
85
+ * Empty orders return zeros and an empty lineTaxMicros.
86
+ * Throws RangeError for unsafe integer inputs or totals, negative rates, or quantity < 1.
87
+ */
88
+ declare function computeOrderTax(lines: TaxLineInput[], pricesIncludeTax: boolean): OrderTaxTotals;
89
+ interface RateTaxLine {
90
+ label: string;
91
+ code?: string;
92
+ ratePpm: number;
93
+ netMinor: number;
94
+ amountMinor: number;
95
+ }
96
+ /**
97
+ * Groups each line's stacked tax rates (ADR-040: each independently taxes the line's full
98
+ * tax-free base) by `code`+`ratePpm`, floors each group's exact tax to minor units, then
99
+ * distributes `orderTaxMinor` minus that floor sum by largest remainder (ties to the higher rate)
100
+ * so the rates sum to exactly `orderTaxMinor`. A line's `netMinor` is in its OWN tax mode
101
+ * (`taxInclusive`): an inclusive line's net already contains its tax, so its tax-free base is
102
+ * `netMinor − roundMicrosToMinor(Σ its taxMicros)`; an exclusive line's base is `netMinor` as is.
103
+ * Shared by a receipt's tax summary and a Z report's per-rate breakdown.
104
+ */
105
+ declare function taxLinesByRate(lines: readonly {
106
+ netMinor: number;
107
+ taxInclusive: boolean;
108
+ taxLines: readonly {
109
+ code?: string;
110
+ ratePpm: number;
111
+ taxMicros: string;
112
+ }[];
113
+ }[], orderTaxMinor: number, taxLabels?: Record<number, string>): RateTaxLine[];
114
+
115
+ /** Tax rates as integer parts per million (19% = 190000). */
62
116
  type TaxRateMap = {
63
117
  default: number;
64
118
  [taxClass: string]: number;
65
119
  };
66
120
  interface TaxContext {
67
- getTaxRate(taxClass?: string): number;
121
+ /** Tax rate for a tax class as integer parts per million (19% = 190000); the default class when omitted or unknown. */
122
+ getTaxRatePpm(taxClass?: string): number;
68
123
  pricesIncludeTax: boolean;
69
124
  }
70
125
 
71
- declare function addTax(priceExclTax: number, rate: number): number;
72
- declare function extractTax(priceInclTax: number, rate: number): number;
73
- declare function calculateTax(price: number, rate: number, pricesIncludeTax: boolean): TaxResult;
74
-
75
126
  interface TaxProviderProps {
76
- rates: Record<string, number>;
127
+ ratesPpm: Record<string, number>;
77
128
  pricesIncludeTax: boolean;
78
129
  children: ReactNode;
79
130
  }
80
- declare function TaxProvider({ rates, pricesIncludeTax, children }: TaxProviderProps): react_jsx_runtime.JSX.Element;
131
+ declare function TaxProvider({ ratesPpm, pricesIncludeTax, children }: TaxProviderProps): react_jsx_runtime.JSX.Element;
81
132
  declare function useTax(): TaxContext;
82
133
 
134
+ type StoreSettingsResolution = {
135
+ status: 'ready';
136
+ settings: StoreSettings;
137
+ choice?: StoreSettingsChoice;
138
+ } | {
139
+ status: 'choose';
140
+ choices: StoreSettingsChoices;
141
+ initial?: StoreSettingsChoice;
142
+ } | {
143
+ status: 'unsupported';
144
+ };
145
+ interface ResolveStoreSettingsOptions {
146
+ connector: TallyConnector;
147
+ context: SyncContext;
148
+ /** The app's stored choice for this store (its own settings; TallyUI never stores it). */
149
+ loadChoice: () => StoreSettingsChoice | undefined | Promise<StoreSettingsChoice | undefined>;
150
+ saveChoice: (choice: StoreSettingsChoice) => void | Promise<void>;
151
+ /** A save that throws after a successful resolve: the pick is asked again next launch. */
152
+ onSaveError?: (error: unknown) => void;
153
+ }
154
+ /**
155
+ * Reads the store settings with the app's choice (TV4). A `choice` argument (a new pick from
156
+ * the choice screen) wins over the stored one and is saved only once it resolves; a stored
157
+ * choice is never re-saved. `choice_required` (including a stale stored choice) gives
158
+ * `choose`, with the tried choice narrowed to what's offered as `initial`, so the screen
159
+ * pre-selects what still matches. Any other error, `failed` included, is rethrown unchanged.
160
+ */
161
+ declare function resolveStoreSettings(options: ResolveStoreSettingsOptions, choice?: StoreSettingsChoice): Promise<StoreSettingsResolution>;
162
+
163
+ /** The replication's context with the settings' opaque pricing context; the key is left out when the settings have none. */
164
+ declare function withPricingContext(context: SyncContext, settings: StoreSettings): SyncContext;
165
+ /** `<TaxProvider>`'s props from the same settings, so its tax inclusivity matches the connector's. */
166
+ declare function taxProviderProps(settings: StoreSettings): Omit<TaxProviderProps, 'children'>;
167
+
168
+ type StoreSettingsState = {
169
+ state: 'loading';
170
+ } | {
171
+ state: 'ready';
172
+ settings: StoreSettings;
173
+ choice?: StoreSettingsChoice;
174
+ } | {
175
+ state: 'choose';
176
+ choices: StoreSettingsChoices;
177
+ initial?: StoreSettingsChoice;
178
+ choose: (choice: StoreSettingsChoice) => void;
179
+ } | {
180
+ state: 'error';
181
+ error: unknown;
182
+ retry: () => void;
183
+ } | {
184
+ state: 'unsupported';
185
+ };
186
+ /**
187
+ * Resolves the store settings on mount, and again when `connector` or `context` changes by
188
+ * identity (a store switch), so memoise both; the last request wins. `loadChoice` and
189
+ * `saveChoice` are read when a request starts, so inline functions don't trigger a re-resolve.
190
+ *
191
+ * ```tsx
192
+ * const store = useStoreSettings({ connector, context, loadChoice, saveChoice });
193
+ * if (store.state === 'choose')
194
+ * return <StoreSettingsChoiceScreen choices={store.choices} initial={store.initial} onSubmit={store.choose} />;
195
+ * if (store.state !== 'ready') return <Loading />; // or an error with store.retry, or the app's own config when unsupported
196
+ * const syncContext = withPricingContext(context, store.settings); // for the replication
197
+ * return <TaxProvider {...taxProviderProps(store.settings)}>{children}</TaxProvider>;
198
+ * ```
199
+ */
200
+ declare function useStoreSettings(options: ResolveStoreSettingsOptions): StoreSettingsState;
201
+
83
202
  interface Repository<T> {
84
203
  findById$(id: string): Observable<T | null>;
85
204
  findAll$(query?: MangoQuery<T>): Observable<T[]>;
@@ -104,16 +223,42 @@ interface Order {
104
223
  payments: Payment[];
105
224
  customer: CustomerSummary | null;
106
225
  note: string;
107
- subtotal: number;
108
- discountTotal: number;
109
- taxTotal: number;
110
- total: number;
111
- balanceDue: number;
112
- changeDue: number;
113
226
  currency: string;
227
+ pricesIncludeTax: boolean;
228
+ subtotalMinor: number;
229
+ discountMinor: number;
230
+ taxMinor: number;
231
+ totalMinor: number;
232
+ display: DisplayTotals;
233
+ paidMinor: number;
234
+ balanceDueMinor: number;
235
+ changeDueMinor: number;
114
236
  createdAt: string;
115
237
  updatedAt: string;
116
238
  }
239
+ /**
240
+ * Figures for showing the cart or a receipt in the store's display mode (ADR-063); never sent to the server.
241
+ * A parked order's saved draft may carry them; they are recomputed from the lines on resume, so never authoritative.
242
+ */
243
+ interface DisplayTotals {
244
+ taxInclusive: boolean;
245
+ subtotalMinor: number;
246
+ discountMinor: number;
247
+ taxMinor: number;
248
+ totalMinor: number;
249
+ lines: DisplayLine[];
250
+ orderDiscountMinor: number;
251
+ }
252
+ /** One line in the display mode (ADR-063): before any discount, with its own discounts as sub-rows. */
253
+ interface DisplayLine {
254
+ lineId: string;
255
+ amountMinor: number;
256
+ discounts: {
257
+ discountId: string;
258
+ label?: string;
259
+ amountMinor: number;
260
+ }[];
261
+ }
117
262
  interface LineItem {
118
263
  id: string;
119
264
  productId: string;
@@ -121,13 +266,22 @@ interface LineItem {
121
266
  name: string;
122
267
  sku: string;
123
268
  imageUrl?: string;
124
- price: number;
269
+ unitPriceMinor: number;
125
270
  quantity: number;
126
- taxRate: number;
127
- taxAmount: number;
271
+ taxLines: LineTaxLine[];
128
272
  discounts: AppliedDiscount[];
129
- discountAmount: number;
130
- lineTotal: number;
273
+ discountMinor: number;
274
+ orderDiscountMinor: number;
275
+ netMinor: number;
276
+ taxMicros: string;
277
+ taxInclusive: boolean;
278
+ /** Set only when the price's tax mode differs from the order's; named price mode → order mode. */
279
+ priceTaxModeConverted?: 'inclusive-to-exclusive' | 'exclusive-to-inclusive';
280
+ }
281
+ interface LineTaxLine {
282
+ code?: string;
283
+ ratePpm: number;
284
+ taxMicros: string;
131
285
  }
132
286
  interface Discount {
133
287
  type: 'percentage' | 'fixed';
@@ -137,12 +291,14 @@ interface Discount {
137
291
  }
138
292
  interface AppliedDiscount extends Discount {
139
293
  id: string;
140
- amount: number;
294
+ amountMinor: number;
141
295
  }
142
296
  interface Payment {
143
297
  id: string;
144
298
  method: string;
145
- amount: number;
299
+ amountMinor: number;
300
+ tenderedMinor?: number;
301
+ changeMinor?: number;
146
302
  reference?: string;
147
303
  }
148
304
  interface CustomerSummary {
@@ -156,6 +312,21 @@ interface PaymentMethod {
156
312
  icon?: string;
157
313
  requiresReference?: boolean;
158
314
  }
315
+ interface AddLineInput {
316
+ productId: string;
317
+ variantId?: string;
318
+ name: string;
319
+ sku?: string;
320
+ imageUrl?: string;
321
+ unitPrice: Money & {
322
+ taxInclusive?: boolean;
323
+ };
324
+ quantity?: number;
325
+ taxRates?: Array<{
326
+ code?: string;
327
+ ratePpm: number;
328
+ }>;
329
+ }
159
330
 
160
331
  interface OrderBuilderOptions {
161
332
  currency: string;
@@ -168,6 +339,7 @@ interface OrderBuilder {
168
339
  variantId?: string;
169
340
  quantity?: number;
170
341
  }): string;
342
+ addLine(input: AddLineInput): string;
171
343
  updateQuantity(lineId: string, quantity: number): void;
172
344
  removeItem(lineId: string): void;
173
345
  applyLineDiscount(lineId: string, discount: Discount): void;
@@ -182,11 +354,23 @@ interface OrderBuilder {
182
354
  }
183
355
  declare function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder;
184
356
 
357
+ /**
358
+ * Splits an order discount across lines in proportion to each line's pre-order-discount amount,
359
+ * in its own tax mode (ADR-062), with largest-remainder rounding to the minor unit.
360
+ *
361
+ * - The shares sum exactly to `totalMinor`, and no share exceeds its line's amount.
362
+ * - Leftover units go to the largest remainders; ties go to the earlier line, so the result is deterministic.
363
+ * - A line with a zero or negative amount gets 0.
364
+ *
365
+ * Throws RangeError unless every input is a safe integer and 0 ≤ totalMinor ≤ Σ positive amounts.
366
+ */
367
+ declare function allocateOrderDiscount(lineAmountsMinor: readonly number[], totalMinor: number): number[];
368
+
185
369
  interface ParkedOrderSummary {
186
370
  id: string;
187
371
  customerName?: string;
188
372
  itemCount: number;
189
- total: number;
373
+ totalMinor: number;
190
374
  parkedAt: string;
191
375
  source: 'local' | 'server';
192
376
  }
@@ -209,8 +393,15 @@ interface ReceiptLineItem {
209
393
  name: string;
210
394
  sku: string;
211
395
  quantity: number;
212
- unitPrice: number;
213
- lineTotal: number;
396
+ unitPriceMinor: number;
397
+ /** After every discount, including the line's order share, in the order's mode. Not the figure to print above the subtotal: use displayAmountMinor. */
398
+ lineTotalMinor: number;
399
+ discountMinor?: number;
400
+ displayAmountMinor: number;
401
+ displayDiscounts: {
402
+ label: string;
403
+ amountMinor: number;
404
+ }[];
214
405
  }
215
406
  interface ReceiptData {
216
407
  header: {
@@ -224,25 +415,28 @@ interface ReceiptData {
224
415
  lineItems: ReceiptLineItem[];
225
416
  discounts: {
226
417
  label: string;
227
- amount: number;
418
+ amountMinor: number;
228
419
  }[];
420
+ orderDiscountMinor: number;
229
421
  totals: {
230
- subtotal: number;
231
- discountTotal: number;
422
+ taxInclusive: boolean;
423
+ subtotalMinor: number;
424
+ discountMinor: number;
232
425
  taxLines: {
233
426
  label: string;
234
- rate: number;
235
- amount: number;
427
+ code?: string;
428
+ ratePpm: number;
429
+ amountMinor: number;
236
430
  }[];
237
- taxTotal: number;
238
- total: number;
431
+ taxMinor: number;
432
+ totalMinor: number;
239
433
  };
240
434
  payments: {
241
435
  method: string;
242
- amount: number;
436
+ amountMinor: number;
243
437
  reference?: string;
244
438
  }[];
245
- changeDue: number;
439
+ changeDueMinor: number;
246
440
  footer: {
247
441
  note?: string;
248
442
  barcode?: string;
@@ -259,4 +453,1538 @@ interface ReceiptConfig {
259
453
 
260
454
  declare function buildReceiptData(order: Order, config: ReceiptConfig): ReceiptData;
261
455
 
262
- export { type AppliedDiscount, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, type Discount, type LineItem, type LogEntry, type LogLevel, type LogSink, type Logger, type Order, type OrderBuilder, type OrderBuilderOptions, type OrderManager, type OrderManagerOptions, type ParkedOrderSummary, type Payment, type PaymentMethod, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type Repository, type TaxContext, TaxProvider, type TaxProviderProps, type TaxRateMap, type TaxResult, addTax, buildReceiptData, calculateTax, callbackSink, consoleSink, createLogger, createOrderBuilder, createOrderManager, createRepository, extractTax, formatCurrency, useCurrencyFormatter, useTax };
456
+ /**
457
+ * Filters product documents by a cashier's search term, through traits, so
458
+ * it works the same on every backend.
459
+ *
460
+ * Every word of the term must appear in the product name, SKU or barcode
461
+ * (case-insensitive, any order). A term that exactly equals a barcode or SKU
462
+ * returns just those products, so a scanner hit is never buried among name
463
+ * matches. An empty term returns the input unchanged.
464
+ */
465
+ declare function searchProducts<Doc>(docs: Doc[], term: string, traits: ProductTraits<Doc>): Doc[];
466
+
467
+ /** The `stock_levels` collection as a live map of key to stock value. */
468
+ declare function stockOverlay$(collection: RxCollection): Observable<Map<string, unknown>>;
469
+ /**
470
+ * When the overlay was last confirmed by a successful pass (ISO 8601), or
471
+ * undefined before any pass. Read from the collection's local document, so a
472
+ * tab or screen that does not hold the runner can show it too.
473
+ */
474
+ declare function stockOverlayAsOf$(collection: RxCollection): Observable<string | undefined>;
475
+
476
+ type PosOrderSyncStatus = 'pending' | 'applied' | 'rejected';
477
+ interface PosOrderLine {
478
+ id: string;
479
+ productId: string;
480
+ variantId?: string;
481
+ name: string;
482
+ sku: string;
483
+ quantity: number;
484
+ unitPriceMinor: number;
485
+ /** Line discounts plus the allocated order-discount share, in the line's own tax mode (ADR-062). */
486
+ discountMinor: number;
487
+ netMinor: number;
488
+ taxLines: Array<{
489
+ code?: string;
490
+ ratePpm: number;
491
+ taxMicros: string;
492
+ }>;
493
+ /** This line's own tax mode; set only when it was converted from the store's (ADR-038 amendment). */
494
+ taxInclusive?: boolean;
495
+ }
496
+ interface PosOrderPayment {
497
+ id: string;
498
+ method: PaymentMethodKind;
499
+ amountMinor: number;
500
+ tenderedMinor?: number;
501
+ changeMinor?: number;
502
+ reference?: string;
503
+ }
504
+ interface PosOrder {
505
+ id: string;
506
+ createdAt: string;
507
+ currency: string;
508
+ pricesIncludeTax: boolean;
509
+ lines: PosOrderLine[];
510
+ subtotalMinor: number;
511
+ discountMinor: number;
512
+ taxMinor: number;
513
+ totalMinor: number;
514
+ payments: PosOrderPayment[];
515
+ customer: {
516
+ id?: string;
517
+ name?: string;
518
+ email?: string;
519
+ } | null;
520
+ note?: string;
521
+ registerId?: string;
522
+ /**
523
+ * The register session the sale was taken in (ADR-032): its closure counts this order.
524
+ * Local only: `toOrderCreateEnvelope` never sends it.
525
+ */
526
+ sessionId?: string;
527
+ /**
528
+ * Set only by `useSale`'s late-sale path (ADR-032): the session the sale was taken for, which
529
+ * refused the stamp after the money was taken. Such an order has no `sessionId`, so no closure
530
+ * counts it. Local only: `toOrderCreateEnvelope` never sends it.
531
+ */
532
+ lateSessionId?: string;
533
+ /** ADR-065: the receipt's display figures, in integer minor units of `currency` at `exponent`. Not written yet. */
534
+ display?: DisplayTotals & {
535
+ currency: string;
536
+ exponent: number;
537
+ };
538
+ /** ADR-065: tax by rate, named as `taxLinesByRate` names them (`amountMinor` is the tax). Not written yet. */
539
+ taxByRate?: Array<{
540
+ ratePpm: number;
541
+ code?: string;
542
+ label?: string;
543
+ netMinor: number;
544
+ amountMinor: number;
545
+ grossMinor: number;
546
+ }>;
547
+ cashierRef?: string;
548
+ syncStatus: PosOrderSyncStatus;
549
+ commandId: string;
550
+ serverRefs?: CommandServerRefs;
551
+ warnings?: CommandWarning[];
552
+ error?: CommandError;
553
+ updatedAt: string;
554
+ }
555
+
556
+ /** RFC 9562 UUIDv7: monotonic with no arguments; stateless with an explicit timestamp or random source. */
557
+ declare function uuidv7(now?: number, randomBytes?: (n: number) => Uint8Array): string;
558
+
559
+ interface FinalizeOptions {
560
+ registerId?: string;
561
+ cashierRef?: string;
562
+ now?: Date;
563
+ newId?: () => string;
564
+ /** The store's `order.create` capability (ADR-062); `undefined` is treated as 1. */
565
+ capabilities?: ServerCapabilities;
566
+ }
567
+ /** Turns a fully paid builder Order into a pending PosOrder without mutating it. */
568
+ declare function finalizeOrder(order: Order, options?: FinalizeOptions): PosOrder;
569
+
570
+ /**
571
+ * Builds the ADR-038 order.create envelope for a PosOrder.
572
+ * A discounted order is version 2 (ADR-062); a discount-free one stays version 1, byte-identical.
573
+ */
574
+ declare function toOrderCreateEnvelope(order: PosOrder, deviceId: string, attempt?: number): CommandEnvelope<OrderCreatePayload>;
575
+
576
+ /**
577
+ * Version 1 adds the optional `sessionId` (ADR-032); nothing else changed from version 0.
578
+ * Version 2 adds three optional fields and changes nothing else: `lateSessionId` (ADR-032, late
579
+ * sale), and ADR-065's `display` and `taxByRate`, declared ahead of the job that writes them so a
580
+ * till migrates once. Create the collection with `posOrderCollection()`, never with this schema
581
+ * alone: RxDB refuses a version above 0 without its migration strategies.
582
+ */
583
+ declare const posOrderSchema: RxJsonSchema<PosOrder>;
584
+ /**
585
+ * The `pos_orders` collection config, with its migration strategies. Open the collection with
586
+ * `addPosOrderCollection(db)`, which uses this and settles the migration safely; adding this config
587
+ * directly leaves RxDB's own open path. `pos_orders` holds sales not yet sent, so no step may drop a
588
+ * document: versions 1 and 2 only add optional fields, so every order from version 0 or 1 passes
589
+ * unchanged.
590
+ *
591
+ * Within one run, RxDB 16.21 keeps an order that fails the new schema's validation: with a
592
+ * validating storage the migration stops with DM4 and the order stays in the older version's
593
+ * storage; without one it is copied as is. **Across runs, RxDB's own open path can lose it**: a
594
+ * failed run can leave its checkpoint past that order, and the next run then removes the older
595
+ * version's storage without copying it. `addPosOrderCollection` resets that checkpoint, so use it
596
+ * (`migration.test.ts`).
597
+ */
598
+ declare function posOrderCollection(): {
599
+ schema: RxJsonSchema<PosOrder>;
600
+ migrationStrategies: MigrationStrategies;
601
+ };
602
+
603
+ /** `addPosOrderCollection`'s logger: attach a sink to see, for example, the status writes a closing open dropped. */
604
+ declare const posOrdersLogger: Logger;
605
+ /**
606
+ * `addPosOrderCollection` stopped, before any further write, because its database is closing: it
607
+ * was called once `close()` had begun, or the close stopped waiting for it (after
608
+ * `POS_ORDER_MIGRATION_CLOSE_WAIT_MS`). No order is lost: reopen the database and call it again.
609
+ */
610
+ declare class PosOrderOpenClosedError extends Error {
611
+ readonly databaseName: string;
612
+ readonly code = "POS_ORDER_OPEN_CLOSED";
613
+ constructor(databaseName: string);
614
+ }
615
+ /**
616
+ * Adds `pos_orders` to `db` and resolves once no order of an older version is left to migrate. The one
617
+ * sanctioned way to open it (ADR-032 amendment 2). On DM4 it rejects after the migration has
618
+ * stopped and closes the collection, so the app can surface the error and open again; it never
619
+ * deletes an order.
620
+ *
621
+ * RxDB 16.21 trusts the status its last migration stored: a leftover `ERROR` rejects
622
+ * `migratePromise` at once while the migration keeps running (a close then interrupts it), and a
623
+ * `DONE` left before a rollback resolves it before the older version's new orders have moved. So the
624
+ * collection is added without `autoMigrate`, the status and a failed run's checkpoint are reset
625
+ * (never an order or its storage), and the migration itself is awaited.
626
+ *
627
+ * A close waits for the whole open, up to `POS_ORDER_MIGRATION_CLOSE_WAIT_MS`. Called once the
628
+ * database's close has begun, or when the close stops waiting, it rejects with
629
+ * `PosOrderOpenClosedError` (`code: 'POS_ORDER_OPEN_CLOSED'`) before any further write: reopen and
630
+ * call it again. Any other rejection (DM4, a storage error) is a real failure.
631
+ *
632
+ * `closeWaitMs` is for tests only; the first open on a database sets it for that database's close.
633
+ */
634
+ declare function addPosOrderCollection(db: RxDatabase, closeWaitMs?: number): Promise<RxCollection<PosOrder>>;
635
+
636
+ /**
637
+ * Returns this device's id (a UUIDv7), stored under `key` in `storage` (web storage, or an app's
638
+ * equivalent) and minted on first use; medusapos passes `'medusapos.register_id'`. With no storage,
639
+ * or one that throws on read or write, it falls back to one id per process.
640
+ */
641
+ declare function getDeviceId(storage: {
642
+ getItem(key: string): string | null;
643
+ setItem(key: string, value: string): void;
644
+ } | null, key: string): string;
645
+
646
+ /**
647
+ * The orders a cashier must look at: rejected, applied with warnings, or taken after their session
648
+ * closed (`lateSessionId`, whatever the sync status), newest first. Never changes `orders`.
649
+ */
650
+ declare function needsAttention(orders: PosOrder[]): PosOrder[];
651
+
652
+ type SaleContent = Pick<PosOrder, 'totalMinor' | 'currency' | 'lines' | 'payments'>;
653
+ /**
654
+ * Whether two orders carry the same money-bearing content: `totalMinor` and `currency`; each line's
655
+ * `id`, `quantity` and `netMinor`, in order; each payment's `method` and `amountMinor`, in order.
656
+ * An order `id` is minted once per sale (`finalizeOrder`), so the same `id` with other content is a defect.
657
+ */
658
+ declare function sameSale(stored: SaleContent, retried: SaleContent): boolean;
659
+ /** A stored order has this order's `id` but other money-bearing content (see `sameSale`): never treated as stored. */
660
+ declare class OrderContentMismatchError extends Error {
661
+ readonly orderId: string;
662
+ constructor(orderId: string);
663
+ }
664
+
665
+ type MovementType = 'paid_in' | 'paid_out' | 'no_sale';
666
+ declare function normalizeAmount(raw: string): string;
667
+ /** Whether the server's `decimal()` would accept this string. */
668
+ declare function isServerDecimal(value: string): boolean;
669
+ /**
670
+ * The field the server would name in its 400, or null when it would accept the movement.
671
+ * A no sale carries no amount — the sheet sends '0' — but still needs a reason.
672
+ */
673
+ declare function movementFieldError(input: {
674
+ type: MovementType;
675
+ amount: string;
676
+ reason: string;
677
+ }): 'amount' | 'reason' | null;
678
+
679
+ /**
680
+ * What the register should hold at close, per tender method: the session's cash float plus
681
+ * captured payments and cash movements — never what the cashier counts. Port provenance
682
+ * (ADR-032 amendment 1): WCPOS `next` `3b5331b5c`.
683
+ *
684
+ * **Refund attribution is deferred.** WCPOS's `attributeRefunds` debits a session's drawer for
685
+ * refunds processed against its captured payments (by stamped session, then by a legacy
686
+ * `refunded_amount` fallback). TallyUI has no refund model yet (ADR-032 amendment 1), so
687
+ * `deriveExpected` covers only the float, the session's captured payment rows, and its
688
+ * paid-in/paid-out cash movements under WCPOS's void rules.
689
+ */
690
+ /** `session_id`, `kind`, `method_id`, `status` kept as WCPOS names them; money is TallyUI's integer-minor-units convention. */
691
+ type LedgerRow = {
692
+ session_id?: string | null;
693
+ kind: string;
694
+ method_id: string;
695
+ status: string;
696
+ amountMinor: number;
697
+ };
698
+ /** `type`, `voids` and `voided_by` are kept as WCPOS names them: neutral across backends. */
699
+ type Movement$1 = {
700
+ id: string;
701
+ session_id: string;
702
+ type: string;
703
+ amountMinor: number;
704
+ voided_by?: string | null;
705
+ voids?: string | null;
706
+ };
707
+ /**
708
+ * Expected totals per tender method: the counted float, plus every captured ledger row for the
709
+ * session (grouped under `cash` for cash rows, else `method_id`), plus its non-voided
710
+ * paid-in/paid-out movements.
711
+ *
712
+ * A movement is excluded when its own `voided_by` is set, or when a `type: 'void'` row's
713
+ * `voids` names its id; the `void` row itself carries no amount of its own.
714
+ */
715
+ declare function deriveExpected({ session, movements, ledgerRowsBySession, }: {
716
+ session: {
717
+ id: string;
718
+ countedFloatMinor: number;
719
+ };
720
+ movements: readonly Movement$1[];
721
+ ledgerRowsBySession: readonly LedgerRow[];
722
+ }): Record<string, number>;
723
+
724
+ /**
725
+ * The pure maths behind a register count: variance against the expected float, a display
726
+ * string for it, and the denomination arithmetic the counting keypad uses. Port provenance
727
+ * (ADR-032 amendment 1): WCPOS `next` `3b5331b5c`.
728
+ *
729
+ * Money here is TallyUI's convention: integer minor units. Everywhere WCPOS assumed two
730
+ * decimals, these take the currency's `exponent` instead — 0 for JPY, 3 for KWD — except
731
+ * `validAmount`, which like WCPOS still takes the cashier's typed text.
732
+ */
733
+ /**
734
+ * Parses a decimal string the cashier typed into integer minor units at `exponent` decimal
735
+ * places, without rounding. Returns `NaN` when the string carries more precision than the
736
+ * currency allows, so a safe-integer check on the result also rejects it.
737
+ */
738
+ declare function parseMinor(text: string, exponent: number): number;
739
+ declare const validAmount: (value: string, exponent: number) => boolean;
740
+ /** Counted minus expected, both already in minor units. */
741
+ declare const countVariance: (countedMinor: number, expectedMinor: number) => number;
742
+ /** Whether the variance exceeds the store's configured threshold (minor units; none means never). */
743
+ declare const overThreshold: (variance: number, thresholdMinor?: number | null) => boolean;
744
+ /**
745
+ * Whether a close's cash count needs a manager's approval: its cash variance against the expected
746
+ * cash is over the threshold. The one definition `RegisterCount` and `useRegisterSession`'s
747
+ * `closeSession` gate share (ADR-032).
748
+ */
749
+ declare const closeNeedsApproval: (countedCashMinor: number, expectedCashMinor: number, thresholdMinor?: number | null) => boolean;
750
+ /** Sums denomination face values (already minor units) by piece count. */
751
+ declare const denominationTotal: (pieces: Record<number, number>) => number;
752
+ /**
753
+ * The variance line under the count: exact, short or over. `format` takes a major-unit
754
+ * amount, as an app's currency formatter expects, so `exponent` converts `variance` back for
755
+ * it; `t` supplies the translated words for the `register.exact`/`register.short`/
756
+ * `register.over` keys. Both are the app's own — neutral here.
757
+ *
758
+ * The direction lives in the word, not a sign (the Front desk, 2026-09-28): a signed amount
759
+ * next to "short" read as a double negative, so the amount here is always its absolute value.
760
+ */
761
+ declare function varianceText(variance: number, exponent: number, format: (value: number) => string, t: (key: string) => string): string;
762
+
763
+ declare const denominations: Record<string, number[]>;
764
+
765
+ /**
766
+ * The register's local collections (ADR-032): sessions, cash movements and closures. Port
767
+ * provenance (ADR-032 amendment 1): WCPOS `next` `3b5331b5c`, where they are at versions 1, 0
768
+ * and 1; each starts at version 0 here.
769
+ *
770
+ * All three are local only, like `pos_orders`: the app creates them and never replicates them
771
+ * (a replicated collection must never take local writes, #53). WCPOS's outbox fields
772
+ * (`sync_status`, attempts, next time, error) and closures' server and receipt fields are gone;
773
+ * sending sessions to a server is registers job c.
774
+ *
775
+ * Money is integer minor units: WCPOS's decimal strings become `*_minor` integers (`amountMinor`
776
+ * on a movement, matching `Movement`), and a tender map (`counted`, `expected`, `variance`, keyed
777
+ * by method) holds integers. People (`opened_by`, `created_by`, ...) are the backend's user id
778
+ * as a string, since not every backend's is a number.
779
+ */
780
+
781
+ type TenderMap = Record<string, number>;
782
+ interface RegisterSession {
783
+ id: string;
784
+ register_id: string;
785
+ /** The app's neutral store key (see `bindRegister`). */
786
+ store_key?: string | null;
787
+ status: 'open' | 'counting' | 'closed';
788
+ /** The store's day the session opened, `yyyy-MM-dd`. */
789
+ business_day?: string;
790
+ opened_at_gmt: string;
791
+ opened_by?: string | null;
792
+ expected_float_minor?: number | null;
793
+ counted_float_minor: number;
794
+ opening_variance_minor?: number | null;
795
+ counting_started_at_gmt?: string | null;
796
+ closed_at_gmt?: string | null;
797
+ closed_by?: string | null;
798
+ approval_required?: boolean;
799
+ approved_by?: string | null;
800
+ counted?: TenderMap | null;
801
+ closure_id?: string | null;
802
+ /** The last local transition, kept for a server to acknowledge (job c). */
803
+ pending_status?: string | null;
804
+ server_status?: string | null;
805
+ status_at?: string | null;
806
+ approver_token?: string | null;
807
+ /** A server's expected figures; cleared before every money action (`requireOpenSession`). */
808
+ server_expected?: TenderMap | null;
809
+ server_sales_count?: number | null;
810
+ }
811
+ interface CashMovement {
812
+ id: string;
813
+ session_id: string;
814
+ type: 'paid_in' | 'paid_out' | 'no_sale' | 'void';
815
+ amountMinor: number;
816
+ reason: string;
817
+ created_at_gmt: string;
818
+ created_by?: string | null;
819
+ /** On a `void` row: the movement it reverses. */
820
+ voids?: string | null;
821
+ /** On a reversed movement: its `void` row. */
822
+ voided_by?: string | null;
823
+ }
824
+ interface Closure {
825
+ id: string;
826
+ session_id: string;
827
+ register_id: string;
828
+ store_key?: string | null;
829
+ business_day?: string;
830
+ closed_by?: string | null;
831
+ corrections_count?: number;
832
+ number: number;
833
+ opened_at: string;
834
+ closed_at: string;
835
+ till_expected: TenderMap;
836
+ expected: TenderMap;
837
+ counted: TenderMap;
838
+ variance: TenderMap;
839
+ period_sales_total_minor: number;
840
+ /** Always 0 until TallyUI has a refund model (ADR-032 amendment 1). */
841
+ period_refunds_total_minor: number;
842
+ perpetual_sales_total_minor: number;
843
+ perpetual_refunds_total_minor: number;
844
+ /** This session's orders still waiting for the outbox, and their payments' total. */
845
+ unsynced_count: number;
846
+ unsynced_total_minor: number;
847
+ software_version: string;
848
+ breakdowns: Record<string, unknown>;
849
+ order_ids: string[];
850
+ movement_ids: string[];
851
+ server_closure_id?: string | null;
852
+ printed_at?: string | null;
853
+ print_count: number;
854
+ number_retried?: boolean;
855
+ }
856
+ declare const registerSessionSchema: RxJsonSchema<RegisterSession>;
857
+ /**
858
+ * Create `register_sessions` with this: the register document (`register-document.ts`) is a
859
+ * local document on this collection, since a Tally database has no database-level ones.
860
+ */
861
+ declare function registerSessionCollection(): {
862
+ readonly schema: RxJsonSchema<RegisterSession>;
863
+ readonly localDocuments: true;
864
+ };
865
+ declare const cashMovementSchema: RxJsonSchema<CashMovement>;
866
+ declare const closureSchema: RxJsonSchema<Closure>;
867
+
868
+ type RegisterHost = RxLocalDocumentMutation<any>;
869
+ type RegisterCounters = {
870
+ last_closure_number: number;
871
+ perpetual_sales_total_minor: number;
872
+ perpetual_refunds_total_minor: number;
873
+ counters_started_at?: string | null;
874
+ };
875
+ /**
876
+ * One register's counters, plus the closure being written: the draft is reserved here, in the
877
+ * same atomic write as its number, and `applied` once its period is in the perpetual totals.
878
+ */
879
+ type RegisterBucket = Partial<RegisterCounters> & {
880
+ closure_reservation?: {
881
+ row: Closure;
882
+ applied: boolean;
883
+ };
884
+ /** Closure ids the orphan-stamp sweep has already checked (ADR-032, the #158 follow-ups): a
885
+ * local-document field, so bounding the sweep needs no schema bump. */
886
+ swept_closure_ids?: string[];
887
+ };
888
+ interface RegisterStore {
889
+ sale_counter: number;
890
+ registers?: Record<string, RegisterBucket>;
891
+ /** The register (drawer) this till is bound to in this store. */
892
+ register_id?: string | null;
893
+ register_name?: string | null;
894
+ }
895
+ interface RegisterDocument {
896
+ id: string;
897
+ name: string;
898
+ /** The caller's platform, for example `'ios'` or `'web'`. */
899
+ platform: string;
900
+ created_at: string;
901
+ /** Keyed by `storeKey`. */
902
+ stores: Record<string, RegisterStore>;
903
+ }
904
+ declare function mintUuid(): string;
905
+ declare function readRegister(host: RegisterHost): Promise<RegisterDocument | null>;
906
+ /** Reads the register document, minting it on first use; concurrent callers get the same one. */
907
+ declare function ensureRegister(host: RegisterHost, platform: string): Promise<RegisterDocument>;
908
+ declare function observeRegister$(host: RegisterHost): rxjs.Observable<RegisterDocument | null>;
909
+ declare function getBoundRegisterId(register: RegisterDocument | null, storeKey: string): string | null;
910
+ declare function readBoundRegister(host: RegisterHost, storeKey: string): Promise<{
911
+ id: string;
912
+ name: string;
913
+ } | null>;
914
+ declare function bindRegister(host: RegisterHost, storeKey: string, register: {
915
+ id: string | null;
916
+ name: string | null;
917
+ }): Promise<void>;
918
+ declare function unbindRegister(host: RegisterHost, storeKey: string): Promise<void>;
919
+ declare function nextSaleCounter(host: RegisterHost, storeKey: string): Promise<number>;
920
+ /**
921
+ * Mints the register's next closure number. Given the closure draft, it reserves the draft with
922
+ * its number and perpetual totals in the same atomic write, and a retry of the same draft gets the
923
+ * same number back. A different draft is refused while an earlier one is reserved but not yet
924
+ * applied (`closure_write_incomplete`).
925
+ */
926
+ declare function mintClosureNumber(host: RegisterHost, storeKey: string, registerId: string, closure?: Closure): Promise<number>;
927
+ /**
928
+ * Adds a closed period to the register's perpetual totals. With `closureId`, it applies that
929
+ * reserved closure once: the totals become the reservation's, and a repeat does nothing.
930
+ */
931
+ declare function advancePerpetual(host: RegisterHost, storeKey: string, registerId: string, period: {
932
+ salesMinor: number;
933
+ refundsMinor: number;
934
+ closureId?: string;
935
+ }): Promise<void>;
936
+
937
+ type RegisterSessionCollection = RxCollection<RegisterSession>;
938
+ type CashMovementCollection = RxCollection<CashMovement>;
939
+ type ClosureCollection = RxCollection<Closure>;
940
+ declare class RegisterSessionRequiredError extends Error {
941
+ constructor();
942
+ }
943
+ /** The session is closed, and a closed session is final: there's no server yet to refuse writes to it. */
944
+ declare class RegisterSessionClosedError extends Error {
945
+ constructor();
946
+ }
947
+ /**
948
+ * The movement is recorded, on a session that closed while it was saved, and no closure is known
949
+ * to count it, so it was kept rather than deleted. The caller must not record it again; job c's
950
+ * server resolves stranded movements. The message can be shown to a cashier as it is.
951
+ */
952
+ declare class RegisterMovementStrandedError extends Error {
953
+ readonly id: string;
954
+ readonly session_id: string;
955
+ constructor(movement: {
956
+ id: string;
957
+ session_id: string;
958
+ });
959
+ }
960
+ declare const openSessionSelector: {
961
+ readonly status: "open";
962
+ };
963
+ /** The open session's id before a money action, or `RegisterSessionRequiredError`; `null` when sessions are off. */
964
+ declare function requireOpenSession(sessions: RegisterSessionCollection | undefined, registerId: string | null, enabled: boolean): Promise<string | null>;
965
+ declare function stampSession(order: PosOrder, sessionId: string, sessions: RegisterSessionCollection): Promise<PosOrder>;
966
+ declare function openSession(sessions: RegisterSessionCollection, input: {
967
+ registerId: string;
968
+ expectedFloatMinor: number | null;
969
+ countedFloatMinor: number;
970
+ openedBy: string;
971
+ /** The store's day, not the device's UTC day. */
972
+ businessDay: {
973
+ year: number;
974
+ month: number;
975
+ day: number;
976
+ };
977
+ storeKey?: string | null;
978
+ }): Promise<rxdb.RxDocument<RegisterSession, {}>>;
979
+ declare const startCounting: (sessions: RegisterSessionCollection, id: string) => Promise<rxdb.RxDocument<RegisterSession, {}>>;
980
+ declare const backToSelling: (sessions: RegisterSessionCollection, id: string) => Promise<rxdb.RxDocument<RegisterSession, {}>>;
981
+ /**
982
+ * Closes the session with its counted tenders (minor units); `timezone` dates a session opened
983
+ * without a business day. `approvedBy`, the manager who approved the count, is written in the same
984
+ * write as the close, and a repeat close keeps the first close's approver as it keeps its count.
985
+ */
986
+ declare function closeSession(sessions: RegisterSessionCollection, id: string, input: {
987
+ counted: Record<string, number>;
988
+ closedBy?: string;
989
+ approvedBy?: string;
990
+ timezone?: string;
991
+ }): Promise<rxdb.RxDocument<RegisterSession, {}>>;
992
+ /**
993
+ * Records a movement on a session that is `open` or `counting`; a closed or missing one is refused.
994
+ * A close that races the insert ends in `RegisterSessionClosedError` (removed) or
995
+ * `RegisterMovementStrandedError` (kept; don't record it again).
996
+ */
997
+ declare function recordMovement(sessions: RegisterSessionCollection, movements: CashMovementCollection, closures: ClosureCollection, input: {
998
+ sessionId: string;
999
+ type: 'paid_in' | 'paid_out' | 'no_sale';
1000
+ amountMinor: number;
1001
+ reason: string;
1002
+ actor: string;
1003
+ }): Promise<rxdb.RxDocument<CashMovement, {}>>;
1004
+ /**
1005
+ * Reverses a movement with a `void` row while its session is live; repeated or concurrent calls
1006
+ * share one reversal. A close that races the writes ends in `RegisterMovementStrandedError` (kept;
1007
+ * don't void it again). Without `closures`, nothing can prove the reversal counted, so a re-read
1008
+ * that finds the session closed always ends in `RegisterMovementStrandedError`.
1009
+ */
1010
+ declare function voidMovement(sessions: RegisterSessionCollection, movements: CashMovementCollection, movementId: string, actor: string, closures?: ClosureCollection): Promise<rxdb.RxDocument<CashMovement, {}>>;
1011
+ /**
1012
+ * Freezes a closed session's figures into its closure, once. The draft is reserved on the register
1013
+ * document with its number (`mintClosureNumber`), so a failed insert or a restarted till reuses the
1014
+ * same snapshot and number, and its period reaches the perpetual totals exactly once.
1015
+ * Only a closed session (`closed` with `closed_at_gmt`) has a closure, and a closed session is
1016
+ * final, so its existing closure is returned as it was frozen. Orders the server rejected still
1017
+ * count in the drawer, because the cash was taken.
1018
+ */
1019
+ declare function writeClosure({ closures, register, storeKey, session, counted, otherTenders, movements, orders, tillExpected, labels, resolveCashierName, softwareVersion, timezone, }: {
1020
+ closures: ClosureCollection;
1021
+ /** The register document's host (`register_sessions`). */
1022
+ register: RegisterHost;
1023
+ storeKey: string;
1024
+ session: RegisterSession;
1025
+ /** Counted cash, minor units. */
1026
+ counted: number;
1027
+ otherTenders: Record<string, number>;
1028
+ movements: readonly CashMovement[];
1029
+ /** Any `pos_orders`; only those whose `sessionId` is this session's are counted. */
1030
+ orders: readonly PosOrder[];
1031
+ tillExpected?: Record<string, number>;
1032
+ labels?: {
1033
+ register_name: string;
1034
+ closed_by_name: string;
1035
+ opened_by_name?: string;
1036
+ approved_by_name?: string;
1037
+ };
1038
+ resolveCashierName?: (id: string) => string;
1039
+ /** The app's version, stamped on the closure. */
1040
+ softwareVersion: string;
1041
+ timezone?: string;
1042
+ }): Promise<rxdb.RxDocument<Closure, {}>>;
1043
+
1044
+ /**
1045
+ * Corrections layered onto a closure's recorded figures (ADR-032): late sales, late cash
1046
+ * movements and recounts, applied in order and deduplicated by id. Port provenance (ADR-032
1047
+ * amendment 1): WCPOS `next` `3b5331b5c` `settled-figures.ts`. Money is TallyUI's integer
1048
+ * minor-units convention: WCPOS's four-decimal-string arithmetic becomes direct integer
1049
+ * arithmetic on a2's `*_minor` fields and tender maps.
1050
+ */
1051
+
1052
+ type Correction = {
1053
+ id: number;
1054
+ type: 'late_sale' | 'late_movement' | 'recount';
1055
+ created_at: string;
1056
+ actor: {
1057
+ id: string;
1058
+ name: string;
1059
+ };
1060
+ approver: {
1061
+ id: string;
1062
+ name: string;
1063
+ } | null;
1064
+ reason: string;
1065
+ figures: {
1066
+ expected_delta?: Record<string, number>;
1067
+ cash_delta?: number;
1068
+ sales_delta?: number;
1069
+ refunds_delta?: number;
1070
+ counted?: Record<string, number>;
1071
+ variance?: Record<string, number>;
1072
+ };
1073
+ };
1074
+ type RecordedFigures = Pick<Closure, 'expected' | 'counted' | 'variance' | 'period_sales_total_minor' | 'period_refunds_total_minor' | 'perpetual_sales_total_minor' | 'perpetual_refunds_total_minor'>;
1075
+ /**
1076
+ * Folds `corrections` onto `recorded`, ordered by `created_at` then `id`, deduplicated by `id`.
1077
+ * A `recount` replaces the supplied tenders in `counted`; a `late_movement` adds its signed
1078
+ * `cash_delta` to expected cash; a `late_sale` adds its `expected_delta` tenders to expected and
1079
+ * its `sales_delta`/`refunds_delta` to both the period and perpetual totals. Variance is
1080
+ * recomputed for every tender named by expected or counted, once any correction applies.
1081
+ * `touched` names every field, or `field.tender`, whose settled value differs from `recorded`.
1082
+ */
1083
+ declare function deriveSettled(recorded: RecordedFigures, corrections: readonly Correction[]): {
1084
+ settled: {
1085
+ expected: {
1086
+ [x: string]: number;
1087
+ };
1088
+ counted: {
1089
+ [x: string]: number;
1090
+ };
1091
+ variance: {
1092
+ [x: string]: number;
1093
+ };
1094
+ period_sales_total_minor: number;
1095
+ period_refunds_total_minor: number;
1096
+ perpetual_sales_total_minor: number;
1097
+ perpetual_refunds_total_minor: number;
1098
+ };
1099
+ touched: Set<string>;
1100
+ };
1101
+
1102
+ declare function exportCsv(rows: readonly Closure[], exponent: number, t: (key: string) => string, names?: Record<string, string>, storeName?: string): string;
1103
+
1104
+ declare const labelKeys: {
1105
+ x_report: string;
1106
+ closure: string;
1107
+ opened: string;
1108
+ closed: string;
1109
+ approver: string;
1110
+ opening_float: string;
1111
+ expected: string;
1112
+ counted: string;
1113
+ variance: string;
1114
+ tenders: string;
1115
+ tender: string;
1116
+ cash_movements: string;
1117
+ time: string;
1118
+ type: string;
1119
+ amount: string;
1120
+ reason: string;
1121
+ sales: string;
1122
+ period_sales: string;
1123
+ period_refunds: string;
1124
+ transactions: string;
1125
+ refunds: string;
1126
+ payment_method: string;
1127
+ cashiers: string;
1128
+ tax_rates: string;
1129
+ tax_rate: string;
1130
+ net: string;
1131
+ tax: string;
1132
+ gross: string;
1133
+ perpetual_totals: string;
1134
+ perpetual_sales: string;
1135
+ perpetual_refunds: string;
1136
+ unsynced_sales: string;
1137
+ short: string;
1138
+ over: string;
1139
+ exact: string;
1140
+ voided: string;
1141
+ paid_in: string;
1142
+ paid_out: string;
1143
+ no_sale: string;
1144
+ void: string;
1145
+ copy: string;
1146
+ };
1147
+
1148
+ type ClosureScope = {
1149
+ from: string;
1150
+ to: string;
1151
+ registerId: string;
1152
+ storeKey?: string;
1153
+ cashier?: string;
1154
+ };
1155
+ /** Clamps a requested scope's `from`/`to` to `today` and to `historyDays` before it. */
1156
+ declare function clampClosureScope(scope: ClosureScope, today: string, historyDays?: number): ClosureScope;
1157
+ /**
1158
+ * Rows within `scope`, newest business day first, then newest `closed_at`. A row with no
1159
+ * `business_day` (a legacy or remote row) gets one from `opened_at` in `timezone`, reusing a2's
1160
+ * `businessDayOf` rather than duplicating it. Register and cashier are optional filters; the
1161
+ * store is not — a row's `store_key` must equal `scope.storeKey` (both `null` when neither is
1162
+ * set), as WCPOS always compared `store_id`.
1163
+ */
1164
+ declare function selectClosureRows(rows: readonly Closure[], scope: ClosureScope, timezone: string): {
1165
+ business_day: string;
1166
+ id: string;
1167
+ session_id: string;
1168
+ register_id: string;
1169
+ store_key?: string | null;
1170
+ closed_by?: string | null;
1171
+ corrections_count?: number;
1172
+ number: number;
1173
+ opened_at: string;
1174
+ closed_at: string;
1175
+ till_expected: {
1176
+ [x: string]: number;
1177
+ };
1178
+ expected: {
1179
+ [x: string]: number;
1180
+ };
1181
+ counted: {
1182
+ [x: string]: number;
1183
+ };
1184
+ variance: {
1185
+ [x: string]: number;
1186
+ };
1187
+ period_sales_total_minor: number;
1188
+ period_refunds_total_minor: number;
1189
+ perpetual_sales_total_minor: number;
1190
+ perpetual_refunds_total_minor: number;
1191
+ unsynced_count: number;
1192
+ unsynced_total_minor: number;
1193
+ software_version: string;
1194
+ breakdowns: Record<string, unknown>;
1195
+ order_ids: string[];
1196
+ movement_ids: string[];
1197
+ server_closure_id?: string | null;
1198
+ printed_at?: string | null;
1199
+ print_count: number;
1200
+ number_retried?: boolean;
1201
+ }[];
1202
+
1203
+ /**
1204
+ * Minor-unit money formatting shared across the register package (moved out of `export-csv.ts` so
1205
+ * `closure-document.ts` reuses the same implementation instead of duplicating it).
1206
+ */
1207
+ /**
1208
+ * `minor` as a fixed-point decimal string with exactly `exponent` places: `-50` at exponent 2 is
1209
+ * `'-0.50'`, `1250` is `'12.50'`, and at exponent 0, `1500` is `'1500'`. Integer maths only —
1210
+ * sign, `Math.trunc`, and the remainder padded to `exponent` digits — no `toFixed` on floats.
1211
+ */
1212
+ declare function minorToDecimal(minor: number, exponent: number): string;
1213
+
1214
+ type Values = Record<string, unknown>;
1215
+ type ClosureContext = {
1216
+ store: Values;
1217
+ currency: string;
1218
+ timezone: string;
1219
+ locale: string;
1220
+ printedAt: string;
1221
+ /** Minor-unit decimal places for every money field the envelope carries. */
1222
+ exponent: number;
1223
+ formatMoney: (value: string) => string;
1224
+ i18n: Record<string, string>;
1225
+ /** Overrides an X-report's `server_expected` tender map, in minor units. */
1226
+ expected?: Record<string, number>;
1227
+ /**
1228
+ * `buildXReportDocument`'s own breakdowns merge only (unlike `buildClosureDocument`'s frozen
1229
+ * `Closure.breakdowns`): its money fields are still expected as decimal strings, not minor
1230
+ * units. Job c's X-report caller, once it exists, converts them before passing this in.
1231
+ */
1232
+ breakdowns?: Values;
1233
+ };
1234
+ declare function formatClosureDate(value: string | null | undefined, context: Pick<ClosureContext, 'timezone' | 'locale'>): Record<string, string>;
1235
+ declare function buildClosureDocument(row: Closure, context: ClosureContext): {
1236
+ closure: {
1237
+ has_sales: boolean;
1238
+ has_perpetual: boolean;
1239
+ } & {
1240
+ breakdowns: Values;
1241
+ store_id: string | null | undefined;
1242
+ till_expected: Record<string, string>;
1243
+ expected: Record<string, string>;
1244
+ counted: Record<string, string>;
1245
+ variance: Record<string, string>;
1246
+ period_sales_total: string;
1247
+ period_refunds_total: string;
1248
+ perpetual_sales_total: string;
1249
+ perpetual_refunds_total: string;
1250
+ unsynced_total: string;
1251
+ id: string;
1252
+ session_id: string;
1253
+ register_id: string;
1254
+ business_day?: string;
1255
+ closed_by?: string | null;
1256
+ corrections_count?: number;
1257
+ number: number;
1258
+ opened_at: string;
1259
+ closed_at: string;
1260
+ unsynced_count: number;
1261
+ software_version: string;
1262
+ order_ids: string[];
1263
+ movement_ids: string[];
1264
+ server_closure_id?: string | null;
1265
+ printed_at?: string | null;
1266
+ print_count: number;
1267
+ number_retried?: boolean;
1268
+ } & {
1269
+ opened_at_gmt: unknown;
1270
+ opened_by: {} | null;
1271
+ closed_by: {} | null;
1272
+ approved_by: {} | null;
1273
+ closed_at_gmt: unknown;
1274
+ opened_at: Record<string, string>;
1275
+ closed_at: Record<string, string>;
1276
+ tenders: ({
1277
+ name: string;
1278
+ label: {};
1279
+ expected: string;
1280
+ counted: string;
1281
+ variance: string;
1282
+ has_variance: boolean;
1283
+ variance_label: string;
1284
+ variance_absolute_display: string;
1285
+ } & Record<"counted_display" | "expected_display" | "variance_display", string>)[];
1286
+ breakdowns: {
1287
+ labels: Values;
1288
+ payment_methods: ({
1289
+ name: {};
1290
+ } & Record<`${string}_display`, string>)[];
1291
+ tax_rates: ({
1292
+ name: {};
1293
+ } & Record<`${string}_display`, string>)[];
1294
+ opening_float: Values & Record<"counted_display" | "expected_display" | "variance_display", string>;
1295
+ movements: {
1296
+ created_at: Record<string, string>;
1297
+ type_label: string;
1298
+ voided: boolean;
1299
+ id: string;
1300
+ reason: string;
1301
+ amount_display: string;
1302
+ }[];
1303
+ cashiers: Values[];
1304
+ };
1305
+ period_sales_total_display: string;
1306
+ period_refunds_total_display: string;
1307
+ perpetual_sales_total_display: string;
1308
+ perpetual_refunds_total_display: string;
1309
+ unsynced_total_display: string;
1310
+ };
1311
+ store: Values;
1312
+ register: {
1313
+ id: unknown;
1314
+ name: {};
1315
+ };
1316
+ software: {
1317
+ name: string;
1318
+ plugin_version: {};
1319
+ };
1320
+ order: {
1321
+ currency: string;
1322
+ printed: Record<string, string>;
1323
+ };
1324
+ fiscal: {
1325
+ immutable_id: string;
1326
+ hash: string;
1327
+ qr_payload: string;
1328
+ tax_agency_code: string;
1329
+ signature_excerpt: string;
1330
+ document_label: string;
1331
+ sequence: null;
1332
+ signed_at: null;
1333
+ extra_fields: never[];
1334
+ document_type: string;
1335
+ receipt_number: string;
1336
+ is_sale_document: boolean;
1337
+ is_refund_document: boolean;
1338
+ is_closure_document: boolean;
1339
+ is_x_report: boolean;
1340
+ is_reprint: boolean;
1341
+ reprint_count: number;
1342
+ };
1343
+ i18n: Record<string, string>;
1344
+ };
1345
+ declare function buildXReportDocument(session: RegisterSession, context: ClosureContext): {
1346
+ closure: {
1347
+ has_sales: boolean;
1348
+ has_perpetual: boolean;
1349
+ } & {
1350
+ id: string;
1351
+ register_id: string;
1352
+ store_id: string | null | undefined;
1353
+ opened_at: string;
1354
+ expected: Record<string, string>;
1355
+ counted: Record<string, string>;
1356
+ variance: Record<string, string>;
1357
+ breakdowns: {
1358
+ opening_float: {
1359
+ expected: string | undefined;
1360
+ counted: string | undefined;
1361
+ variance: string | undefined;
1362
+ };
1363
+ };
1364
+ } & {
1365
+ opened_at_gmt: unknown;
1366
+ opened_by: {} | null;
1367
+ closed_by: {} | null;
1368
+ approved_by: {} | null;
1369
+ closed_at_gmt: unknown;
1370
+ opened_at: Record<string, string>;
1371
+ closed_at: Record<string, string>;
1372
+ tenders: ({
1373
+ name: string;
1374
+ label: {};
1375
+ expected: string;
1376
+ counted: string;
1377
+ variance: string;
1378
+ has_variance: boolean;
1379
+ variance_label: string;
1380
+ variance_absolute_display: string;
1381
+ } & Record<"counted_display" | "expected_display" | "variance_display", string>)[];
1382
+ breakdowns: {
1383
+ labels: Values;
1384
+ payment_methods: ({
1385
+ name: {};
1386
+ } & Record<`${string}_display`, string>)[];
1387
+ tax_rates: ({
1388
+ name: {};
1389
+ } & Record<`${string}_display`, string>)[];
1390
+ opening_float: Values & Record<"counted_display" | "expected_display" | "variance_display", string>;
1391
+ movements: {
1392
+ created_at: Record<string, string>;
1393
+ type_label: string;
1394
+ voided: boolean;
1395
+ id: string;
1396
+ reason: string;
1397
+ amount_display: string;
1398
+ }[];
1399
+ cashiers: Values[];
1400
+ };
1401
+ period_sales_total_display: string;
1402
+ period_refunds_total_display: string;
1403
+ perpetual_sales_total_display: string;
1404
+ perpetual_refunds_total_display: string;
1405
+ unsynced_total_display: string;
1406
+ };
1407
+ store: Values;
1408
+ register: {
1409
+ id: unknown;
1410
+ name: {};
1411
+ };
1412
+ software: {
1413
+ name: string;
1414
+ plugin_version: {};
1415
+ };
1416
+ order: {
1417
+ currency: string;
1418
+ printed: Record<string, string>;
1419
+ };
1420
+ fiscal: {
1421
+ immutable_id: string;
1422
+ hash: string;
1423
+ qr_payload: string;
1424
+ tax_agency_code: string;
1425
+ signature_excerpt: string;
1426
+ document_label: string;
1427
+ sequence: null;
1428
+ signed_at: null;
1429
+ extra_fields: never[];
1430
+ document_type: string;
1431
+ receipt_number: string;
1432
+ is_sale_document: boolean;
1433
+ is_refund_document: boolean;
1434
+ is_closure_document: boolean;
1435
+ is_x_report: boolean;
1436
+ is_reprint: boolean;
1437
+ reprint_count: number;
1438
+ };
1439
+ i18n: Record<string, string>;
1440
+ };
1441
+
1442
+ /** The register-session logging scope; the app attaches its own sinks (console, remote, toast). */
1443
+ declare const registerFactsLogger: Logger;
1444
+ type Actor = {
1445
+ id: string;
1446
+ name: string;
1447
+ };
1448
+ type Pair = {
1449
+ sessionId: string | undefined;
1450
+ registerId: string | null | undefined;
1451
+ };
1452
+ type Human = Pair & {
1453
+ actor: Actor;
1454
+ };
1455
+ type Movement = Pair & {
1456
+ movementId: string;
1457
+ movementType: CashMovement['type'];
1458
+ amount: number;
1459
+ };
1460
+ type SimpleKind = 'counting-started' | 'counting-abandoned' | 'approval-refused' | 'x-report-dispatched' | 'drawer-dispatched';
1461
+ type RegisterFact = (Human & {
1462
+ kind: 'session-opened';
1463
+ amount: number;
1464
+ variance: RegisterSession['opening_variance_minor'];
1465
+ }) | (Human & {
1466
+ kind: SimpleKind;
1467
+ }) | (Human & {
1468
+ kind: 'session-closed';
1469
+ closureId: string;
1470
+ } & Pick<Closure, 'number' | 'counted' | 'variance'>) | (Human & Movement & {
1471
+ kind: 'movement-recorded';
1472
+ }) | (Human & Movement & {
1473
+ kind: 'movement-voided';
1474
+ voids: string;
1475
+ }) | (Human & {
1476
+ kind: 'approval-granted';
1477
+ approvedBy: string | null;
1478
+ }) | (Human & {
1479
+ kind: 'variance-over-threshold';
1480
+ variance: number;
1481
+ threshold: number | undefined;
1482
+ }) | (Pair & {
1483
+ kind: 'late-sale';
1484
+ orderId: string;
1485
+ actor?: Actor;
1486
+ });
1487
+ declare function recordRegisterFact(fact: RegisterFact): void;
1488
+
1489
+ /** A sale is at tender, so the till can't count or close under it. Its message can be shown to a cashier as it is. */
1490
+ declare class RegisterTenderInProgressError extends Error {
1491
+ constructor();
1492
+ }
1493
+ /** The register already has an `open` or `counting` session, or an open is in flight. Its message can be shown to a cashier as it is. */
1494
+ declare class RegisterSessionAlreadyOpenError extends Error {
1495
+ constructor();
1496
+ }
1497
+ /** An earlier session's close didn't finish (its closure is unwritten or unapplied); close it first. Its message can be shown to a cashier as it is. */
1498
+ declare class RegisterCloseIncompleteError extends Error {
1499
+ constructor();
1500
+ }
1501
+ /** The count is over `varianceThreshold` and the close carries no `approvedBy`. Its message is `RegisterCount`'s refusal copy, so it can be shown to a cashier as it is. */
1502
+ declare class RegisterApprovalRequiredError extends Error {
1503
+ constructor();
1504
+ }
1505
+ interface UseRegisterSessionOptions {
1506
+ /** `register_sessions`, created with `registerSessionCollection()`; `null` while it opens. */
1507
+ sessions: RegisterSessionCollection | null;
1508
+ /** `cash_movements`; `null` while it opens. */
1509
+ movements: CashMovementCollection | null;
1510
+ /** `closures`; `null` while it opens. */
1511
+ closures: ClosureCollection | null;
1512
+ /** `pos_orders`: a session's sales are those whose `sessionId` is its id. */
1513
+ orders: RxCollection<PosOrder> | null;
1514
+ /** The register document's host, for the closure number and perpetual totals (`writeClosure`); `null` while it opens. */
1515
+ register: RegisterHost | null;
1516
+ /** The app's neutral store key (see `bindRegister`). */
1517
+ storeKey: string;
1518
+ /** The register (drawer) this till is bound to, or `null` when it isn't bound. */
1519
+ registerId: string | null;
1520
+ /** The store uses register sessions; `false` turns everything off, as WCPOS's `sessionsOn`. */
1521
+ enabled: boolean;
1522
+ /** The signed-in cashier, for the facts and `opened_by`/`closed_by`. */
1523
+ actor: Actor;
1524
+ /** The store's IANA zone, or `'device'`: the business day a session opens on. */
1525
+ timezone: string;
1526
+ /** The app's version, stamped on the closure. */
1527
+ softwareVersion: string;
1528
+ /** A sale is at tender (`sale.stage.kind === 'tender'`): counting and closing refuse. */
1529
+ tenderInProgress: boolean;
1530
+ /** The count variance, in minor units, over which the app asks for approval. */
1531
+ varianceThreshold?: number;
1532
+ /** `'HH:mm'` on the device's clock; an open session after it is `overdue`. */
1533
+ expectedCloseTime?: string;
1534
+ /** The cashier may not see expected figures while counting. */
1535
+ blind?: boolean;
1536
+ /** The closure's register name, and a person id's display name. */
1537
+ labels?: {
1538
+ registerName?: string;
1539
+ resolveCashierName?: (id: string) => string;
1540
+ };
1541
+ }
1542
+ declare function useRegisterSession(options: UseRegisterSessionOptions): {
1543
+ session: RegisterSession | null;
1544
+ movements: CashMovement[];
1545
+ expected: Record<string, number>;
1546
+ salesCount: number;
1547
+ overdue: boolean;
1548
+ /** A `closeSession` for this register is in flight, in any hook instance: the session can already be stored `closed` while its closure isn't written yet. */
1549
+ closing: boolean;
1550
+ lastClosure: Closure;
1551
+ lastClosed: RegisterSession;
1552
+ varianceThreshold: number | undefined;
1553
+ enabled: boolean;
1554
+ blind: boolean;
1555
+ /** Pass straight to useSale's `session` option: `complete()` then stamps through `stampSession`. */
1556
+ saleSession: {
1557
+ id: string;
1558
+ sessions: RegisterSessionCollection;
1559
+ } | undefined;
1560
+ /** The open session's id, `null` when sessions are off, else `RegisterSessionRequiredError`. Call it when tender starts and before a card terminal captures. */
1561
+ requireOpen: () => Promise<string | null>;
1562
+ /** `requireOpen()`, returning `{ id, sessions }`: pass it to useSale's `startTender`, which pins it (the rendered `saleSession` can lag). */
1563
+ requireSaleSession: () => Promise<{
1564
+ id: string;
1565
+ sessions: RegisterSessionCollection;
1566
+ } | null>;
1567
+ actions: {
1568
+ /**
1569
+ * Refuses with `RegisterSessionAlreadyOpenError` while the register has a live session or
1570
+ * another open is running (in any hook instance), and with `RegisterCloseIncompleteError`
1571
+ * while an earlier close hasn't finished: a new session would lock the till behind it.
1572
+ */
1573
+ openSession: (input: {
1574
+ expectedFloatMinor: number | null;
1575
+ countedFloatMinor: number;
1576
+ }) => Promise<rxdb.RxDocument<RegisterSession, {}>>;
1577
+ startCounting: () => Promise<rxdb.RxDocument<RegisterSession, {}>>;
1578
+ backToSelling: () => Promise<rxdb.RxDocument<RegisterSession, {}>>;
1579
+ /**
1580
+ * `counted` maps each tender to its counted minor units. An interrupted close resumes with
1581
+ * the count persisted on the session, not the one passed to the retry (WCPOS uses the retry's).
1582
+ * Over `varianceThreshold` (`closeNeedsApproval`, as `RegisterCount`), a close without
1583
+ * `approvedBy` throws `RegisterApprovalRequiredError` before any write, blind or not; a
1584
+ * resumed close (the stored session already closed) isn't gated again. `approvedBy` reaches the Z
1585
+ * (`breakdowns.approved_by`), and `approvedByName` its `approved_by_name`.
1586
+ * One close per register runs at a time, in any hook instance: a call while one is in flight joins
1587
+ * it, getting its closure or error, and its own `counted`, `approvedBy` and `approvedByName` are ignored.
1588
+ */
1589
+ closeSession: (input: {
1590
+ counted: Record<string, number>;
1591
+ approvedBy?: string;
1592
+ approvedByName?: string;
1593
+ }) => Promise<rxdb.RxDocument<Closure, {}>>;
1594
+ recordMovement: (input: {
1595
+ type: "paid_in" | "paid_out" | "no_sale";
1596
+ amountMinor: number;
1597
+ reason: string;
1598
+ }) => Promise<rxdb.RxDocument<CashMovement, {}>>;
1599
+ voidMovement: (id: string) => Promise<rxdb.RxDocument<CashMovement, {}>>;
1600
+ };
1601
+ };
1602
+
1603
+ type CatalogueEntry<Doc> = {
1604
+ product: Doc;
1605
+ variant: VariantSummary;
1606
+ };
1607
+ /** Every variant of every product, in product order then variant order. */
1608
+ declare function catalogueEntries<Doc>(products: Doc[], traits: ProductTraits<Doc>): CatalogueEntry<Doc>[];
1609
+ /** Barcode-then-SKU lookup across all products. */
1610
+ declare function findEntryByCode<Doc>(entries: CatalogueEntry<Doc>[], code: string): CatalogueEntry<Doc> | undefined;
1611
+ /** Display the resolved price in the requested currency. */
1612
+ declare function variantPriceLabel(variant: VariantSummary, currency: string, locale?: string): string | undefined;
1613
+
1614
+ /** Logs a throw from onSaleCompleted for a confirmed pending completion newSale() has abandoned (Continue): nothing else would ever surface it. */
1615
+ declare const saleLogger: Logger;
1616
+ type SaleStage = {
1617
+ kind: 'cart';
1618
+ } | {
1619
+ kind: 'tender';
1620
+ method: 'cash' | 'external';
1621
+ } | {
1622
+ kind: 'receipt';
1623
+ order: Order;
1624
+ posOrder: PosOrder;
1625
+ };
1626
+ /** TallyUI finalizeOrder's refusal below order.create v2 (c19a203), shown when the discount is applied; finalize stays the backstop. */
1627
+ declare const DISCOUNTS_UNSUPPORTED = "finalize: discounts are not supported by the server yet (order.create v2)";
1628
+ /** Every sale change refuses with this while a completion is pending (see `complete()`). */
1629
+ declare const SALE_SAVING = "This sale is being saved. Retry to finish it.";
1630
+ /** Call under a `TaxProvider`: its tax context and the settings' currency price every sale. */
1631
+ declare function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
1632
+ registerId: string;
1633
+ cashierRef: string;
1634
+ capabilities?: ServerCapabilities;
1635
+ /**
1636
+ * When set, `complete()` stamps the finalized order with this session before `onSaleCompleted`.
1637
+ * `complete()` runs after the money is taken, so a refused stamp (the session closed or went
1638
+ * missing) never stops the sale: it goes on to `onSaleCompleted` and the receipt with
1639
+ * `lateSessionId` instead of `sessionId`, and a `late-sale` register fact is recorded (ADR-032).
1640
+ * The session stamped is the one in force when the tender started (`startTender`), pinned for that
1641
+ * tender: this option going undefined mid-tender (the session closed) doesn't skip the stamp. A
1642
+ * tender that pinned none, with a session here at `complete()`, stamps it and logs a warning.
1643
+ */
1644
+ session?: {
1645
+ id: string;
1646
+ sessions: RegisterSessionCollection;
1647
+ };
1648
+ onSaleCompleted?: (posOrder: PosOrder) => Promise<void> | void;
1649
+ /**
1650
+ * Asked after `onSaleCompleted` throws, or on a `newSale()` refused mid-save: whether that order is confirmed stored
1651
+ * (medusapos: `useOrderOutbox`'s `isStored`). Only a true answer sets `canContinue`; without it, a failed save offers Retry only.
1652
+ */
1653
+ isStored?: (posOrder: PosOrder) => Promise<boolean>;
1654
+ /** Tests only: overrides HUNG_SAVE_CHECK_MS, so a test can shrink the hung-save poll's interval. */
1655
+ hungSaveCheckMs?: number;
1656
+ }): {
1657
+ order: Order;
1658
+ stage: SaleStage;
1659
+ error: string | null;
1660
+ idle: boolean;
1661
+ /** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
1662
+ saving: boolean;
1663
+ /** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
1664
+ canContinue: boolean;
1665
+ add(entry: CatalogueEntry<any>, traits: ProductTraits<any>): void;
1666
+ setQuantity(lineId: string, quantity: number): void;
1667
+ remove(lineId: string): void;
1668
+ /** A line's discount, or the order's without a line; returns the refusal to show, or null once applied. */
1669
+ applyDiscount(lineId: string | null, discount: Discount): string | null;
1670
+ removeDiscount(id: string): void;
1671
+ /**
1672
+ * Pins `options.session` for this tender when given, else the rendered `session` option. Pass the
1673
+ * session `useRegisterSession`'s `requireSaleSession()` returned: the rendered one can lag a session
1674
+ * opened just before (#170 race). A repeat call mid-tender keeps the first pin.
1675
+ */
1676
+ startTender(method: "cash" | "external", options?: {
1677
+ session?: {
1678
+ id: string;
1679
+ sessions: RegisterSessionCollection;
1680
+ };
1681
+ }): void;
1682
+ setTender: (tender: {
1683
+ method: "cash" | "external";
1684
+ amountMinor: number;
1685
+ reference?: string;
1686
+ } | null) => void;
1687
+ cancelTender(): void;
1688
+ /**
1689
+ * Finalizes the tender, stamps the session (see `session`), hands the order to `onSaleCompleted`,
1690
+ * then shows the receipt. `saving` turns true and the sale locks (every change sets SALE_SAVING)
1691
+ * from this call's entry, not only once the order is built; a refused finalize unlocks it again,
1692
+ * with the refusal's error. Idempotent for one tender attempt (DECISIONS, ADR-052): once the order
1693
+ * is built, that order is the sale, kept as the pending completion, because `onSaleCompleted` may
1694
+ * already have stored it before failing. If `onSaleCompleted` throws, the error is set and the tender stays;
1695
+ * calling `complete()` again reuses the pending completion exactly (the same `id`, `commandId`
1696
+ * and `createdAt`, no new stamp and no second late-sale fact) and hands it to `onSaleCompleted`
1697
+ * again, so that must accept an order it already stored (as `useOrderOutbox.record` does). The
1698
+ * pending completion is cleared once `onSaleCompleted` resolves, or by `newSale()`. A call while
1699
+ * another is in flight returns that call's promise, and a call on the receipt does nothing.
1700
+ */
1701
+ complete: () => Promise<void>;
1702
+ /**
1703
+ * Starts a new, empty sale. Refused with SALE_SAVING, changing nothing, while a save is still
1704
+ * building or stamping (nothing yet to ask `isStored` about), and while a pending completion
1705
+ * exists and isn't confirmed stored — the ways out are Retry (`complete()`) or Continue, once
1706
+ * `canContinue` (the Front desk, 2026-09-27; #149 review). So an app that doesn't pass `isStored`
1707
+ * gets Retry only after a failed save. A refusal while a save is still delivering its order asks
1708
+ * `isStored` afresh, so a hung save whose order is stored can still offer Continue. Past those, it
1709
+ * abandons a confirmed pending completion (never handed to `onSaleCompleted` again: it's stored; a
1710
+ * save still in flight carries on in the background, a throw logged at error) and starts the new
1711
+ * sale outright — from the receipt, or an idle cart, there's nothing pending to abandon. Abandoning
1712
+ * clears the screen, never the record (the Front desk, 2026-09-25). `newSale()` never deletes,
1713
+ * updates or requeues `pos_orders` itself.
1714
+ */
1715
+ newSale(): void;
1716
+ /**
1717
+ * Continue after a failed save whose order is confirmed stored (`canContinue`): exactly `newSale()`.
1718
+ * The order stays pending in the outbox, which will send it; it isn't handed to `onSaleCompleted` again, and
1719
+ * its receipt is not shown. Otherwise does nothing.
1720
+ */
1721
+ continueSale(): void;
1722
+ };
1723
+
1724
+ declare class CartError extends Error {
1725
+ }
1726
+ declare function addEntryToCart<Doc>(builder: OrderBuilder, entry: CatalogueEntry<Doc>, traits: ProductTraits<Doc>, currency: string): string;
1727
+
1728
+ type TransportOutcome = {
1729
+ kind: 'results';
1730
+ results: CommandResult[];
1731
+ } | {
1732
+ kind: 'unauthorized';
1733
+ } | {
1734
+ kind: 'refused';
1735
+ status: number;
1736
+ reason: string;
1737
+ } | {
1738
+ kind: 'retry';
1739
+ reason: string;
1740
+ retryAfterMs?: number;
1741
+ };
1742
+ interface CommandTransport {
1743
+ send(batch: CommandEnvelope<OrderCreatePayload>[]): Promise<TransportOutcome>;
1744
+ }
1745
+ interface OutboxState {
1746
+ pending: number;
1747
+ sending: boolean;
1748
+ lastRetryReason?: string;
1749
+ nextAttemptAt?: number;
1750
+ /** Set after 3 consecutive 401s; the app should ask the cashier to sign in, then call flush(). */
1751
+ authRequired?: boolean;
1752
+ /** Set when the server refused a whole batch (HTTP 400, 403, 413, 415, 422). No order is changed; sending pauses until the next flush(). */
1753
+ refused?: {
1754
+ status: number;
1755
+ reason: string;
1756
+ };
1757
+ }
1758
+
1759
+ interface HttpTransportOptions {
1760
+ baseUrl: string;
1761
+ getHeaders: () => Record<string, string> | Promise<Record<string, string>>;
1762
+ fetch?: typeof fetch;
1763
+ timeoutMs?: number;
1764
+ }
1765
+ declare function createHttpCommandTransport(options: HttpTransportOptions): CommandTransport;
1766
+
1767
+ interface OrderOutboxOptions {
1768
+ collection: RxCollection<PosOrder>;
1769
+ transport: CommandTransport;
1770
+ deviceId: string;
1771
+ batchSize?: number;
1772
+ initialBackoffMs?: number;
1773
+ maxBackoffMs?: number;
1774
+ random?: () => number;
1775
+ now?: () => number;
1776
+ }
1777
+ interface OrderOutbox {
1778
+ /** Sends pending orders until empty or retrying. Concurrent calls share one run. */
1779
+ flush(): Promise<void>;
1780
+ /** Moves rejected orders (all, or those whose id is listed) back to pending with a new commandId, then flushes.
1781
+ * Orders rejected with idempotency_mismatch are left for reconciliation. Resolves to the number requeued. */
1782
+ requeue(orderIds?: string[]): Promise<number>;
1783
+ /** Watches for pending orders and sends them. */
1784
+ start(): void;
1785
+ /** Stops watching and retrying. */
1786
+ stop(): void;
1787
+ state$: Observable<OutboxState>;
1788
+ }
1789
+ declare function createOrderOutbox(options: OrderOutboxOptions): OrderOutbox;
1790
+
1791
+ /** Logs a retried order found stored under another `commandId` (warn), and a content mismatch (error). */
1792
+ declare const outboxLogger: Logger;
1793
+ interface UseOrderOutboxOptions {
1794
+ /** Which order store to use (medusapos: the backend's base URL); `null` means no store. A change reopens. */
1795
+ storeKey: string | null;
1796
+ /** Opens the order store for `storeKey`; `close()` is called when the key or device id changes, or on unmount. */
1797
+ open(storeKey: string): Promise<{
1798
+ orders: RxCollection<PosOrder>;
1799
+ close(): Promise<void>;
1800
+ }>;
1801
+ /** Builds the command transport for `storeKey` (the app's HTTP transport and auth headers). Read once per open. */
1802
+ transport(storeKey: string): CommandTransport;
1803
+ /** The device id sent on every command (see `getDeviceId`). A change reopens. */
1804
+ deviceId: string;
1805
+ /** Called with `state.sending`, and with `false` on cleanup (medusapos: live-tab's `markBusy('outbox', …)`). */
1806
+ onBusy?(busy: boolean): void;
1807
+ /** Called when `open` rejects, even if the key has changed since (medusapos: reports storage worker failures). */
1808
+ onOpenError?(error: unknown): void;
1809
+ }
1810
+ interface UseOrderOutboxResult {
1811
+ /** The open orders collection, or `null` until the current store is ready. */
1812
+ orders: RxCollection<PosOrder> | null;
1813
+ /** The outbox state, or idle until the current store is ready. */
1814
+ state: OutboxState;
1815
+ /** The newest 50 orders, newest first; empty until the current store is ready. */
1816
+ recent: PosOrder[];
1817
+ /**
1818
+ * Stores a finalized order, then flushes. Throws the opening error, or "Orders are not ready.", before the store is ready.
1819
+ * Recording an order whose `id` is already stored, not deleted, with the same money-bearing content (`sameSale`;
1820
+ * a retried `complete()`) counts as stored, never overwrites it, and still flushes, whatever the stored `commandId`
1821
+ * (a requeue mints a new one; the difference is logged at warn). Other content rejects with `OrderContentMismatchError`.
1822
+ */
1823
+ record(posOrder: PosOrder): Promise<void>;
1824
+ /**
1825
+ * Whether the current store holds this order `id`, not deleted, with the same money-bearing content (`sameSale`),
1826
+ * whatever its `commandId`. A primary-key read on the storage instance, past RxDB's query cache. False before the
1827
+ * store is ready; a content mismatch is false and logged at error.
1828
+ */
1829
+ isStored(order: PosOrder): Promise<boolean>;
1830
+ /** Sends pending orders; does nothing before the current store is ready. */
1831
+ flush(): Promise<void>;
1832
+ /** Moves rejected orders back to pending (see `OrderOutbox.requeue`); resolves to 0 before the current store is ready. */
1833
+ requeue(orderIds?: string[]): Promise<number>;
1834
+ /**
1835
+ * The count of `record()` calls not yet settled (resolved or rejected). An app holds sign-out
1836
+ * (closing the store) while this is above 0, because a close that lands under a write stuck in
1837
+ * storage can wait forever, and TallyUI deliberately doesn't bound that close (#155).
1838
+ */
1839
+ savesInFlight: number;
1840
+ }
1841
+ /** Opens the order store for `storeKey`, runs its outbox and watches the recent orders (lifted from medusapos/app, ADR-052). */
1842
+ declare function useOrderOutbox(options: UseOrderOutboxOptions): UseOrderOutboxResult;
1843
+
1844
+ /** The transport a payment terminal is reached over: hardware-neutral, not tied to any backend. */
1845
+ type PaymentTransport = 'bluetooth' | 'usb' | 'network' | 'tap_to_pay';
1846
+ type TenderView = 'select' | 'amount' | 'cancel';
1847
+ type TenderLineId = string | number;
1848
+ type TenderPlan = {
1849
+ kind: 'even';
1850
+ ways: number;
1851
+ from: number;
1852
+ } | {
1853
+ kind: 'fixed';
1854
+ firstMinor: number;
1855
+ title: string | null;
1856
+ from: number;
1857
+ } | {
1858
+ kind: 'items';
1859
+ lineIds: TenderLineId[];
1860
+ firstMinor: number;
1861
+ ways: number;
1862
+ from: number;
1863
+ };
1864
+ type SplitTab = 'even' | 'amount' | 'percent' | 'item';
1865
+ interface TenderState {
1866
+ view: TenderView;
1867
+ /** Method id being tendered; null in the 'select' and 'cancel' views. */
1868
+ methodId: string | null;
1869
+ readerId: string | null;
1870
+ transport: PaymentTransport | null;
1871
+ /** Keypad entry in minor units. */
1872
+ entryMinor: number;
1873
+ /** False until the cashier has touched the keypad since the entry was pre-filled. */
1874
+ entryDirty: boolean;
1875
+ plan: TenderPlan | null;
1876
+ splitView: boolean;
1877
+ splitTab: SplitTab;
1878
+ pickedLineIds: TenderLineId[];
1879
+ linesPaidBy: Record<TenderLineId, string[]>;
1880
+ }
1881
+ /** '0'..'9' plus the two edit keys. There is deliberately no decimal key: digits shift in from the right. */
1882
+ type TenderKey = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'clear' | 'backspace';
1883
+ type TenderAction = {
1884
+ type: 'pick-method';
1885
+ methodId: string;
1886
+ prefillMinor: number;
1887
+ readerId: string | null;
1888
+ transport?: PaymentTransport | null;
1889
+ } | {
1890
+ type: 'pick-transport';
1891
+ transport: PaymentTransport;
1892
+ } | {
1893
+ type: 'pick-reader';
1894
+ readerId: string | null;
1895
+ } | {
1896
+ type: 'tender-started';
1897
+ } | {
1898
+ type: 'key';
1899
+ key: TenderKey;
1900
+ } | {
1901
+ type: 'set-entry';
1902
+ minor: number;
1903
+ } | {
1904
+ type: 'back';
1905
+ } | {
1906
+ type: 'tender-recorded';
1907
+ rowsSinceFrom: {
1908
+ title: string;
1909
+ amountMinor: number;
1910
+ }[];
1911
+ /** The order's balance once this row counts, so the next leg can be pre-typed. */
1912
+ balanceMinor: number;
1913
+ } | {
1914
+ type: 'open-split';
1915
+ } | {
1916
+ type: 'close-split';
1917
+ } | {
1918
+ type: 'set-split-tab';
1919
+ tab: SplitTab;
1920
+ } | {
1921
+ type: 'toggle-split-line';
1922
+ lineId: TenderLineId;
1923
+ } | {
1924
+ type: 'set-plan';
1925
+ plan: TenderPlan;
1926
+ balanceMinor: number;
1927
+ } | {
1928
+ type: 'clear-plan';
1929
+ balanceMinor: number;
1930
+ } | {
1931
+ type: 'arm-custom';
1932
+ } | {
1933
+ type: 'request-cancel';
1934
+ } | {
1935
+ type: 'reset';
1936
+ };
1937
+ /** $9,999,999.99 at two decimals — a till will never legitimately take more, and it stops a stuck key running the display off the screen. */
1938
+ declare const MAX_TENDER_MINOR = 999999999;
1939
+ declare const initialTenderState: TenderState;
1940
+ /**
1941
+ * Reducer init. An order whose method the checkout store already holds — a URL seed, or a
1942
+ * tab the cashier is coming back to — reopens its keypad prefilled with the balance, exactly
1943
+ * as a tap on that tile would. Everything else starts from scratch.
1944
+ */
1945
+ declare function initTenderState({ methodId, balanceMinor, }: {
1946
+ methodId: string | null;
1947
+ balanceMinor: number;
1948
+ }): TenderState;
1949
+ declare function tenderReducer(state: TenderState, action: TenderAction): TenderState;
1950
+ /**
1951
+ * What the leg applies to the order. Cash may be tendered above the balance;
1952
+ * the excess is change, never an overpayment on the order.
1953
+ */
1954
+ declare function appliedMinor(entryMinor: number, balanceMinor: number): number;
1955
+ /** Change handed back. Zero for any tender whose method cannot give change. */
1956
+ declare function changeMinor(entryMinor: number, appliedAmountMinor: number, givesChange: boolean): number;
1957
+ /**
1958
+ * Quick tendered amounts under a cash keypad: the balance itself, then the next
1959
+ * whole 5, 10 and 50 above it. Deduped, ascending, never below the balance.
1960
+ * The caller supplies those major-unit steps already scaled to minor units.
1961
+ */
1962
+ declare function quickTenderedAmounts(balanceMinor: number, stepsMinor: readonly number[]): number[];
1963
+ /**
1964
+ * Even split shares. Returns the next tender's share, rounded half-up to the
1965
+ * minor unit; the last leg remains whatever balance is left.
1966
+ */
1967
+ declare function evenSplitShareMinor(balanceMinor: number, ways: number): number;
1968
+ /** Completion is derived from payment rows, never an extra counter in the reducer. */
1969
+ declare function activePlan(plan: TenderPlan | null, taken: number, balanceMinor: number): TenderPlan | null;
1970
+ interface PlanLeg {
1971
+ minor: number;
1972
+ state: 'done' | 'now' | 'todo' | 'rest';
1973
+ title?: string;
1974
+ }
1975
+ /** Label pieces stay untranslated here; the flow supplies localized copy and item names. */
1976
+ declare function planLegs(plan: TenderPlan, rowsSinceFrom: {
1977
+ minor: number;
1978
+ title: string;
1979
+ }[], balanceMinor: number): {
1980
+ legs: PlanLeg[];
1981
+ thisPaymentMinor: number;
1982
+ label: {
1983
+ n: number;
1984
+ ways: number;
1985
+ rest: boolean;
1986
+ title: string | null;
1987
+ };
1988
+ };
1989
+
1990
+ export { type Actor, type AddLineInput, type AppliedDiscount, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, 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 OutboxState, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderSyncStatus, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterMovementStrandedError, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TransportOutcome, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureSchema, computeOrderTax, consoleSink, countVariance, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, initTenderState, initialTenderState, isServerDecimal, labelKeys, minorToDecimal, mintClosureNumber, mintUuid, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, observeRegister$, openSession, openSessionSelector, outboxLogger, overThreshold, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, recordMovement, recordRegisterFact, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveStoreSettings, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxLinesByRate, taxMicros, taxProviderProps, tenderReducer, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure };