@teincfood/core 0.1.3 → 0.1.5
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/adapters/services.d.ts +5 -0
- package/dist/adapters/services.js +16 -1
- package/dist/index.d.ts +34 -2
- package/dist/index.js +60 -2
- package/dist/pricing/engine.d.ts +58 -0
- package/dist/pricing/engine.js +82 -0
- package/dist/pricing/engine.test.d.ts +1 -0
- package/dist/pricing/engine.test.js +142 -0
- package/dist/reference/api.d.ts +18 -1
- package/dist/reference/api.js +25 -2
- package/dist/reference/capabilities.d.ts +21 -0
- package/dist/reference/capabilities.js +81 -0
- package/dist/reference/service.js +8 -0
- package/dist/reference/types.d.ts +1 -1
- package/dist/sync/command-queue.service.d.ts +1 -0
- package/dist/sync/command-queue.service.js +75 -20
- package/dist/sync/offline-capable.d.ts +22 -0
- package/dist/sync/offline-capable.js +41 -0
- package/dist/sync/order-snapshots.service.d.ts +7 -0
- package/dist/sync/order-snapshots.service.js +33 -7
- package/dist/sync/pending-orders.service.js +26 -1
- package/dist/sync/transport-rest.js +17 -1
- package/dist/types/pos.types.d.ts +1 -0
- package/dist/utils/constants.d.ts +6 -4
- package/dist/utils/constants.js +34 -8
- package/dist/utils/sync-logger.js +12 -2
- package/package.json +1 -1
|
@@ -36,12 +36,17 @@ export declare const kioskService: {
|
|
|
36
36
|
export declare const menuService: {
|
|
37
37
|
fetchBusinessMenuItems: (_businessId: string, _page: number) => Promise<unknown>;
|
|
38
38
|
fetchBusinessCategories: (_businessId: string) => Promise<unknown>;
|
|
39
|
+
createMenuItem: (_businessId: string, _payload: Record<string, unknown>) => Promise<unknown>;
|
|
40
|
+
updateMenuItem: (_itemId: string, _payload: Record<string, unknown>) => Promise<unknown>;
|
|
41
|
+
deleteMenuItem: (_itemId: string) => Promise<unknown>;
|
|
42
|
+
createCategory: (_businessId: string, _payload: Record<string, unknown>) => Promise<unknown>;
|
|
39
43
|
};
|
|
40
44
|
export declare const businessService: {
|
|
41
45
|
fetchBusinessMembers: (_businessId: string) => Promise<unknown>;
|
|
42
46
|
fetchBusinessTaxRules: (_businessId: string) => Promise<unknown>;
|
|
43
47
|
fetchBusinessById: (_businessId: string) => Promise<unknown>;
|
|
44
48
|
fetchOperatingHours: (_businessId: string) => Promise<unknown>;
|
|
49
|
+
fetchMyBusinessMembership: (_businessId: string) => Promise<unknown>;
|
|
45
50
|
};
|
|
46
51
|
export declare const rolesService: {
|
|
47
52
|
fetchBusinessRoles: () => Promise<unknown>;
|
|
@@ -47,7 +47,7 @@ export const kioskService = {
|
|
|
47
47
|
notImpl("kioskService.assignKioskOrderToRider");
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
|
-
// menu.service etc. for reference fallback
|
|
50
|
+
// menu.service etc. for reference fallback + command dispatch
|
|
51
51
|
export const menuService = {
|
|
52
52
|
fetchBusinessMenuItems: async (_businessId, _page) => {
|
|
53
53
|
notImpl("menuService.fetchBusinessMenuItems");
|
|
@@ -55,6 +55,18 @@ export const menuService = {
|
|
|
55
55
|
fetchBusinessCategories: async (_businessId) => {
|
|
56
56
|
notImpl("menuService.fetchBusinessCategories");
|
|
57
57
|
},
|
|
58
|
+
createMenuItem: async (_businessId, _payload) => {
|
|
59
|
+
notImpl("menuService.createMenuItem");
|
|
60
|
+
},
|
|
61
|
+
updateMenuItem: async (_itemId, _payload) => {
|
|
62
|
+
notImpl("menuService.updateMenuItem");
|
|
63
|
+
},
|
|
64
|
+
deleteMenuItem: async (_itemId) => {
|
|
65
|
+
notImpl("menuService.deleteMenuItem");
|
|
66
|
+
},
|
|
67
|
+
createCategory: async (_businessId, _payload) => {
|
|
68
|
+
notImpl("menuService.createCategory");
|
|
69
|
+
},
|
|
58
70
|
};
|
|
59
71
|
export const businessService = {
|
|
60
72
|
fetchBusinessMembers: async (_businessId) => {
|
|
@@ -69,6 +81,9 @@ export const businessService = {
|
|
|
69
81
|
fetchOperatingHours: async (_businessId) => {
|
|
70
82
|
notImpl("businessService.fetchOperatingHours");
|
|
71
83
|
},
|
|
84
|
+
fetchMyBusinessMembership: async (_businessId) => {
|
|
85
|
+
notImpl("businessService.fetchMyBusinessMembership");
|
|
86
|
+
},
|
|
72
87
|
};
|
|
73
88
|
export const rolesService = {
|
|
74
89
|
fetchBusinessRoles: async () => {
|
package/dist/index.d.ts
CHANGED
|
@@ -6,25 +6,55 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export * from "./types/api";
|
|
8
8
|
export * from "./types/pos.types";
|
|
9
|
+
export { calculatePricing, calculateTaxLines, calculateExclusiveTax, calculateInclusiveTaxShare, } from "./pricing/engine";
|
|
10
|
+
export type { PricingInput, PricingResult } from "./pricing/engine";
|
|
9
11
|
export * from "./utils/constants";
|
|
10
12
|
export * from "./utils/currency";
|
|
11
13
|
export * from "./utils/error";
|
|
12
14
|
export * from "./utils/uuid";
|
|
13
15
|
export * from "./utils/sync-logger";
|
|
16
|
+
export { PHONE_COUNTRIES, getDialCodeFromCountry, getCountryOption, normalizePhoneForCountry, isLikelyE164 } from "./utils/phone";
|
|
17
|
+
export type { PhoneCountryOption } from "./utils/phone";
|
|
18
|
+
export * from "./utils/formatCurrency";
|
|
19
|
+
export { formatDate, formatRelativeTime } from "./utils/formatDate";
|
|
20
|
+
export * from "./utils/validation";
|
|
21
|
+
export * from "./utils/responsive";
|
|
22
|
+
export * from "./utils/copyToClipBoard";
|
|
14
23
|
export * from "./db/migrations";
|
|
24
|
+
export * from "./db/operations";
|
|
15
25
|
export * as dbOps from "./db/operations";
|
|
16
|
-
export { openSyncDatabase, getSyncDatabase } from "./db/connection";
|
|
26
|
+
export { openSyncDatabase, getSyncDatabase, closeSyncDatabase } from "./db/connection";
|
|
17
27
|
export * from "./sync/types";
|
|
18
|
-
export
|
|
28
|
+
export * from "./sync/use-sync-reconciliation";
|
|
29
|
+
export { SyncEngine, syncEngine } from "./sync/engine";
|
|
30
|
+
export * from "./sync/command-builder";
|
|
31
|
+
export * from "./sync/command-queue.service";
|
|
19
32
|
export { commandQueueService } from "./sync/command-queue.service";
|
|
20
33
|
export { buildCommand } from "./sync/command-builder";
|
|
21
34
|
export { generateLocalOrderNumber, generateLocalCalloutNumber, isLocalOrderNumber } from "./sync/order-number";
|
|
22
35
|
export { orderSnapshotsService } from "./sync/order-snapshots.service";
|
|
23
36
|
export * from "./sync/pending-orders.service";
|
|
37
|
+
export { restTransport } from "./sync/transport-rest";
|
|
38
|
+
export * from "./sync/transport-rest";
|
|
39
|
+
export { localNodeTransport } from "./sync/transport-local-node";
|
|
40
|
+
export * from "./sync/transport-local-node";
|
|
41
|
+
export * from "./sync/order-snapshots.service";
|
|
42
|
+
export { hydratePendingOrders, applyPendingOrderEvent, applyOrderStateChangeEvent } from "./sync/pending-orders.service";
|
|
43
|
+
export { runOfflineCapableMutation } from "./sync/offline-capable";
|
|
44
|
+
export type { OfflineCapableOptions } from "./sync/offline-capable";
|
|
24
45
|
export { orderRepository } from "./repositories/order.repository";
|
|
46
|
+
export type { CreateOrderInput, AssignRiderInput, OrderActionInput } from "./repositories/order.repository";
|
|
25
47
|
export { posRepository } from "./repositories/pos.repository";
|
|
48
|
+
export type { POSOrderPreviewInput } from "./repositories/pos.repository";
|
|
26
49
|
export * from "./reference/types";
|
|
50
|
+
export * from "./reference/operations";
|
|
51
|
+
export { fetchReferenceVersions, fetchReferenceData, fetchReferenceChanges, setReferenceLocalNodeFetcher } from "./reference/api";
|
|
52
|
+
export type { ReferenceLocalNodeFetcher } from "./reference/api";
|
|
53
|
+
export { emitReferenceChanged, applyReferenceChangeEvent, syncReferenceDataOnReconnect, registerReferenceEventEngine, registerReferenceInvalidator } from "./reference/events";
|
|
54
|
+
export * from "./reference/events";
|
|
55
|
+
export * from "./reference/service";
|
|
27
56
|
export { referenceSyncService } from "./reference/service";
|
|
57
|
+
export * from "./reference/capabilities";
|
|
28
58
|
export * from "./adapters/sqlite";
|
|
29
59
|
export * from "./adapters/kv";
|
|
30
60
|
export * from "./adapters/connectivity";
|
|
@@ -41,6 +71,8 @@ export interface CoreConfig {
|
|
|
41
71
|
sqliteDriver?: SqliteDriver;
|
|
42
72
|
kvDriver?: KVDriver;
|
|
43
73
|
httpClient?: HttpClient;
|
|
74
|
+
apiBaseUrl?: string;
|
|
75
|
+
wsBaseUrl?: string;
|
|
44
76
|
/** Optional initial connectivity override */
|
|
45
77
|
initialConnectivity?: {
|
|
46
78
|
isOnline?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -7,30 +7,55 @@
|
|
|
7
7
|
// ── Types ──
|
|
8
8
|
export * from "./types/api";
|
|
9
9
|
export * from "./types/pos.types";
|
|
10
|
+
// ── Pricing (server-parity) ──
|
|
11
|
+
export { calculatePricing, calculateTaxLines, calculateExclusiveTax, calculateInclusiveTaxShare, } from "./pricing/engine";
|
|
10
12
|
// ── Utils ──
|
|
11
13
|
export * from "./utils/constants";
|
|
12
14
|
export * from "./utils/currency";
|
|
13
15
|
export * from "./utils/error";
|
|
14
16
|
export * from "./utils/uuid";
|
|
15
17
|
export * from "./utils/sync-logger";
|
|
18
|
+
export { PHONE_COUNTRIES, getDialCodeFromCountry, getCountryOption, normalizePhoneForCountry, isLikelyE164 } from "./utils/phone";
|
|
19
|
+
export * from "./utils/formatCurrency";
|
|
20
|
+
export { formatDate, formatRelativeTime } from "./utils/formatDate";
|
|
21
|
+
export * from "./utils/validation";
|
|
22
|
+
export * from "./utils/responsive";
|
|
23
|
+
export * from "./utils/copyToClipBoard";
|
|
16
24
|
// ── DB ──
|
|
17
25
|
export * from "./db/migrations";
|
|
26
|
+
export * from "./db/operations";
|
|
18
27
|
export * as dbOps from "./db/operations";
|
|
19
|
-
export { openSyncDatabase, getSyncDatabase } from "./db/connection";
|
|
28
|
+
export { openSyncDatabase, getSyncDatabase, closeSyncDatabase } from "./db/connection";
|
|
20
29
|
// ── Sync ──
|
|
21
30
|
export * from "./sync/types";
|
|
22
|
-
export
|
|
31
|
+
export * from "./sync/use-sync-reconciliation";
|
|
32
|
+
export { SyncEngine, syncEngine } from "./sync/engine";
|
|
33
|
+
export * from "./sync/command-builder";
|
|
34
|
+
export * from "./sync/command-queue.service";
|
|
23
35
|
export { commandQueueService } from "./sync/command-queue.service";
|
|
24
36
|
export { buildCommand } from "./sync/command-builder";
|
|
25
37
|
export { generateLocalOrderNumber, generateLocalCalloutNumber, isLocalOrderNumber } from "./sync/order-number";
|
|
26
38
|
export { orderSnapshotsService } from "./sync/order-snapshots.service";
|
|
27
39
|
export * from "./sync/pending-orders.service";
|
|
40
|
+
export { restTransport } from "./sync/transport-rest";
|
|
41
|
+
export * from "./sync/transport-rest";
|
|
42
|
+
export { localNodeTransport } from "./sync/transport-local-node";
|
|
43
|
+
export * from "./sync/transport-local-node";
|
|
44
|
+
export * from "./sync/order-snapshots.service";
|
|
45
|
+
export { hydratePendingOrders, applyPendingOrderEvent, applyOrderStateChangeEvent } from "./sync/pending-orders.service";
|
|
46
|
+
export { runOfflineCapableMutation } from "./sync/offline-capable";
|
|
28
47
|
// ── Repositories ──
|
|
29
48
|
export { orderRepository } from "./repositories/order.repository";
|
|
30
49
|
export { posRepository } from "./repositories/pos.repository";
|
|
31
50
|
// ── Reference ──
|
|
32
51
|
export * from "./reference/types";
|
|
52
|
+
export * from "./reference/operations";
|
|
53
|
+
export { fetchReferenceVersions, fetchReferenceData, fetchReferenceChanges, setReferenceLocalNodeFetcher } from "./reference/api";
|
|
54
|
+
export { emitReferenceChanged, applyReferenceChangeEvent, syncReferenceDataOnReconnect, registerReferenceEventEngine, registerReferenceInvalidator } from "./reference/events";
|
|
55
|
+
export * from "./reference/events";
|
|
56
|
+
export * from "./reference/service";
|
|
33
57
|
export { referenceSyncService } from "./reference/service";
|
|
58
|
+
export * from "./reference/capabilities";
|
|
34
59
|
// ── Adapters (for host apps to supply) ──
|
|
35
60
|
export * from "./adapters/sqlite";
|
|
36
61
|
export * from "./adapters/kv";
|
|
@@ -45,19 +70,52 @@ import { setSqliteDriver } from "./adapters/sqlite";
|
|
|
45
70
|
import { setKVDriver } from "./adapters/kv";
|
|
46
71
|
import { setConnectivityState } from "./adapters/connectivity";
|
|
47
72
|
import { setHttpClient } from "./adapters/http";
|
|
73
|
+
import { API_BASE_URL, WS_BASE_URL, setApiBaseUrl, setWsBaseUrl } from "./utils/constants";
|
|
48
74
|
import { SyncEngine } from "./sync/engine";
|
|
49
75
|
import { restTransport } from "./sync/transport-rest";
|
|
50
76
|
import { localNodeTransport } from "./sync/transport-local-node";
|
|
51
77
|
let engine = null;
|
|
52
78
|
export function createTeincCore(config = {}) {
|
|
79
|
+
try {
|
|
80
|
+
console.debug("[core] createTeincCore called", {
|
|
81
|
+
hasSqliteDriver: !!config.sqliteDriver,
|
|
82
|
+
hasKvDriver: !!config.kvDriver,
|
|
83
|
+
hasHttpClient: !!config.httpClient,
|
|
84
|
+
apiBaseUrl: config.apiBaseUrl ?? "(default)",
|
|
85
|
+
wsBaseUrl: config.wsBaseUrl ?? "(default)",
|
|
86
|
+
initialConnectivity: config.initialConnectivity ?? null,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
53
90
|
if (config.sqliteDriver)
|
|
54
91
|
setSqliteDriver(config.sqliteDriver);
|
|
55
92
|
if (config.kvDriver)
|
|
56
93
|
setKVDriver(config.kvDriver);
|
|
57
94
|
if (config.httpClient)
|
|
58
95
|
setHttpClient(config.httpClient);
|
|
96
|
+
if (config.apiBaseUrl)
|
|
97
|
+
setApiBaseUrl(config.apiBaseUrl);
|
|
98
|
+
else {
|
|
99
|
+
try {
|
|
100
|
+
console.debug("[core] apiBaseUrl not provided by app, using default", API_BASE_URL);
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
}
|
|
104
|
+
if (config.wsBaseUrl)
|
|
105
|
+
setWsBaseUrl(config.wsBaseUrl);
|
|
106
|
+
else {
|
|
107
|
+
try {
|
|
108
|
+
console.debug("[core] wsBaseUrl not provided by app, using default", WS_BASE_URL);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
}
|
|
59
112
|
if (config.initialConnectivity)
|
|
60
113
|
setConnectivityState(config.initialConnectivity);
|
|
114
|
+
// Debug: confirm env vars as seen by core after injection
|
|
115
|
+
try {
|
|
116
|
+
console.debug("[core] final URLs", { API_BASE_URL, WS_BASE_URL });
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
61
119
|
engine = new SyncEngine({ transports: [restTransport, localNodeTransport] });
|
|
62
120
|
return { engine };
|
|
63
121
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pricing engine — server-parity POS pricing.
|
|
3
|
+
*
|
|
4
|
+
* Pure port of the backend `calculate_items` / `calculate_tax_lines` /
|
|
5
|
+
* `calculate_pricing` (TeincFoodBackend.Orders). Used as the instant local
|
|
6
|
+
* preview and offline fallback so terminals agree on pricing even when the
|
|
7
|
+
* cloud preview endpoint is unreachable. The cloud preview remains authoritative
|
|
8
|
+
* when it lands; this engine reproduces the same math exactly.
|
|
9
|
+
*
|
|
10
|
+
* Tax semantics (matching backend):
|
|
11
|
+
* - inclusive taxes are EMBEDDED in the menu price → never added to the total,
|
|
12
|
+
* only shown in `tax_lines[].is_inclusive` for the breakdown.
|
|
13
|
+
* - exclusive taxes are ADDED on top of the (discounted) subtotal.
|
|
14
|
+
* - `tax_minor` is the sum of ALL lines (display breakdown); `total_minor`
|
|
15
|
+
* adds only `exclusive_tax_minor`.
|
|
16
|
+
*/
|
|
17
|
+
import type { POSCartItem } from "../types/pos.types";
|
|
18
|
+
import type { TaxRule } from "../types/api/business.api";
|
|
19
|
+
import type { OrderTaxLine } from "../types/api/seller.api";
|
|
20
|
+
export interface PricingInput {
|
|
21
|
+
/** Cart items — `unit_price_minor` already includes variant + addons. */
|
|
22
|
+
items: POSCartItem[];
|
|
23
|
+
/** Active tax rules from cached reference data. */
|
|
24
|
+
taxRules: TaxRule[];
|
|
25
|
+
/** Optional promo discount in minor units. */
|
|
26
|
+
discount_minor?: number;
|
|
27
|
+
/** Delivery fee in minor units (POS is 0, but kept for parity). */
|
|
28
|
+
delivery_fee_minor?: number;
|
|
29
|
+
/** Tip in minor units. */
|
|
30
|
+
tip_minor?: number;
|
|
31
|
+
currency_code?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface PricingResult {
|
|
34
|
+
subtotal_minor: number;
|
|
35
|
+
discounted_subtotal_minor: number;
|
|
36
|
+
delivery_fee_minor: number;
|
|
37
|
+
discount_minor: number;
|
|
38
|
+
tax_minor: number;
|
|
39
|
+
exclusive_tax_minor: number;
|
|
40
|
+
tip_minor: number;
|
|
41
|
+
total_minor: number;
|
|
42
|
+
currency_code: string;
|
|
43
|
+
tax_lines: OrderTaxLine[];
|
|
44
|
+
}
|
|
45
|
+
/** Backend parity: div((amount * rate + 5000), 10000) with integer math. */
|
|
46
|
+
export declare function calculateExclusiveTax(taxableAmountMinor: number, rateBasisPoints: number): number;
|
|
47
|
+
/** Backend parity: inclusive tax share = div(amount*rate + 5000, 10000 + rate). */
|
|
48
|
+
export declare function calculateInclusiveTaxShare(taxableAmountMinor: number, rateBasisPoints: number): number;
|
|
49
|
+
export declare function calculateTaxLines(subtotal_minor: number, taxRules: TaxRule[], discount_minor?: number, delivery_fee_minor?: number): {
|
|
50
|
+
tax_lines: OrderTaxLine[];
|
|
51
|
+
tax_minor: number;
|
|
52
|
+
exclusive_tax_minor: number;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Compute POS pricing mirrors backend `calculate_pos_pricing`:
|
|
56
|
+
* total = discounted_subtotal + exclusive_tax_minor (+ tip when provided).
|
|
57
|
+
*/
|
|
58
|
+
export declare function calculatePricing(input: PricingInput): PricingResult;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pricing engine — server-parity POS pricing.
|
|
3
|
+
*
|
|
4
|
+
* Pure port of the backend `calculate_items` / `calculate_tax_lines` /
|
|
5
|
+
* `calculate_pricing` (TeincFoodBackend.Orders). Used as the instant local
|
|
6
|
+
* preview and offline fallback so terminals agree on pricing even when the
|
|
7
|
+
* cloud preview endpoint is unreachable. The cloud preview remains authoritative
|
|
8
|
+
* when it lands; this engine reproduces the same math exactly.
|
|
9
|
+
*
|
|
10
|
+
* Tax semantics (matching backend):
|
|
11
|
+
* - inclusive taxes are EMBEDDED in the menu price → never added to the total,
|
|
12
|
+
* only shown in `tax_lines[].is_inclusive` for the breakdown.
|
|
13
|
+
* - exclusive taxes are ADDED on top of the (discounted) subtotal.
|
|
14
|
+
* - `tax_minor` is the sum of ALL lines (display breakdown); `total_minor`
|
|
15
|
+
* adds only `exclusive_tax_minor`.
|
|
16
|
+
*/
|
|
17
|
+
/** Backend parity: div((amount * rate + 5000), 10000) with integer math. */
|
|
18
|
+
export function calculateExclusiveTax(taxableAmountMinor, rateBasisPoints) {
|
|
19
|
+
if (taxableAmountMinor <= 0 || rateBasisPoints <= 0)
|
|
20
|
+
return 0;
|
|
21
|
+
return Math.floor((taxableAmountMinor * rateBasisPoints + 5000) / 10000);
|
|
22
|
+
}
|
|
23
|
+
/** Backend parity: inclusive tax share = div(amount*rate + 5000, 10000 + rate). */
|
|
24
|
+
export function calculateInclusiveTaxShare(taxableAmountMinor, rateBasisPoints) {
|
|
25
|
+
if (taxableAmountMinor <= 0 || rateBasisPoints <= 0)
|
|
26
|
+
return 0;
|
|
27
|
+
return Math.floor((taxableAmountMinor * rateBasisPoints + 5000) / (10000 + rateBasisPoints));
|
|
28
|
+
}
|
|
29
|
+
export function calculateTaxLines(subtotal_minor, taxRules, discount_minor = 0, delivery_fee_minor = 0) {
|
|
30
|
+
const discounted = Math.max(subtotal_minor - discount_minor, 0);
|
|
31
|
+
let tax_minor = 0;
|
|
32
|
+
let exclusive_tax_minor = 0;
|
|
33
|
+
const tax_lines = taxRules.map((rule) => {
|
|
34
|
+
const taxableAmount = rule.applies_to === "subtotal_plus_delivery"
|
|
35
|
+
? discounted + delivery_fee_minor
|
|
36
|
+
: discounted;
|
|
37
|
+
const taxAmount = rule.is_inclusive
|
|
38
|
+
? calculateInclusiveTaxShare(taxableAmount, rule.rate_basis_points)
|
|
39
|
+
: calculateExclusiveTax(taxableAmount, rule.rate_basis_points);
|
|
40
|
+
tax_minor += taxAmount;
|
|
41
|
+
if (!rule.is_inclusive)
|
|
42
|
+
exclusive_tax_minor += taxAmount;
|
|
43
|
+
return {
|
|
44
|
+
id: rule.id,
|
|
45
|
+
business_tax_rule_id: rule.id,
|
|
46
|
+
name: rule.name,
|
|
47
|
+
code: rule.code,
|
|
48
|
+
country_code: rule.country_code,
|
|
49
|
+
rate_basis_points: rule.rate_basis_points,
|
|
50
|
+
taxable_amount_minor: taxableAmount,
|
|
51
|
+
tax_amount_minor: taxAmount,
|
|
52
|
+
applies_to: rule.applies_to,
|
|
53
|
+
is_inclusive: rule.is_inclusive,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
return { tax_lines, tax_minor, exclusive_tax_minor };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compute POS pricing mirrors backend `calculate_pos_pricing`:
|
|
60
|
+
* total = discounted_subtotal + exclusive_tax_minor (+ tip when provided).
|
|
61
|
+
*/
|
|
62
|
+
export function calculatePricing(input) {
|
|
63
|
+
const subtotal_minor = input.items.reduce((sum, item) => sum + item.unit_price_minor * item.quantity, 0);
|
|
64
|
+
const discount_minor = input.discount_minor ?? 0;
|
|
65
|
+
const delivery_fee_minor = input.delivery_fee_minor ?? 0;
|
|
66
|
+
const tip_minor = input.tip_minor ?? 0;
|
|
67
|
+
const currency_code = input.currency_code ?? "GHS";
|
|
68
|
+
const { tax_lines, tax_minor, exclusive_tax_minor } = calculateTaxLines(subtotal_minor, input.taxRules, discount_minor, delivery_fee_minor);
|
|
69
|
+
const discounted = Math.max(subtotal_minor - discount_minor, 0);
|
|
70
|
+
return {
|
|
71
|
+
subtotal_minor,
|
|
72
|
+
discounted_subtotal_minor: discounted,
|
|
73
|
+
delivery_fee_minor,
|
|
74
|
+
discount_minor,
|
|
75
|
+
tax_minor,
|
|
76
|
+
exclusive_tax_minor,
|
|
77
|
+
tip_minor,
|
|
78
|
+
total_minor: discounted + delivery_fee_minor + exclusive_tax_minor + tip_minor,
|
|
79
|
+
currency_code,
|
|
80
|
+
tax_lines,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-parity tests for the pricing engine.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the backend ExUnit vectors in
|
|
5
|
+
* pos_controller_test.exs (inclusive / exclusive / mixed) and the
|
|
6
|
+
* rounding formulas in Orders.calculate_tax_amount. Golden values below come
|
|
7
|
+
* directly from the Elixir implementation so the two runtimes never diverge.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import { calculatePricing, calculateTaxLines, calculateExclusiveTax, calculateInclusiveTaxShare, } from "./engine";
|
|
11
|
+
function cart(unitPriceMinor, quantity = 1, addonsMinor = []) {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
id: "cart-1",
|
|
15
|
+
menu_item: {
|
|
16
|
+
id: "item-1",
|
|
17
|
+
name: "Test Item",
|
|
18
|
+
description: null,
|
|
19
|
+
basePrice: { amountMinor: unitPriceMinor, currencyCode: "GHS" },
|
|
20
|
+
image_url: null,
|
|
21
|
+
is_available: true,
|
|
22
|
+
dietary_tags: [],
|
|
23
|
+
category_id: "cat-1",
|
|
24
|
+
business_id: "biz-1",
|
|
25
|
+
category: null,
|
|
26
|
+
variants: [],
|
|
27
|
+
extras: [],
|
|
28
|
+
inserted_at: "",
|
|
29
|
+
updated_at: "",
|
|
30
|
+
},
|
|
31
|
+
quantity,
|
|
32
|
+
selected_variant: null,
|
|
33
|
+
selected_addons: addonsMinor.map((price_minor, i) => ({
|
|
34
|
+
addon_id: `addon-${i}`,
|
|
35
|
+
name: `Addon ${i}`,
|
|
36
|
+
price_minor,
|
|
37
|
+
})),
|
|
38
|
+
notes: "",
|
|
39
|
+
// unit_price_minor already includes variant + addons (matches pos.store)
|
|
40
|
+
unit_price_minor: unitPriceMinor,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function taxRule(overrides) {
|
|
45
|
+
return {
|
|
46
|
+
id: "tax-1",
|
|
47
|
+
business_id: "biz-1",
|
|
48
|
+
country_code: "GH",
|
|
49
|
+
applies_to: "subtotal",
|
|
50
|
+
is_inclusive: false,
|
|
51
|
+
is_active: true,
|
|
52
|
+
display_order: 0,
|
|
53
|
+
inserted_at: "",
|
|
54
|
+
updated_at: "",
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
describe("calculateExclusiveTax / calculateInclusiveTaxShare (backend rounding)", () => {
|
|
59
|
+
it("computes exclusive tax = floor(amount*rate/10000)", () => {
|
|
60
|
+
expect(calculateExclusiveTax(2400, 1000)).toBe(240); // 10%
|
|
61
|
+
expect(calculateExclusiveTax(1200, 1000)).toBe(120);
|
|
62
|
+
expect(calculateExclusiveTax(2400, 0)).toBe(0);
|
|
63
|
+
expect(calculateExclusiveTax(0, 1000)).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
it("computes inclusive share = floor(amount*rate/(10000+rate))", () => {
|
|
66
|
+
// Backend: div(2400 * 7500 + 5000, 107500) = 1028
|
|
67
|
+
expect(calculateInclusiveTaxShare(2400, 7500)).toBe(1028);
|
|
68
|
+
expect(calculateInclusiveTaxShare(0, 7500)).toBe(0);
|
|
69
|
+
expect(calculateInclusiveTaxShare(2400, 0)).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("calculateTaxLines", () => {
|
|
73
|
+
it("returns exclusive-only additions for exclusive rules", () => {
|
|
74
|
+
const rules = [taxRule({ name: "Service", code: "service", rate_basis_points: 1000 })];
|
|
75
|
+
const { tax_lines, tax_minor, exclusive_tax_minor } = calculateTaxLines(2400, rules);
|
|
76
|
+
expect(tax_minor).toBe(240);
|
|
77
|
+
expect(exclusive_tax_minor).toBe(240);
|
|
78
|
+
expect(tax_lines[0].is_inclusive).toBe(false);
|
|
79
|
+
expect(tax_lines[0].tax_amount_minor).toBe(240);
|
|
80
|
+
});
|
|
81
|
+
it("returns embedded amounts for inclusive rules (not added to total)", () => {
|
|
82
|
+
const rules = [taxRule({ name: "Incl VAT", code: "incl_vat", rate_basis_points: 7500, is_inclusive: true })];
|
|
83
|
+
const { tax_lines, tax_minor, exclusive_tax_minor } = calculateTaxLines(2400, rules);
|
|
84
|
+
expect(tax_minor).toBe(1028); // display share
|
|
85
|
+
expect(exclusive_tax_minor).toBe(0); // nothing added to total
|
|
86
|
+
expect(tax_lines[0].is_inclusive).toBe(true);
|
|
87
|
+
expect(tax_lines[0].tax_amount_minor).toBe(1028);
|
|
88
|
+
});
|
|
89
|
+
it("applies tax to discounted subtotal first", () => {
|
|
90
|
+
const rules = [taxRule({ name: "Service", code: "service", rate_basis_points: 1000 })];
|
|
91
|
+
const { exclusive_tax_minor } = calculateTaxLines(2400, rules, 400, 0); // 2000 taxable
|
|
92
|
+
expect(exclusive_tax_minor).toBe(200);
|
|
93
|
+
});
|
|
94
|
+
it("honors applies_to = subtotal_plus_delivery", () => {
|
|
95
|
+
const rules = [
|
|
96
|
+
taxRule({ name: "Service", code: "service", rate_basis_points: 1000, applies_to: "subtotal_plus_delivery" }),
|
|
97
|
+
];
|
|
98
|
+
const { exclusive_tax_minor } = calculateTaxLines(2400, rules, 0, 600); // 3000 taxable
|
|
99
|
+
expect(exclusive_tax_minor).toBe(300);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("calculatePricing (POS parity with backend)", () => {
|
|
103
|
+
it("no tax → total == subtotal", () => {
|
|
104
|
+
const pricing = calculatePricing({ items: cart(1200, 2), taxRules: [], currency_code: "GHS" });
|
|
105
|
+
expect(pricing.subtotal_minor).toBe(2400);
|
|
106
|
+
expect(pricing.total_minor).toBe(2400);
|
|
107
|
+
expect(pricing.exclusive_tax_minor).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
it("inclusive-only tax does NOT inflate total (backend vector: 2400 → 2400)", () => {
|
|
110
|
+
const rules = [taxRule({ name: "Incl VAT", code: "incl_vat", rate_basis_points: 7500, is_inclusive: true })];
|
|
111
|
+
const pricing = calculatePricing({ items: cart(1200, 2), taxRules: rules, currency_code: "GHS" });
|
|
112
|
+
expect(pricing.subtotal_minor).toBe(2400);
|
|
113
|
+
expect(pricing.total_minor).toBe(2400); // NOT 2400 + 168
|
|
114
|
+
expect(pricing.exclusive_tax_minor).toBe(0);
|
|
115
|
+
expect(pricing.tax_minor).toBe(1028);
|
|
116
|
+
expect(pricing.tax_lines).toHaveLength(1);
|
|
117
|
+
expect(pricing.tax_lines[0].is_inclusive).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it("exclusive tax is added on top (backend vector: 2400 → 2640)", () => {
|
|
120
|
+
const rules = [taxRule({ name: "Service", code: "service", rate_basis_points: 1000 })];
|
|
121
|
+
const pricing = calculatePricing({ items: cart(1200, 2), taxRules: rules, currency_code: "GHS" });
|
|
122
|
+
expect(pricing.subtotal_minor).toBe(2400);
|
|
123
|
+
expect(pricing.exclusive_tax_minor).toBe(240);
|
|
124
|
+
expect(pricing.total_minor).toBe(2640);
|
|
125
|
+
});
|
|
126
|
+
it("mixed inclusive + exclusive adds only the exclusive portion (backend vector: 2640)", () => {
|
|
127
|
+
const rules = [
|
|
128
|
+
taxRule({ name: "Incl VAT", code: "incl_vat", rate_basis_points: 7500, is_inclusive: true }),
|
|
129
|
+
taxRule({ name: "Service", code: "service", rate_basis_points: 1000 }),
|
|
130
|
+
];
|
|
131
|
+
const pricing = calculatePricing({ items: cart(1200, 2), taxRules: rules, currency_code: "GHS" });
|
|
132
|
+
expect(pricing.total_minor).toBe(2640);
|
|
133
|
+
expect(pricing.exclusive_tax_minor).toBe(240);
|
|
134
|
+
expect(pricing.tax_minor).toBe(1028 + 240); // display breakdown
|
|
135
|
+
expect(pricing.tax_lines).toHaveLength(2);
|
|
136
|
+
});
|
|
137
|
+
it("applies tips when provided (POS total = subtotal + excl tax + tip)", () => {
|
|
138
|
+
const rules = [taxRule({ name: "Service", code: "service", rate_basis_points: 1000 })];
|
|
139
|
+
const pricing = calculatePricing({ items: cart(1200, 2), taxRules: rules, tip_minor: 500, currency_code: "GHS" });
|
|
140
|
+
expect(pricing.total_minor).toBe(2400 + 240 + 500);
|
|
141
|
+
});
|
|
142
|
+
});
|
package/dist/reference/api.d.ts
CHANGED
|
@@ -4,7 +4,24 @@
|
|
|
4
4
|
* These endpoints are expected on the backend. If they are not available,
|
|
5
5
|
* the ReferenceSyncService falls back to legacy menu endpoints.
|
|
6
6
|
*/
|
|
7
|
-
import type { ReferenceDataKey, ReferenceVersionInfo, ReferenceChangeSet } from "./types";
|
|
7
|
+
import type { ReferenceDataKey, ReferenceVersionInfo, ReferenceChangeSet, ReferenceDataRecord } from "./types";
|
|
8
|
+
/**
|
|
9
|
+
* Optional LAN supplier: when the cloud is unreachable (offline + connected to
|
|
10
|
+
* a Local Node), the host app can serve cached reference data from the node via
|
|
11
|
+
* the LAN instead of failing. Injected by the host (mobile/desktop) once at
|
|
12
|
+
* bootstrap; null means no LAN reference path.
|
|
13
|
+
*/
|
|
14
|
+
export type ReferenceLocalNodeFetcher = (options: {
|
|
15
|
+
businessId: string;
|
|
16
|
+
keys: ReferenceDataKey[];
|
|
17
|
+
}) => Promise<Partial<Record<ReferenceDataKey, ReferenceDataRecord | null>>>;
|
|
18
|
+
export declare function setReferenceLocalNodeFetcher(fetcher: ReferenceLocalNodeFetcher | null): void;
|
|
8
19
|
export declare function fetchReferenceVersions(businessId: string): Promise<ReferenceVersionInfo[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Fetch reference data for the requested keys. Falls back to the Local Node
|
|
22
|
+
* (LAN) when the cloud call fails and a LAN fetcher is configured — so a
|
|
23
|
+
* no-internet terminal can still converge after a `reference_data.changed`
|
|
24
|
+
* event relayed over the LAN.
|
|
25
|
+
*/
|
|
9
26
|
export declare function fetchReferenceData(businessId: string, keys: ReferenceDataKey[]): Promise<Record<ReferenceDataKey, unknown>>;
|
|
10
27
|
export declare function fetchReferenceChanges<T>(businessId: string, key: ReferenceDataKey, fromVersion: string): Promise<ReferenceChangeSet<T>>;
|
package/dist/reference/api.js
CHANGED
|
@@ -5,13 +5,36 @@
|
|
|
5
5
|
* the ReferenceSyncService falls back to legacy menu endpoints.
|
|
6
6
|
*/
|
|
7
7
|
import { api } from "../adapters/http";
|
|
8
|
+
let localNodeFetcher = null;
|
|
9
|
+
export function setReferenceLocalNodeFetcher(fetcher) {
|
|
10
|
+
localNodeFetcher = fetcher;
|
|
11
|
+
}
|
|
8
12
|
export async function fetchReferenceVersions(businessId) {
|
|
9
13
|
const response = await api.get(`/businesses/${businessId}/reference-versions`);
|
|
10
14
|
return response.data.data;
|
|
11
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Fetch reference data for the requested keys. Falls back to the Local Node
|
|
18
|
+
* (LAN) when the cloud call fails and a LAN fetcher is configured — so a
|
|
19
|
+
* no-internet terminal can still converge after a `reference_data.changed`
|
|
20
|
+
* event relayed over the LAN.
|
|
21
|
+
*/
|
|
12
22
|
export async function fetchReferenceData(businessId, keys) {
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
try {
|
|
24
|
+
const response = await api.get(`/businesses/${businessId}/reference-data`, { params: { keys: keys.join(",") } });
|
|
25
|
+
return response.data.data;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (!localNodeFetcher)
|
|
29
|
+
throw error;
|
|
30
|
+
const cached = await localNodeFetcher({ businessId, keys });
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
if (cached[key]?.payload != null)
|
|
34
|
+
out[key] = cached[key].payload;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
15
38
|
}
|
|
16
39
|
export async function fetchReferenceChanges(businessId, key, fromVersion) {
|
|
17
40
|
const response = await api.get(`/businesses/${businessId}/reference-changes`, { params: { key, from_version: fromVersion } });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capabilities reference — offline-first.
|
|
3
|
+
*
|
|
4
|
+
* Standardises business-membership capabilities for both mobile and desktop.
|
|
5
|
+
* The backend `GET /businesses/:id/my-membership` is the source of truth;
|
|
6
|
+
* the result is mirrored to `reference_data` key `capabilities` so drawer/nav
|
|
7
|
+
* filtering works offline and survives restarts.
|
|
8
|
+
*
|
|
9
|
+
* Host apps wire `businessService.fetchMyBusinessMembership` via adapters/services.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Standard hook for both apps — businessId comes from the host's active business store.
|
|
13
|
+
* Returns the same shape as the previous app-level useMyBusinessCapabilities.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useBusinessCapabilities(businessId?: string | null): import("@tanstack/react-query").UseQueryResult<{
|
|
16
|
+
role: string;
|
|
17
|
+
source: string;
|
|
18
|
+
capabilities: string[];
|
|
19
|
+
status: string;
|
|
20
|
+
}, Error>;
|
|
21
|
+
export declare function useHasCapabilityFor(businessId: string | undefined, capability: string): boolean;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capabilities reference — offline-first.
|
|
3
|
+
*
|
|
4
|
+
* Standardises business-membership capabilities for both mobile and desktop.
|
|
5
|
+
* The backend `GET /businesses/:id/my-membership` is the source of truth;
|
|
6
|
+
* the result is mirrored to `reference_data` key `capabilities` so drawer/nav
|
|
7
|
+
* filtering works offline and survives restarts.
|
|
8
|
+
*
|
|
9
|
+
* Host apps wire `businessService.fetchMyBusinessMembership` via adapters/services.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useState } from "react";
|
|
12
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
13
|
+
import { openSyncDatabase } from "../db/connection";
|
|
14
|
+
import { getReferenceData, setReferenceData } from "./operations";
|
|
15
|
+
import { businessService } from "../adapters/services";
|
|
16
|
+
const MEMBERSHIP_KEYS = {
|
|
17
|
+
all: ["my-membership"],
|
|
18
|
+
byBusiness: (id) => [...MEMBERSHIP_KEYS.all, id],
|
|
19
|
+
};
|
|
20
|
+
function useCapabilitiesPlaceholder(businessId) {
|
|
21
|
+
const [cached, setCached] = useState(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!businessId) {
|
|
24
|
+
setCached(null);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const db = await openSyncDatabase();
|
|
31
|
+
const rec = await getReferenceData(db, businessId, "capabilities");
|
|
32
|
+
if (!cancelled && rec?.payload)
|
|
33
|
+
setCached(rec.payload);
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
})();
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
};
|
|
40
|
+
}, [businessId]);
|
|
41
|
+
return cached;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Standard hook for both apps — businessId comes from the host's active business store.
|
|
45
|
+
* Returns the same shape as the previous app-level useMyBusinessCapabilities.
|
|
46
|
+
*/
|
|
47
|
+
export function useBusinessCapabilities(businessId) {
|
|
48
|
+
const queryClient = useQueryClient();
|
|
49
|
+
void queryClient;
|
|
50
|
+
const cached = useCapabilitiesPlaceholder(businessId ?? null);
|
|
51
|
+
return useQuery({
|
|
52
|
+
queryKey: MEMBERSHIP_KEYS.byBusiness(businessId ?? ""),
|
|
53
|
+
queryFn: async () => {
|
|
54
|
+
const res = (await businessService.fetchMyBusinessMembership(businessId));
|
|
55
|
+
try {
|
|
56
|
+
const db = await openSyncDatabase();
|
|
57
|
+
await setReferenceData(db, businessId, {
|
|
58
|
+
key: "capabilities",
|
|
59
|
+
version: `membership-${Date.now()}`,
|
|
60
|
+
payload: res,
|
|
61
|
+
downloadedAt: new Date().toISOString(),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
return res;
|
|
66
|
+
},
|
|
67
|
+
enabled: !!businessId,
|
|
68
|
+
staleTime: Infinity,
|
|
69
|
+
placeholderData: cached ?? undefined,
|
|
70
|
+
select: (data) => ({
|
|
71
|
+
role: data.data.role,
|
|
72
|
+
source: data.data.source,
|
|
73
|
+
capabilities: data.data.capabilities,
|
|
74
|
+
status: data.data.status,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function useHasCapabilityFor(businessId, capability) {
|
|
79
|
+
const { data } = useBusinessCapabilities(businessId);
|
|
80
|
+
return data?.capabilities.includes(capability) ?? false;
|
|
81
|
+
}
|
|
@@ -23,6 +23,7 @@ const DEFAULT_SYNC_KEYS = [
|
|
|
23
23
|
"business:profile",
|
|
24
24
|
"operating-hours",
|
|
25
25
|
"subscription:entitlements",
|
|
26
|
+
"capabilities",
|
|
26
27
|
];
|
|
27
28
|
class ReferenceSyncService {
|
|
28
29
|
constructor() {
|
|
@@ -204,6 +205,13 @@ class ReferenceSyncService {
|
|
|
204
205
|
version: this.hashPayload(response),
|
|
205
206
|
};
|
|
206
207
|
}
|
|
208
|
+
case "capabilities": {
|
|
209
|
+
const response = await businessService.fetchMyBusinessMembership(businessId);
|
|
210
|
+
return {
|
|
211
|
+
payload: response,
|
|
212
|
+
version: this.hashPayload(response),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
207
215
|
default:
|
|
208
216
|
throw new Error(`No fallback fetch available for ${key}`);
|
|
209
217
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* It is synchronized separately from transactional data through the
|
|
9
9
|
* ReferenceSyncService.
|
|
10
10
|
*/
|
|
11
|
-
export type ReferenceDataKey = "menu:items" | "menu:categories" | "roles" | "members" | "tax-rules" | "business:profile" | "operating-hours" | "subscription:entitlements";
|
|
11
|
+
export type ReferenceDataKey = "menu:items" | "menu:categories" | "roles" | "members" | "tax-rules" | "business:profile" | "operating-hours" | "subscription:entitlements" | "capabilities";
|
|
12
12
|
export interface ReferenceDataRecord<T = unknown> {
|
|
13
13
|
key: ReferenceDataKey;
|
|
14
14
|
version: string;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { Command, DomainEvent, PersistedCommand } from "./types";
|
|
12
12
|
import type { SyncEngine } from "./engine";
|
|
13
|
+
export declare function getQueuedStatus(commandType: string): string | null;
|
|
13
14
|
declare class CommandQueueService {
|
|
14
15
|
private replaying;
|
|
15
16
|
private syncEngine;
|
|
@@ -14,8 +14,34 @@ import { syncLogger } from "../utils/sync-logger";
|
|
|
14
14
|
import { generateEventId } from "../utils/uuid";
|
|
15
15
|
import { generateLocalOrderNumber, generateLocalCalloutNumber, } from "./order-number";
|
|
16
16
|
const MAX_RETRIES = 5;
|
|
17
|
+
const QUEUED_STATUS_BY_COMMAND = {
|
|
18
|
+
"order:accept": "accepted",
|
|
19
|
+
"order:reject": "cancelled",
|
|
20
|
+
"order:start_preparing": "preparing",
|
|
21
|
+
"order:mark_ready": "ready",
|
|
22
|
+
"order:complete_pickup": "delivered",
|
|
23
|
+
"order:cancel": "cancelled",
|
|
24
|
+
"delivery:assign": "assigned",
|
|
25
|
+
"delivery:confirm_handover": "delivered",
|
|
26
|
+
};
|
|
27
|
+
export function getQueuedStatus(commandType) {
|
|
28
|
+
return QUEUED_STATUS_BY_COMMAND[commandType] ?? null;
|
|
29
|
+
}
|
|
17
30
|
function buildOptimisticEvent(command) {
|
|
18
31
|
const payload = command.payload;
|
|
32
|
+
if (command.command_type === "order:create") {
|
|
33
|
+
return {
|
|
34
|
+
event_id: generateEventId(),
|
|
35
|
+
event_type: `${command.command_type.replace(":", ".")}_queued`,
|
|
36
|
+
event_version: "1.0",
|
|
37
|
+
entity_id: command.entity_id ?? command.command_id,
|
|
38
|
+
user_id: command.user_id,
|
|
39
|
+
device_id: command.device_id,
|
|
40
|
+
payload: buildOptimisticOrder(command, payload),
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const queuedStatus = getQueuedStatus(command.command_type);
|
|
19
45
|
return {
|
|
20
46
|
event_id: generateEventId(),
|
|
21
47
|
event_type: `${command.command_type.replace(":", ".")}_queued`,
|
|
@@ -23,13 +49,15 @@ function buildOptimisticEvent(command) {
|
|
|
23
49
|
entity_id: command.entity_id ?? command.command_id,
|
|
24
50
|
user_id: command.user_id,
|
|
25
51
|
device_id: command.device_id,
|
|
26
|
-
payload:
|
|
27
|
-
|
|
28
|
-
:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
52
|
+
payload: {
|
|
53
|
+
...payload,
|
|
54
|
+
command_id: command.command_id,
|
|
55
|
+
status: "pending",
|
|
56
|
+
// Used by offline read-model patching so the list reflects the queued transition.
|
|
57
|
+
to_status: queuedStatus,
|
|
58
|
+
order_id: command.entity_id,
|
|
59
|
+
delivery_id: command.entity_id,
|
|
60
|
+
},
|
|
33
61
|
timestamp: new Date().toISOString(),
|
|
34
62
|
};
|
|
35
63
|
}
|
|
@@ -136,17 +164,17 @@ class CommandQueueService {
|
|
|
136
164
|
order_number: event.payload.order_number ?? null,
|
|
137
165
|
status: event.payload.status,
|
|
138
166
|
});
|
|
139
|
-
// Persist
|
|
140
|
-
// rehydrated
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
167
|
+
// Persist optimistic events so they survive restarts and can be
|
|
168
|
+
// rehydrated offline. For order:create this is the full order; for
|
|
169
|
+
// transitions it carries to_status so the read model can reflect the
|
|
170
|
+
// queued status immediately.
|
|
171
|
+
try {
|
|
172
|
+
await insertEvent(db, { event, origin: "local" });
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
syncLogger.error("CommandQueueService", "Failed to persist optimistic event", {
|
|
176
|
+
error: String(error),
|
|
177
|
+
});
|
|
150
178
|
}
|
|
151
179
|
return event;
|
|
152
180
|
}
|
|
@@ -188,6 +216,24 @@ class CommandQueueService {
|
|
|
188
216
|
}
|
|
189
217
|
catch (error) {
|
|
190
218
|
const message = error instanceof Error ? error.message : String(error);
|
|
219
|
+
// Distinguish retryable (network/5xx/timeout) vs non-retryable (4xx validation)
|
|
220
|
+
// so a rejected order does not block the entire outbox.
|
|
221
|
+
const apiErr = error;
|
|
222
|
+
const isNonRetryable = apiErr?.name === "ApiError" &&
|
|
223
|
+
typeof apiErr.status === "number" &&
|
|
224
|
+
apiErr.status >= 400 &&
|
|
225
|
+
apiErr.status < 500;
|
|
226
|
+
if (isNonRetryable) {
|
|
227
|
+
await updateCommandStatus(db, command.command_id, "failed", `permanent:${message}`);
|
|
228
|
+
syncLogger.warn("CommandQueueService", "Queued command rejected by server (not retrying)", {
|
|
229
|
+
command_id: command.command_id,
|
|
230
|
+
command_type: command.command_type,
|
|
231
|
+
status: apiErr.status,
|
|
232
|
+
error: message,
|
|
233
|
+
});
|
|
234
|
+
// Do not throw — allow replay to continue to next command.
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
191
237
|
await updateCommandStatus(db, command.command_id, "failed", message);
|
|
192
238
|
syncLogger.command(parsedCommand, "failed", { error: message });
|
|
193
239
|
throw error;
|
|
@@ -218,8 +264,17 @@ class CommandQueueService {
|
|
|
218
264
|
try {
|
|
219
265
|
await this.processCommand(command);
|
|
220
266
|
}
|
|
221
|
-
catch {
|
|
222
|
-
|
|
267
|
+
catch (e) {
|
|
268
|
+
const apiErr = e;
|
|
269
|
+
const isNonRetryable = apiErr?.name === "ApiError" &&
|
|
270
|
+
typeof apiErr.status === "number" &&
|
|
271
|
+
apiErr.status >= 400 &&
|
|
272
|
+
apiErr.status < 500;
|
|
273
|
+
if (isNonRetryable) {
|
|
274
|
+
// Already marked permanent in processCommand — continue to next.
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// Network/5xx — stop to preserve ordering; retry later.
|
|
223
278
|
break;
|
|
224
279
|
}
|
|
225
280
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline-capable mutation runner.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a direct REST service call (used by menu edits etc.) so that when the
|
|
5
|
+
* request fails with a NETWORK error (offline / cloud unreachable), the
|
|
6
|
+
* operation is retried through the Sync Engine instead — which routes it to
|
|
7
|
+
* the Local Node, or persists it in the durable outbox for replay when
|
|
8
|
+
* connectivity returns. Definitive server rejections (4xx) are NOT touched:
|
|
9
|
+
* they are real business decisions and must surface to the caller.
|
|
10
|
+
*
|
|
11
|
+
* Keeps the online path and its result shape unchanged.
|
|
12
|
+
*/
|
|
13
|
+
export interface OfflineCapableOptions<TService, TCommandPayload> {
|
|
14
|
+
/** Direct REST call — used online. */
|
|
15
|
+
run: () => Promise<TService>;
|
|
16
|
+
/** Sync-engine command fallback (Local Node / outbox). */
|
|
17
|
+
commandType: string;
|
|
18
|
+
businessId: string;
|
|
19
|
+
entityId?: string;
|
|
20
|
+
payload: TCommandPayload;
|
|
21
|
+
}
|
|
22
|
+
export declare function runOfflineCapableMutation<TService, TCommandPayload>(options: OfflineCapableOptions<TService, TCommandPayload>): Promise<TService>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline-capable mutation runner.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a direct REST service call (used by menu edits etc.) so that when the
|
|
5
|
+
* request fails with a NETWORK error (offline / cloud unreachable), the
|
|
6
|
+
* operation is retried through the Sync Engine instead — which routes it to
|
|
7
|
+
* the Local Node, or persists it in the durable outbox for replay when
|
|
8
|
+
* connectivity returns. Definitive server rejections (4xx) are NOT touched:
|
|
9
|
+
* they are real business decisions and must surface to the caller.
|
|
10
|
+
*
|
|
11
|
+
* Keeps the online path and its result shape unchanged.
|
|
12
|
+
*/
|
|
13
|
+
import { syncEngine } from "./engine";
|
|
14
|
+
import { buildCommand } from "./command-builder";
|
|
15
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
16
|
+
import { ApiError } from "../adapters/http";
|
|
17
|
+
export async function runOfflineCapableMutation(options) {
|
|
18
|
+
try {
|
|
19
|
+
return await options.run();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof ApiError && error.status >= 400 && error.status < 500) {
|
|
23
|
+
// Definitive rejection — surface it, do not queue.
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
// Network error (no response), timeout, or transport failure: route through
|
|
27
|
+
// the Sync Engine (LAN node → outbox). The engine itself decides.
|
|
28
|
+
syncLogger.warn("OfflineCapable", "Direct call failed, routing via Sync Engine", {
|
|
29
|
+
command_type: options.commandType,
|
|
30
|
+
error: error instanceof Error ? error.message : String(error),
|
|
31
|
+
});
|
|
32
|
+
const command = buildCommand({
|
|
33
|
+
commandType: options.commandType,
|
|
34
|
+
businessId: options.businessId,
|
|
35
|
+
entityId: options.entityId,
|
|
36
|
+
payload: options.payload,
|
|
37
|
+
});
|
|
38
|
+
const events = await syncEngine.execute(command);
|
|
39
|
+
return events[0]?.payload;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -22,6 +22,13 @@ declare class OrderSnapshotsService {
|
|
|
22
22
|
* duplicates).
|
|
23
23
|
*/
|
|
24
24
|
persistOrderEvent(event: DomainEvent, origin: "local" | "lan" | "cloud"): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* Patch the latest status for a queued transition (e.g. accept_queued)
|
|
27
|
+
* carrying to_status/order_id. Used so offline devices see the new status
|
|
28
|
+
* immediately and after restart via the durable snapshot.
|
|
29
|
+
*/
|
|
30
|
+
applyQueuedStatusEvent(event: DomainEvent, origin?: "local" | "lan" | "cloud"): Promise<void>;
|
|
31
|
+
private applyOrderStatusToReadModelInternal;
|
|
25
32
|
/**
|
|
26
33
|
* Patch the latest status of a known order inside every list snapshot for the
|
|
27
34
|
* business, so offline devices see live status changes without a refetch.
|
|
@@ -50,17 +50,29 @@ class OrderSnapshotsService {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Patch the latest status
|
|
54
|
-
*
|
|
53
|
+
* Patch the latest status for a queued transition (e.g. accept_queued)
|
|
54
|
+
* carrying to_status/order_id. Used so offline devices see the new status
|
|
55
|
+
* immediately and after restart via the durable snapshot.
|
|
55
56
|
*/
|
|
56
|
-
async
|
|
57
|
+
async applyQueuedStatusEvent(event, origin = "local") {
|
|
57
58
|
const payload = event.payload;
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
if (!orderId || !toStatus || event.event_type !== "order.status_changed") {
|
|
59
|
+
const toStatus = payload?.to_status;
|
|
60
|
+
const orderId = payload?.order_id ?? event.entity_id;
|
|
61
|
+
if (!toStatus || !orderId)
|
|
62
62
|
return;
|
|
63
|
+
try {
|
|
64
|
+
const db = await openSyncDatabase();
|
|
65
|
+
await insertEvent(db, { event, origin });
|
|
66
|
+
await this.applyOrderStatusToReadModelInternal(db, orderId, toStatus, event);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
syncLogger.error("OrderSnapshots", "Failed to apply queued status", {
|
|
70
|
+
error: String(error),
|
|
71
|
+
event_type: event.event_type,
|
|
72
|
+
});
|
|
63
73
|
}
|
|
74
|
+
}
|
|
75
|
+
async applyOrderStatusToReadModelInternal(db, orderId, toStatus, event) {
|
|
64
76
|
try {
|
|
65
77
|
const lists = await getEntitySnapshotsByType(db, "seller_orders");
|
|
66
78
|
for (const list of lists) {
|
|
@@ -85,6 +97,20 @@ class OrderSnapshotsService {
|
|
|
85
97
|
});
|
|
86
98
|
}
|
|
87
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Patch the latest status of a known order inside every list snapshot for the
|
|
102
|
+
* business, so offline devices see live status changes without a refetch.
|
|
103
|
+
*/
|
|
104
|
+
async applyOrderStatusToReadModel(db, event) {
|
|
105
|
+
const payload = event.payload;
|
|
106
|
+
const orderId = payload?.order_id;
|
|
107
|
+
const toStatus = payload?.to_status ??
|
|
108
|
+
payload?.status;
|
|
109
|
+
if (!orderId || !toStatus || event.event_type !== "order.status_changed") {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await this.applyOrderStatusToReadModelInternal(db, orderId, toStatus, event);
|
|
113
|
+
}
|
|
88
114
|
/**
|
|
89
115
|
* Write through a fetched order list so it is available offline. Used by the
|
|
90
116
|
* seller and POS order queries. `scope` distinguishes complementary lists
|
|
@@ -26,6 +26,16 @@ const ORDER_STATE_EVENT_TYPES = new Set([
|
|
|
26
26
|
"delivery.status_changed",
|
|
27
27
|
"delivery.assigned",
|
|
28
28
|
]);
|
|
29
|
+
const QUEUED_STATUS_EVENT_TYPES = new Set([
|
|
30
|
+
"order.accept_queued",
|
|
31
|
+
"order.reject_queued",
|
|
32
|
+
"order.start_preparing_queued",
|
|
33
|
+
"order.mark_ready_queued",
|
|
34
|
+
"order.complete_pickup_queued",
|
|
35
|
+
"order.cancel_queued",
|
|
36
|
+
"delivery.assign_queued",
|
|
37
|
+
"delivery.confirm_handover_queued",
|
|
38
|
+
]);
|
|
29
39
|
let invalidator = null;
|
|
30
40
|
/**
|
|
31
41
|
* Register a callback used to invalidate TanStack Query caches when a pending
|
|
@@ -88,7 +98,8 @@ export async function hydratePendingOrders() {
|
|
|
88
98
|
*/
|
|
89
99
|
export async function applyOrderStateChangeEvent(event, origin = "local") {
|
|
90
100
|
try {
|
|
91
|
-
|
|
101
|
+
const isQueued = QUEUED_STATUS_EVENT_TYPES.has(event.event_type);
|
|
102
|
+
if (!ORDER_STATE_EVENT_TYPES.has(event.event_type) && !isQueued)
|
|
92
103
|
return;
|
|
93
104
|
syncLogger.debug("OrderState", "Order state changed", {
|
|
94
105
|
origin,
|
|
@@ -97,6 +108,12 @@ export async function applyOrderStateChangeEvent(event, origin = "local") {
|
|
|
97
108
|
});
|
|
98
109
|
// Persist the event into the durable journal and patch the offline read
|
|
99
110
|
// model so every terminal keeps the same order states after a restart.
|
|
111
|
+
// Queued transitions carry to_status/order_id so they patch instantly offline.
|
|
112
|
+
if (isQueued) {
|
|
113
|
+
await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
|
|
114
|
+
invalidator?.();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
100
117
|
await orderSnapshotsService.persistOrderEvent(event, origin);
|
|
101
118
|
invalidator?.();
|
|
102
119
|
}
|
|
@@ -118,6 +135,14 @@ export async function applyPendingOrderEvent(event, origin = "local") {
|
|
|
118
135
|
await insertEvent(db, { event, origin });
|
|
119
136
|
return;
|
|
120
137
|
}
|
|
138
|
+
if (QUEUED_STATUS_EVENT_TYPES.has(event.event_type)) {
|
|
139
|
+
// Queued transitions: patch the read model so listing reflects new status offline.
|
|
140
|
+
await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
|
|
141
|
+
const db = await openSyncDatabase();
|
|
142
|
+
await insertEvent(db, { event, origin });
|
|
143
|
+
invalidator?.();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
121
146
|
if (event.event_type === "order.created") {
|
|
122
147
|
const payload = event.payload;
|
|
123
148
|
const localOrderNumber = payload?.local_order_number;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* resulting domain events. This transport is used when the cloud API is
|
|
6
6
|
* reachable.
|
|
7
7
|
*/
|
|
8
|
-
import { posService, sellerService, kioskService } from "../adapters/services";
|
|
8
|
+
import { posService, sellerService, kioskService, menuService } from "../adapters/services";
|
|
9
9
|
import { syncLogger } from "../utils/sync-logger";
|
|
10
10
|
import { generateEventId } from "../utils/uuid";
|
|
11
11
|
export class RestTransport {
|
|
@@ -88,6 +88,22 @@ export class RestTransport {
|
|
|
88
88
|
const response = await sellerService.confirmHandover(entity_id);
|
|
89
89
|
return this.wrapResponse("delivery.status_changed", response, entity_id, timestamp, command);
|
|
90
90
|
}
|
|
91
|
+
case "menu:create_item": {
|
|
92
|
+
const response = await menuService.createMenuItem(business_id, payload);
|
|
93
|
+
return this.wrapResponse("menu.item_created", response, entity_id, timestamp, command);
|
|
94
|
+
}
|
|
95
|
+
case "menu:update_item": {
|
|
96
|
+
const response = await menuService.updateMenuItem(entity_id, payload);
|
|
97
|
+
return this.wrapResponse("menu.item_updated", response, entity_id, timestamp, command);
|
|
98
|
+
}
|
|
99
|
+
case "menu:delete_item": {
|
|
100
|
+
const response = await menuService.deleteMenuItem(entity_id);
|
|
101
|
+
return this.wrapResponse("menu.item_deleted", response, entity_id, timestamp, command);
|
|
102
|
+
}
|
|
103
|
+
case "menu:create_category": {
|
|
104
|
+
const response = await menuService.createCategory(business_id, payload);
|
|
105
|
+
return this.wrapResponse("menu.category_created", response, entity_id, timestamp, command);
|
|
106
|
+
}
|
|
91
107
|
default:
|
|
92
108
|
throw new Error(`RestTransport: unsupported command type ${command_type}`);
|
|
93
109
|
}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Application-wide constants — plain config, no expo dependencies.
|
|
3
3
|
* Values can be overridden via createTeincCore config (future).
|
|
4
4
|
*/
|
|
5
|
-
/** Base URL for the API —
|
|
6
|
-
export declare
|
|
7
|
-
/** WebSocket URL for Phoenix Channels */
|
|
8
|
-
export declare
|
|
5
|
+
/** Base URL for the API — set at runtime via createTeincCore({ apiBaseUrl }) from the host app's .env */
|
|
6
|
+
export declare let API_BASE_URL: string;
|
|
7
|
+
/** WebSocket URL for Phoenix Channels — set at runtime via createTeincCore({ wsBaseUrl }) */
|
|
8
|
+
export declare let WS_BASE_URL: string;
|
|
9
|
+
export declare function setApiBaseUrl(url: string | undefined): void;
|
|
10
|
+
export declare function setWsBaseUrl(url: string | undefined): void;
|
|
9
11
|
/** App metadata */
|
|
10
12
|
export declare const APP_NAME = "Teinc Food Business";
|
|
11
13
|
export declare const APP_VERSION = "1.0.0";
|
package/dist/utils/constants.js
CHANGED
|
@@ -2,14 +2,40 @@
|
|
|
2
2
|
* Application-wide constants — plain config, no expo dependencies.
|
|
3
3
|
* Values can be overridden via createTeincCore config (future).
|
|
4
4
|
*/
|
|
5
|
-
/** Base URL for the API —
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
/** Base URL for the API — set at runtime via createTeincCore({ apiBaseUrl }) from the host app's .env */
|
|
6
|
+
export let API_BASE_URL = "http://localhost:4000/api/v1";
|
|
7
|
+
/** WebSocket URL for Phoenix Channels — set at runtime via createTeincCore({ wsBaseUrl }) */
|
|
8
|
+
export let WS_BASE_URL = "ws://localhost:4000";
|
|
9
|
+
export function setApiBaseUrl(url) {
|
|
10
|
+
if (url) {
|
|
11
|
+
API_BASE_URL = url;
|
|
12
|
+
try {
|
|
13
|
+
console.debug("[core:constants] API_BASE_URL set", url);
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
try {
|
|
19
|
+
console.debug("[core:constants] API_BASE_URL using default", API_BASE_URL);
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function setWsBaseUrl(url) {
|
|
25
|
+
if (url) {
|
|
26
|
+
WS_BASE_URL = url;
|
|
27
|
+
try {
|
|
28
|
+
console.debug("[core:constants] WS_BASE_URL set", url);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
try {
|
|
34
|
+
console.debug("[core:constants] WS_BASE_URL using default", WS_BASE_URL);
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
13
39
|
/** App metadata */
|
|
14
40
|
export const APP_NAME = "Teinc Food Business";
|
|
15
41
|
export const APP_VERSION = "1.0.0";
|
|
@@ -29,10 +29,20 @@ class SyncLogger {
|
|
|
29
29
|
this.warn = (namespace, message, data) => this.log("warn", namespace, message, data);
|
|
30
30
|
this.error = (namespace, message, data) => this.log("error", namespace, message, data);
|
|
31
31
|
try {
|
|
32
|
-
|
|
32
|
+
const isDev = typeof __DEV__ !== "undefined" ? __DEV__ : false;
|
|
33
|
+
if (typeof isDev !== "undefined") {
|
|
34
|
+
this.enabled = isDev;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
try {
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
const nodeEnv = typeof process !== "undefined" ? process?.env?.NODE_ENV : undefined;
|
|
42
|
+
this.enabled = nodeEnv !== "production";
|
|
33
43
|
}
|
|
34
44
|
catch {
|
|
35
|
-
this.enabled =
|
|
45
|
+
this.enabled = true;
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
48
|
setEnabled(enabled) {
|
package/package.json
CHANGED