@tallyui/pos 0.1.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.
Files changed (37) hide show
  1. package/dist/index.d.ts +262 -0
  2. package/dist/index.js +632 -0
  3. package/package.json +39 -0
  4. package/src/currency/currency-provider.test.tsx +36 -0
  5. package/src/currency/currency-provider.tsx +32 -0
  6. package/src/currency/format-currency.test.ts +36 -0
  7. package/src/currency/format-currency.ts +18 -0
  8. package/src/currency/index.ts +3 -0
  9. package/src/index.ts +34 -0
  10. package/src/logging/index.ts +3 -0
  11. package/src/logging/logger.test.ts +154 -0
  12. package/src/logging/logger.ts +67 -0
  13. package/src/logging/sinks.test.ts +60 -0
  14. package/src/logging/sinks.ts +46 -0
  15. package/src/logging/types.ts +26 -0
  16. package/src/order/discount-engine.test.ts +152 -0
  17. package/src/order/index.ts +13 -0
  18. package/src/order/order-builder.test.ts +246 -0
  19. package/src/order/order-builder.ts +273 -0
  20. package/src/order/order-manager.test.ts +209 -0
  21. package/src/order/order-manager.ts +175 -0
  22. package/src/order/payment.test.ts +91 -0
  23. package/src/order/types.ts +66 -0
  24. package/src/receipt/build-receipt-data.test.ts +166 -0
  25. package/src/receipt/build-receipt-data.ts +56 -0
  26. package/src/receipt/index.ts +2 -0
  27. package/src/receipt/types.ts +42 -0
  28. package/src/repository/create-repository.test.ts +166 -0
  29. package/src/repository/create-repository.ts +74 -0
  30. package/src/repository/index.ts +2 -0
  31. package/src/repository/types.ts +15 -0
  32. package/src/tax/calculate.test.ts +59 -0
  33. package/src/tax/calculate.ts +32 -0
  34. package/src/tax/index.ts +4 -0
  35. package/src/tax/tax-provider.test.tsx +51 -0
  36. package/src/tax/tax-provider.tsx +36 -0
  37. package/src/tax/types.ts +15 -0
