@tallyui/pos 0.1.0 → 2.0.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +1770 -42
  3. package/dist/index.js +3422 -204
  4. package/package.json +21 -10
  5. package/src/currency/currency-provider.tsx +12 -4
  6. package/src/currency/index.ts +1 -2
  7. package/src/index.ts +45 -4
  8. package/src/order/allocate-order-discount.ts +34 -0
  9. package/src/order/index.ts +5 -0
  10. package/src/order/order-builder.ts +240 -95
  11. package/src/order/order-manager.ts +23 -42
  12. package/src/order/types.ts +67 -15
  13. package/src/outbox/http-transport.ts +66 -0
  14. package/src/outbox/index.ts +7 -0
  15. package/src/outbox/order-outbox.ts +198 -0
  16. package/src/outbox/types.ts +22 -0
  17. package/src/outbox/use-order-outbox.ts +161 -0
  18. package/src/pos-order/command.ts +35 -0
  19. package/src/pos-order/device-id.ts +23 -0
  20. package/src/pos-order/finalize.ts +81 -0
  21. package/src/pos-order/index.ts +10 -0
  22. package/src/pos-order/needs-attention.ts +11 -0
  23. package/src/pos-order/open.ts +206 -0
  24. package/src/pos-order/same-sale.ts +26 -0
  25. package/src/pos-order/schema.ts +100 -0
  26. package/src/pos-order/types.ts +67 -0
  27. package/src/pos-order/uuidv7.ts +54 -0
  28. package/src/product/index.ts +2 -0
  29. package/src/product/search-products.ts +29 -0
  30. package/src/product/stock.ts +25 -0
  31. package/src/receipt/build-receipt-data.ts +36 -22
  32. package/src/receipt/types.ts +16 -10
  33. package/src/register/__fixtures__/closure-local-row.json +61 -0
  34. package/src/register/__fixtures__/closure.json +197 -0
  35. package/src/register/__fixtures__/corrections-dst.json +63 -0
  36. package/src/register/closure-document.ts +270 -0
  37. package/src/register/closure-rows.ts +65 -0
  38. package/src/register/document-labels.ts +46 -0
  39. package/src/register/expected.ts +66 -0
  40. package/src/register/export-csv.ts +74 -0
  41. package/src/register/facts.ts +109 -0
  42. package/src/register/index.ts +30 -0
  43. package/src/register/money.ts +18 -0
  44. package/src/register/movement-input.ts +59 -0
  45. package/src/register/register-count.denominations.ts +11 -0
  46. package/src/register/register-count.helpers.ts +64 -0
  47. package/src/register/register-document.ts +243 -0
  48. package/src/register/schemas.ts +175 -0
  49. package/src/register/session-store.ts +541 -0
  50. package/src/register/settled-figures.ts +98 -0
  51. package/src/register/use-register-session.ts +425 -0
  52. package/src/rxdb/index.ts +2 -0
  53. package/src/sale/cart.ts +22 -0
  54. package/src/sale/catalogue.ts +21 -0
  55. package/src/sale/index.ts +5 -0
  56. package/src/sale/use-sale.ts +337 -0
  57. package/src/store-settings/index.ts +5 -0
  58. package/src/store-settings/map-store-settings.ts +13 -0
  59. package/src/store-settings/resolve-store-settings.ts +65 -0
  60. package/src/store-settings/use-store-settings.ts +69 -0
  61. package/src/tax/exact.ts +155 -0
  62. package/src/tax/index.ts +3 -2
  63. package/src/tax/tax-provider.tsx +13 -12
  64. package/src/tax/types.ts +3 -7
  65. package/src/tender/index.ts +2 -0
  66. package/src/tender/tender-state.ts +326 -0
  67. package/src/currency/currency-provider.test.tsx +0 -36
  68. package/src/currency/format-currency.test.ts +0 -36
  69. package/src/currency/format-currency.ts +0 -18
  70. package/src/logging/logger.test.ts +0 -154
  71. package/src/logging/sinks.test.ts +0 -60
  72. package/src/order/discount-engine.test.ts +0 -152
  73. package/src/order/order-builder.test.ts +0 -246
  74. package/src/order/order-manager.test.ts +0 -209
  75. package/src/order/payment.test.ts +0 -91
  76. package/src/receipt/build-receipt-data.test.ts +0 -166
  77. package/src/repository/create-repository.test.ts +0 -166
  78. package/src/tax/calculate.test.ts +0 -59
  79. package/src/tax/calculate.ts +0 -32
  80. package/src/tax/tax-provider.test.tsx +0 -51
