@tallyui/pos 0.1.0 → 3.0.0-next.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/LICENSE +21 -0
- package/dist/index.d.ts +2031 -43
- package/dist/index.js +4629 -205
- package/package.json +23 -12
- package/src/currency/currency-provider.tsx +12 -4
- package/src/currency/index.ts +1 -2
- package/src/index.ts +53 -4
- package/src/order/allocate-order-discount.ts +34 -0
- package/src/order/index.ts +8 -0
- package/src/order/order-builder.ts +256 -95
- package/src/order/order-manager.ts +26 -43
- package/src/order/tax-figures.ts +23 -0
- package/src/order/types.ts +73 -15
- package/src/outbox/backend-not-found.ts +27 -0
- package/src/outbox/http-transport.ts +67 -0
- package/src/outbox/index.ts +9 -0
- package/src/outbox/logger.ts +3 -0
- package/src/outbox/order-outbox.ts +502 -0
- package/src/outbox/register-outbox.ts +250 -0
- package/src/outbox/types.test-d.ts +22 -0
- package/src/outbox/types.ts +36 -0
- package/src/outbox/use-order-outbox.ts +175 -0
- package/src/pos-order/__fixtures__/order-create-v3.json +97 -0
- package/src/pos-order/command.ts +123 -0
- package/src/pos-order/device-id.ts +23 -0
- package/src/pos-order/finalize.ts +219 -0
- package/src/pos-order/index.ts +10 -0
- package/src/pos-order/needs-attention.ts +16 -0
- package/src/pos-order/open.ts +269 -0
- package/src/pos-order/same-sale.ts +26 -0
- package/src/pos-order/schema.ts +130 -0
- package/src/pos-order/types.ts +93 -0
- package/src/pos-order/uuidv7.ts +54 -0
- package/src/product/index.ts +2 -0
- package/src/product/search-products.ts +32 -0
- package/src/product/stock.ts +25 -0
- package/src/receipt/build-receipt-data.ts +39 -23
- package/src/receipt/types.ts +17 -10
- package/src/register/__fixtures__/closure-local-row.json +61 -0
- package/src/register/__fixtures__/closure.json +197 -0
- package/src/register/__fixtures__/corrections-dst.json +63 -0
- package/src/register/closure-document.ts +277 -0
- package/src/register/closure-rows.ts +65 -0
- package/src/register/document-labels.ts +46 -0
- package/src/register/expected.ts +66 -0
- package/src/register/export-csv.ts +74 -0
- package/src/register/facts.ts +109 -0
- package/src/register/index.ts +32 -0
- package/src/register/money.ts +18 -0
- package/src/register/movement-input.ts +59 -0
- package/src/register/register-commands.ts +147 -0
- package/src/register/register-count.denominations.ts +11 -0
- package/src/register/register-count.helpers.ts +64 -0
- package/src/register/register-document.ts +268 -0
- package/src/register/schemas.ts +175 -0
- package/src/register/session-store.ts +570 -0
- package/src/register/settled-figures.ts +98 -0
- package/src/register/use-register-session.ts +469 -0
- package/src/rxdb/index.ts +2 -0
- package/src/sale/cart.ts +23 -0
- package/src/sale/catalogue.ts +21 -0
- package/src/sale/index.ts +5 -0
- package/src/sale/use-sale.ts +394 -0
- package/src/store-settings/index.ts +5 -0
- package/src/store-settings/map-store-settings.ts +18 -0
- package/src/store-settings/resolve-store-settings.ts +65 -0
- package/src/store-settings/use-store-settings.ts +84 -0
- package/src/tax/exact.ts +218 -0
- package/src/tax/index.ts +4 -3
- package/src/tax/tax-provider.tsx +35 -12
- package/src/tax/types.ts +8 -6
- package/src/tender/index.ts +2 -0
- package/src/tender/tender-state.ts +326 -0
- package/src/currency/currency-provider.test.tsx +0 -36
- package/src/currency/format-currency.test.ts +0 -36
- package/src/currency/format-currency.ts +0 -18
- package/src/logging/logger.test.ts +0 -154
- package/src/logging/sinks.test.ts +0 -60
- package/src/order/discount-engine.test.ts +0 -152
- package/src/order/order-builder.test.ts +0 -246
- package/src/order/order-manager.test.ts +0 -209
- package/src/order/payment.test.ts +0 -91
- package/src/receipt/build-receipt-data.test.ts +0 -166
- package/src/repository/create-repository.test.ts +0 -166
- package/src/tax/calculate.test.ts +0 -59
- package/src/tax/calculate.ts +0 -32
- package/src/tax/tax-provider.test.tsx +0 -51
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { firstValueFrom } from 'rxjs';
|
|
3
|
-
import { createOrderBuilder } from './order-builder';
|
|
4
|
-
import type { ProductTraits } from '@tallyui/core';
|
|
5
|
-
import type { TaxContext } from '../tax/types';
|
|
6
|
-
|
|
7
|
-
const traits: ProductTraits = {
|
|
8
|
-
getId: (doc) => doc.id,
|
|
9
|
-
getName: (doc) => doc.name,
|
|
10
|
-
getSku: (doc) => doc.sku ?? '',
|
|
11
|
-
getPrice: (doc) => String(doc.price),
|
|
12
|
-
getRegularPrice: (doc) => String(doc.price),
|
|
13
|
-
getSalePrice: () => undefined,
|
|
14
|
-
isOnSale: () => false,
|
|
15
|
-
getImageUrl: (doc) => doc.imageUrl,
|
|
16
|
-
getImageUrls: (doc) => doc.imageUrl ? [doc.imageUrl] : [],
|
|
17
|
-
getDescription: () => undefined,
|
|
18
|
-
getStockStatus: () => 'instock',
|
|
19
|
-
getStockQuantity: () => null,
|
|
20
|
-
hasVariants: () => false,
|
|
21
|
-
getType: () => 'simple',
|
|
22
|
-
getBarcode: () => undefined,
|
|
23
|
-
getCategoryNames: () => [],
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const taxContext: TaxContext = {
|
|
27
|
-
getTaxRate: () => 0.1,
|
|
28
|
-
pricesIncludeTax: false,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const productDoc = { id: 'p1', name: 'Espresso', sku: 'ESP-001', price: 4.50 };
|
|
32
|
-
const productDoc2 = { id: 'p2', name: 'Latte', sku: 'LAT-001', price: 5.00 };
|
|
33
|
-
|
|
34
|
-
describe('OrderBuilder', () => {
|
|
35
|
-
it('starts with an empty draft order', async () => {
|
|
36
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
37
|
-
const order = await firstValueFrom(builder.order$);
|
|
38
|
-
expect(order.status).toBe('draft');
|
|
39
|
-
expect(order.lineItems).toHaveLength(0);
|
|
40
|
-
expect(order.subtotal).toBe(0);
|
|
41
|
-
expect(order.total).toBe(0);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('adds a product as a line item', async () => {
|
|
45
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
46
|
-
builder.addProduct(productDoc, traits);
|
|
47
|
-
|
|
48
|
-
const order = await firstValueFrom(builder.order$);
|
|
49
|
-
expect(order.lineItems).toHaveLength(1);
|
|
50
|
-
expect(order.lineItems[0].name).toBe('Espresso');
|
|
51
|
-
expect(order.lineItems[0].price).toBe(4.50);
|
|
52
|
-
expect(order.lineItems[0].quantity).toBe(1);
|
|
53
|
-
expect(order.lineItems[0].taxRate).toBe(0.1);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('increments quantity when adding same product twice', async () => {
|
|
57
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
58
|
-
builder.addProduct(productDoc, traits);
|
|
59
|
-
builder.addProduct(productDoc, traits);
|
|
60
|
-
|
|
61
|
-
const order = await firstValueFrom(builder.order$);
|
|
62
|
-
expect(order.lineItems).toHaveLength(1);
|
|
63
|
-
expect(order.lineItems[0].quantity).toBe(2);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('treats different products as separate line items', async () => {
|
|
67
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
68
|
-
builder.addProduct(productDoc, traits);
|
|
69
|
-
builder.addProduct(productDoc2, traits);
|
|
70
|
-
|
|
71
|
-
const order = await firstValueFrom(builder.order$);
|
|
72
|
-
expect(order.lineItems).toHaveLength(2);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('calculates line totals with tax (exclusive)', async () => {
|
|
76
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
77
|
-
builder.addProduct(productDoc, traits);
|
|
78
|
-
|
|
79
|
-
const order = await firstValueFrom(builder.order$);
|
|
80
|
-
const line = order.lineItems[0];
|
|
81
|
-
expect(line.taxAmount).toBeCloseTo(0.45);
|
|
82
|
-
expect(line.lineTotal).toBeCloseTo(4.95);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('calculates order totals', async () => {
|
|
86
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
87
|
-
builder.addProduct(productDoc, traits);
|
|
88
|
-
builder.addProduct(productDoc2, traits);
|
|
89
|
-
|
|
90
|
-
const order = await firstValueFrom(builder.order$);
|
|
91
|
-
expect(order.subtotal).toBeCloseTo(9.50);
|
|
92
|
-
expect(order.taxTotal).toBeCloseTo(0.95);
|
|
93
|
-
expect(order.total).toBeCloseTo(10.45);
|
|
94
|
-
expect(order.balanceDue).toBeCloseTo(10.45);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('updates line item quantity', async () => {
|
|
98
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
99
|
-
const lineId = builder.addProduct(productDoc, traits);
|
|
100
|
-
builder.updateQuantity(lineId, 3);
|
|
101
|
-
|
|
102
|
-
const order = await firstValueFrom(builder.order$);
|
|
103
|
-
expect(order.lineItems[0].quantity).toBe(3);
|
|
104
|
-
expect(order.subtotal).toBeCloseTo(13.50);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('removes a line item', async () => {
|
|
108
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
109
|
-
const lineId = builder.addProduct(productDoc, traits);
|
|
110
|
-
builder.addProduct(productDoc2, traits);
|
|
111
|
-
builder.removeItem(lineId);
|
|
112
|
-
|
|
113
|
-
const order = await firstValueFrom(builder.order$);
|
|
114
|
-
expect(order.lineItems).toHaveLength(1);
|
|
115
|
-
expect(order.lineItems[0].name).toBe('Latte');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('sets and clears customer', async () => {
|
|
119
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
120
|
-
builder.setCustomer({ id: 'c1', name: 'Alice', email: 'alice@example.com' });
|
|
121
|
-
|
|
122
|
-
let order = await firstValueFrom(builder.order$);
|
|
123
|
-
expect(order.customer?.name).toBe('Alice');
|
|
124
|
-
|
|
125
|
-
builder.setCustomer(null);
|
|
126
|
-
order = await firstValueFrom(builder.order$);
|
|
127
|
-
expect(order.customer).toBeNull();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('sets note', async () => {
|
|
131
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
132
|
-
builder.setNote('Extra hot');
|
|
133
|
-
|
|
134
|
-
const order = await firstValueFrom(builder.order$);
|
|
135
|
-
expect(order.note).toBe('Extra hot');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('clears the order', async () => {
|
|
139
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
140
|
-
builder.addProduct(productDoc, traits);
|
|
141
|
-
builder.setNote('Test');
|
|
142
|
-
builder.clear();
|
|
143
|
-
|
|
144
|
-
const order = await firstValueFrom(builder.order$);
|
|
145
|
-
expect(order.lineItems).toHaveLength(0);
|
|
146
|
-
expect(order.note).toBe('');
|
|
147
|
-
expect(order.subtotal).toBe(0);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('handles tax-inclusive pricing', async () => {
|
|
151
|
-
const inclTax: TaxContext = { getTaxRate: () => 0.1, pricesIncludeTax: true };
|
|
152
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext: inclTax });
|
|
153
|
-
builder.addProduct(productDoc, traits);
|
|
154
|
-
|
|
155
|
-
const order = await firstValueFrom(builder.order$);
|
|
156
|
-
const line = order.lineItems[0];
|
|
157
|
-
expect(line.taxAmount).toBeCloseTo(0.41, 1);
|
|
158
|
-
expect(line.lineTotal).toBeCloseTo(4.50);
|
|
159
|
-
expect(order.subtotal).toBeCloseTo(4.09, 1);
|
|
160
|
-
expect(order.total).toBeCloseTo(4.50);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('adds product with specific quantity', async () => {
|
|
164
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
165
|
-
builder.addProduct(productDoc, traits, { quantity: 5 });
|
|
166
|
-
|
|
167
|
-
const order = await firstValueFrom(builder.order$);
|
|
168
|
-
expect(order.lineItems[0].quantity).toBe(5);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('removes line item when quantity set to 0', async () => {
|
|
172
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
173
|
-
const lineId = builder.addProduct(productDoc, traits);
|
|
174
|
-
builder.updateQuantity(lineId, 0);
|
|
175
|
-
|
|
176
|
-
const order = await firstValueFrom(builder.order$);
|
|
177
|
-
expect(order.lineItems).toHaveLength(0);
|
|
178
|
-
expect(order.subtotal).toBe(0);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('removes line item when quantity set to negative', async () => {
|
|
182
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
183
|
-
const lineId = builder.addProduct(productDoc, traits);
|
|
184
|
-
builder.updateQuantity(lineId, -1);
|
|
185
|
-
|
|
186
|
-
const order = await firstValueFrom(builder.order$);
|
|
187
|
-
expect(order.lineItems).toHaveLength(0);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('getSnapshot() matches order$ emission', async () => {
|
|
191
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
192
|
-
builder.addProduct(productDoc, traits);
|
|
193
|
-
|
|
194
|
-
const fromObservable = await firstValueFrom(builder.order$);
|
|
195
|
-
const fromSnapshot = builder.getSnapshot();
|
|
196
|
-
expect(fromSnapshot.id).toBe(fromObservable.id);
|
|
197
|
-
expect(fromSnapshot.lineItems).toEqual(fromObservable.lineItems);
|
|
198
|
-
expect(fromSnapshot.total).toBe(fromObservable.total);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('clear() resets payments, customer, and discounts', async () => {
|
|
202
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
203
|
-
const lineId = builder.addProduct(productDoc, traits);
|
|
204
|
-
builder.setCustomer({ id: 'c1', name: 'Alice' });
|
|
205
|
-
builder.addPayment({ method: 'cash', amount: 5 });
|
|
206
|
-
builder.applyLineDiscount(lineId, { type: 'fixed', value: 1 });
|
|
207
|
-
builder.applyOrderDiscount({ type: 'percentage', value: 10 });
|
|
208
|
-
builder.clear();
|
|
209
|
-
|
|
210
|
-
const order = await firstValueFrom(builder.order$);
|
|
211
|
-
expect(order.lineItems).toHaveLength(0);
|
|
212
|
-
expect(order.payments).toHaveLength(0);
|
|
213
|
-
expect(order.discounts).toHaveLength(0);
|
|
214
|
-
expect(order.customer).toBeNull();
|
|
215
|
-
expect(order.note).toBe('');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('handles product with undefined price gracefully', async () => {
|
|
219
|
-
const noPriceTraits = { ...traits, getPrice: () => undefined };
|
|
220
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
221
|
-
builder.addProduct({ id: 'p1', name: 'Free Item' }, noPriceTraits);
|
|
222
|
-
|
|
223
|
-
const order = await firstValueFrom(builder.order$);
|
|
224
|
-
expect(order.lineItems[0].price).toBe(0);
|
|
225
|
-
expect(order.total).toBe(0);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('adds same product with different variantIds as separate lines', async () => {
|
|
229
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
230
|
-
builder.addProduct(productDoc, traits, { variantId: 'small' });
|
|
231
|
-
builder.addProduct(productDoc, traits, { variantId: 'large' });
|
|
232
|
-
|
|
233
|
-
const order = await firstValueFrom(builder.order$);
|
|
234
|
-
expect(order.lineItems).toHaveLength(2);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('merges same product+variant on second add', async () => {
|
|
238
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
239
|
-
builder.addProduct(productDoc, traits, { variantId: 'small' });
|
|
240
|
-
builder.addProduct(productDoc, traits, { variantId: 'small' });
|
|
241
|
-
|
|
242
|
-
const order = await firstValueFrom(builder.order$);
|
|
243
|
-
expect(order.lineItems).toHaveLength(1);
|
|
244
|
-
expect(order.lineItems[0].quantity).toBe(2);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { firstValueFrom } from 'rxjs';
|
|
3
|
-
import { createRxDatabase, addRxPlugin, type RxDatabase } from 'rxdb';
|
|
4
|
-
import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode';
|
|
5
|
-
import { getRxStorageMemory } from 'rxdb/plugins/storage-memory';
|
|
6
|
-
import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv';
|
|
7
|
-
import { createOrderManager } from './order-manager';
|
|
8
|
-
import type { ProductTraits } from '@tallyui/core';
|
|
9
|
-
import type { TaxContext } from '../tax/types';
|
|
10
|
-
|
|
11
|
-
addRxPlugin(RxDBDevModePlugin);
|
|
12
|
-
|
|
13
|
-
const storage = wrappedValidateAjvStorage({ storage: getRxStorageMemory() });
|
|
14
|
-
|
|
15
|
-
const traits: ProductTraits = {
|
|
16
|
-
getId: (doc) => doc.id,
|
|
17
|
-
getName: (doc) => doc.name,
|
|
18
|
-
getSku: () => '',
|
|
19
|
-
getPrice: (doc) => String(doc.price),
|
|
20
|
-
getRegularPrice: (doc) => String(doc.price),
|
|
21
|
-
getSalePrice: () => undefined,
|
|
22
|
-
isOnSale: () => false,
|
|
23
|
-
getImageUrl: () => undefined,
|
|
24
|
-
getImageUrls: () => [],
|
|
25
|
-
getDescription: () => undefined,
|
|
26
|
-
getStockStatus: () => 'instock',
|
|
27
|
-
getStockQuantity: () => null,
|
|
28
|
-
hasVariants: () => false,
|
|
29
|
-
getType: () => 'simple',
|
|
30
|
-
getBarcode: () => undefined,
|
|
31
|
-
getCategoryNames: () => [],
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const taxContext: TaxContext = { getTaxRate: () => 0, pricesIncludeTax: false };
|
|
35
|
-
const product = { id: 'p1', name: 'Coffee', price: 5 };
|
|
36
|
-
|
|
37
|
-
const draftsSchema = {
|
|
38
|
-
version: 0,
|
|
39
|
-
primaryKey: 'id',
|
|
40
|
-
type: 'object' as const,
|
|
41
|
-
properties: {
|
|
42
|
-
id: { type: 'string', maxLength: 100 },
|
|
43
|
-
data: { type: 'string' },
|
|
44
|
-
customerName: { type: 'string' },
|
|
45
|
-
itemCount: { type: 'integer', minimum: 0, maximum: 2147483647, multipleOf: 1 },
|
|
46
|
-
total: { type: 'number' },
|
|
47
|
-
parkedAt: { type: 'string' },
|
|
48
|
-
},
|
|
49
|
-
required: ['id', 'data'] as const,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
describe('OrderManager', () => {
|
|
53
|
-
let db: RxDatabase;
|
|
54
|
-
|
|
55
|
-
beforeEach(async () => {
|
|
56
|
-
db = await createRxDatabase({
|
|
57
|
-
name: `test_mgr_${Date.now()}`,
|
|
58
|
-
storage,
|
|
59
|
-
multiInstance: false,
|
|
60
|
-
ignoreDuplicate: true,
|
|
61
|
-
});
|
|
62
|
-
await db.addCollections({ pos_drafts: { schema: draftsSchema } });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
afterEach(async () => {
|
|
66
|
-
await db?.close();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('starts with an active order', async () => {
|
|
70
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
71
|
-
const builder = await firstValueFrom(mgr.activeOrder$);
|
|
72
|
-
expect(builder).toBeDefined();
|
|
73
|
-
|
|
74
|
-
const order = await firstValueFrom(builder.order$);
|
|
75
|
-
expect(order.status).toBe('draft');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('creates a new order and switches to it', async () => {
|
|
79
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
80
|
-
|
|
81
|
-
const first = await firstValueFrom(mgr.activeOrder$);
|
|
82
|
-
const firstOrder = await firstValueFrom(first.order$);
|
|
83
|
-
|
|
84
|
-
mgr.newOrder();
|
|
85
|
-
const second = await firstValueFrom(mgr.activeOrder$);
|
|
86
|
-
const secondOrder = await firstValueFrom(second.order$);
|
|
87
|
-
|
|
88
|
-
expect(firstOrder.id).not.toBe(secondOrder.id);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('parks an order to local storage and lists it', async () => {
|
|
92
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
93
|
-
|
|
94
|
-
const builder = await firstValueFrom(mgr.activeOrder$);
|
|
95
|
-
builder.addProduct(product, traits);
|
|
96
|
-
builder.setCustomer({ id: 'c1', name: 'Alice' });
|
|
97
|
-
|
|
98
|
-
await mgr.parkCurrentOrder();
|
|
99
|
-
|
|
100
|
-
const parked = await firstValueFrom(mgr.parkedOrders$);
|
|
101
|
-
expect(parked).toHaveLength(1);
|
|
102
|
-
expect(parked[0].customerName).toBe('Alice');
|
|
103
|
-
expect(parked[0].itemCount).toBe(1);
|
|
104
|
-
expect(parked[0].source).toBe('local');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('resumes a parked order', async () => {
|
|
108
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
109
|
-
|
|
110
|
-
const builder = await firstValueFrom(mgr.activeOrder$);
|
|
111
|
-
builder.addProduct(product, traits);
|
|
112
|
-
const orderId = (await firstValueFrom(builder.order$)).id;
|
|
113
|
-
|
|
114
|
-
await mgr.parkCurrentOrder();
|
|
115
|
-
await mgr.resumeOrder(orderId);
|
|
116
|
-
|
|
117
|
-
const resumed = await firstValueFrom(mgr.activeOrder$);
|
|
118
|
-
const order = await firstValueFrom(resumed.order$);
|
|
119
|
-
expect(order.id).toBe(orderId);
|
|
120
|
-
expect(order.lineItems).toHaveLength(1);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('deletes a parked order', async () => {
|
|
124
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
125
|
-
|
|
126
|
-
const builder = await firstValueFrom(mgr.activeOrder$);
|
|
127
|
-
builder.addProduct(product, traits);
|
|
128
|
-
const orderId = (await firstValueFrom(builder.order$)).id;
|
|
129
|
-
|
|
130
|
-
await mgr.parkCurrentOrder();
|
|
131
|
-
await mgr.deleteParkedOrder(orderId);
|
|
132
|
-
|
|
133
|
-
const parked = await firstValueFrom(mgr.parkedOrders$);
|
|
134
|
-
expect(parked).toHaveLength(0);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('preserves line-level discounts through park/resume', async () => {
|
|
138
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
139
|
-
|
|
140
|
-
const builder = await firstValueFrom(mgr.activeOrder$);
|
|
141
|
-
const lineId = builder.addProduct(product, traits);
|
|
142
|
-
builder.applyLineDiscount(lineId, { type: 'percentage', value: 10, label: '10% off' });
|
|
143
|
-
const orderId = (await firstValueFrom(builder.order$)).id;
|
|
144
|
-
|
|
145
|
-
await mgr.parkCurrentOrder();
|
|
146
|
-
await mgr.resumeOrder(orderId);
|
|
147
|
-
|
|
148
|
-
const resumed = await firstValueFrom(mgr.activeOrder$);
|
|
149
|
-
const order = await firstValueFrom(resumed.order$);
|
|
150
|
-
expect(order.lineItems[0].discounts).toHaveLength(1);
|
|
151
|
-
expect(order.lineItems[0].discounts[0].label).toBe('10% off');
|
|
152
|
-
expect(order.lineItems[0].discountAmount).toBeCloseTo(0.50); // 10% of $5
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('preserves order-level discounts through park/resume', async () => {
|
|
156
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
157
|
-
|
|
158
|
-
const builder = await firstValueFrom(mgr.activeOrder$);
|
|
159
|
-
builder.addProduct(product, traits);
|
|
160
|
-
builder.applyOrderDiscount({ type: 'fixed', value: 1, label: 'Loyalty' });
|
|
161
|
-
const orderId = (await firstValueFrom(builder.order$)).id;
|
|
162
|
-
|
|
163
|
-
await mgr.parkCurrentOrder();
|
|
164
|
-
await mgr.resumeOrder(orderId);
|
|
165
|
-
|
|
166
|
-
const resumed = await firstValueFrom(mgr.activeOrder$);
|
|
167
|
-
const order = await firstValueFrom(resumed.order$);
|
|
168
|
-
expect(order.discounts).toHaveLength(1);
|
|
169
|
-
expect(order.discounts[0].label).toBe('Loyalty');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('preserves payments through park/resume', async () => {
|
|
173
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
174
|
-
|
|
175
|
-
const builder = await firstValueFrom(mgr.activeOrder$);
|
|
176
|
-
builder.addProduct(product, traits);
|
|
177
|
-
builder.addPayment({ method: 'cash', amount: 3 });
|
|
178
|
-
const orderId = (await firstValueFrom(builder.order$)).id;
|
|
179
|
-
|
|
180
|
-
await mgr.parkCurrentOrder();
|
|
181
|
-
await mgr.resumeOrder(orderId);
|
|
182
|
-
|
|
183
|
-
const resumed = await firstValueFrom(mgr.activeOrder$);
|
|
184
|
-
const order = await firstValueFrom(resumed.order$);
|
|
185
|
-
expect(order.payments).toHaveLength(1);
|
|
186
|
-
expect(order.payments[0].method).toBe('cash');
|
|
187
|
-
expect(order.payments[0].amount).toBe(3);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('throws when resuming non-existent order', async () => {
|
|
191
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
192
|
-
await expect(mgr.resumeOrder('nonexistent')).rejects.toThrow('Parked order nonexistent not found');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('parking creates a fresh active order', async () => {
|
|
196
|
-
const mgr = createOrderManager({ currency: 'USD', taxContext, draftsCollection: db.pos_drafts });
|
|
197
|
-
|
|
198
|
-
const builder = await firstValueFrom(mgr.activeOrder$);
|
|
199
|
-
builder.addProduct(product, traits);
|
|
200
|
-
const oldId = (await firstValueFrom(builder.order$)).id;
|
|
201
|
-
|
|
202
|
-
await mgr.parkCurrentOrder();
|
|
203
|
-
|
|
204
|
-
const newBuilder = await firstValueFrom(mgr.activeOrder$);
|
|
205
|
-
const newOrder = await firstValueFrom(newBuilder.order$);
|
|
206
|
-
expect(newOrder.id).not.toBe(oldId);
|
|
207
|
-
expect(newOrder.lineItems).toHaveLength(0);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { firstValueFrom } from 'rxjs';
|
|
3
|
-
import { createOrderBuilder } from './order-builder';
|
|
4
|
-
import type { ProductTraits } from '@tallyui/core';
|
|
5
|
-
import type { TaxContext } from '../tax/types';
|
|
6
|
-
|
|
7
|
-
const traits: ProductTraits = {
|
|
8
|
-
getId: (doc) => doc.id,
|
|
9
|
-
getName: (doc) => doc.name,
|
|
10
|
-
getSku: () => '',
|
|
11
|
-
getPrice: (doc) => String(doc.price),
|
|
12
|
-
getRegularPrice: (doc) => String(doc.price),
|
|
13
|
-
getSalePrice: () => undefined,
|
|
14
|
-
isOnSale: () => false,
|
|
15
|
-
getImageUrl: () => undefined,
|
|
16
|
-
getImageUrls: () => [],
|
|
17
|
-
getDescription: () => undefined,
|
|
18
|
-
getStockStatus: () => 'instock',
|
|
19
|
-
getStockQuantity: () => null,
|
|
20
|
-
hasVariants: () => false,
|
|
21
|
-
getType: () => 'simple',
|
|
22
|
-
getBarcode: () => undefined,
|
|
23
|
-
getCategoryNames: () => [],
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const taxContext: TaxContext = { getTaxRate: () => 0, pricesIncludeTax: false };
|
|
27
|
-
|
|
28
|
-
describe('payment splitting', () => {
|
|
29
|
-
it('tracks a single payment', async () => {
|
|
30
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
31
|
-
builder.addProduct({ id: 'p1', name: 'Item', price: 20 }, traits);
|
|
32
|
-
builder.addPayment({ method: 'cash', amount: 20 });
|
|
33
|
-
|
|
34
|
-
const order = await firstValueFrom(builder.order$);
|
|
35
|
-
expect(order.payments).toHaveLength(1);
|
|
36
|
-
expect(order.balanceDue).toBe(0);
|
|
37
|
-
expect(order.changeDue).toBe(0);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('splits payment across two methods', async () => {
|
|
41
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
42
|
-
builder.addProduct({ id: 'p1', name: 'Item', price: 35.50 }, traits);
|
|
43
|
-
builder.addPayment({ method: 'cash', amount: 20 });
|
|
44
|
-
builder.addPayment({ method: 'card', amount: 15.50 });
|
|
45
|
-
|
|
46
|
-
const order = await firstValueFrom(builder.order$);
|
|
47
|
-
expect(order.payments).toHaveLength(2);
|
|
48
|
-
expect(order.balanceDue).toBe(0);
|
|
49
|
-
expect(order.changeDue).toBe(0);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('calculates balance due when partially paid', async () => {
|
|
53
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
54
|
-
builder.addProduct({ id: 'p1', name: 'Item', price: 30 }, traits);
|
|
55
|
-
builder.addPayment({ method: 'card', amount: 10 });
|
|
56
|
-
|
|
57
|
-
const order = await firstValueFrom(builder.order$);
|
|
58
|
-
expect(order.balanceDue).toBeCloseTo(20);
|
|
59
|
-
expect(order.changeDue).toBe(0);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('calculates change due when overpaid with cash', async () => {
|
|
63
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
64
|
-
builder.addProduct({ id: 'p1', name: 'Item', price: 17 }, traits);
|
|
65
|
-
builder.addPayment({ method: 'cash', amount: 20 });
|
|
66
|
-
|
|
67
|
-
const order = await firstValueFrom(builder.order$);
|
|
68
|
-
expect(order.balanceDue).toBe(0);
|
|
69
|
-
expect(order.changeDue).toBeCloseTo(3);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('removes a payment', async () => {
|
|
73
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
74
|
-
builder.addProduct({ id: 'p1', name: 'Item', price: 20 }, traits);
|
|
75
|
-
const paymentId = builder.addPayment({ method: 'cash', amount: 20 });
|
|
76
|
-
|
|
77
|
-
builder.removePayment(paymentId);
|
|
78
|
-
const order = await firstValueFrom(builder.order$);
|
|
79
|
-
expect(order.payments).toHaveLength(0);
|
|
80
|
-
expect(order.balanceDue).toBe(20);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('includes payment reference', async () => {
|
|
84
|
-
const builder = createOrderBuilder({ currency: 'USD', taxContext });
|
|
85
|
-
builder.addProduct({ id: 'p1', name: 'Item', price: 10 }, traits);
|
|
86
|
-
builder.addPayment({ method: 'card', amount: 10, reference: 'TXN-4567' });
|
|
87
|
-
|
|
88
|
-
const order = await firstValueFrom(builder.order$);
|
|
89
|
-
expect(order.payments[0].reference).toBe('TXN-4567');
|
|
90
|
-
});
|
|
91
|
-
});
|