@tallyui/pos 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { Money, TaxRounding, StoreSettings, TallyConnector, SyncContext, StoreSettingsChoice, StoreSettingsChoices, ProductTraits, ProductCategory, TraitContext, OrderCreateLine, PaymentMethodKind, CommandServerRefs, CommandWarning, CommandError, ServerCapabilities, OrderCreateEnvelope, RegisterCommandType, RegisterCommandResult, VariantSummary, CommandResult, AnyCommandEnvelope, CommandEnvelope, OrderCreatePayload, RegisterCommandEnvelope } from '@tallyui/core';
3
+ import { Money, TaxRounding, StoreSettings, TallyConnector, SyncContext, StoreSettingsChoice, StoreSettingsChoices, ProductTraits, ProductCategory, TraitContext, OrderCreateLine, PaymentMethodKind, CommandServerRefs, CommandWarning, CommandError, ServerCapabilities, OrderCreateEnvelope, RegisterCommandType, RegisterCommandResult, RegisterSessionTransitionPayload, VariantSummary, CommandResult, AnyCommandEnvelope, CommandEnvelope, OrderCreatePayload, RegisterCommandEnvelope } from '@tallyui/core';
4
4
  export { getProductStock, withStockOverlay } from '@tallyui/core';
5
5
  export { countFresh, readFresh, watchFresh } from '@tallyui/core/rxdb';
6
6
  import * as rxdb from 'rxdb';
@@ -911,27 +911,10 @@ declare class PosOrderOpenClosedError extends Error {
911
911
  constructor(databaseName: string);
912
912
  }
913
913
  /**
914
- * Adds `pos_orders` to `db` and resolves once no order of an older version is left to migrate. The one
915
- * sanctioned way to open it (ADR-032 amendment 2). On DM4 it rejects after the migration has
916
- * stopped and closes the collection, so the app can surface the error and open again; it never
917
- * deletes an order.
918
- *
919
- * RxDB 17.5.0 trusts the status its last migration stored: a leftover `ERROR` rejects
920
- * `migratePromise` at once while the migration keeps running (a close then interrupts it), and a
921
- * `DONE` left before a rollback resolves it before the older version's new orders have moved. So the
922
- * collection is added without `autoMigrate` and the migration is started and awaited directly: RxDB
923
- * 17.5's `startMigration()` ignores a leftover status (only `migratePromise()` trusts a leftover
924
- * `DONE`), which lets a rolled-back or interrupted run migrate again and recovers its orders. The
925
- * status is reset first so its record describes the new run, and a failed run's checkpoint is
926
- * removed (RxDB bug 1); never an order or its storage.
927
- *
928
- * A close waits for the open, up to `POS_ORDER_MIGRATION_CLOSE_WAIT_MS`, and cancels a running
929
- * migration (RxDB 17.5). Called once the database's close has begun, when the close cancels its
930
- * migration, or when the close stops waiting, it rejects with `PosOrderOpenClosedError`
931
- * (`code: 'POS_ORDER_OPEN_CLOSED'`) before any further write: reopen and call it again. Any other
932
- * rejection (DM4, a storage error) is a real failure.
933
- *
934
- * `closeWaitMs` is for tests only; the first open on a database sets it for that database's close.
914
+ * Adds `pos_orders` to `db` and resolves once no order of an older version is left to migrate.
915
+ * On DM4 it rejects after the migration has stopped and closes the collection. It rejects with
916
+ * `PosOrderOpenClosedError` when the database is closing. `closeWaitMs` is for tests only.
917
+ * See `openMigratedCollection` for the migration and close mechanism.
935
918
  */
936
919
  declare function addPosOrderCollection(db: RxDatabase, closeWaitMs?: number): Promise<RxCollection<PosOrder>>;
937
920
 
@@ -1072,7 +1055,7 @@ declare const denominations: Record<string, number[]>;
1072
1055
  /**
1073
1056
  * The register's local collections (ADR-032): sessions, cash movements and closures. Port
1074
1057
  * provenance (ADR-032 amendment 1): WCPOS `next` `3b5331b5c`, where they are at versions 1, 0
1075
- * and 1; each starts at version 0 here.
1058
+ * and 1; sessions are at version 1 here (ADR-078), movements and closures at version 0.
1076
1059
  *
1077
1060
  * All three are local only, like `pos_orders`: the app creates them and never replicates them
1078
1061
  * (a replicated collection must never take local writes, #53). WCPOS's outbox fields
@@ -1091,7 +1074,10 @@ interface RegisterSession {
1091
1074
  register_id: string;
1092
1075
  /** The app's neutral store key (see `bindRegister`). */
1093
1076
  store_key?: string | null;
1094
- status: 'open' | 'counting' | 'closed';
1077
+ /** Stored status is an open string so a later local state costs no migration (ADR-078 decision 9); the union names today's states. */
1078
+ status: 'open' | 'counting' | 'closed' | 'conflict' | 'superseded';
1079
+ /** The store session this local id was resumed as (ADR-078 decision 2), never the session that superseded it; null until a resume. */
1080
+ server_session_id?: string | null;
1095
1081
  /** The store's day the session opened, `yyyy-MM-dd`. */
