@tallyui/pos 3.0.0-next.0 → 3.0.0-next.1
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
|
@@ -177,7 +177,7 @@ declare function resolveStoreSettings(options: ResolveStoreSettingsOptions, choi
|
|
|
177
177
|
/** The replication's context with the settings' opaque pricing context; the key is left out when the settings have none. */
|
|
178
178
|
declare function withPricingContext(context: SyncContext, settings: StoreSettings): SyncContext;
|
|
179
179
|
/**
|
|
180
|
-
* `<TaxProvider>`'s props from the same settings, so its tax inclusivity and
|
|
180
|
+
* `<TaxProvider>`'s props from the same settings, so its tax inclusivity, rounding and rate codes match the store's. `custom`
|
|
181
181
|
* rounding passes none, the default (#287). An explicit `rounding` prop after the spread still wins, since JSX
|
|
182
182
|
* applies props in order; so `<TaxProvider {...taxProviderProps(s)} rounding={undefined}>` wipes the store's rounding.
|
|
183
183
|
*/
|
|
@@ -198,6 +198,7 @@ type StoreSettingsState = {
|
|
|
198
198
|
state: 'error';
|
|
199
199
|
error: unknown;
|
|
200
200
|
retry: () => void;
|
|
201
|
+
nextRetryAt?: number;
|
|
201
202
|
} | {
|
|
202
203
|
state: 'unsupported';
|
|
203
204
|
};
|
|
@@ -208,6 +209,7 @@ type StoreSettingsState = {
|
|
|
208
209
|
* The ready settings carry the store's `taxRounding` from the capabilities, so `taxProviderProps` passes it on (#324).
|
|
209
210
|
* The capabilities are read beside the settings, before `ready`, so each resolve emits the settings once, with the
|
|
210
211
|
* rounding already known: no later change of `settings` holds a sale.
|
|
212
|
+
* Unknown rounding keeps settings unresolved and retries by itself; show "Can't reach the store's settings yet. Retrying…" while `nextRetryAt` is set.
|
|
211
213
|
*
|
|
212
214
|
* ```tsx
|
|
213
215
|
* const store = useStoreSettings({ connector, context, loadChoice, saveChoice });
|
package/dist/index.js
CHANGED
|
@@ -319,26 +319,45 @@ function withPricingContext(context, settings) {
|
|
|
319
319
|
}
|
|
320
320
|
function taxProviderProps(settings) {
|
|
321
321
|
const rounding = settings.taxRounding?.granularity === "custom" ? void 0 : settings.taxRounding;
|
|
322
|
-
return { ratesPpm: settings.taxRatesPpm, pricesIncludeTax: settings.pricesIncludeTax, ...rounding && { rounding } };
|
|
322
|
+
return { ratesPpm: settings.taxRatesPpm, pricesIncludeTax: settings.pricesIncludeTax, ...rounding && { rounding }, ...settings.taxRateCodes && { rateCodes: settings.taxRateCodes } };
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
// src/store-settings/use-store-settings.ts
|
|
326
326
|
import { useCallback as useCallback2, useEffect, useRef, useState } from "react";
|
|
327
327
|
var LOADING = { state: "loading" };
|
|
328
|
+
var StoreCapabilitiesUnavailableError = class extends Error {
|
|
329
|
+
constructor(cause) {
|
|
330
|
+
super("Store capabilities unavailable", { cause });
|
|
331
|
+
this.name = "StoreCapabilitiesUnavailableError";
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
var INITIAL_RETRY_MS = 5e3;
|
|
335
|
+
var MAX_RETRY_MS = 5 * 6e4;
|
|
328
336
|
function readTaxRounding({ connector, context }) {
|
|
329
337
|
if (context.capabilities) return Promise.resolve(context.capabilities.taxRounding);
|
|
330
338
|
if (!connector.storeSettings || !connector.capabilities) return Promise.resolve(void 0);
|
|
331
339
|
const read = connector.capabilities;
|
|
332
|
-
return Promise.resolve().then(() => read(context)).then(
|
|
340
|
+
return Promise.resolve().then(() => read(context)).then(
|
|
341
|
+
(capabilities) => {
|
|
342
|
+
if (!capabilities) throw new StoreCapabilitiesUnavailableError();
|
|
343
|
+
return capabilities.taxRounding;
|
|
344
|
+
},
|
|
345
|
+
(cause) => {
|
|
346
|
+
throw new StoreCapabilitiesUnavailableError(cause);
|
|
347
|
+
}
|
|
348
|
+
);
|
|
333
349
|
}
|
|
334
350
|
function useStoreSettings(options) {
|
|
335
351
|
const { connector, context } = options;
|
|
336
352
|
const optionsRef = useRef(options);
|
|
337
353
|
optionsRef.current = options;
|
|
338
354
|
const requestRef = useRef(0);
|
|
355
|
+
const retryTimerRef = useRef(void 0);
|
|
356
|
+
const retryDelayRef = useRef(INITIAL_RETRY_MS);
|
|
339
357
|
const [state, setState] = useState(LOADING);
|
|
340
358
|
const resolve = useCallback2((choice, from, known) => {
|
|
341
359
|
if (from !== void 0 && from !== requestRef.current) return;
|
|
360
|
+
clearTimeout(retryTimerRef.current);
|
|
342
361
|
const request = ++requestRef.current;
|
|
343
362
|
const current = () => request === requestRef.current;
|
|
344
363
|
setState(LOADING);
|
|
@@ -346,6 +365,7 @@ function useStoreSettings(options) {
|
|
|
346
365
|
Promise.all([resolveStoreSettings(optionsRef.current, choice), rounding]).then(
|
|
347
366
|
([result, taxRounding]) => {
|
|
348
367
|
if (!current()) return;
|
|
368
|
+
retryDelayRef.current = INITIAL_RETRY_MS;
|
|
349
369
|
if (result.status === "ready")
|
|
350
370
|
setState({ state: "ready", settings: taxRounding ? { ...result.settings, taxRounding } : result.settings, choice: result.choice });
|
|
351
371
|
else if (result.status === "choose")
|
|
@@ -354,13 +374,23 @@ function useStoreSettings(options) {
|
|
|
354
374
|
},
|
|
355
375
|
// A retry keeps the choice that failed: a new pick is saved only once it resolves.
|
|
356
376
|
(error) => {
|
|
357
|
-
if (current())
|
|
377
|
+
if (!current()) return;
|
|
378
|
+
const retry = () => resolve(choice, request);
|
|
379
|
+
if (error instanceof StoreCapabilitiesUnavailableError) {
|
|
380
|
+
const delay = retryDelayRef.current;
|
|
381
|
+
const nextRetryAt = Date.now() + delay;
|
|
382
|
+
retryDelayRef.current = Math.min(delay * 2, MAX_RETRY_MS);
|
|
383
|
+
retryTimerRef.current = setTimeout(retry, delay);
|
|
384
|
+
setState({ state: "error", error, retry, nextRetryAt });
|
|
385
|
+
} else setState({ state: "error", error, retry });
|
|
358
386
|
}
|
|
359
387
|
);
|
|
360
388
|
}, []);
|
|
361
389
|
useEffect(() => {
|
|
362
390
|
resolve();
|
|
363
391
|
return () => {
|
|
392
|
+
clearTimeout(retryTimerRef.current);
|
|
393
|
+
retryDelayRef.current = INITIAL_RETRY_MS;
|
|
364
394
|
requestRef.current += 1;
|
|
365
395
|
};
|
|
366
396
|
}, [connector, context, resolve]);
|
|
@@ -3980,6 +4010,7 @@ function createOrderOutbox(options) {
|
|
|
3980
4010
|
}
|
|
3981
4011
|
const bare = ({ serverFailures, _rev, _meta, ...data }) => data;
|
|
3982
4012
|
const failuresOnly = (before, after) => !deepEqual2(before.serverFailures, after.serverFailures) && deepEqual2(bare(before), bare(after));
|
|
4013
|
+
const recordsSentForm = (before, after) => deepEqual2(bare({ ...freezeSentForm(before), sentVersion: before.sentVersion ?? after.sentVersion }), bare(after));
|
|
3983
4014
|
async function updateState(patch = {}) {
|
|
3984
4015
|
const pending = await countFresh(collection, { syncStatus: "pending" });
|
|
3985
4016
|
const rejected = await countFresh(collection, { syncStatus: "rejected" });
|
|
@@ -4311,7 +4342,7 @@ function createOrderOutbox(options) {
|
|
|
4311
4342
|
stopped = false;
|
|
4312
4343
|
watchBackendMissing();
|
|
4313
4344
|
if (!subscription) subscription = collection.$.subscribe((event) => {
|
|
4314
|
-
if (event.documentData?.syncStatus === "pending" && (event.operation === "INSERT" || event.operation === "UPDATE" && !(event.previousDocumentData !== void 0 && failuresOnly(event.previousDocumentData, event.documentData)))) {
|
|
4345
|
+
if (event.documentData?.syncStatus === "pending" && (event.operation === "INSERT" || event.operation === "UPDATE" && !(event.previousDocumentData !== void 0 && (failuresOnly(event.previousDocumentData, event.documentData) || recordsSentForm(event.previousDocumentData, event.documentData))))) {
|
|
4315
4346
|
insertedDuringRun = true;
|
|
4316
4347
|
flush().catch(() => {
|
|
4317
4348
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tallyui/pos",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.1",
|
|
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.0.0-next.
|
|
30
|
+
"@tallyui/core": "^3.0.0-next.1",
|
|
31
31
|
"react": ">=18",
|
|
32
32
|
"rxdb": "~17.5.0",
|
|
33
33
|
"rxjs": ">=7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@tallyui/core": "3.0.0-next.
|
|
36
|
+
"@tallyui/core": "3.0.0-next.1",
|
|
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.0.0-next.
|
|
42
|
-
"@tallyui/connector-woocommerce": "3.0.0-next.
|
|
43
|
-
"@tallyui/database": "3.0.0-next.
|
|
41
|
+
"@tallyui/connector-medusa": "3.0.0-next.1",
|
|
42
|
+
"@tallyui/connector-woocommerce": "3.0.0-next.1",
|
|
43
|
+
"@tallyui/database": "3.0.0-next.1"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsup",
|
|
@@ -166,6 +166,10 @@ export function createOrderOutbox(options: OrderOutboxOptions): OrderOutbox {
|
|
|
166
166
|
const bare = ({ serverFailures, _rev, _meta, ...data }: RxDocumentData<PosOrder>) => data;
|
|
167
167
|
const failuresOnly = (before: RxDocumentData<PosOrder>, after: RxDocumentData<PosOrder>) =>
|
|
168
168
|
!deepEqual(before.serverFailures, after.serverFailures) && deepEqual(bare(before), bare(after));
|
|
169
|
+
// The outbox's own write of an order's sent form (#300): the form frozen, and sentVersion stored on a first send.
|
|
170
|
+
// Not new work (medusapos/app#160). A changed sentVersion (a downgrade) is not this write.
|
|
171
|
+
const recordsSentForm = (before: RxDocumentData<PosOrder>, after: RxDocumentData<PosOrder>) =>
|
|
172
|
+
deepEqual(bare({ ...freezeSentForm(before), sentVersion: before.sentVersion ?? after.sentVersion } as RxDocumentData<PosOrder>), bare(after));
|
|
169
173
|
|
|
170
174
|
// Every read bypasses the query cache. RxDB 17 fixed RxDB 16.21.1's bug 4 (rxdb#7067);
|
|
171
175
|
// readFresh, countFresh and watchFresh remain correct public API.
|
|
@@ -482,7 +486,8 @@ export function createOrderOutbox(options: OrderOutboxOptions): OrderOutbox {
|
|
|
482
486
|
if (!subscription) subscription = collection.$.subscribe((event) => {
|
|
483
487
|
// An UPDATE without previousDocumentData counts as a change: a needless send costs a request, a skipped one a sale.
|
|
484
488
|
if (event.documentData?.syncStatus === 'pending' && (event.operation === 'INSERT' || event.operation === 'UPDATE' &&
|
|
485
|
-
!(event.previousDocumentData !== undefined && failuresOnly(event.previousDocumentData, event.documentData)
|
|
489
|
+
!(event.previousDocumentData !== undefined && (failuresOnly(event.previousDocumentData, event.documentData)
|
|
490
|
+
|| recordsSentForm(event.previousDocumentData, event.documentData))))) {
|
|
486
491
|
insertedDuringRun = true;
|
|
487
492
|
flush().catch(() => {});
|
|
488
493
|
}
|
|
@@ -8,11 +8,11 @@ export function withPricingContext(context: SyncContext, settings: StoreSettings
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* `<TaxProvider>`'s props from the same settings, so its tax inclusivity and
|
|
11
|
+
* `<TaxProvider>`'s props from the same settings, so its tax inclusivity, rounding and rate codes match the store's. `custom`
|
|
12
12
|
* rounding passes none, the default (#287). An explicit `rounding` prop after the spread still wins, since JSX
|
|
13
13
|
* applies props in order; so `<TaxProvider {...taxProviderProps(s)} rounding={undefined}>` wipes the store's rounding.
|
|
14
14
|
*/
|
|
15
15
|
export function taxProviderProps(settings: StoreSettings): Omit<TaxProviderProps, 'children'> {
|
|
16
16
|
const rounding = settings.taxRounding?.granularity === 'custom' ? undefined : settings.taxRounding;
|
|
17
|
-
return { ratesPpm: settings.taxRatesPpm, pricesIncludeTax: settings.pricesIncludeTax, ...(rounding && { rounding }) };
|
|
17
|
+
return { ratesPpm: settings.taxRatesPpm, pricesIncludeTax: settings.pricesIncludeTax, ...(rounding && { rounding }), ...(settings.taxRateCodes && { rateCodes: settings.taxRateCodes }) };
|
|
18
18
|
}
|
|
@@ -7,19 +7,35 @@ export type StoreSettingsState =
|
|
|
7
7
|
| { state: 'loading' }
|
|
8
8
|
| { state: 'ready'; settings: StoreSettings; choice?: StoreSettingsChoice }
|
|
9
9
|
| { state: 'choose'; choices: StoreSettingsChoices; initial?: StoreSettingsChoice; choose: (choice: StoreSettingsChoice) => void }
|
|
10
|
-
| { state: 'error'; error: unknown; retry: () => void }
|
|
10
|
+
| { state: 'error'; error: unknown; retry: () => void; nextRetryAt?: number }
|
|
11
11
|
| { state: 'unsupported' };
|
|
12
12
|
|
|
13
13
|
// One object, so re-entering loading while already loading doesn't re-render.
|
|
14
14
|
const LOADING: StoreSettingsState = { state: 'loading' };
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
export class StoreCapabilitiesUnavailableError extends Error {
|
|
17
|
+
constructor(cause?: unknown) {
|
|
18
|
+
super('Store capabilities unavailable', { cause });
|
|
19
|
+
this.name = 'StoreCapabilitiesUnavailableError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Exponential retry for unknown rounding: 5 seconds, doubling up to 5 minutes.
|
|
24
|
+
const INITIAL_RETRY_MS = 5_000;
|
|
25
|
+
const MAX_RETRY_MS = 5 * 60_000;
|
|
26
|
+
|
|
27
|
+
// The store's taxRounding (#324): the context's capabilities, else one read of the connector's.
|
|
18
28
|
function readTaxRounding({ connector, context }: ResolveStoreSettingsOptions): Promise<TaxRounding | undefined> {
|
|
19
29
|
if (context.capabilities) return Promise.resolve(context.capabilities.taxRounding);
|
|
20
30
|
if (!connector.storeSettings || !connector.capabilities) return Promise.resolve(undefined);
|
|
21
31
|
const read = connector.capabilities;
|
|
22
|
-
return Promise.resolve().then(() => read(context)).then(
|
|
32
|
+
return Promise.resolve().then(() => read(context)).then(
|
|
33
|
+
(capabilities) => {
|
|
34
|
+
if (!capabilities) throw new StoreCapabilitiesUnavailableError();
|
|
35
|
+
return capabilities.taxRounding;
|
|
36
|
+
},
|
|
37
|
+
(cause) => { throw new StoreCapabilitiesUnavailableError(cause); },
|
|
38
|
+
);
|
|
23
39
|
}
|
|
24
40
|
|
|
25
41
|
/**
|
|
@@ -29,6 +45,7 @@ function readTaxRounding({ connector, context }: ResolveStoreSettingsOptions): P
|
|
|
29
45
|
* The ready settings carry the store's `taxRounding` from the capabilities, so `taxProviderProps` passes it on (#324).
|
|
30
46
|
* The capabilities are read beside the settings, before `ready`, so each resolve emits the settings once, with the
|
|
31
47
|
* rounding already known: no later change of `settings` holds a sale.
|
|
48
|
+
* Unknown rounding keeps settings unresolved and retries by itself; show "Can't reach the store's settings yet. Retrying…" while `nextRetryAt` is set.
|
|
32
49
|
*
|
|
33
50
|
* ```tsx
|
|
34
51
|
* const store = useStoreSettings({ connector, context, loadChoice, saveChoice });
|
|
@@ -44,6 +61,8 @@ export function useStoreSettings(options: ResolveStoreSettingsOptions): StoreSet
|
|
|
44
61
|
const optionsRef = useRef(options);
|
|
45
62
|
optionsRef.current = options;
|
|
46
63
|
const requestRef = useRef(0);
|
|
64
|
+
const retryTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
65
|
+
const retryDelayRef = useRef(INITIAL_RETRY_MS);
|
|
47
66
|
const [state, setState] = useState<StoreSettingsState>(LOADING);
|
|
48
67
|
|
|
49
68
|
// `from`, when given, is the request that created the `choose`/`retry` calling this: a call
|
|
@@ -52,6 +71,7 @@ export function useStoreSettings(options: ResolveStoreSettingsOptions): StoreSet
|
|
|
52
71
|
// a pick doesn't read it again.
|
|
53
72
|
const resolve = useCallback((choice?: StoreSettingsChoice, from?: number, known?: Promise<TaxRounding | undefined>) => {
|
|
54
73
|
if (from !== undefined && from !== requestRef.current) return;
|
|
74
|
+
clearTimeout(retryTimerRef.current);
|
|
55
75
|
const request = ++requestRef.current;
|
|
56
76
|
const current = () => request === requestRef.current;
|
|
57
77
|
setState(LOADING);
|
|
@@ -59,6 +79,7 @@ export function useStoreSettings(options: ResolveStoreSettingsOptions): StoreSet
|
|
|
59
79
|
Promise.all([resolveStoreSettings(optionsRef.current, choice), rounding]).then(
|
|
60
80
|
([result, taxRounding]) => {
|
|
61
81
|
if (!current()) return;
|
|
82
|
+
retryDelayRef.current = INITIAL_RETRY_MS;
|
|
62
83
|
if (result.status === 'ready')
|
|
63
84
|
setState({ state: 'ready', settings: taxRounding ? { ...result.settings, taxRounding } : result.settings, choice: result.choice });
|
|
64
85
|
else if (result.status === 'choose')
|
|
@@ -67,7 +88,15 @@ export function useStoreSettings(options: ResolveStoreSettingsOptions): StoreSet
|
|
|
67
88
|
},
|
|
68
89
|
// A retry keeps the choice that failed: a new pick is saved only once it resolves.
|
|
69
90
|
(error: unknown) => {
|
|
70
|
-
if (current())
|
|
91
|
+
if (!current()) return;
|
|
92
|
+
const retry = () => resolve(choice, request);
|
|
93
|
+
if (error instanceof StoreCapabilitiesUnavailableError) {
|
|
94
|
+
const delay = retryDelayRef.current;
|
|
95
|
+
const nextRetryAt = Date.now() + delay;
|
|
96
|
+
retryDelayRef.current = Math.min(delay * 2, MAX_RETRY_MS);
|
|
97
|
+
retryTimerRef.current = setTimeout(retry, delay);
|
|
98
|
+
setState({ state: 'error', error, retry, nextRetryAt });
|
|
99
|
+
} else setState({ state: 'error', error, retry });
|
|
71
100
|
},
|
|
72
101
|
);
|
|
73
102
|
}, []);
|
|
@@ -76,6 +105,8 @@ export function useStoreSettings(options: ResolveStoreSettingsOptions): StoreSet
|
|
|
76
105
|
resolve();
|
|
77
106
|
// Discards the in-flight result on a store switch or unmount.
|
|
78
107
|
return () => {
|
|
108
|
+
clearTimeout(retryTimerRef.current);
|
|
109
|
+
retryDelayRef.current = INITIAL_RETRY_MS;
|
|
79
110
|
requestRef.current += 1;
|
|
80
111
|
};
|
|
81
112
|
}, [connector, context, resolve]);
|