package/dist/index.js ADDED
@@ -0,0 +1,632 @@
1
+ // src/logging/logger.ts
2
+ function createLogger(scope = "root") {
3
+ const sinks = /* @__PURE__ */ new Map();
4
+ function log(level, message, data) {
5
+ const entry = {
6
+ level,
7
+ scope,
8
+ message,
9
+ data,
10
+ timestamp: Date.now()
11
+ };
12
+ for (const sink of sinks.values()) {
13
+ if (!sink.levels.includes(level)) continue;
14
+ if (sink.scopes && !sink.scopes.includes(scope)) continue;
15
+ sink.write(entry);
16
+ }
17
+ }
18
+ return {
19
+ debug: (message, data) => log("debug", message, data),
20
+ info: (message, data) => log("info", message, data),
21
+ warn: (message, data) => log("warn", message, data),
22
+ error: (message, data) => log("error", message, data),
23
+ createScope(name) {
24
+ return createChildLogger(sinks, name);
25
+ },
26
+ addSink(sink) {
27
+ sinks.set(sink.id, sink);
28
+ },
29
+ removeSink(sinkId) {
30
+ sinks.delete(sinkId);
31
+ }
32
+ };
33
+ }
34
+ function logWithSinks(sinks, level, scope, message, data) {
35
+ const entry = { level, scope, message, data, timestamp: Date.now() };
36
+ for (const sink of sinks.values()) {
37
+ if (!sink.levels.includes(level)) continue;
38
+ if (sink.scopes && !sink.scopes.includes(scope)) continue;
39
+ sink.write(entry);
40
+ }
41
+ }
42
+ function createChildLogger(sinks, scope) {
43
+ return {
44
+ debug: (message, data) => logWithSinks(sinks, "debug", scope, message, data),
45
+ info: (message, data) => logWithSinks(sinks, "info", scope, message, data),
46
+ warn: (message, data) => logWithSinks(sinks, "warn", scope, message, data),
47
+ error: (message, data) => logWithSinks(sinks, "error", scope, message, data),
48
+ createScope: (childName) => createChildLogger(sinks, childName),
49
+ addSink: (sink) => sinks.set(sink.id, sink),
50
+ removeSink: (sinkId) => sinks.delete(sinkId)
51
+ };
52
+ }
53
+
54
+ // src/logging/sinks.ts
55
+ function consoleSink(options) {
56
+ return {
57
+ id: options.id ?? "console",
58
+ levels: options.levels,
59
+ scopes: options.scopes,
60
+ write(entry) {
61
+ const prefix = `[${entry.scope}]`;
62
+ const args = entry.data ? [prefix, entry.message, entry.data] : [prefix, entry.message];
63
+ switch (entry.level) {
64
+ case "error":
65
+ console.error(...args);
66
+ break;
67
+ case "warn":
68
+ console.warn(...args);
69
+ break;
70
+ default:
71
+ console.log(...args);
72
+ }
73
+ }
74
+ };
75
+ }
76
+ function callbackSink(options) {
77
+ return {
78
+ id: options.id,
79
+ levels: options.levels,
80
+ scopes: options.scopes,
81
+ write: options.callback
82
+ };
83
+ }
84
+
85
+ // src/currency/format-currency.ts
86
+ var formatters = /* @__PURE__ */ new Map();
87
+ function getFormatter(currencyCode, locale) {
88
+ const key = `${currencyCode}:${locale ?? "default"}`;
89
+ let formatter = formatters.get(key);
90
+ if (!formatter) {
91
+ formatter = new Intl.NumberFormat(locale, {
92
+ style: "currency",
93
+ currency: currencyCode
94
+ });
95
+ formatters.set(key, formatter);
96
+ }
97
+ return formatter;
98
+ }
99
+ function formatCurrency(amount, currencyCode, locale) {
100
+ return getFormatter(currencyCode, locale).format(amount);
101
+ }
102
+
103
+ // src/currency/currency-provider.tsx
104
+ import { createContext, useContext, useMemo, useCallback } from "react";
105
+ import { jsx } from "react/jsx-runtime";
106
+ var CurrencyCtx = createContext(null);
107
+ function CurrencyProvider({ currencyCode, locale, children }) {
108
+ const value = useMemo(() => ({ currencyCode, locale }), [currencyCode, locale]);
109
+ return /* @__PURE__ */ jsx(CurrencyCtx.Provider, { value, children });
110
+ }
111
+ function useCurrencyFormatter() {
112
+ const ctx = useContext(CurrencyCtx);
113
+ if (!ctx) {
114
+ throw new Error("useCurrencyFormatter() must be used within a <CurrencyProvider>");
115
+ }
116
+ return useCallback(
117
+ (amount) => formatCurrency(amount, ctx.currencyCode, ctx.locale),
118
+ [ctx.currencyCode, ctx.locale]
119
+ );
120
+ }
121
+
122
+ // src/tax/calculate.ts
123
+ function round2(n) {
124
+ return Math.round(n * 100) / 100;
125
+ }
126
+ function addTax(priceExclTax, rate) {
127
+ return round2(priceExclTax * (1 + rate));
128
+ }
129
+ function extractTax(priceInclTax, rate) {
130
+ if (rate === 0) return 0;
131
+ return round2(priceInclTax - priceInclTax / (1 + rate));
132
+ }
133
+ function calculateTax(price, rate, pricesIncludeTax) {
134
+ if (pricesIncludeTax) {
135
+ const taxAmount2 = extractTax(price, rate);
136
+ return {
137
+ priceInclTax: round2(price),
138
+ priceExclTax: round2(price - taxAmount2),
139
+ taxAmount: taxAmount2
140
+ };
141
+ }
142
+ const taxAmount = round2(price * rate);
143
+ return {
144
+ priceExclTax: round2(price),
145
+ priceInclTax: round2(price + taxAmount),
146
+ taxAmount
147
+ };
148
+ }
149
+
150
+ // src/tax/tax-provider.tsx
151
+ import { createContext as createContext2, useContext as useContext2, useMemo as useMemo2 } from "react";
152
+ import { jsx as jsx2 } from "react/jsx-runtime";
153
+ var TaxCtx = createContext2(null);
154
+ function TaxProvider({ rates, pricesIncludeTax, children }) {
155
+ const value = useMemo2(
156
+ () => ({
157
+ getTaxRate(taxClass) {
158
+ if (taxClass && taxClass in rates) {
159
+ return rates[taxClass];
160
+ }
161
+ return rates.default ?? 0;
162
+ },
163
+ pricesIncludeTax
164
+ }),
165
+ [rates, pricesIncludeTax]
166
+ );
167
+ return /* @__PURE__ */ jsx2(TaxCtx.Provider, { value, children });
168
+ }
169
+ function useTax() {
170
+ const ctx = useContext2(TaxCtx);
171
+ if (!ctx) {
172
+ throw new Error("useTax() must be used within a <TaxProvider>");
173
+ }
174
+ return ctx;
175
+ }
176
+
177
+ // src/repository/create-repository.ts
178
+ import { map } from "rxjs";
179
+ function createRepository(collection) {
180
+ return {
181
+ findById$(id) {
182
+ return collection.findOne(id).$.pipe(
183
+ map((doc) => doc ? doc.toJSON() : null)
184
+ );
185
+ },
186
+ findAll$(query) {
187
+ const q = query ? collection.find(query) : collection.find();
188
+ return q.$.pipe(
189
+ map((docs) => docs.map((d) => d.toJSON()))
190
+ );
191
+ },
192
+ search$(term, fields) {
193
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
194
+ const regex = new RegExp(escaped, "i");
195
+ return collection.find().$.pipe(
196
+ map(
197
+ (docs) => docs.filter((doc) => {
198
+ const json = doc.toJSON();
199
+ return fields.some((field) => {
200
+ const val = json[field];
201
+ return typeof val === "string" && regex.test(val);
202
+ });
203
+ }).map((d) => d.toJSON())
204
+ )
205
+ );
206
+ },
207
+ count$() {
208
+ return collection.count().$.pipe(
209
+ map((count) => count)
210
+ );
211
+ },
212
+ async create(data) {
213
+ const doc = await collection.insert(data);
214
+ return doc.toJSON();
215
+ },
216
+ async update(id, patch) {
217
+ const doc = await collection.findOne(id).exec();
218
+ if (!doc) throw new Error(`Document ${id} not found`);
219
+ const updated = await doc.incrementalPatch(patch);
220
+ return updated.toJSON();
221
+ },
222
+ async remove(id) {
223
+ const doc = await collection.findOne(id).exec();
224
+ if (!doc) throw new Error(`Document ${id} not found`);
225
+ await doc.remove();
226
+ },
227
+ async bulkCreate(items) {
228
+ const result = await collection.bulkInsert(items);
229
+ return result.success.map((d) => d.toJSON());
230
+ },
231
+ async bulkRemove(ids) {
232
+ await collection.bulkRemove(ids);
233
+ }
234
+ };
235
+ }
236
+
237
+ // src/order/order-builder.ts
238
+ import { BehaviorSubject } from "rxjs";
239
+ var nextId = 0;
240
+ function uid() {
241
+ return `${Date.now()}-${++nextId}`;
242
+ }
243
+ function createOrderBuilder(options) {
244
+ const { currency, taxContext } = options;
245
+ const orderId = options.id ?? uid();
246
+ let lineItems = [];
247
+ let orderDiscounts = [];
248
+ let payments = [];
249
+ let customer = null;
250
+ let note = "";
251
+ const now = (/* @__PURE__ */ new Date()).toISOString();
252
+ const subject = new BehaviorSubject(buildOrder());
253
+ function recalculateLine(line) {
254
+ const gross = line.price * line.quantity;
255
+ const recalcedDiscounts = line.discounts.map((d) => ({
256
+ ...d,
257
+ amount: computeDiscountAmount(d, gross)
258
+ }));
259
+ const discountAmount = recalcedDiscounts.reduce((sum, d) => sum + d.amount, 0);
260
+ const afterDiscount = gross - discountAmount;
261
+ const tax = calculateTax(afterDiscount, line.taxRate, taxContext.pricesIncludeTax);
262
+ return {
263
+ ...line,
264
+ discounts: recalcedDiscounts,
265
+ discountAmount,
266
+ taxAmount: tax.taxAmount,
267
+ lineTotal: taxContext.pricesIncludeTax ? afterDiscount : afterDiscount + tax.taxAmount
268
+ };
269
+ }
270
+ function computeDiscountAmount(discount, base) {
271
+ if (discount.type === "percentage") {
272
+ return Math.round(base * discount.value / 100 * 100) / 100;
273
+ }
274
+ return Math.min(discount.value, base);
275
+ }
276
+ function buildOrder() {
277
+ const subtotalLines = lineItems.reduce((sum, li) => {
278
+ const lineGross = li.price * li.quantity - li.discountAmount;
279
+ if (taxContext.pricesIncludeTax) {
280
+ const tax = calculateTax(lineGross, li.taxRate, true);
281
+ return sum + tax.priceExclTax;
282
+ }
283
+ return sum + lineGross;
284
+ }, 0);
285
+ const subtotal = Math.round(subtotalLines * 100) / 100;
286
+ const recalcedOrderDiscounts = orderDiscounts.map((d) => ({
287
+ ...d,
288
+ amount: computeDiscountAmount(d, subtotal)
289
+ }));
290
+ const discountTotal = lineItems.reduce((sum, li) => sum + li.discountAmount, 0) + recalcedOrderDiscounts.reduce((sum, d) => sum + d.amount, 0);
291
+ const taxTotal = lineItems.reduce((sum, li) => sum + li.taxAmount, 0);
292
+ const total = lineItems.reduce((sum, li) => sum + li.lineTotal, 0) - recalcedOrderDiscounts.reduce((sum, d) => sum + d.amount, 0);
293
+ const roundedTotal = Math.round(total * 100) / 100;
294
+ const paidAmount = payments.reduce((sum, p) => sum + p.amount, 0);
295
+ const balanceDue = Math.max(0, Math.round((roundedTotal - paidAmount) * 100) / 100);
296
+ const changeDue = Math.max(0, Math.round((paidAmount - roundedTotal) * 100) / 100);
297
+ return {
298
+ id: orderId,
299
+ status: "draft",
300
+ lineItems: [...lineItems],
301
+ discounts: recalcedOrderDiscounts,
302
+ payments: [...payments],
303
+ customer,
304
+ note,
305
+ subtotal,
306
+ discountTotal: Math.round(discountTotal * 100) / 100,
307
+ taxTotal: Math.round(taxTotal * 100) / 100,
308
+ total: roundedTotal,
309
+ balanceDue,
310
+ changeDue,
311
+ currency,
312
+ createdAt: now,
313
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
314
+ };
315
+ }
316
+ function emit() {
317
+ subject.next(buildOrder());
318
+ }
319
+ return {
320
+ order$: subject.asObservable(),
321
+ addProduct(doc, traits, opts) {
322
+ const productId = traits.getId(doc);
323
+ const variantId = opts?.variantId;
324
+ const quantity = opts?.quantity ?? 1;
325
+ const existing = lineItems.find(
326
+ (li) => li.productId === productId && li.variantId === variantId
327
+ );
328
+ if (existing) {
329
+ existing.quantity += quantity;
330
+ lineItems = lineItems.map(
331
+ (li) => li.id === existing.id ? recalculateLine(existing) : li
332
+ );
333
+ emit();
334
+ return existing.id;
335
+ }
336
+ const priceStr = traits.getPrice(doc);
337
+ const price = priceStr ? parseFloat(priceStr) : 0;
338
+ const taxRate = taxContext.getTaxRate();
339
+ const lineId = uid();
340
+ const line = {
341
+ id: lineId,
342
+ productId,
343
+ variantId,
344
+ name: traits.getName(doc),
345
+ sku: traits.getSku(doc) ?? "",
346
+ imageUrl: traits.getImageUrl(doc),
347
+ price,
348
+ quantity,
349
+ taxRate,
350
+ taxAmount: 0,
351
+ discounts: [],
352
+ discountAmount: 0,
353
+ lineTotal: 0
354
+ };
355
+ lineItems = [...lineItems, recalculateLine(line)];
356
+ emit();
357
+ return lineId;
358
+ },
359
+ updateQuantity(lineId, quantity) {
360
+ if (quantity <= 0) {
361
+ lineItems = lineItems.filter((li) => li.id !== lineId);
362
+ } else {
363
+ lineItems = lineItems.map(
364
+ (li) => li.id === lineId ? recalculateLine({ ...li, quantity }) : li
365
+ );
366
+ }
367
+ emit();
368
+ },
369
+ removeItem(lineId) {
370
+ lineItems = lineItems.filter((li) => li.id !== lineId);
371
+ emit();
372
+ },
373
+ applyLineDiscount(lineId, discount) {
374
+ lineItems = lineItems.map((li) => {
375
+ if (li.id !== lineId) return li;
376
+ const base = li.price * li.quantity;
377
+ const applied = {
378
+ ...discount,
379
+ id: uid(),
380
+ amount: computeDiscountAmount(discount, base)
381
+ };
382
+ return recalculateLine({ ...li, discounts: [...li.discounts, applied] });
383
+ });
384
+ emit();
385
+ },
386
+ applyOrderDiscount(discount) {
387
+ const applied = {
388
+ ...discount,
389
+ id: uid(),
390
+ amount: 0
391
+ };
392
+ orderDiscounts = [...orderDiscounts, applied];
393
+ emit();
394
+ },
395
+ removeDiscount(discountId) {
396
+ orderDiscounts = orderDiscounts.filter((d) => d.id !== discountId);
397
+ lineItems = lineItems.map((li) => {
398
+ const filtered = li.discounts.filter((d) => d.id !== discountId);
399
+ if (filtered.length !== li.discounts.length) {
400
+ return recalculateLine({ ...li, discounts: filtered });
401
+ }
402
+ return li;
403
+ });
404
+ emit();
405
+ },
406
+ addPayment(payment) {
407
+ const id = uid();
408
+ payments = [...payments, { ...payment, id }];
409
+ emit();
410
+ return id;
411
+ },
412
+ removePayment(paymentId) {
413
+ payments = payments.filter((p) => p.id !== paymentId);
414
+ emit();
415
+ },
416
+ setCustomer(c) {
417
+ customer = c;
418
+ emit();
419
+ },
420
+ setNote(n) {
421
+ note = n;
422
+ emit();
423
+ },
424
+ clear() {
425
+ lineItems = [];
426
+ orderDiscounts = [];
427
+ payments = [];
428
+ customer = null;
429
+ note = "";
430
+ emit();
431
+ },
432
+ getSnapshot() {
433
+ return subject.getValue();
434
+ }
435
+ };
436
+ }
437
+
438
+ // src/order/order-manager.ts
439
+ import { BehaviorSubject as BehaviorSubject2, map as map2 } from "rxjs";
440
+ function createOrderManager(options) {
441
+ const { currency, taxContext, draftsCollection } = options;
442
+ let activeBuilder = createOrderBuilder({ currency, taxContext });
443
+ const activeSubject = new BehaviorSubject2(activeBuilder);
444
+ const parkedOrders$ = draftsCollection.find().$.pipe(
445
+ map2(
446
+ (docs) => docs.map((doc) => {
447
+ const json = doc.toJSON();
448
+ return {
449
+ id: json.id,
450
+ customerName: json.customerName || void 0,
451
+ itemCount: json.itemCount ?? 0,
452
+ total: json.total ?? 0,
453
+ parkedAt: json.parkedAt ?? "",
454
+ source: "local"
455
+ };
456
+ })
457
+ )
458
+ );
459
+ return {
460
+ activeOrder$: activeSubject.asObservable(),
461
+ parkedOrders$,
462
+ newOrder() {
463
+ activeBuilder = createOrderBuilder({ currency, taxContext });
464
+ activeSubject.next(activeBuilder);
465
+ return activeBuilder;
466
+ },
467
+ async parkCurrentOrder() {
468
+ const snapshot = activeBuilder.getSnapshot();
469
+ await draftsCollection.upsert({
470
+ id: snapshot.id,
471
+ data: JSON.stringify(snapshot),
472
+ customerName: snapshot.customer?.name ?? "",
473
+ itemCount: snapshot.lineItems.length,
474
+ total: snapshot.total,
475
+ parkedAt: (/* @__PURE__ */ new Date()).toISOString()
476
+ });
477
+ activeBuilder = createOrderBuilder({ currency, taxContext });
478
+ activeSubject.next(activeBuilder);
479
+ return snapshot.id;
480
+ },
481
+ async resumeOrder(orderId) {
482
+ const doc = await draftsCollection.findOne(orderId).exec();
483
+ if (!doc) throw new Error(`Parked order ${orderId} not found`);
484
+ const json = doc.toJSON();
485
+ const savedOrder = JSON.parse(json.data);
486
+ const builder = createOrderBuilder({
487
+ currency,
488
+ taxContext,
489
+ id: savedOrder.id
490
+ });
491
+ if (savedOrder.customer) {
492
+ builder.setCustomer(savedOrder.customer);
493
+ }
494
+ if (savedOrder.note) {
495
+ builder.setNote(savedOrder.note);
496
+ }
497
+ for (const line of savedOrder.lineItems) {
498
+ const syntheticTraits = {
499
+ getId: (d) => d.productId,
500
+ getName: (d) => d.name,
501
+ getSku: (d) => d.sku,
502
+ getPrice: (d) => String(d.price),
503
+ getRegularPrice: (d) => String(d.price),
504
+ getSalePrice: () => void 0,
505
+ isOnSale: () => false,
506
+ getImageUrl: (d) => d.imageUrl,
507
+ getImageUrls: (d) => d.imageUrl ? [d.imageUrl] : [],
508
+ getDescription: () => void 0,
509
+ getStockStatus: () => "instock",
510
+ getStockQuantity: () => null,
511
+ hasVariants: () => false,
512
+ getType: () => "simple",
513
+ getBarcode: () => void 0,
514
+ getCategoryNames: () => []
515
+ };
516
+ builder.addProduct(line, syntheticTraits, {
517
+ variantId: line.variantId,
518
+ quantity: line.quantity
519
+ });
520
+ }
521
+ const resumedOrder = builder.getSnapshot();
522
+ for (const savedLine of savedOrder.lineItems) {
523
+ const newLine = resumedOrder.lineItems.find(
524
+ (li) => li.productId === savedLine.productId && li.variantId === savedLine.variantId
525
+ );
526
+ if (newLine) {
527
+ for (const discount of savedLine.discounts) {
528
+ builder.applyLineDiscount(newLine.id, {
529
+ type: discount.type,
530
+ value: discount.value,
531
+ label: discount.label,
532
+ couponCode: discount.couponCode
533
+ });
534
+ }
535
+ }
536
+ }
537
+ for (const discount of savedOrder.discounts) {
538
+ builder.applyOrderDiscount({
539
+ type: discount.type,
540
+ value: discount.value,
541
+ label: discount.label,
542
+ couponCode: discount.couponCode
543
+ });
544
+ }
545
+ for (const payment of savedOrder.payments) {
546
+ builder.addPayment({
547
+ method: payment.method,
548
+ amount: payment.amount,
549
+ reference: payment.reference
550
+ });
551
+ }
552
+ await doc.remove();
553
+ activeBuilder = builder;
554
+ activeSubject.next(activeBuilder);
555
+ return builder;
556
+ },
557
+ async deleteParkedOrder(orderId) {
558
+ const doc = await draftsCollection.findOne(orderId).exec();
559
+ if (doc) await doc.remove();
560
+ }
561
+ };
562
+ }
563
+
564
+ // src/receipt/build-receipt-data.ts
565
+ function buildReceiptData(order, config) {
566
+ const taxByRate = /* @__PURE__ */ new Map();
567
+ for (const line of order.lineItems) {
568
+ const existing = taxByRate.get(line.taxRate) ?? 0;
569
+ taxByRate.set(line.taxRate, existing + line.taxAmount);
570
+ }
571
+ const taxLines = Array.from(taxByRate.entries()).map(([rate, amount]) => ({
572
+ label: config.taxLabels?.[rate] ?? `Tax ${Math.round(rate * 100)}%`,
573
+ rate,
574
+ amount: Math.round(amount * 100) / 100
575
+ }));
576
+ return {
577
+ header: {
578
+ storeName: config.storeName,
579
+ storeAddress: config.storeAddress,
580
+ orderNumber: order.id,
581
+ date: order.createdAt,
582
+ cashier: config.cashier,
583
+ register: config.register
584
+ },
585
+ lineItems: order.lineItems.map((li) => ({
586
+ name: li.name,
587
+ sku: li.sku,
588
+ quantity: li.quantity,
589
+ unitPrice: li.price,
590
+ lineTotal: li.lineTotal
591
+ })),
592
+ discounts: order.discounts.map((d) => ({
593
+ label: d.label ?? d.couponCode ?? `${d.type} discount`,
594
+ amount: d.amount
595
+ })),
596
+ totals: {
597
+ subtotal: order.subtotal,
598
+ discountTotal: order.discountTotal,
599
+ taxLines,
600
+ taxTotal: order.taxTotal,
601
+ total: order.total
602
+ },
603
+ payments: order.payments.map((p) => ({
604
+ method: p.method,
605
+ amount: p.amount,
606
+ reference: p.reference
607
+ })),
608
+ changeDue: order.changeDue,
609
+ footer: {
610
+ note: order.note || void 0,
611
+ barcode: order.id
612
+ },
613
+ currency: order.currency
614
+ };
615
+ }
616
+ export {
617
+ CurrencyProvider,
618
+ TaxProvider,
619
+ addTax,
620
+ buildReceiptData,
621
+ calculateTax,
622
+ callbackSink,
623
+ consoleSink,
624
+ createLogger,
625
+ createOrderBuilder,
626
+ createOrderManager,
627
+ createRepository,
628
+ extractTax,
629
+ formatCurrency,
630
+ useCurrencyFormatter,
631
+ useTax
632
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@tallyui/pos",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Shared POS business logic for Tally UI — tax, orders, payments, discounts, receipts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "source": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "source": "./src/index.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "license": "MIT",
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "vitest run"
25
+ },
26
+ "peerDependencies": {
27
+ "@tallyui/core": "workspace:*",
28
+ "react": ">=18",
29
+ "rxdb": ">=16",
30
+ "rxjs": ">=7"
31
+ },
32
+ "devDependencies": {
33
+ "@tallyui/core": "workspace:*",
34
+ "@types/react": ">=18",
35
+ "react": "^19.2.4",
36
+ "rxdb": "16.21.1",
37
+ "rxjs": "7.8.2"
38
+ }
39
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import type { ReactNode } from 'react';
4
+ import { CurrencyProvider, useCurrencyFormatter } from './currency-provider';
5
+
6
+ function wrapper(currency: string, locale?: string) {
7
+ return function Wrapper({ children }: { children: ReactNode }) {
8
+ return (
9
+ <CurrencyProvider currencyCode={currency} locale={locale}>
10
+ {children}
11
+ </CurrencyProvider>
12
+ );
13
+ };
14
+ }
15
+
16
+ describe('CurrencyProvider + useCurrencyFormatter', () => {
17
+ it('formats using provided currency and locale', () => {
18
+ const { result } = renderHook(() => useCurrencyFormatter(), {
19
+ wrapper: wrapper('USD', 'en-US'),
20
+ });
21
+ expect(result.current(10.5)).toBe('$10.50');
22
+ });
23
+
24
+ it('formats EUR with German locale', () => {
25
+ const { result } = renderHook(() => useCurrencyFormatter(), {
26
+ wrapper: wrapper('EUR', 'de-DE'),
27
+ });
28
+ expect(result.current(10.5).replace(/\s/g, ' ')).toBe('10,50 €');
29
+ });
30
+
31
+ it('throws when used outside provider', () => {
32
+ expect(() => {
33
+ renderHook(() => useCurrencyFormatter());
34
+ }).toThrow('useCurrencyFormatter() must be used within a <CurrencyProvider>');
35
+ });
36
+ });