1096
1082
  business_day?: string;
1097
1083
  opened_at_gmt: string;
@@ -1162,16 +1148,31 @@ interface Closure {
1162
1148
  }
1163
1149
  declare const registerSessionSchema: RxJsonSchema<RegisterSession>;
1164
1150
  /**
1165
- * Create `register_sessions` with this: the register document (`register-document.ts`) is a
1166
- * local document on this collection, since a Tally database has no database-level ones.
1151
+ * @deprecated Use `addRegisterSessionCollection` to open register_sessions.
1152
+ * Kept so old callers compile but fail at start instead of migrating on RxDB's own path.
1167
1153
  */
1168
- declare function registerSessionCollection(): {
1169
- readonly schema: RxJsonSchema<RegisterSession>;
1170
- readonly localDocuments: true;
1171
- };
1154
+ declare function registerSessionCollection(): never;
1172
1155
  declare const cashMovementSchema: RxJsonSchema<CashMovement>;
1173
1156
  declare const closureSchema: RxJsonSchema<Closure>;
1174
1157
 
1158
+ /** `addRegisterSessionCollection`'s logger: attach a sink to see dropped status writes during close. */
1159
+ declare const registerSessionsLogger: Logger;
1160
+ /** How long a close waits for the open; see `openMigratedCollection` for the cancellation mechanism. */
1161
+ declare const REGISTER_SESSION_MIGRATION_CLOSE_WAIT_MS = 10000;
1162
+ /** The database is closing; no session is lost. Reopen the database and call the opener again. */
1163
+ declare class RegisterSessionOpenClosedError extends Error {
1164
+ readonly databaseName: string;
1165
+ readonly code = "REGISTER_SESSION_OPEN_CLOSED";
1166
+ constructor(databaseName: string);
1167
+ }
1168
+ /**
1169
+ * Adds `register_sessions` and resolves once no older session is left to migrate; the register
1170
+ * document, a local document, is kept. On DM4 it rejects after migration stops and closes the collection.
1171
+ * A closing database rejects with `RegisterSessionOpenClosedError`; `closeWaitMs` is for tests only.
1172
+ * See `openMigratedCollection` for the migration and close mechanism.
1173
+ */
1174
+ declare function addRegisterSessionCollection(db: RxDatabase, closeWaitMs?: number): Promise<RxCollection<RegisterSession>>;
1175
+
1175
1176
  type RegisterHost = RxLocalDocumentMutation<any>;