@@ -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
- });
@@ -1,166 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { buildReceiptData } from './build-receipt-data';
3
- import type { Order } from '../order/types';
4
- import type { ReceiptConfig } from './types';
5
-
6
- const baseOrder: Order = {
7
- id: 'order-001',
8
- status: 'completed',
9
- lineItems: [
10
- {
11
- id: 'li1',
12
- productId: 'p1',
13
- name: 'Espresso',
14
- sku: 'ESP-001',
15
- price: 4.5,
16
- quantity: 2,
17
- taxRate: 0.1,
18
- taxAmount: 0.9,
19
- discounts: [],
20
- discountAmount: 0,
21
- lineTotal: 9.9,
22
- },
23
- {
24
- id: 'li2',
25
- productId: 'p2',
26
- name: 'Muffin',
27
- sku: 'MUF-001',
28
- price: 3.0,
29
- quantity: 1,
30
- taxRate: 0.05,
31
- taxAmount: 0.15,
32
- discounts: [],
33
- discountAmount: 0,
34
- lineTotal: 3.15,
35
- },
36
- ],
37
- discounts: [{ id: 'd1', type: 'fixed', value: 1, amount: 1, label: 'Loyalty' }],
38
- payments: [{ id: 'pay1', method: 'cash', amount: 15.0 }],
39
- customer: { id: 'c1', name: 'Alice' },
40
- note: 'Extra hot',
41
- subtotal: 12.0,
42
- discountTotal: 1.0,
43
- taxTotal: 1.05,
44
- total: 12.05,
45
- balanceDue: 0,
46
- changeDue: 2.95,
47
- currency: 'USD',
48
- createdAt: '2026-02-26T10:00:00Z',
49
- updatedAt: '2026-02-26T10:05:00Z',
50
- };
51
-
52
- const config: ReceiptConfig = {
53
- storeName: 'Test Coffee Shop',
54
- storeAddress: '123 Main St',
55
- cashier: 'Bob',
56
- register: 'POS-1',
57
- taxLabels: { 0.1: 'VAT 10%', 0.05: 'Reduced VAT 5%' },
58
- };
59
-
60
- describe('buildReceiptData', () => {
61
- it('builds receipt header from config and order', () => {
62
- const receipt = buildReceiptData(baseOrder, config);
63
- expect(receipt.header.storeName).toBe('Test Coffee Shop');
64
- expect(receipt.header.storeAddress).toBe('123 Main St');
65
- expect(receipt.header.orderNumber).toBe('order-001');
66
- expect(receipt.header.cashier).toBe('Bob');
67
- expect(receipt.header.register).toBe('POS-1');
68
- expect(receipt.header.date).toBe('2026-02-26T10:00:00Z');
69
- });
70
-
71
- it('maps line items', () => {
72
- const receipt = buildReceiptData(baseOrder, config);
73
- expect(receipt.lineItems).toHaveLength(2);
74
- expect(receipt.lineItems[0]).toEqual({
75
- name: 'Espresso',
76
- sku: 'ESP-001',
77
- quantity: 2,
78
- unitPrice: 4.5,
79
- lineTotal: 9.9,
80
- });
81
- });
82
-
83
- it('maps discounts', () => {
84
- const receipt = buildReceiptData(baseOrder, config);
85
- expect(receipt.discounts).toHaveLength(1);
86
- expect(receipt.discounts[0]).toEqual({ label: 'Loyalty', amount: 1 });
87
- });
88
-
89
- it('groups tax lines by rate with labels', () => {
90
- const receipt = buildReceiptData(baseOrder, config);
91
- expect(receipt.totals.taxLines).toHaveLength(2);
92
- expect(receipt.totals.taxLines).toContainEqual({
93
- label: 'VAT 10%',
94
- rate: 0.1,
95
- amount: 0.9,
96
- });
97
- expect(receipt.totals.taxLines).toContainEqual({
98
- label: 'Reduced VAT 5%',
99
- rate: 0.05,
100
- amount: 0.15,
101
- });
102
- });
103
-
104
- it('uses default tax label when not configured', () => {
105
- const receipt = buildReceiptData(baseOrder, { storeName: 'Shop' });
106
- const labels = receipt.totals.taxLines.map((t) => t.label);
107
- expect(labels).toContain('Tax 10%');
108
- expect(labels).toContain('Tax 5%');
109
- });
110
-
111
- it('calculates totals', () => {
112
- const receipt = buildReceiptData(baseOrder, config);
113
- expect(receipt.totals.subtotal).toBe(12.0);
114
- expect(receipt.totals.discountTotal).toBe(1.0);
115
- expect(receipt.totals.taxTotal).toBe(1.05);
116
- expect(receipt.totals.total).toBe(12.05);
117
- });
118
-
119
- it('maps payments', () => {
120
- const receipt = buildReceiptData(baseOrder, config);
121
- expect(receipt.payments).toHaveLength(1);
122
- expect(receipt.payments[0]).toEqual({ method: 'cash', amount: 15.0 });
123
- });
124
-
125
- it('includes change due and footer', () => {
126
- const receipt = buildReceiptData(baseOrder, config);
127
- expect(receipt.changeDue).toBe(2.95);
128
- expect(receipt.footer.note).toBe('Extra hot');
129
- expect(receipt.footer.barcode).toBe('order-001');
130
- });
131
-
132
- it('includes currency', () => {
133
- const receipt = buildReceiptData(baseOrder, config);
134
- expect(receipt.currency).toBe('USD');
135
- });
136
-
137
- it('handles empty order with no line items', () => {
138
- const emptyOrder: Order = {
139
- ...baseOrder,
140
- lineItems: [],
141
- discounts: [],
142
- payments: [],
143
- subtotal: 0,
144
- discountTotal: 0,
145
- taxTotal: 0,
146
- total: 0,
147
- balanceDue: 0,
148
- changeDue: 0,
149
- };
150
- const receipt = buildReceiptData(emptyOrder, config);
151
- expect(receipt.lineItems).toHaveLength(0);
152
- expect(receipt.totals.taxLines).toHaveLength(0);
153
- expect(receipt.totals.total).toBe(0);
154
- });
155
-
156
- it('uses fallback label for discount without label or couponCode', () => {
157
- const orderWithUnlabeledDiscount: Order = {
158
- ...baseOrder,
159
- discounts: [
160
- { id: 'd1', type: 'percentage', value: 10, amount: 1.20 },
161
- ],
162
- };
163
- const receipt = buildReceiptData(orderWithUnlabeledDiscount, config);
164
- expect(receipt.discounts[0].label).toBe('percentage discount');
165
- });
166
- });
@@ -1,166 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { createRxDatabase, addRxPlugin, type RxDatabase } from 'rxdb';
3
- import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode';
4
- import { getRxStorageMemory } from 'rxdb/plugins/storage-memory';
5
- import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv';
6
- import { firstValueFrom } from 'rxjs';
7
- import { createRepository } from './create-repository';
8
-
9
- addRxPlugin(RxDBDevModePlugin);
10
-
11
- const storage = wrappedValidateAjvStorage({ storage: getRxStorageMemory() });
12
-
13
- const testSchema = {
14
- version: 0,
15
- primaryKey: 'id',
16
- type: 'object' as const,
17
- properties: {
18
- id: { type: 'string', maxLength: 100 },
19
- name: { type: 'string' },
20
- category: { type: 'string' },
21
- },
22
- required: ['id', 'name'] as const,
23
- };
24
-
25
- type TestDoc = { id: string; name: string; category?: string };
26
-
27
- describe('createRepository', () => {
28
- let db: RxDatabase;
29
-
30
- beforeEach(async () => {
31
- db = await createRxDatabase({
32
- name: `test_repo_${Date.now()}`,
33
- storage,
34
- multiInstance: false,
35
- ignoreDuplicate: true,
36
- });
37
- await db.addCollections({ items: { schema: testSchema } });
38
- });
39
-
40
- afterEach(async () => {
41
- await db?.close();
42
- });
43
-
44
- it('creates a document and retrieves it by id', async () => {
45
- const repo = createRepository<TestDoc>(db.items);
46
- const doc = await repo.create({ id: '1', name: 'Widget' });
47
- expect(doc.name).toBe('Widget');
48
-
49
- const found = await firstValueFrom(repo.findById$('1'));
50
- expect(found).not.toBeNull();
51
- expect(found!.name).toBe('Widget');
52
- });
53
-
54
- it('returns null for non-existent id', async () => {
55
- const repo = createRepository<TestDoc>(db.items);
56
- const found = await firstValueFrom(repo.findById$('nonexistent'));
57
- expect(found).toBeNull();
58
- });
59
-
60
- it('finds all documents', async () => {
61
- const repo = createRepository<TestDoc>(db.items);
62
- await repo.create({ id: '1', name: 'A' });
63
- await repo.create({ id: '2', name: 'B' });
64
-
65
- const all = await firstValueFrom(repo.findAll$());
66
- expect(all).toHaveLength(2);
67
- });
68
-
69
- it('finds documents with a mango query', async () => {
70
- const repo = createRepository<TestDoc>(db.items);
71
- await repo.create({ id: '1', name: 'A', category: 'x' });
72
- await repo.create({ id: '2', name: 'B', category: 'y' });
73
-
74
- const filtered = await firstValueFrom(
75
- repo.findAll$({ selector: { category: 'x' } }),
76
- );
77
- expect(filtered).toHaveLength(1);
78
- expect(filtered[0].name).toBe('A');
79
- });
80
-
81
- it('updates a document', async () => {
82
- const repo = createRepository<TestDoc>(db.items);
83
- await repo.create({ id: '1', name: 'Old' });
84
- const updated = await repo.update('1', { name: 'New' });
85
- expect(updated.name).toBe('New');
86
- });
87
-
88
- it('removes a document', async () => {
89
- const repo = createRepository<TestDoc>(db.items);
90
- await repo.create({ id: '1', name: 'A' });
91
- await repo.remove('1');
92
-
93
- const all = await firstValueFrom(repo.findAll$());
94
- expect(all).toHaveLength(0);
95
- });
96
-
97
- it('bulk creates documents', async () => {
98
- const repo = createRepository<TestDoc>(db.items);
99
- const docs = await repo.bulkCreate([
100
- { id: '1', name: 'A' },
101
- { id: '2', name: 'B' },
102
- { id: '3', name: 'C' },
103
- ]);
104
- expect(docs).toHaveLength(3);
105
-
106
- const all = await firstValueFrom(repo.findAll$());
107
- expect(all).toHaveLength(3);
108
- });
109
-
110
- it('bulk removes documents', async () => {
111
- const repo = createRepository<TestDoc>(db.items);
112
- await repo.bulkCreate([
113
- { id: '1', name: 'A' },
114
- { id: '2', name: 'B' },
115
- { id: '3', name: 'C' },
116
- ]);
117
- await repo.bulkRemove(['1', '3']);
118
-
119
- const all = await firstValueFrom(repo.findAll$());
120
- expect(all).toHaveLength(1);
121
- expect(all[0].id).toBe('2');
122
- });
123
-
124
- it('count$ returns document count', async () => {
125
- const repo = createRepository<TestDoc>(db.items);
126
- const count0 = await firstValueFrom(repo.count$());
127
- expect(count0).toBe(0);
128
-
129
- await repo.create({ id: '1', name: 'A' });
130
- await repo.create({ id: '2', name: 'B' });
131
-
132
- const count2 = await firstValueFrom(repo.count$());
133
- expect(count2).toBe(2);
134
- });
135
-
136
- it('search$ filters by text across specified fields', async () => {
137
- const repo = createRepository<TestDoc>(db.items);
138
- await repo.create({ id: '1', name: 'Coffee Beans' });
139
- await repo.create({ id: '2', name: 'Tea Leaves' });
140
- await repo.create({ id: '3', name: 'Coffee Grinder' });
141
-
142
- const results = await firstValueFrom(repo.search$('coffee', ['name']));
143
- expect(results).toHaveLength(2);
144
- });
145
-
146
- it('search$ handles regex special characters in search term', async () => {
147
- const repo = createRepository<TestDoc>(db.items);
148
- await repo.create({ id: '1', name: 'Coffee (Organic)' });
149
- await repo.create({ id: '2', name: 'Tea' });
150
-
151
- // This should not throw — the parentheses are escaped
152
- const results = await firstValueFrom(repo.search$('(Organic)', ['name']));
153
- expect(results).toHaveLength(1);
154
- expect(results[0].name).toBe('Coffee (Organic)');
155
- });
156
-
157
- it('throws when updating a non-existent document', async () => {
158
- const repo = createRepository<TestDoc>(db.items);
159
- await expect(repo.update('nonexistent', { name: 'New' })).rejects.toThrow('Document nonexistent not found');
160
- });
161
-
162
- it('throws when removing a non-existent document', async () => {
163
- const repo = createRepository<TestDoc>(db.items);
164
- await expect(repo.remove('nonexistent')).rejects.toThrow('Document nonexistent not found');
165
- });
166
- });
@@ -1,59 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { calculateTax, extractTax, addTax } from './calculate';
3
-
4
- describe('addTax', () => {
5
- it('adds 10% tax to $10.00', () => {
6
- expect(addTax(10, 0.1)).toBeCloseTo(11);
7
- });
8
-
9
- it('adds 0% tax', () => {
10
- expect(addTax(10, 0)).toBe(10);
11
- });
12
-
13
- it('adds 20% tax to $25.00', () => {
14
- expect(addTax(25, 0.2)).toBeCloseTo(30);
15
- });
16
- });
17
-
18
- describe('extractTax', () => {
19
- it('extracts 10% tax from $11.00 inclusive', () => {
20
- expect(extractTax(11, 0.1)).toBeCloseTo(1);
21
- });
22
-
23
- it('extracts 0% tax', () => {
24
- expect(extractTax(10, 0)).toBe(0);
25
- });
26
-
27
- it('extracts 20% tax from $30.00 inclusive', () => {
28
- expect(extractTax(30, 0.2)).toBeCloseTo(5);
29
- });
30
- });
31
-
32
- describe('calculateTax', () => {
33
- it('calculates from tax-exclusive price', () => {
34
- const result = calculateTax(10, 0.1, false);
35
- expect(result.priceExclTax).toBeCloseTo(10);
36
- expect(result.taxAmount).toBeCloseTo(1);
37
- expect(result.priceInclTax).toBeCloseTo(11);
38
- });
39
-
40
- it('calculates from tax-inclusive price', () => {
41
- const result = calculateTax(11, 0.1, true);
42
- expect(result.priceInclTax).toBeCloseTo(11);
43
- expect(result.taxAmount).toBeCloseTo(1);
44
- expect(result.priceExclTax).toBeCloseTo(10);
45
- });
46
-
47
- it('handles zero rate', () => {
48
- const result = calculateTax(10, 0, false);
49
- expect(result.priceExclTax).toBe(10);
50
- expect(result.taxAmount).toBe(0);
51
- expect(result.priceInclTax).toBe(10);
52
- });
53
-
54
- it('rounds to 2 decimal places', () => {
55
- const result = calculateTax(10.33, 0.075, false);
56
- expect(result.taxAmount).toBeCloseTo(0.77, 2);
57
- expect(result.priceInclTax).toBeCloseTo(11.10, 2);
58
- });
59
- });
@@ -1,32 +0,0 @@
1
- import type { TaxResult } from './types';
2
-
3
- function round2(n: number): number {
4
- return Math.round(n * 100) / 100;
5
- }
6
-
7
- export function addTax(priceExclTax: number, rate: number): number {
8
- return round2(priceExclTax * (1 + rate));
9
- }
10
-
11
- export function extractTax(priceInclTax: number, rate: number): number {
12
- if (rate === 0) return 0;
13
- return round2(priceInclTax - priceInclTax / (1 + rate));
14
- }
15
-
16
- export function calculateTax(price: number, rate: number, pricesIncludeTax: boolean): TaxResult {
17
- if (pricesIncludeTax) {
18
- const taxAmount = extractTax(price, rate);
19
- return {
20
- priceInclTax: round2(price),
21
- priceExclTax: round2(price - taxAmount),
22
- taxAmount,
23
- };
24
- }
25
-
26
- const taxAmount = round2(price * rate);
27
- return {
28
- priceExclTax: round2(price),
29
- priceInclTax: round2(price + taxAmount),
30
- taxAmount,
31
- };
32
- }
@@ -1,51 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { renderHook } from '@testing-library/react';
3
- import type { ReactNode } from 'react';
4
- import { TaxProvider, useTax } from './tax-provider';
5
-
6
- function wrapper(rates: Record<string, number>, pricesIncludeTax = false) {
7
- return function Wrapper({ children }: { children: ReactNode }) {
8
- return (
9
- <TaxProvider rates={rates} pricesIncludeTax={pricesIncludeTax}>
10
- {children}
11
- </TaxProvider>
12
- );
13
- };
14
- }
15
-
16
- describe('TaxProvider + useTax', () => {
17
- it('provides default tax rate', () => {
18
- const { result } = renderHook(() => useTax(), {
19
- wrapper: wrapper({ default: 0.1 }),
20
- });
21
- expect(result.current.getTaxRate()).toBe(0.1);
22
- });
23
-
24
- it('provides tax rate by class', () => {
25
- const { result } = renderHook(() => useTax(), {
26
- wrapper: wrapper({ default: 0.2, reduced: 0.05, zero: 0 }),
27
- });
28
- expect(result.current.getTaxRate('reduced')).toBe(0.05);
29
- expect(result.current.getTaxRate('zero')).toBe(0);
30
- });
31
-
32
- it('falls back to default for unknown tax class', () => {
33
- const { result } = renderHook(() => useTax(), {
34
- wrapper: wrapper({ default: 0.1 }),
35
- });
36
- expect(result.current.getTaxRate('nonexistent')).toBe(0.1);
37
- });
38
-
39
- it('provides pricesIncludeTax flag', () => {
40
- const { result } = renderHook(() => useTax(), {
41
- wrapper: wrapper({ default: 0.1 }, true),
42
- });
43
- expect(result.current.pricesIncludeTax).toBe(true);
44
- });
45
-
46
- it('throws when used outside provider', () => {
47
- expect(() => {
48
- renderHook(() => useTax());
49
- }).toThrow('useTax() must be used within a <TaxProvider>');
50
- });
51
- });