@tallyui/pos 3.5.3 → 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 +113 -32
- package/dist/index.js +179 -38
- package/package.json +6 -6
- package/src/index.ts +6 -5
- package/src/outbox/register-outbox.ts +9 -4
- package/src/pos-order/needs-attention.ts +2 -1
- package/src/pos-order/open.ts +10 -234
- package/src/receipt/build-receipt-data.ts +9 -0
- package/src/receipt/index.ts +2 -1
- package/src/receipt/types.ts +22 -0
- package/src/register/index.ts +2 -1
- package/src/register/open.ts +30 -0
- package/src/register/register-commands.ts +5 -3
- package/src/register/register-document.ts +1 -1
- package/src/register/schemas.ts +27 -9
- package/src/register/session-store.ts +20 -1
- package/src/register/use-register-session.ts +7 -2
- package/src/rxdb/open-migrated-collection.ts +256 -0
- package/src/tender/index.ts +2 -0
- package/src/tender/tender-void.ts +66 -0
package/dist/index.js
CHANGED
|
@@ -1364,6 +1364,9 @@ function useParkedSales(drafts) {
|
|
|
1364
1364
|
return { parked, discard };
|
|
1365
1365
|
}
|
|
1366
1366
|
|
|
1367
|
+
// src/receipt/types.ts
|
|
1368
|
+
var RECEIPT_SCHEMA_VERSION = "1.4.0";
|
|
1369
|
+
|
|
1367
1370
|
// src/receipt/build-receipt-data.ts
|
|
1368
1371
|
function buildReceiptData(order, config) {
|
|
1369
1372
|
const taxedLines = [...order.lineItems.map((line) => order.taxRounding?.granularity === "woocommerce" && line.netMicros !== void 0 ? { ...line, taxInclusive: order.pricesIncludeTax } : line), ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({
|
|
@@ -1387,6 +1390,23 @@ function buildReceiptData(order, config) {
|
|
|
1387
1390
|
const label = (d) => d.label ?? d.couponCode ?? `${d.type} discount`;
|
|
1388
1391
|
const customer = order.customer?.name || order.customer?.email;
|
|
1389
1392
|
return {
|
|
1393
|
+
schemaVersion: RECEIPT_SCHEMA_VERSION,
|
|
1394
|
+
software: {
|
|
1395
|
+
name: "WCPOS",
|
|
1396
|
+
plugin_version: "",
|
|
1397
|
+
app_version: "",
|
|
1398
|
+
app_build: "",
|
|
1399
|
+
platform: "",
|
|
1400
|
+
...config.software
|
|
1401
|
+
},
|
|
1402
|
+
register: { id: config.register ?? "", name: config.registerName ?? "" },
|
|
1403
|
+
fiscal: {
|
|
1404
|
+
document_type: "sale",
|
|
1405
|
+
is_reprint: false,
|
|
1406
|
+
reprint_count: 0,
|
|
1407
|
+
qr_payload: "",
|
|
1408
|
+
...config.fiscal
|
|
1409
|
+
},
|
|
1390
1410
|
header: {
|
|
1391
1411
|
storeName: config.storeName,
|
|
1392
1412
|
storeAddress: config.storeAddress,
|
|
@@ -2101,7 +2121,7 @@ function posOrderCollection() {
|
|
|
2101
2121
|
};
|
|
2102
2122
|
}
|
|
2103
2123
|
|
|
2104
|
-
// src/
|
|
2124
|
+
// src/rxdb/open-migrated-collection.ts
|
|
2105
2125
|
import {
|
|
2106
2126
|
deepEqual,
|
|
2107
2127
|
defaultConflictHandler,
|
|
@@ -2115,16 +2135,6 @@ import {
|
|
|
2115
2135
|
} from "rxdb";
|
|
2116
2136
|
import { getOldCollectionMeta, migrateDocumentData } from "rxdb/plugins/migration-schema";
|
|
2117
2137
|
import { Subject, takeUntil } from "rxjs";
|
|
2118
|
-
var posOrdersLogger = createLogger("pos-orders");
|
|
2119
|
-
var POS_ORDER_MIGRATION_CLOSE_WAIT_MS = 1e4;
|
|
2120
|
-
var PosOrderOpenClosedError = class extends Error {
|
|
2121
|
-
constructor(databaseName) {
|
|
2122
|
-
super(`addPosOrderCollection: database ${databaseName} closed during the open; reopen it and open pos_orders again`);
|
|
2123
|
-
this.databaseName = databaseName;
|
|
2124
|
-
this.name = "PosOrderOpenClosedError";
|
|
2125
|
-
}
|
|
2126
|
-
code = "POS_ORDER_OPEN_CLOSED";
|
|
2127
|
-
};
|
|
2128
2138
|
var latestOpens = /* @__PURE__ */ new WeakMap();
|
|
2129
2139
|
var closedUnderOpen = /* @__PURE__ */ new WeakSet();
|
|
2130
2140
|
function waitWithTimeout(promise, ms) {
|
|
@@ -2137,35 +2147,40 @@ function waitWithTimeout(promise, ms) {
|
|
|
2137
2147
|
});
|
|
2138
2148
|
}
|
|
2139
2149
|
async function writeOverStaleCopies(collection, from, to, stopIfClosing) {
|
|
2150
|
+
const primaryKey = collection.schema.primaryPath;
|
|
2140
2151
|
const handler = rxStorageInstanceToReplicationHandler(to, defaultConflictHandler, collection.database.token, true);
|
|
2141
2152
|
for (let page = await getChangedDocumentsSince(from, 200); page.documents.length > 0; page = await getChangedDocumentsSince(from, 200, page.checkpoint)) {
|
|
2142
2153
|
stopIfClosing();
|
|
2143
|
-
const copies = new Map((await to.findDocumentsById(page.documents.map((doc) => doc
|
|
2144
|
-
const rows = await Promise.all(page.documents.filter((doc) => copies.has(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) })));
|
|
2145
2156
|
const stale = rows.filter((row) => row.newDocumentState && !deepEqual(row.assumedMasterState, row.newDocumentState));
|
|
2146
2157
|
if (stale.length > 0) await handler.masterWrite(stale);
|
|
2147
2158
|
}
|
|
2148
2159
|
}
|
|
2149
|
-
async function
|
|
2150
|
-
|
|
2151
|
-
if (db.
|
|
2152
|
-
|
|
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);
|
|
2153
2167
|
if (!previous) {
|
|
2154
|
-
db.onClose.push(() => waitWithTimeout(latestOpens.get(db), closeWaitMs).then((settled) => {
|
|
2168
|
+
db.onClose.push(() => waitWithTimeout(latestOpens.get(db).get(name), closeWaitMs).then((settled) => {
|
|
2155
2169
|
if (!settled) closedUnderOpen.add(db);
|
|
2156
2170
|
}));
|
|
2157
2171
|
}
|
|
2158
|
-
const opening =
|
|
2159
|
-
|
|
2172
|
+
const opening = openCollection(db, opener, previous);
|
|
2173
|
+
opens.set(name, Promise.all([previous, opening.catch(() => void 0)]));
|
|
2160
2174
|
return opening;
|
|
2161
2175
|
}
|
|
2162
|
-
async function
|
|
2176
|
+
async function openCollection(db, opener, previous) {
|
|
2177
|
+
const { name, creator, logger, closedError } = opener;
|
|
2163
2178
|
let cancelled = false;
|
|
2164
2179
|
const closing = () => cancelled || closedUnderOpen.has(db) || db.closed;
|
|
2165
2180
|
const stopIfClosing = () => {
|
|
2166
|
-
if (closing()) throw
|
|
2181
|
+
if (closing()) throw closedError(db.name);
|
|
2167
2182
|
};
|
|
2168
|
-
const {
|
|
2183
|
+
const { [name]: collection } = await db.addCollections({ [name]: { ...creator(), autoMigrate: false } });
|
|
2169
2184
|
const state = collection.getMigrationState();
|
|
2170
2185
|
try {
|
|
2171
2186
|
stopIfClosing();
|
|
@@ -2188,7 +2203,7 @@ async function openPosOrders(db, previous) {
|
|
|
2188
2203
|
stopIfClosing();
|
|
2189
2204
|
const checkpoint = await db.storage.createStorageInstance({
|
|
2190
2205
|
databaseName: db.name,
|
|
2191
|
-
collectionName: `rx-migration-state-meta
|
|
2206
|
+
collectionName: `rx-migration-state-meta-${name}-${old.version}`,
|
|
2192
2207
|
databaseInstanceToken: db.token,
|
|
2193
2208
|
multiInstance: db.multiInstance,
|
|
2194
2209
|
options: {},
|
|
@@ -2214,7 +2229,7 @@ async function openPosOrders(db, previous) {
|
|
|
2214
2229
|
} });
|
|
2215
2230
|
const internalStore = gate(db.internalStore, async (key, first) => {
|
|
2216
2231
|
const ids = key === "bulkWrite" ? first.map((row) => row.document.id) : first;
|
|
2217
|
-
|
|
2232
|
+
logger.warn(
|
|
2218
2233
|
`The database closed during the migration: dropped a ${key === "bulkWrite" ? "status write" : "status read"}`,
|
|
2219
2234
|
{ database: db.name, ids }
|
|
2220
2235
|
);
|
|
@@ -2224,7 +2239,7 @@ async function openPosOrders(db, previous) {
|
|
|
2224
2239
|
const migrateStorage = state.migrateStorage.bind(state);
|
|
2225
2240
|
state.migrateStorage = async (from, current, batchSize) => {
|
|
2226
2241
|
stopIfClosing();
|
|
2227
|
-
const to = gate(current, () => Promise.reject(
|
|
2242
|
+
const to = gate(current, () => Promise.reject(closedError(db.name)));
|
|
2228
2243
|
if (from.collectionName === collection.name) await writeOverStaleCopies(collection, from, to, stopIfClosing);
|
|
2229
2244
|
stopIfClosing();
|
|
2230
2245
|
return migrateStorage(new Proxy(from, { get: (target, key) => {
|
|
@@ -2259,8 +2274,29 @@ async function openPosOrders(db, previous) {
|
|
|
2259
2274
|
throw newRxError("DM4", { collection: collection.name, error: status?.error });
|
|
2260
2275
|
} catch (error) {
|
|
2261
2276
|
await collection.close();
|
|
2262
|
-
throw closing() ?
|
|
2277
|
+
throw closing() ? closedError(db.name) : error;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
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";
|
|
2263
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);
|
|
2264
2300
|
}
|
|
2265
2301
|
|
|
2266
2302
|
// src/pos-order/device-id.ts
|
|
@@ -2282,7 +2318,7 @@ function getDeviceId(storage, key) {
|
|
|
2282
2318
|
// src/pos-order/needs-attention.ts
|
|
2283
2319
|
import { knownWarnings } from "@tallyui/core";
|
|
2284
2320
|
function needsAttention(orders, options = {}) {
|
|
2285
|
-
return orders.filter((order) => order.syncStatus === "rejected" || (order.localWarnings?.length ?? 0) > 0 || order.syncStatus === "applied" && knownWarnings(order.warnings).
|
|
2321
|
+
return orders.filter((order) => order.syncStatus === "rejected" || (order.localWarnings?.length ?? 0) > 0 || order.syncStatus === "applied" && knownWarnings(order.warnings).some((warning) => warning.code !== "register_session_unknown") || order.lateSessionId !== void 0 || order.syncStatus === "pending" && options.stuckCommandIds?.includes(order.commandId) === true).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
2286
2322
|
}
|
|
2287
2323
|
|
|
2288
2324
|
// src/pos-order/same-sale.ts
|
|
@@ -2375,6 +2411,7 @@ var denominations = {
|
|
|
2375
2411
|
// src/register/schemas.ts
|
|
2376
2412
|
import { addRxPlugin as addRxPlugin2 } from "rxdb";
|
|
2377
2413
|
import { RxDBLocalDocumentsPlugin } from "rxdb/plugins/local-documents";
|
|
2414
|
+
import { RxDBMigrationSchemaPlugin as RxDBMigrationSchemaPlugin2 } from "rxdb/plugins/migration-schema";
|
|
2378
2415
|
var id = { type: "string", maxLength: 36 };
|
|
2379
2416
|
var text = { type: "string" };
|
|
2380
2417
|
var nullableText = { type: ["string", "null"] };
|
|
@@ -2383,7 +2420,7 @@ var nullableMinor = { type: ["integer", "null"] };
|
|
|
2383
2420
|
var tenders = { type: "object", additionalProperties: minor };
|
|
2384
2421
|
var registerSessionSchema = {
|
|
2385
2422
|
title: "Register sessions",
|
|
2386
|
-
version:
|
|
2423
|
+
version: 1,
|
|
2387
2424
|
primaryKey: "id",
|
|
2388
2425
|
type: "object",
|
|
2389
2426
|
additionalProperties: false,
|
|
@@ -2391,7 +2428,8 @@ var registerSessionSchema = {
|
|
|
2391
2428
|
id,
|
|
2392
2429
|
register_id: id,
|
|
2393
2430
|
store_key: nullableText,
|
|
2394
|
-
status: { type: "string",
|
|
2431
|
+
status: { type: "string", maxLength: 20 },
|
|
2432
|
+
server_session_id: { ...nullableText, default: null },
|
|
2395
2433
|
business_day: { type: "string", maxLength: 10 },
|
|
2396
2434
|
opened_at_gmt: text,
|
|
2397
2435
|
opened_by: nullableText,
|
|
@@ -2415,9 +2453,16 @@ var registerSessionSchema = {
|
|
|
2415
2453
|
required: ["id", "register_id", "status", "opened_at_gmt", "counted_float_minor"],
|
|
2416
2454
|
indexes: [["register_id", "status"]]
|
|
2417
2455
|
};
|
|
2418
|
-
function
|
|
2456
|
+
function registerSessionCreator() {
|
|
2419
2457
|
addRxPlugin2(RxDBLocalDocumentsPlugin);
|
|
2420
|
-
|
|
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).");
|
|
2421
2466
|
}
|
|
2422
2467
|
var cashMovementSchema = {
|
|
2423
2468
|
title: "Cash movements",
|
|
@@ -2501,6 +2546,27 @@ var closureSchema = {
|
|
|
2501
2546
|
indexes: [["register_id"], ["session_id"]]
|
|
2502
2547
|
};
|
|
2503
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
|
+
|
|
2504
2570
|
// src/register/register-document.ts
|
|
2505
2571
|
import { map as map4 } from "rxjs";
|
|
2506
2572
|
var REGISTER = "register";
|
|
@@ -2740,6 +2806,13 @@ var RegisterSessionClosedError = class extends Error {
|
|
|
2740
2806
|
this.name = "RegisterSessionClosedError";
|
|
2741
2807
|
}
|
|
2742
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
|
+
};
|
|
2743
2816
|
var RegisterMovementAmountError = class extends Error {
|
|
2744
2817
|
constructor(type, amountMinor) {
|
|
2745
2818
|
super(`movement_amount_invalid:${type}:${amountMinor}`);
|
|
@@ -2765,8 +2838,13 @@ var RegisterMovementStrandedError = class extends Error {
|
|
|
2765
2838
|
var TRANSITIONS = {
|
|
2766
2839
|
open: ["counting", "closed"],
|
|
2767
2840
|
counting: ["open", "closed"],
|
|
2768
|
-
closed: []
|
|
2841
|
+
closed: [],
|
|
2842
|
+
conflict: [],
|
|
2843
|
+
superseded: []
|
|
2769
2844
|
};
|
|
2845
|
+
function isKnownSessionStatus(status) {
|
|
2846
|
+
return Object.hasOwn(TRANSITIONS, status);
|
|
2847
|
+
}
|
|
2770
2848
|
async function readSession(sessions, id2) {
|
|
2771
2849
|
const [stored] = await sessions.storageInstance.findDocumentsById([id2], false);
|
|
2772
2850
|
return stored;
|
|
@@ -2778,6 +2856,7 @@ async function readClosure(closures, id2) {
|
|
|
2778
2856
|
async function requireLiveSession(sessions, id2) {
|
|
2779
2857
|
const session = await readSession(sessions, id2);
|
|
2780
2858
|
if (!session) throw new RegisterSessionRequiredError();
|
|
2859
|
+
if (!isKnownSessionStatus(session.status)) throw new RegisterNeedsUpgradeError();
|
|
2781
2860
|
if (session.status === "closed") throw new RegisterSessionClosedError();
|
|
2782
2861
|
return session;
|
|
2783
2862
|
}
|
|
@@ -2855,6 +2934,7 @@ async function transition(sessions, id2, status, extra = {}) {
|
|
|
2855
2934
|
if (row.status === "closed" && status === "closed") return row;
|
|
2856
2935
|
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2857
2936
|
return row.incrementalModify((doc) => {
|
|
2937
|
+
if (!isKnownSessionStatus(doc.status)) throw new RegisterNeedsUpgradeError();
|
|
2858
2938
|
if (doc.status === "closed" && status === "closed") return doc;
|
|
2859
2939
|
if (doc.status === "closed") throw new RegisterSessionClosedError();
|
|
2860
2940
|
if (!TRANSITIONS[doc.status].includes(status)) throw new Error(`invalid_session_transition:${doc.status}->${status}`);
|
|
@@ -3591,6 +3671,7 @@ function closureCommand(c) {
|
|
|
3591
3671
|
}
|
|
3592
3672
|
var chains = /* @__PURE__ */ new WeakMap();
|
|
3593
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";
|
|
3594
3675
|
let registers = chains.get(commands);
|
|
3595
3676
|
if (!registers) chains.set(commands, registers = /* @__PURE__ */ new Map());
|
|
3596
3677
|
const run = (registers.get(registerId) ?? Promise.resolve()).then(async () => {
|
|
@@ -3605,12 +3686,12 @@ function reconcileRegisterCommands({ commands, sessions, movements, closures, ho
|
|
|
3605
3686
|
readFresh(movements, { selector: { session_id: session.id } }),
|
|
3606
3687
|
session.closure_id ? closures.storageInstance.findDocumentsById([session.closure_id], false) : Promise.resolve([])
|
|
3607
3688
|
]);
|
|
3608
|
-
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 })]);
|
|
3609
3690
|
events.push(
|
|
3610
3691
|
...entries.filter((entry) => entry.type !== "void").map(movementCommand),
|
|
3611
3692
|
...entries.filter((entry) => entry.type === "void").map(movementCommand)
|
|
3612
3693
|
);
|
|
3613
|
-
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 }));
|
|
3614
3695
|
return { session, number: closureRows[0]?.number, built: [
|
|
3615
3696
|
sessionOpenCommand(session),
|
|
3616
3697
|
...events,
|
|
@@ -3679,6 +3760,7 @@ var closedFacts = /* @__PURE__ */ new Set();
|
|
|
3679
3760
|
var reservationOf = (register, storeKey, registerId) => register?.stores[storeKey]?.registers?.[registerId]?.closure_reservation;
|
|
3680
3761
|
var unwritten = (row, closureRows) => row.status === "closed" && row.closure_id === row.id && !closureRows.some((closure) => closure.id === row.id);
|
|
3681
3762
|
function currentSession(rows, closureRows, reservation) {
|
|
3763
|
+
if (rows.some((row) => !isKnownSessionStatus(row.status))) return null;
|
|
3682
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;
|
|
3683
3765
|
}
|
|
3684
3766
|
function ledgerRows(orders) {
|
|
@@ -3805,6 +3887,8 @@ function useRegisterSession(options) {
|
|
|
3805
3887
|
const nameOf = (id2) => !id2 ? "" : id2 === actor.id ? actor.name : labels?.resolveCashierName?.(id2) ?? "";
|
|
3806
3888
|
return {
|
|
3807
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,
|
|
3808
3892
|
movements: entries,
|
|
3809
3893
|
expected,
|
|
3810
3894
|
salesCount: sales.length,
|
|
@@ -3827,6 +3911,7 @@ function useRegisterSession(options) {
|
|
|
3827
3911
|
* Refuses with `RegisterSessionAlreadyOpenError` while the register has a live session or
|
|
3828
3912
|
* another open is running (in any hook instance), and with `RegisterCloseIncompleteError`
|
|
3829
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`.
|
|
3830
3915
|
*/
|
|
3831
3916
|
openSession: async (input) => {
|
|
3832
3917
|
const key = registerId ?? "";
|
|
@@ -3834,6 +3919,7 @@ function useRegisterSession(options) {
|
|
|
3834
3919
|
const run = (async () => {
|
|
3835
3920
|
const { sessions: sessions2, closures: closures2, registerId: registerId2, register: register2 } = live();
|
|
3836
3921
|
const rows = await readFresh(sessions2, { selector: { register_id: registerId2 } });
|
|
3922
|
+
if (rows.some((row2) => !isKnownSessionStatus(row2.status))) throw new RegisterNeedsUpgradeError();
|
|
3837
3923
|
if (rows.some((row2) => row2.status !== "closed")) throw new RegisterSessionAlreadyOpenError();
|
|
3838
3924
|
const closureRows = await readFresh(closures2, { selector: { register_id: registerId2 } });
|
|
3839
3925
|
const reservation = reservationOf(await readRegister(register2), storeKey, registerId2);
|
|
@@ -5086,6 +5172,7 @@ function createOrderOutbox(options) {
|
|
|
5086
5172
|
|
|
5087
5173
|
// src/outbox/register-outbox.ts
|
|
5088
5174
|
import { BehaviorSubject as BehaviorSubject5 } from "rxjs";
|
|
5175
|
+
var NON_BLOCKING_REJECTIONS = /* @__PURE__ */ new Set(["register_session_superseded", "register_session_abandoned"]);
|
|
5089
5176
|
function plainCommand({
|
|
5090
5177
|
key,
|
|
5091
5178
|
registerId,
|
|
@@ -5165,8 +5252,9 @@ function createRegisterOutbox(options) {
|
|
|
5165
5252
|
selector: { syncStatus: { $in: ["pending", "rejected"] } },
|
|
5166
5253
|
sort: [{ seq: "asc" }, { key: "asc" }]
|
|
5167
5254
|
})) {
|
|
5168
|
-
if (command.syncStatus === "rejected")
|
|
5169
|
-
|
|
5255
|
+
if (command.syncStatus === "rejected") {
|
|
5256
|
+
if (!NON_BLOCKING_REJECTIONS.has(command.error?.code ?? "")) blocked.add(command.registerId);
|
|
5257
|
+
} else if (!blocked.has(command.registerId)) sendable2.add(command.commandId);
|
|
5170
5258
|
}
|
|
5171
5259
|
for (const id2 of clocks.keys()) if (!sendable2.has(id2)) clocks.delete(id2);
|
|
5172
5260
|
state$.next({ ...state$.value, ...patch, pending, stuck: stuckState() });
|
|
@@ -5202,9 +5290,10 @@ function createRegisterOutbox(options) {
|
|
|
5202
5290
|
});
|
|
5203
5291
|
const commands = [];
|
|
5204
5292
|
for (const command of ledger) {
|
|
5293
|
+
if (command.syncStatus === "rejected" && NON_BLOCKING_REJECTIONS.has(command.error?.code ?? "")) continue;
|
|
5205
5294
|
if (command.syncStatus === "rejected") break;
|
|
5206
5295
|
commands.push(command);
|
|
5207
|
-
if (commands.length >= batchSize) break;
|
|
5296
|
+
if (command.type === "register.session.open" || commands.length >= batchSize) break;
|
|
5208
5297
|
}
|
|
5209
5298
|
if (stopped) {
|
|
5210
5299
|
stoppedDuringRun = true;
|
|
@@ -5707,6 +5796,49 @@ function planLegs(plan, rowsSinceFrom, balanceMinor) {
|
|
|
5707
5796
|
const title = plan.kind === "fixed" && taken === 0 ? plan.title : null;
|
|
5708
5797
|
return { legs, thisPaymentMinor: each, label: { n: taken + 1, ways, rest, title } };
|
|
5709
5798
|
}
|
|
5799
|
+
|
|
5800
|
+
// src/tender/tender-void.ts
|
|
5801
|
+
var tenderVoidSchema = {
|
|
5802
|
+
title: "Tender voids",
|
|
5803
|
+
version: 0,
|
|
5804
|
+
primaryKey: "paymentId",
|
|
5805
|
+
type: "object",
|
|
5806
|
+
additionalProperties: false,
|
|
5807
|
+
properties: {
|
|
5808
|
+
paymentId: { type: "string", maxLength: 64 },
|
|
5809
|
+
saleId: { type: "string", maxLength: 64 },
|
|
5810
|
+
type: { type: "string", enum: ["void"], maxLength: 4 },
|
|
5811
|
+
// Not an enum: PaymentMethodKind grows, and a stored enum would need a schema migration per new kind.
|
|
5812
|
+
method: { type: "string", maxLength: 32 },
|
|
5813
|
+
amountMinor: { type: "integer" },
|
|
5814
|
+
tenderedMinor: { type: "integer" },
|
|
5815
|
+
reference: { type: "string" },
|
|
5816
|
+
currency: { type: "string", maxLength: 3 },
|
|
5817
|
+
reason: { type: "string", enum: ["removed", "cancelled"] },
|
|
5818
|
+
deviceTime: { type: "string", maxLength: 40 },
|
|
5819
|
+
deviceTz: { type: "string" },
|
|
5820
|
+
registerId: { type: "string" },
|
|
5821
|
+
sessionId: { type: "string" },
|
|
5822
|
+
cashierRef: { type: "string" }
|
|
5823
|
+
},
|
|
5824
|
+
required: [
|
|
5825
|
+
"paymentId",
|
|
5826
|
+
"saleId",
|
|
5827
|
+
"type",
|
|
5828
|
+
"method",
|
|
5829
|
+
"amountMinor",
|
|
5830
|
+
"currency",
|
|
5831
|
+
"reason",
|
|
5832
|
+
"deviceTime",
|
|
5833
|
+
"deviceTz",
|
|
5834
|
+
"registerId",
|
|
5835
|
+
"cashierRef"
|
|
5836
|
+
],
|
|
5837
|
+
indexes: [["saleId"], ["deviceTime"]]
|
|
5838
|
+
};
|
|
5839
|
+
function tenderVoidCollection() {
|
|
5840
|
+
return { schema: tenderVoidSchema };
|
|
5841
|
+
}
|
|
5710
5842
|
export {
|
|
5711
5843
|
CartError,
|
|
5712
5844
|
CurrencyProvider,
|
|
@@ -5717,14 +5849,18 @@ export {
|
|
|
5717
5849
|
OrderContentMismatchError,
|
|
5718
5850
|
PRODUCT_SORT_FIELDS,
|
|
5719
5851
|
PosOrderOpenClosedError,
|
|
5852
|
+
RECEIPT_SCHEMA_VERSION,
|
|
5853
|
+
REGISTER_SESSION_MIGRATION_CLOSE_WAIT_MS,
|
|
5720
5854
|
RegisterApprovalRequiredError,
|
|
5721
5855
|
RegisterCloseIncompleteError,
|
|
5722
5856
|
RegisterIdInvalidError,
|
|
5723
5857
|
RegisterMovementAmountError,
|
|
5724
5858
|
RegisterMovementReasonError,
|
|
5725
5859
|
RegisterMovementStrandedError,
|
|
5860
|
+
RegisterNeedsUpgradeError,
|
|
5726
5861
|
RegisterSessionAlreadyOpenError,
|
|
5727
5862
|
RegisterSessionClosedError,
|
|
5863
|
+
RegisterSessionOpenClosedError,
|
|
5728
5864
|
RegisterSessionRequiredError,
|
|
5729
5865
|
RegisterTenderInProgressError,
|
|
5730
5866
|
SALE_SAVING,
|
|
@@ -5734,6 +5870,7 @@ export {
|
|
|
5734
5870
|
activePlan,
|
|
5735
5871
|
addEntryToCart,
|
|
5736
5872
|
addPosOrderCollection,
|
|
5873
|
+
addRegisterSessionCollection,
|
|
5737
5874
|
advancePerpetual,
|
|
5738
5875
|
allocateOrderDiscount,
|
|
5739
5876
|
appliedMinor,
|
|
@@ -5780,6 +5917,7 @@ export {
|
|
|
5780
5917
|
inCategory,
|
|
5781
5918
|
initTenderState,
|
|
5782
5919
|
initialTenderState,
|
|
5920
|
+
isKnownSessionStatus,
|
|
5783
5921
|
isServerDecimal,
|
|
5784
5922
|
labelKeys,
|
|
5785
5923
|
listCategories,
|
|
@@ -5820,6 +5958,7 @@ export {
|
|
|
5820
5958
|
registerFactsLogger,
|
|
5821
5959
|
registerSessionCollection,
|
|
5822
5960
|
registerSessionSchema,
|
|
5961
|
+
registerSessionsLogger,
|
|
5823
5962
|
requireOpenSession,
|
|
5824
5963
|
resolveGridColumns,
|
|
5825
5964
|
resolveStoreSettings,
|
|
@@ -5842,6 +5981,8 @@ export {
|
|
|
5842
5981
|
taxMicros,
|
|
5843
5982
|
taxProviderProps,
|
|
5844
5983
|
tenderReducer,
|
|
5984
|
+
tenderVoidCollection,
|
|
5985
|
+
tenderVoidSchema,
|
|
5845
5986
|
toOrderCreateEnvelope,
|
|
5846
5987
|
unbindRegister,
|
|
5847
5988
|
useCurrencyCode,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tallyui/pos",
|
|
3
|
-
"version": "3.
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
-
"@tallyui/connector-woocommerce": "3.
|
|
43
|
-
"@tallyui/database": "3.
|
|
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
|
@@ -51,8 +51,8 @@ export type {
|
|
|
51
51
|
} from './order';
|
|
52
52
|
|
|
53
53
|
// Receipt
|
|
54
|
-
export { buildReceiptData } from './receipt';
|
|
55
|
-
export type { ReceiptData, ReceiptLineItem, ReceiptConfig } from './receipt';
|
|
54
|
+
export { buildReceiptData, RECEIPT_SCHEMA_VERSION } from './receipt';
|
|
55
|
+
export type { ReceiptData, ReceiptLineItem, ReceiptConfig, ReceiptSoftware, ReceiptRegister, ReceiptFiscal } from './receipt';
|
|
56
56
|
|
|
57
57
|
// Product
|
|
58
58
|
export { searchProducts, sortProducts, productSortValue, PRODUCT_SORT_FIELDS, type ProductSort, type ProductSortValue, catalogueViewReducer, normalizeCatalogueViewState, resolveGridColumns, DEFAULT_CATALOGUE_VIEW_STATE, type CatalogueView, type CatalogueGridColumns, type CatalogueViewState, type CatalogueViewAction, withStockOverlay, getProductStock, stockOverlay$, stockOverlayAsOf$, productCategories, listCategories, inCategory } from './product';
|
|
@@ -69,19 +69,20 @@ export type { RegisterOutboxOptions, RegisterOutbox, UseRegisterOutboxOptions, U
|
|
|
69
69
|
export { createBackendNotFound } from './outbox/backend-not-found';
|
|
70
70
|
export type { BackendNotFound } from './outbox/backend-not-found';
|
|
71
71
|
export type { HttpTransportOptions, OrderOutboxOptions, OrderOutbox, TransportOutcome, CommandTransport, OrderTransportContext, OutboxState, UseOrderOutboxOptions, UseOrderOutboxResult } from './outbox';
|
|
72
|
-
export { tenderReducer, initTenderState, initialTenderState, appliedMinor, changeMinor, quickTenderedAmounts, evenSplitShareMinor, activePlan, planLegs, MAX_TENDER_MINOR } from './tender';
|
|
73
|
-
export type { PaymentTransport, TenderView, TenderLineId, TenderPlan, SplitTab, TenderState, TenderKey, TenderAction, PlanLeg } from './tender';
|
|
72
|
+
export { tenderReducer, initTenderState, initialTenderState, appliedMinor, changeMinor, quickTenderedAmounts, evenSplitShareMinor, activePlan, planLegs, MAX_TENDER_MINOR, tenderVoidSchema, tenderVoidCollection } from './tender';
|
|
73
|
+
export type { PaymentTransport, TenderView, TenderLineId, TenderPlan, SplitTab, TenderState, TenderKey, TenderAction, PlanLeg, TenderVoid } from './tender';
|
|
74
74
|
|
|
75
75
|
// Register
|
|
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';
|
|
@@ -7,6 +7,9 @@ import { createBackendNotFound, type BackendNotFound } from './backend-not-found
|
|
|
7
7
|
import { STUCK_AFTER_MS } from './order-outbox';
|
|
8
8
|
import type { CommandTransport, OutboxState } from './types';
|
|
9
9
|
|
|
10
|
+
/** Final rejections that do not stop their register's ledger (ADR-078): the commands behind them are still sent. */
|
|
11
|
+
export const NON_BLOCKING_REJECTIONS: ReadonlySet<string> = new Set(['register_session_superseded', 'register_session_abandoned']);
|
|
12
|
+
|
|
10
13
|
export interface RegisterOutboxOptions {
|
|
11
14
|
collection: RxCollection<RegisterCommand>;
|
|
12
15
|
transport: CommandTransport<RegisterCommandEnvelope>;
|
|
@@ -68,7 +71,7 @@ export function createRegisterOutbox(options: RegisterOutboxOptions): RegisterOu
|
|
|
68
71
|
// like a 503, or `no_progress`); a running clock keeps its `since` and takes the latest reason. A `network` failure
|
|
69
72
|
// pauses every clock (`pausedAt`), and the next answer of any kind resumes them, moving `since` on by the offline
|
|
70
73
|
// gap, so `since` is now minus the answered time. A clock clears when its command is no longer pending under its
|
|
71
|
-
// commandId (applied or rejected), or when a rejected command comes ahead of it in its ledger (it is not being
|
|
74
|
+
// commandId (applied or rejected), or when a blocking rejected command comes ahead of it in its ledger (it is not being
|
|
72
75
|
// sent: Front desk, 2026-09-30); commands never sent have none. Nothing is isolated: a stuck command at the head
|
|
73
76
|
// of a register's ledger holds up the commands behind it, by design, since register facts apply in `seq` order.
|
|
74
77
|
// In memory only: a restart starts with no clocks, and the first answered failure after it starts them afresh
|
|
@@ -96,8 +99,9 @@ export function createRegisterOutbox(options: RegisterOutboxOptions): RegisterOu
|
|
|
96
99
|
const blocked = new Set<string>();
|
|
97
100
|
if (clocks.size) for (const command of await readFresh(collection, { selector: { syncStatus: { $in: ['pending', 'rejected'] } },
|
|
98
101
|
sort: [{ seq: 'asc' }, { key: 'asc' }] })) {
|
|
99
|
-
if (command.syncStatus === 'rejected')
|
|
100
|
-
|
|
102
|
+
if (command.syncStatus === 'rejected') {
|
|
103
|
+
if (!NON_BLOCKING_REJECTIONS.has(command.error?.code ?? '')) blocked.add(command.registerId);
|
|
104
|
+
} else if (!blocked.has(command.registerId)) sendable.add(command.commandId);
|
|
101
105
|
}
|
|
102
106
|
for (const id of clocks.keys()) if (!sendable.has(id)) clocks.delete(id);
|
|
103
107
|
state$.next({ ...state$.value, ...patch, pending, stuck: stuckState() });
|
|
@@ -127,9 +131,10 @@ export function createRegisterOutbox(options: RegisterOutboxOptions): RegisterOu
|
|
|
127
131
|
});
|
|
128
132
|
const commands: RegisterCommand[] = [];
|
|
129
133
|
for (const command of ledger) {
|
|
134
|
+
if (command.syncStatus === 'rejected' && NON_BLOCKING_REJECTIONS.has(command.error?.code ?? '')) continue;
|
|
130
135
|
if (command.syncStatus === 'rejected') break;
|
|
131
136
|
commands.push(command);
|
|
132
|
-
if (commands.length >= batchSize) break;
|
|
137
|
+
if (command.type === 'register.session.open' || commands.length >= batchSize) break;
|
|
133
138
|
}
|
|
134
139
|
if (stopped) { stoppedDuringRun = true; return; }
|
|
135
140
|
if (!commands.length) continue;
|
|
@@ -6,11 +6,12 @@ import type { PosOrder } from './types';
|
|
|
6
6
|
* session closed (`lateSessionId`) or with local warnings (whatever the sync status), or pending with a `commandId` in
|
|
7
7
|
* `stuckCommandIds` (the outbox's `OutboxState.stuck`: the store keeps failing it), newest first.
|
|
8
8
|
* A warning code this till version doesn't know is not shown here (`knownWarnings`). Never changes `orders`.
|
|
9
|
+
* `register_session_unknown` is not shown, because the store counts the order once its session arrives (ADR-078).
|
|
9
10
|
*/
|
|
10
11
|
export function needsAttention(orders: PosOrder[], options: { stuckCommandIds?: readonly string[] } = {}): PosOrder[] {
|
|
11
12
|
return orders.filter((order) => order.syncStatus === 'rejected'
|
|
12
13
|
|| (order.localWarnings?.length ?? 0) > 0
|
|
13
|
-
|| (order.syncStatus === 'applied' && knownWarnings(order.warnings).
|
|
14
|
+
|| (order.syncStatus === 'applied' && knownWarnings(order.warnings).some((warning) => warning.code !== 'register_session_unknown')) || order.lateSessionId !== undefined
|
|
14
15
|
|| (order.syncStatus === 'pending' && options.stuckCommandIds?.includes(order.commandId) === true))
|
|
15
16
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
16
17
|
}
|