1176
1177
  type RegisterCounters = {
1177
1178
  last_closure_number: number;
@@ -1255,6 +1256,11 @@ declare class RegisterSessionRequiredError extends Error {
1255
1256
  declare class RegisterSessionClosedError extends Error {
1256
1257
  constructor();
1257
1258
  }
1259
+ /** A stored session status outside the known set (written by a later build); its message can be shown to a cashier as it is. */
1260
+ declare class RegisterNeedsUpgradeError extends Error {
1261
+ readonly code = "REGISTER_NEEDS_UPGRADE";
1262
+ constructor();
1263
+ }
1258
1264
  /** ADR-068 6a: movement amounts must be safe integers, positive for paid_in/paid_out and zero for no_sale. */
1259
1265
  declare class RegisterMovementAmountError extends Error {
1260
1266
  constructor(type: 'paid_in' | 'paid_out' | 'no_sale', amountMinor: number);
@@ -1275,6 +1281,8 @@ declare class RegisterMovementStrandedError extends Error {
1275
1281
  session_id: string;
1276
1282
  });
1277
1283
  }
1284
+ /** Whether this build knows the stored session status. */
1285
+ declare function isKnownSessionStatus(status: string): boolean;
1278
1286
  declare const openSessionSelector: {
1279
1287
  readonly status: "open";
1280
1288
  };
@@ -1836,6 +1844,7 @@ type BuiltCommand = Pick<RegisterCommand, 'key' | 'type' | 'payload'> & {
1836
1844
  };
1837
1845
  declare function sessionOpenCommand(s: RegisterSession): BuiltCommand;
1838
1846
  declare function sessionTransitionCommand(s: RegisterSession & {
1847
+ status: RegisterSessionTransitionPayload['status'];
1839
1848
  status_at: string;
1840
1849
  }): BuiltCommand;
1841
1850
  declare function movementCommand(m: CashMovement): BuiltCommand;
@@ -1870,7 +1879,7 @@ declare class RegisterApprovalRequiredError extends Error {
1870
1879
  constructor();
1871
1880
  }
1872
1881
  interface UseRegisterSessionOptions {
1873
- /** `register_sessions`, created with `registerSessionCollection()`; `null` while it opens. */
1882
+ /** `register_sessions`, created with `addRegisterSessionCollection`; `null` while it opens. */
1874
1883
  sessions: RegisterSessionCollection | null;
1875
1884
  /** `cash_movements`; `null` while it opens. */
1876
1885
  movements: CashMovementCollection | null;
@@ -1911,6 +1920,8 @@ interface UseRegisterSessionOptions {
1911
1920
  }
1912
1921
  declare function useRegisterSession(options: UseRegisterSessionOptions): {
1913
1922
  session: RegisterSession | null;
1923
+ /** Any unknown session status in the current snapshot means the register needs upgrade. */
1924
+ needsUpgrade: boolean;
1914
1925
  movements: CashMovement[];
1915
1926
  expected: Record<string, number>;
1916
1927
  salesCount: number;
@@ -1939,6 +1950,7 @@ declare function useRegisterSession(options: UseRegisterSessionOptions): {
1939
1950
  * Refuses with `RegisterSessionAlreadyOpenError` while the register has a live session or
1940
1951
  * another open is running (in any hook instance), and with `RegisterCloseIncompleteError`
1941
1952
  * while an earlier close hasn't finished: a new session would lock the till behind it.
1953
+ * An unknown stored status refuses with `RegisterNeedsUpgradeError`.
1942
1954
  */
1943
1955
  openSession: (input: {
1944
1956
  expectedFloatMinor: number | null;
@@ -2565,4 +2577,4 @@ declare function tenderVoidCollection(): {
2565
2577
  readonly schema: RxJsonSchema<TenderVoid>;
2566
2578
  };
2567
2579
 
2568
- export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type CatalogueGridColumns, type CatalogueView, type CatalogueViewAction, type CatalogueViewState, type ChargeInput, type ChargeLine, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DEFAULT_CATALOGUE_VIEW_STATE, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FeeLine, 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 OrderTransportContext, type OutboxState, PRODUCT_SORT_FIELDS, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type ProductSort, type ProductSortValue, RECEIPT_SCHEMA_VERSION, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptFiscal, type ReceiptLineItem, type ReceiptRegister, type ReceiptSoftware, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type ShippingLine, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TenderVoid, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterOutboxOptions, type UseRegisterOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, catalogueViewReducer, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, inCategory, initTenderState, initialTenderState, isServerDecimal, labelKeys, listCategories, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, normalizeCatalogueViewState, observeRegister$, openSession, openSessionSelector, orderDraftSchema, outboxLogger, overThreshold, parkedOrderSummaries$, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, productCategories, productSortValue, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveGridColumns, resolveStoreSettings, restoreOrderDraft, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, sortProducts, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, tenderVoidCollection, tenderVoidSchema, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useParkedSales, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure, writeOrderDraft };
2580
+ export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type CatalogueGridColumns, type CatalogueView, type CatalogueViewAction, type CatalogueViewState, type ChargeInput, type ChargeLine, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DEFAULT_CATALOGUE_VIEW_STATE, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FeeLine, 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 OrderTransportContext, type OutboxState, PRODUCT_SORT_FIELDS, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type ProductSort, type ProductSortValue, RECEIPT_SCHEMA_VERSION, REGISTER_SESSION_MIGRATION_CLOSE_WAIT_MS, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptFiscal, type ReceiptLineItem, type ReceiptRegister, type ReceiptSoftware, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, RegisterNeedsUpgradeError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionOpenClosedError, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type ShippingLine, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TenderVoid, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterOutboxOptions, type UseRegisterOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, addRegisterSessionCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, catalogueViewReducer, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, inCategory, initTenderState, initialTenderState, isKnownSessionStatus, isServerDecimal, labelKeys, listCategories, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, normalizeCatalogueViewState, observeRegister$, openSession, openSessionSelector, orderDraftSchema, outboxLogger, overThreshold, parkedOrderSummaries$, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, productCategories, productSortValue, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, registerSessionsLogger, requireOpenSession, resolveGridColumns, resolveStoreSettings, restoreOrderDraft, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, sortProducts, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, tenderVoidCollection, tenderVoidSchema, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useParkedSales, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure, writeOrderDraft };
package/dist/index.js CHANGED
@@ -2121,7 +2121,7 @@ function posOrderCollection() {
2121
2121
  };
2122
2122
  }
2123
2123
 
2124
- // src/pos-order/open.ts
2124
+ // src/rxdb/open-migrated-collection.ts
2125
2125
  import {
2126
2126
  deepEqual,
2127
2127
  defaultConflictHandler,
@@ -2135,16 +2135,6 @@ import {
2135
2135
  } from "rxdb";
2136
2136
  import { getOldCollectionMeta, migrateDocumentData } from "rxdb/plugins/migration-schema";
2137
2137
  import { Subject, takeUntil } from "rxjs";
2138
- var posOrdersLogger = createLogger("pos-orders");
2139
- var POS_ORDER_MIGRATION_CLOSE_WAIT_MS = 1e4;
2140
- var PosOrderOpenClosedError = class extends Error {
2141
- constructor(databaseName) {
2142
- super(`addPosOrderCollection: database ${databaseName} closed during the open; reopen it and open pos_orders again`);
2143
- this.databaseName = databaseName;
2144
- this.name = "PosOrderOpenClosedError";
2145
- }
2146
- code = "POS_ORDER_OPEN_CLOSED";
2147
- };
2148
2138
  var latestOpens = /* @__PURE__ */ new WeakMap();
2149
2139
  var closedUnderOpen = /* @__PURE__ */ new WeakSet();
2150
2140
  function waitWithTimeout(promise, ms) {
@@ -2157,35 +2147,40 @@ function waitWithTimeout(promise, ms) {
2157
2147
  });
2158
2148
  }
2159
2149
  async function writeOverStaleCopies(collection, from, to, stopIfClosing) {
2150
+ const primaryKey = collection.schema.primaryPath;
2160
2151
  const handler = rxStorageInstanceToReplicationHandler(to, defaultConflictHandler, collection.database.token, true);
2161
2152
  for (let page = await getChangedDocumentsSince(from, 200); page.documents.length > 0; page = await getChangedDocumentsSince(from, 200, page.checkpoint)) {
2162
2153
  stopIfClosing();
2163
- const copies = new Map((await to.findDocumentsById(page.documents.map((doc) => doc.id), false)).map((copy) => [copy.id, copy]));
2164
- const rows = await Promise.all(page.documents.filter((doc) => copies.has(doc.id) && !doc._deleted).map(async (doc) => ({ assumedMasterState: copies.get(doc.id), newDocumentState: await migrateDocumentData(collection, from.schema.version, doc) })));
2154
+ const copies = new Map((await to.findDocumentsById(page.documents.map((doc) => doc[primaryKey]), false)).map((copy) => [copy[primaryKey], copy]));
2155
+ const rows = await Promise.all(page.documents.filter((doc) => copies.has(doc[primaryKey]) && !doc._deleted).map(async (doc) => ({ assumedMasterState: copies.get(doc[primaryKey]), newDocumentState: await migrateDocumentData(collection, from.schema.version, doc) })));
2165
2156
  const stale = rows.filter((row) => row.newDocumentState && !deepEqual(row.assumedMasterState, row.newDocumentState));
2166
2157
  if (stale.length > 0) await handler.masterWrite(stale);
2167
2158
  }
2168
2159
  }
2169
- async function addPosOrderCollection(db, closeWaitMs = POS_ORDER_MIGRATION_CLOSE_WAIT_MS) {
2170
- if (db.multiInstance) throw new Error("addPosOrderCollection: multiInstance databases are not supported (ADR-061)");
2171
- if (db.closed || db.closePromise) throw new PosOrderOpenClosedError(db.name);
2172
- const previous = latestOpens.get(db);
2160
+ async function openMigratedCollection(db, opener, closeWaitMs) {
2161
+ const { name, label, closedError } = opener;
2162
+ if (db.multiInstance) throw new Error(`${label}: multiInstance databases are not supported (ADR-061)`);
2163
+ if (db.closed || db.closePromise) throw closedError(db.name);
2164
+ const opens = latestOpens.get(db) ?? /* @__PURE__ */ new Map();
2165
+ latestOpens.set(db, opens);
2166
+ const previous = opens.get(name);
2173
2167
  if (!previous) {
2174
- db.onClose.push(() => waitWithTimeout(latestOpens.get(db), closeWaitMs).then((settled) => {
2168
+ db.onClose.push(() => waitWithTimeout(latestOpens.get(db).get(name), closeWaitMs).then((settled) => {
2175
2169
  if (!settled) closedUnderOpen.add(db);
2176
2170
  }));
2177
2171
  }
2178
- const opening = openPosOrders(db, previous);
2179
- latestOpens.set(db, Promise.all([previous, opening.catch(() => void 0)]));
2172
+ const opening = openCollection(db, opener, previous);
2173
+ opens.set(name, Promise.all([previous, opening.catch(() => void 0)]));
2180
2174
  return opening;
2181
2175
  }
2182
- async function openPosOrders(db, previous) {
2176
+ async function openCollection(db, opener, previous) {
2177
+ const { name, creator, logger, closedError } = opener;
2183
2178
  let cancelled = false;
2184
2179
  const closing = () => cancelled || closedUnderOpen.has(db) || db.closed;
2185
2180
  const stopIfClosing = () => {
2186
- if (closing()) throw new PosOrderOpenClosedError(db.name);
2181
+ if (closing()) throw closedError(db.name);
2187
2182
  };
2188
- const { pos_orders: collection } = await db.addCollections({ pos_orders: { ...posOrderCollection(), autoMigrate: false } });
2183
+ const { [name]: collection } = await db.addCollections({ [name]: { ...creator(), autoMigrate: false } });
2189
2184
  const state = collection.getMigrationState();
2190
2185
  try {
2191
2186
  stopIfClosing();
@@ -2208,7 +2203,7 @@ async function openPosOrders(db, previous) {
2208
2203
  stopIfClosing();
2209
2204
  const checkpoint = await db.storage.createStorageInstance({
2210
2205
  databaseName: db.name,
2211
- collectionName: `rx-migration-state-meta-pos_orders-${old.version}`,
2206
+ collectionName: `rx-migration-state-meta-${name}-${old.version}`,
2212
2207
  databaseInstanceToken: db.token,
2213
2208
  multiInstance: db.multiInstance,
2214
2209
  options: {},
@@ -2234,7 +2229,7 @@ async function openPosOrders(db, previous) {
2234
2229
  } });
2235
2230
  const internalStore = gate(db.internalStore, async (key, first) => {
2236
2231
  const ids = key === "bulkWrite" ? first.map((row) => row.document.id) : first;
2237
- posOrdersLogger.warn(
2232
+ logger.warn(
2238
2233
  `The database closed during the migration: dropped a ${key === "bulkWrite" ? "status write" : "status read"}`,
2239
2234
  { database: db.name, ids }
2240
2235
  );
@@ -2244,7 +2239,7 @@ async function openPosOrders(db, previous) {
2244
2239
  const migrateStorage = state.migrateStorage.bind(state);
2245
2240
  state.migrateStorage = async (from, current, batchSize) => {
2246
2241
  stopIfClosing();
2247
- const to = gate(current, () => Promise.reject(new PosOrderOpenClosedError(db.name)));
2242
+ const to = gate(current, () => Promise.reject(closedError(db.name)));
2248
2243
  if (from.collectionName === collection.name) await writeOverStaleCopies(collection, from, to, stopIfClosing);
2249
2244
  stopIfClosing();
2250
2245
  return migrateStorage(new Proxy(from, { get: (target, key) => {
@@ -2279,10 +2274,31 @@ async function openPosOrders(db, previous) {
2279
2274
  throw newRxError("DM4", { collection: collection.name, error: status?.error });
2280
2275
  } catch (error) {
2281
2276
  await collection.close();
2282
- throw closing() ? new PosOrderOpenClosedError(db.name) : error;
2277
+ throw closing() ? closedError(db.name) : error;
2283
2278
  }
2284
2279
  }
2285
2280
 
2281
+ // src/pos-order/open.ts
2282
+ var posOrdersLogger = createLogger("pos-orders");
2283
+ var POS_ORDER_MIGRATION_CLOSE_WAIT_MS = 1e4;
2284
+ var PosOrderOpenClosedError = class extends Error {
2285
+ constructor(databaseName) {
2286
+ super(`addPosOrderCollection: database ${databaseName} closed during the open; reopen it and open pos_orders again`);
2287
+ this.databaseName = databaseName;
2288
+ this.name = "PosOrderOpenClosedError";
2289
+ }
2290
+ code = "POS_ORDER_OPEN_CLOSED";
2291
+ };
2292
+ function addPosOrderCollection(db, closeWaitMs = POS_ORDER_MIGRATION_CLOSE_WAIT_MS) {
2293
+ return openMigratedCollection(db, {
2294
+ name: "pos_orders",
2295
+ creator: posOrderCollection,
2296
+ label: "addPosOrderCollection",
2297
+ logger: posOrdersLogger,
2298
+ closedError: (name) => new PosOrderOpenClosedError(name)
2299
+ }, closeWaitMs);
2300
+ }
2301
+
2286
2302
  // src/pos-order/device-id.ts
2287
2303
  var processId;
2288
2304
  function getDeviceId(storage, key) {
@@ -2395,6 +2411,7 @@ var denominations = {
2395
2411
  // src/register/schemas.ts
2396
2412
  import { addRxPlugin as addRxPlugin2 } from "rxdb";
2397
2413
  import { RxDBLocalDocumentsPlugin } from "rxdb/plugins/local-documents";
2414
+ import { RxDBMigrationSchemaPlugin as RxDBMigrationSchemaPlugin2 } from "rxdb/plugins/migration-schema";
2398
2415
  var id = { type: "string", maxLength: 36 };
2399
2416
  var text = { type: "string" };
2400
2417
  var nullableText = { type: ["string", "null"] };
@@ -2403,7 +2420,7 @@ var nullableMinor = { type: ["integer", "null"] };
2403
2420
  var tenders = { type: "object", additionalProperties: minor };
2404
2421
  var registerSessionSchema = {
2405
2422
  title: "Register sessions",
2406
- version: 0,
2423
+ version: 1,
2407
2424
  primaryKey: "id",
2408
2425
  type: "object",
2409
2426
  additionalProperties: false,
@@ -2411,7 +2428,8 @@ var registerSessionSchema = {
2411
2428
  id,
2412
2429
  register_id: id,
2413
2430
  store_key: nullableText,
2414
- status: { type: "string", enum: ["open", "counting", "closed"], maxLength: 8 },
2431
+ status: { type: "string", maxLength: 20 },
2432
+ server_session_id: { ...nullableText, default: null },
2415
2433
  business_day: { type: "string", maxLength: 10 },
2416
2434
  opened_at_gmt: text,
2417
2435
  opened_by: nullableText,
@@ -2435,9 +2453,16 @@ var registerSessionSchema = {
2435
2453
  required: ["id", "register_id", "status", "opened_at_gmt", "counted_float_minor"],
2436
2454
  indexes: [["register_id", "status"]]
2437
2455
  };
2438
- function registerSessionCollection() {
2456
+ function registerSessionCreator() {
2439
2457
  addRxPlugin2(RxDBLocalDocumentsPlugin);
2440
- return { schema: registerSessionSchema, localDocuments: true };
2458
+ addRxPlugin2(RxDBMigrationSchemaPlugin2);
2459
+ return { schema: registerSessionSchema, localDocuments: true, migrationStrategies: { 1: (doc) => {
2460
+ doc.server_session_id ??= null;
2461
+ return doc;
2462
+ } } };
2463
+ }
2464
+ function registerSessionCollection() {
2465
+ throw new Error("registerSessionCollection() is gone: open register_sessions with await addRegisterSessionCollection(db). Adding it with addCollections would run RxDB's own migration of schema version 1, which can lose sessions (ADR-078, #371).");
2441
2466
  }
2442
2467
  var cashMovementSchema = {
2443
2468
  title: "Cash movements",
@@ -2521,6 +2546,27 @@ var closureSchema = {
2521
2546
  indexes: [["register_id"], ["session_id"]]
2522
2547
  };
2523
2548
 
2549
+ // src/register/open.ts
2550
+ var registerSessionsLogger = createLogger("register-sessions");
2551
+ var REGISTER_SESSION_MIGRATION_CLOSE_WAIT_MS = 1e4;
2552
+ var RegisterSessionOpenClosedError = class extends Error {
2553
+ constructor(databaseName) {
2554
+ super(`addRegisterSessionCollection: database ${databaseName} closed during the open; reopen it and open register_sessions again`);
2555
+ this.databaseName = databaseName;
2556
+ this.name = "RegisterSessionOpenClosedError";
2557
+ }
2558
+ code = "REGISTER_SESSION_OPEN_CLOSED";
2559
+ };
2560
+ function addRegisterSessionCollection(db, closeWaitMs = REGISTER_SESSION_MIGRATION_CLOSE_WAIT_MS) {
2561
+ return openMigratedCollection(db, {
2562
+ name: "register_sessions",
2563
+ creator: registerSessionCreator,
2564
+ label: "addRegisterSessionCollection",
2565
+ logger: registerSessionsLogger,
2566
+ closedError: (name) => new RegisterSessionOpenClosedError(name)
2567
+ }, closeWaitMs);
2568
+ }
2569
+
2524
2570
  // src/register/register-document.ts
2525
2571
  import { map as map4 } from "rxjs";
2526
2572
  var REGISTER = "register";
@@ -2760,6 +2806,13 @@ var RegisterSessionClosedError = class extends Error {
2760
2806
  this.name = "RegisterSessionClosedError";
2761
2807
  }
2762
2808
  };
2809
+ var RegisterNeedsUpgradeError = class extends Error {
2810
+ code = "REGISTER_NEEDS_UPGRADE";
2811
+ constructor() {
2812
+ super("This till has register data from a newer version of the app. Update the app to continue.");
2813
+ this.name = "RegisterNeedsUpgradeError";
2814
+ }
2815
+ };
2763
2816
  var RegisterMovementAmountError = class extends Error {
2764
2817
  constructor(type, amountMinor) {
2765
2818
  super(`movement_amount_invalid:${type}:${amountMinor}`);
@@ -2785,8 +2838,13 @@ var RegisterMovementStrandedError = class extends Error {
2785
2838
  var TRANSITIONS = {
2786
2839
  open: ["counting", "closed"],
2787
2840
  counting: ["open", "closed"],
2788
- closed: []
2841
+ closed: [],
2842
+ conflict: [],
2843
+ superseded: []
2789
2844
  };
2845
+ function isKnownSessionStatus(status) {
2846
+ return Object.hasOwn(TRANSITIONS, status);
2847
+ }
2790
2848
  async function readSession(sessions, id2) {
2791
2849
  const [stored] = await sessions.storageInstance.findDocumentsById([id2], false);
2792
2850
  return stored;
@@ -2798,6 +2856,7 @@ async function readClosure(closures, id2) {
2798
2856
  async function requireLiveSession(sessions, id2) {
2799
2857
  const session = await readSession(sessions, id2);
2800
2858
  if (!session) throw new RegisterSessionRequiredError();
2859
+ if (!isKnownSessionStatus(session.status)) throw new RegisterNeedsUpgradeError();
2801
2860
  if (session.status === "closed") throw new RegisterSessionClosedError();
2802
2861
  return session;
2803
2862
  }
@@ -2875,6 +2934,7 @@ async function transition(sessions, id2, status, extra = {}) {
2875
2934
  if (row.status === "closed" && status === "closed") return row;
2876
2935
  const at2 = (/* @__PURE__ */ new Date()).toISOString();
2877
2936
  return row.incrementalModify((doc) => {
2937
+ if (!isKnownSessionStatus(doc.status)) throw new RegisterNeedsUpgradeError();
2878
2938
  if (doc.status === "closed" && status === "closed") return doc;
2879
2939
  if (doc.status === "closed") throw new RegisterSessionClosedError();
2880
2940
  if (!TRANSITIONS[doc.status].includes(status)) throw new Error(`invalid_session_transition:${doc.status}->${status}`);
@@ -3611,6 +3671,7 @@ function closureCommand(c) {
3611
3671
  }
3612
3672
  var chains = /* @__PURE__ */ new WeakMap();
3613
3673
  function reconcileRegisterCommands({ commands, sessions, movements, closures, host, storeKey, registerId, now, observed }) {
3674
+ const sentStatus = (s) => s.status === "open" || s.status === "counting" || s.status === "closed";
3614
3675
  let registers = chains.get(commands);
3615
3676
  if (!registers) chains.set(commands, registers = /* @__PURE__ */ new Map());
3616
3677
  const run = (registers.get(registerId) ?? Promise.resolve()).then(async () => {
@@ -3625,12 +3686,12 @@ function reconcileRegisterCommands({ commands, sessions, movements, closures, ho
3625
3686
  readFresh(movements, { selector: { session_id: session.id } }),
3626
3687
  session.closure_id ? closures.storageInstance.findDocumentsById([session.closure_id], false) : Promise.resolve([])
3627
3688
  ]);
3628
- const events = (observed ?? []).filter((row) => row.id === session.id && !transitioned.has(session.id) && row.status !== session.status && row.status !== "closed").flatMap((row) => row.status_at == null ? [] : [sessionTransitionCommand({ ...row, status_at: row.status_at })]);
3689
+ const events = (observed ?? []).filter((row) => row.id === session.id && !transitioned.has(session.id) && row.status !== session.status && row.status !== "closed").flatMap((row) => row.status_at == null || !sentStatus(row) ? [] : [sessionTransitionCommand({ ...row, status_at: row.status_at })]);
3629
3690
  events.push(
3630
3691
  ...entries.filter((entry) => entry.type !== "void").map(movementCommand),
3631
3692
  ...entries.filter((entry) => entry.type === "void").map(movementCommand)
3632
3693
  );
3633
- if (session.status_at != null) events.push(sessionTransitionCommand({ ...session, status_at: session.status_at }));
3694
+ if (session.status_at != null && sentStatus(session)) events.push(sessionTransitionCommand({ ...session, status_at: session.status_at }));
3634
3695
  return { session, number: closureRows[0]?.number, built: [
3635
3696
  sessionOpenCommand(session),
3636
3697
  ...events,
@@ -3699,6 +3760,7 @@ var closedFacts = /* @__PURE__ */ new Set();
3699
3760
  var reservationOf = (register, storeKey, registerId) => register?.stores[storeKey]?.registers?.[registerId]?.closure_reservation;
3700
3761
  var unwritten = (row, closureRows) => row.status === "closed" && row.closure_id === row.id && !closureRows.some((closure) => closure.id === row.id);
3701
3762
  function currentSession(rows, closureRows, reservation) {
3763
+ if (rows.some((row) => !isKnownSessionStatus(row.status))) return null;
3702
3764
  return (reservation && !reservation.applied ? rows.find((row) => row.id === reservation.row.session_id) : void 0) ?? rows.find((row) => row.status !== "closed") ?? rows.find((row) => unwritten(row, closureRows)) ?? null;
3703
3765
  }
3704
3766
  function ledgerRows(orders) {
@@ -3825,6 +3887,8 @@ function useRegisterSession(options) {
3825
3887
  const nameOf = (id2) => !id2 ? "" : id2 === actor.id ? actor.name : labels?.resolveCashierName?.(id2) ?? "";
3826
3888
  return {
3827
3889
  session,
3890
+ /** Any unknown session status in the current snapshot means the register needs upgrade. */
3891
+ needsUpgrade: data?.rows.some((row) => !isKnownSessionStatus(row.status)) ?? false,
3828
3892
  movements: entries,
3829
3893
  expected,
3830
3894
  salesCount: sales.length,
@@ -3847,6 +3911,7 @@ function useRegisterSession(options) {
3847
3911
  * Refuses with `RegisterSessionAlreadyOpenError` while the register has a live session or
3848
3912
  * another open is running (in any hook instance), and with `RegisterCloseIncompleteError`
3849
3913
  * while an earlier close hasn't finished: a new session would lock the till behind it.
3914
+ * An unknown stored status refuses with `RegisterNeedsUpgradeError`.
3850
3915
  */
3851
3916
  openSession: async (input) => {
3852
3917
  const key = registerId ?? "";
@@ -3854,6 +3919,7 @@ function useRegisterSession(options) {
3854
3919
  const run = (async () => {
3855
3920
  const { sessions: sessions2, closures: closures2, registerId: registerId2, register: register2 } = live();
3856
3921
  const rows = await readFresh(sessions2, { selector: { register_id: registerId2 } });
3922
+ if (rows.some((row2) => !isKnownSessionStatus(row2.status))) throw new RegisterNeedsUpgradeError();
3857
3923
  if (rows.some((row2) => row2.status !== "closed")) throw new RegisterSessionAlreadyOpenError();
3858
3924
  const closureRows = await readFresh(closures2, { selector: { register_id: registerId2 } });
3859
3925
  const reservation = reservationOf(await readRegister(register2), storeKey, registerId2);
@@ -5784,14 +5850,17 @@ export {
5784
5850
  PRODUCT_SORT_FIELDS,
5785
5851
  PosOrderOpenClosedError,
5786
5852
  RECEIPT_SCHEMA_VERSION,
5853
+ REGISTER_SESSION_MIGRATION_CLOSE_WAIT_MS,
5787
5854
  RegisterApprovalRequiredError,
5788
5855
  RegisterCloseIncompleteError,
5789
5856
  RegisterIdInvalidError,
5790
5857
  RegisterMovementAmountError,
5791
5858
  RegisterMovementReasonError,
5792
5859
  RegisterMovementStrandedError,
5860
+ RegisterNeedsUpgradeError,
5793
5861
  RegisterSessionAlreadyOpenError,
5794
5862
  RegisterSessionClosedError,
5863
+ RegisterSessionOpenClosedError,
5795
5864
  RegisterSessionRequiredError,
5796
5865
  RegisterTenderInProgressError,
5797
5866
  SALE_SAVING,
@@ -5801,6 +5870,7 @@ export {
5801
5870
  activePlan,
5802
5871
  addEntryToCart,
5803
5872
  addPosOrderCollection,
5873
+ addRegisterSessionCollection,
5804
5874
  advancePerpetual,
5805
5875
  allocateOrderDiscount,
5806
5876
  appliedMinor,
@@ -5847,6 +5917,7 @@ export {
5847
5917
  inCategory,
5848
5918
  initTenderState,
5849
5919
  initialTenderState,
5920
+ isKnownSessionStatus,
5850
5921
  isServerDecimal,
5851
5922
  labelKeys,
5852
5923
  listCategories,
@@ -5887,6 +5958,7 @@ export {
5887
5958
  registerFactsLogger,
5888
5959
  registerSessionCollection,
5889
5960
  registerSessionSchema,
5961
+ registerSessionsLogger,
5890
5962
  requireOpenSession,
5891
5963
  resolveGridColumns,
5892
5964
  resolveStoreSettings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/pos",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "type": "module",
5
5
  "description": "Shared POS business logic for Tally UI — tax, orders, payments, discounts, receipts",
6
6
  "main": "./dist/index.js",
@@ -27,20 +27,20 @@
27
27
  "directory": "packages/pos"
28
28
  },
29
29
  "peerDependencies": {
30
- "@tallyui/core": "^3.6.0",
30
+ "@tallyui/core": "^3.7.0",
31
31
  "react": ">=18",
32
32
  "rxdb": "~17.5.0",
33
33
  "rxjs": ">=7"
34
34
  },
35
35
  "devDependencies": {
36
- "@tallyui/core": "3.6.0",
36
+ "@tallyui/core": "3.7.0",
37
37
  "@types/react": ">=18",
38
38
  "react": "^19.2.4",
39
39
  "rxdb": "17.5.0",
40
40
  "rxjs": "7.8.2",
41
- "@tallyui/connector-medusa": "3.6.0",
42
- "@tallyui/connector-woocommerce": "3.6.0",
43
- "@tallyui/database": "3.6.0"
41
+ "@tallyui/connector-medusa": "3.7.0",
42
+ "@tallyui/connector-woocommerce": "3.7.0",
43
+ "@tallyui/database": "3.7.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",
package/src/index.ts CHANGED
@@ -76,12 +76,13 @@ export type { PaymentTransport, TenderView, TenderLineId, TenderPlan, SplitTab,
76
76
  export { normalizeAmount, isServerDecimal, movementFieldError, deriveExpected, parseMinor, validAmount, countVariance, overThreshold, closeNeedsApproval, denominationTotal, varianceText, denominations } from './register';
77
77
  export type { MovementType, LedgerRow, Movement } from './register';
78
78
  export { registerSessionSchema, registerSessionCollection, cashMovementSchema, closureSchema } from './register';
79
+ export { addRegisterSessionCollection, RegisterSessionOpenClosedError, registerSessionsLogger, REGISTER_SESSION_MIGRATION_CLOSE_WAIT_MS } from './register';
79
80
  export type { RegisterSession, CashMovement, Closure } from './register';
80
81
  export { registerCommandSchema, registerCommandCollection, registerCommandsLogger, sessionOpenCommand, sessionTransitionCommand, movementCommand, closureCommand, reconcileRegisterCommands } from './register';
81
82
  export type { RegisterCommand, RegisterCommandCollection } from './register';
82
83
  export { mintUuid, readRegister, ensureRegister, observeRegister$, getBoundRegisterId, readBoundRegister, bindRegister, unbindRegister, nextSaleCounter, mintClosureNumber, advancePerpetual, RegisterIdInvalidError } from './register';
83
84
  export type { RegisterHost, RegisterCounters, RegisterBucket, RegisterStore, RegisterDocument } from './register';
84
- export { RegisterSessionRequiredError, RegisterSessionClosedError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, openSessionSelector, requireOpenSession, stampSession, openSession, startCounting, backToSelling, closeSession, recordMovement, voidMovement, writeClosure } from './register';
85
+ export { RegisterSessionRequiredError, RegisterSessionClosedError, RegisterNeedsUpgradeError, isKnownSessionStatus, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, openSessionSelector, requireOpenSession, stampSession, openSession, startCounting, backToSelling, closeSession, recordMovement, voidMovement, writeClosure } from './register';
85
86
  export type { RegisterSessionCollection, CashMovementCollection, ClosureCollection } from './register';
86
87
  export { deriveSettled, exportCsv, labelKeys, clampClosureScope, selectClosureRows } from './register';
87
88
  export type { Correction, RecordedFigures, ClosureScope } from './register';