payment-kit 1.21.13 → 1.21.14
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/api/src/crons/payment-stat.ts +31 -23
- package/api/src/libs/invoice.ts +29 -4
- package/api/src/libs/product.ts +28 -4
- package/api/src/routes/checkout-sessions.ts +46 -1
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/invoices.ts +63 -2
- package/api/src/routes/payment-stats.ts +244 -22
- package/api/src/routes/products.ts +3 -0
- package/api/src/routes/tax-rates.ts +220 -0
- package/api/src/store/migrations/20251001-add-tax-code-to-products.ts +20 -0
- package/api/src/store/migrations/20251001-create-tax-rates.ts +17 -0
- package/api/src/store/migrations/20251007-relate-tax-rate-to-invoice.ts +24 -0
- package/api/src/store/migrations/20251009-add-tax-behavior.ts +21 -0
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/invoice-item.ts +10 -0
- package/api/src/store/models/price.ts +7 -0
- package/api/src/store/models/product.ts +7 -0
- package/api/src/store/models/tax-rate.ts +352 -0
- package/api/tests/models/tax-rate.spec.ts +777 -0
- package/blocklet.yml +2 -2
- package/package.json +6 -6
- package/public/currencies/dollar.png +0 -0
- package/src/components/collapse.tsx +3 -2
- package/src/components/drawer-form.tsx +2 -1
- package/src/components/invoice/list.tsx +38 -1
- package/src/components/invoice/table.tsx +48 -2
- package/src/components/metadata/form.tsx +2 -2
- package/src/components/payment-intent/list.tsx +19 -1
- package/src/components/payouts/list.tsx +19 -1
- package/src/components/price/currency-select.tsx +105 -48
- package/src/components/price/form.tsx +3 -1
- package/src/components/product/form.tsx +79 -5
- package/src/components/refund/list.tsx +20 -1
- package/src/components/subscription/items/actions.tsx +25 -15
- package/src/components/subscription/list.tsx +16 -1
- package/src/components/tax/actions.tsx +140 -0
- package/src/components/tax/filter-toolbar.tsx +230 -0
- package/src/components/tax/tax-code-select.tsx +633 -0
- package/src/components/tax/tax-rate-form.tsx +177 -0
- package/src/components/tax/tax-utils.ts +38 -0
- package/src/components/tax/taxCodes.json +10882 -0
- package/src/components/uploader.tsx +3 -0
- package/src/locales/en.tsx +152 -0
- package/src/locales/zh.tsx +149 -0
- package/src/pages/admin/billing/invoices/detail.tsx +1 -1
- package/src/pages/admin/index.tsx +2 -0
- package/src/pages/admin/overview.tsx +1114 -322
- package/src/pages/admin/products/vendors/index.tsx +4 -2
- package/src/pages/admin/tax/create.tsx +104 -0
- package/src/pages/admin/tax/detail.tsx +476 -0
- package/src/pages/admin/tax/edit.tsx +126 -0
- package/src/pages/admin/tax/index.tsx +86 -0
- package/src/pages/admin/tax/list.tsx +334 -0
- package/src/pages/customer/subscription/change-payment.tsx +1 -1
- package/src/pages/home.tsx +6 -3
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
import { TaxRate } from '../../src/store/models';
|
|
2
|
+
|
|
3
|
+
describe('TaxRate.findMatchingRate', () => {
|
|
4
|
+
let mockFindAll: jest.SpyInstance;
|
|
5
|
+
let mockSequelize: any;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockSequelize = {
|
|
9
|
+
fn: jest.fn(),
|
|
10
|
+
col: jest.fn(),
|
|
11
|
+
where: jest.fn((fn, value) => ({ fn, value })),
|
|
12
|
+
literal: jest.fn((str) => str),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Mock the sequelize instance
|
|
16
|
+
Object.defineProperty(TaxRate, 'sequelize', {
|
|
17
|
+
value: mockSequelize,
|
|
18
|
+
writable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line require-await
|
|
23
|
+
mockFindAll = jest.spyOn(TaxRate, 'findAll').mockImplementation(async () => []);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
jest.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('Input validation', () => {
|
|
31
|
+
it('should return null when country is empty', async () => {
|
|
32
|
+
const result = await TaxRate.findMatchingRate({
|
|
33
|
+
country: '',
|
|
34
|
+
state: 'CA',
|
|
35
|
+
postalCode: '90210',
|
|
36
|
+
taxCode: 'digital',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result).toBeNull();
|
|
40
|
+
expect(mockFindAll).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return null when country is whitespace', async () => {
|
|
44
|
+
const result = await TaxRate.findMatchingRate({
|
|
45
|
+
country: ' ',
|
|
46
|
+
state: 'CA',
|
|
47
|
+
postalCode: '90210',
|
|
48
|
+
taxCode: 'digital',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result).toBeNull();
|
|
52
|
+
expect(mockFindAll).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should normalize country to uppercase', async () => {
|
|
56
|
+
mockFindAll.mockResolvedValueOnce([]);
|
|
57
|
+
|
|
58
|
+
await TaxRate.findMatchingRate({
|
|
59
|
+
country: 'us',
|
|
60
|
+
state: 'CA',
|
|
61
|
+
postalCode: '90210',
|
|
62
|
+
taxCode: 'digital',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const whereClause = mockFindAll.mock.calls[0][0].where;
|
|
66
|
+
expect(whereClause).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('No candidates found', () => {
|
|
71
|
+
it('should return null when no rates exist for country', async () => {
|
|
72
|
+
mockFindAll.mockResolvedValueOnce([]);
|
|
73
|
+
|
|
74
|
+
const result = await TaxRate.findMatchingRate({
|
|
75
|
+
country: 'US',
|
|
76
|
+
state: 'CA',
|
|
77
|
+
postalCode: '90210',
|
|
78
|
+
taxCode: 'digital',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(result).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return null when no active rates exist', async () => {
|
|
85
|
+
mockFindAll.mockResolvedValueOnce([
|
|
86
|
+
{
|
|
87
|
+
id: 'txr_1',
|
|
88
|
+
country: 'US',
|
|
89
|
+
state: 'CA',
|
|
90
|
+
postal_code: '90210',
|
|
91
|
+
tax_code: 'digital',
|
|
92
|
+
percentage: 8.5,
|
|
93
|
+
active: false,
|
|
94
|
+
livemode: true,
|
|
95
|
+
display_name: 'Inactive Rate',
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const result = await TaxRate.findMatchingRate({
|
|
100
|
+
country: 'US',
|
|
101
|
+
state: 'CA',
|
|
102
|
+
postalCode: '90210',
|
|
103
|
+
taxCode: 'digital',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return null when no rates match livemode', async () => {
|
|
110
|
+
mockFindAll.mockResolvedValueOnce([
|
|
111
|
+
{
|
|
112
|
+
id: 'txr_1',
|
|
113
|
+
country: 'US',
|
|
114
|
+
state: 'CA',
|
|
115
|
+
postal_code: '90210',
|
|
116
|
+
tax_code: 'digital',
|
|
117
|
+
percentage: 8.5,
|
|
118
|
+
active: true,
|
|
119
|
+
livemode: false,
|
|
120
|
+
display_name: 'Test Mode Rate',
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const result = await TaxRate.findMatchingRate({
|
|
125
|
+
country: 'US',
|
|
126
|
+
state: 'CA',
|
|
127
|
+
postalCode: '90210',
|
|
128
|
+
taxCode: 'digital',
|
|
129
|
+
livemode: true,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(result).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('Postal code matching priority', () => {
|
|
137
|
+
it('should prefer exact postal match over state-only match', async () => {
|
|
138
|
+
const rateA = {
|
|
139
|
+
id: 'txr_state',
|
|
140
|
+
country: 'US',
|
|
141
|
+
state: 'CA',
|
|
142
|
+
postal_code: null,
|
|
143
|
+
tax_code: 'digital',
|
|
144
|
+
percentage: 7.5,
|
|
145
|
+
active: true,
|
|
146
|
+
livemode: true,
|
|
147
|
+
display_name: 'CA Digital Tax',
|
|
148
|
+
updated_at: new Date('2024-01-01'),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const rateB = {
|
|
152
|
+
id: 'txr_postal',
|
|
153
|
+
country: 'US',
|
|
154
|
+
state: 'CA',
|
|
155
|
+
postal_code: '90210',
|
|
156
|
+
tax_code: 'digital',
|
|
157
|
+
percentage: 9.5,
|
|
158
|
+
active: true,
|
|
159
|
+
livemode: true,
|
|
160
|
+
display_name: 'CA 90210 Digital Tax',
|
|
161
|
+
updated_at: new Date('2024-01-01'),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
mockFindAll.mockResolvedValueOnce([rateA, rateB]);
|
|
165
|
+
|
|
166
|
+
const result = await TaxRate.findMatchingRate({
|
|
167
|
+
country: 'US',
|
|
168
|
+
state: 'CA',
|
|
169
|
+
postalCode: '90210',
|
|
170
|
+
taxCode: 'digital',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(result?.id).toBe('txr_postal');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should match wildcard postal codes', async () => {
|
|
177
|
+
const rate = {
|
|
178
|
+
id: 'txr_wildcard',
|
|
179
|
+
country: 'US',
|
|
180
|
+
state: 'CA',
|
|
181
|
+
postal_code: '902*',
|
|
182
|
+
tax_code: 'digital',
|
|
183
|
+
percentage: 9.0,
|
|
184
|
+
active: true,
|
|
185
|
+
livemode: true,
|
|
186
|
+
display_name: 'CA 902* Digital Tax',
|
|
187
|
+
updated_at: new Date('2024-01-01'),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
mockFindAll.mockResolvedValueOnce([rate]);
|
|
191
|
+
|
|
192
|
+
const result = await TaxRate.findMatchingRate({
|
|
193
|
+
country: 'US',
|
|
194
|
+
state: 'CA',
|
|
195
|
+
postalCode: '90210',
|
|
196
|
+
taxCode: 'digital',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(result?.id).toBe('txr_wildcard');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should prefer longer wildcard prefix', async () => {
|
|
203
|
+
const rateA = {
|
|
204
|
+
id: 'txr_short',
|
|
205
|
+
country: 'US',
|
|
206
|
+
state: 'CA',
|
|
207
|
+
postal_code: '90*',
|
|
208
|
+
tax_code: 'digital',
|
|
209
|
+
percentage: 8.0,
|
|
210
|
+
active: true,
|
|
211
|
+
livemode: true,
|
|
212
|
+
display_name: 'CA 90* Digital Tax',
|
|
213
|
+
updated_at: new Date('2024-01-01'),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const rateB = {
|
|
217
|
+
id: 'txr_long',
|
|
218
|
+
country: 'US',
|
|
219
|
+
state: 'CA',
|
|
220
|
+
postal_code: '9021*',
|
|
221
|
+
tax_code: 'digital',
|
|
222
|
+
percentage: 9.0,
|
|
223
|
+
active: true,
|
|
224
|
+
livemode: true,
|
|
225
|
+
display_name: 'CA 9021* Digital Tax',
|
|
226
|
+
updated_at: new Date('2024-01-01'),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
mockFindAll.mockResolvedValueOnce([rateA, rateB]);
|
|
230
|
+
|
|
231
|
+
const result = await TaxRate.findMatchingRate({
|
|
232
|
+
country: 'US',
|
|
233
|
+
state: 'CA',
|
|
234
|
+
postalCode: '90210',
|
|
235
|
+
taxCode: 'digital',
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(result?.id).toBe('txr_long');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should reject wildcard that does not match', async () => {
|
|
242
|
+
const rate = {
|
|
243
|
+
id: 'txr_no_match',
|
|
244
|
+
country: 'US',
|
|
245
|
+
state: 'CA',
|
|
246
|
+
postal_code: '91*',
|
|
247
|
+
tax_code: 'digital',
|
|
248
|
+
percentage: 9.0,
|
|
249
|
+
active: true,
|
|
250
|
+
livemode: true,
|
|
251
|
+
display_name: 'CA 91* Digital Tax',
|
|
252
|
+
updated_at: new Date('2024-01-01'),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
mockFindAll.mockResolvedValueOnce([rate]);
|
|
256
|
+
|
|
257
|
+
const result = await TaxRate.findMatchingRate({
|
|
258
|
+
country: 'US',
|
|
259
|
+
state: 'CA',
|
|
260
|
+
postalCode: '90210',
|
|
261
|
+
taxCode: 'digital',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(result).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should reject when rate has postal but user does not provide', async () => {
|
|
268
|
+
const rate = {
|
|
269
|
+
id: 'txr_postal_required',
|
|
270
|
+
country: 'US',
|
|
271
|
+
state: 'CA',
|
|
272
|
+
postal_code: '90210',
|
|
273
|
+
tax_code: 'digital',
|
|
274
|
+
percentage: 9.5,
|
|
275
|
+
active: true,
|
|
276
|
+
livemode: true,
|
|
277
|
+
display_name: 'CA 90210 Digital Tax',
|
|
278
|
+
updated_at: new Date('2024-01-01'),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
mockFindAll.mockResolvedValueOnce([rate]);
|
|
282
|
+
|
|
283
|
+
const result = await TaxRate.findMatchingRate({
|
|
284
|
+
country: 'US',
|
|
285
|
+
state: 'CA',
|
|
286
|
+
taxCode: 'digital',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(result).toBeNull();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('State matching priority', () => {
|
|
294
|
+
it('should prefer state match over country-only match', async () => {
|
|
295
|
+
const rateA = {
|
|
296
|
+
id: 'txr_country',
|
|
297
|
+
country: 'US',
|
|
298
|
+
state: null,
|
|
299
|
+
postal_code: null,
|
|
300
|
+
tax_code: 'digital',
|
|
301
|
+
percentage: 5.0,
|
|
302
|
+
active: true,
|
|
303
|
+
livemode: true,
|
|
304
|
+
display_name: 'US Digital Tax',
|
|
305
|
+
updated_at: new Date('2024-01-01'),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const rateB = {
|
|
309
|
+
id: 'txr_state',
|
|
310
|
+
country: 'US',
|
|
311
|
+
state: 'CA',
|
|
312
|
+
postal_code: null,
|
|
313
|
+
tax_code: 'digital',
|
|
314
|
+
percentage: 7.5,
|
|
315
|
+
active: true,
|
|
316
|
+
livemode: true,
|
|
317
|
+
display_name: 'CA Digital Tax',
|
|
318
|
+
updated_at: new Date('2024-01-01'),
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
mockFindAll.mockResolvedValueOnce([rateA, rateB]);
|
|
322
|
+
|
|
323
|
+
const result = await TaxRate.findMatchingRate({
|
|
324
|
+
country: 'US',
|
|
325
|
+
state: 'CA',
|
|
326
|
+
taxCode: 'digital',
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(result?.id).toBe('txr_state');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should reject when rate has state but user does not provide', async () => {
|
|
333
|
+
const rate = {
|
|
334
|
+
id: 'txr_state_required',
|
|
335
|
+
country: 'US',
|
|
336
|
+
state: 'CA',
|
|
337
|
+
postal_code: null,
|
|
338
|
+
tax_code: 'digital',
|
|
339
|
+
percentage: 7.5,
|
|
340
|
+
active: true,
|
|
341
|
+
livemode: true,
|
|
342
|
+
display_name: 'CA Digital Tax',
|
|
343
|
+
updated_at: new Date('2024-01-01'),
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
mockFindAll.mockResolvedValueOnce([rate]);
|
|
347
|
+
|
|
348
|
+
const result = await TaxRate.findMatchingRate({
|
|
349
|
+
country: 'US',
|
|
350
|
+
taxCode: 'digital',
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
expect(result).toBeNull();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should normalize state to lowercase for comparison', async () => {
|
|
357
|
+
const rate = {
|
|
358
|
+
id: 'txr_state',
|
|
359
|
+
country: 'US',
|
|
360
|
+
state: 'ca',
|
|
361
|
+
postal_code: null,
|
|
362
|
+
tax_code: 'digital',
|
|
363
|
+
percentage: 7.5,
|
|
364
|
+
active: true,
|
|
365
|
+
livemode: true,
|
|
366
|
+
display_name: 'CA Digital Tax',
|
|
367
|
+
updated_at: new Date('2024-01-01'),
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
mockFindAll.mockResolvedValueOnce([rate]);
|
|
371
|
+
|
|
372
|
+
const result = await TaxRate.findMatchingRate({
|
|
373
|
+
country: 'US',
|
|
374
|
+
state: 'CA',
|
|
375
|
+
taxCode: 'digital',
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
expect(result?.id).toBe('txr_state');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('Tax code matching priority', () => {
|
|
383
|
+
it('should prefer exact tax code match', async () => {
|
|
384
|
+
const rateA = {
|
|
385
|
+
id: 'txr_general',
|
|
386
|
+
country: 'US',
|
|
387
|
+
state: 'CA',
|
|
388
|
+
postal_code: null,
|
|
389
|
+
tax_code: null,
|
|
390
|
+
percentage: 7.5,
|
|
391
|
+
active: true,
|
|
392
|
+
livemode: true,
|
|
393
|
+
display_name: 'CA General Tax',
|
|
394
|
+
updated_at: new Date('2024-01-01'),
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const rateB = {
|
|
398
|
+
id: 'txr_digital',
|
|
399
|
+
country: 'US',
|
|
400
|
+
state: 'CA',
|
|
401
|
+
postal_code: null,
|
|
402
|
+
tax_code: 'digital',
|
|
403
|
+
percentage: 9.5,
|
|
404
|
+
active: true,
|
|
405
|
+
livemode: true,
|
|
406
|
+
display_name: 'CA Digital Tax',
|
|
407
|
+
updated_at: new Date('2024-01-01'),
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
mockFindAll.mockResolvedValueOnce([rateA, rateB]);
|
|
411
|
+
|
|
412
|
+
const result = await TaxRate.findMatchingRate({
|
|
413
|
+
country: 'US',
|
|
414
|
+
state: 'CA',
|
|
415
|
+
taxCode: 'digital',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect(result?.id).toBe('txr_digital');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should fallback to general rate when no tax code match', async () => {
|
|
422
|
+
const rate = {
|
|
423
|
+
id: 'txr_general',
|
|
424
|
+
country: 'US',
|
|
425
|
+
state: 'CA',
|
|
426
|
+
postal_code: null,
|
|
427
|
+
tax_code: null,
|
|
428
|
+
percentage: 7.5,
|
|
429
|
+
active: true,
|
|
430
|
+
livemode: true,
|
|
431
|
+
display_name: 'CA General Tax',
|
|
432
|
+
updated_at: new Date('2024-01-01'),
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
mockFindAll.mockResolvedValueOnce([rate]);
|
|
436
|
+
|
|
437
|
+
const result = await TaxRate.findMatchingRate({
|
|
438
|
+
country: 'US',
|
|
439
|
+
state: 'CA',
|
|
440
|
+
taxCode: 'digital',
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(result?.id).toBe('txr_general');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should reject when tax code does not match', async () => {
|
|
447
|
+
const rate = {
|
|
448
|
+
id: 'txr_physical',
|
|
449
|
+
country: 'US',
|
|
450
|
+
state: 'CA',
|
|
451
|
+
postal_code: null,
|
|
452
|
+
tax_code: 'physical',
|
|
453
|
+
percentage: 8.5,
|
|
454
|
+
active: true,
|
|
455
|
+
livemode: true,
|
|
456
|
+
display_name: 'CA Physical Goods Tax',
|
|
457
|
+
updated_at: new Date('2024-01-01'),
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
mockFindAll.mockResolvedValueOnce([rate]);
|
|
461
|
+
|
|
462
|
+
const result = await TaxRate.findMatchingRate({
|
|
463
|
+
country: 'US',
|
|
464
|
+
state: 'CA',
|
|
465
|
+
taxCode: 'digital',
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(result).toBeNull();
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe('Hierarchical priority', () => {
|
|
473
|
+
it('should prioritize postal over state+tax combination', async () => {
|
|
474
|
+
const rateA = {
|
|
475
|
+
id: 'txr_state_tax',
|
|
476
|
+
country: 'US',
|
|
477
|
+
state: 'CA',
|
|
478
|
+
postal_code: null,
|
|
479
|
+
tax_code: 'digital',
|
|
480
|
+
percentage: 7.5,
|
|
481
|
+
active: true,
|
|
482
|
+
livemode: true,
|
|
483
|
+
display_name: 'CA Digital Tax',
|
|
484
|
+
updated_at: new Date('2024-01-01'),
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const rateB = {
|
|
488
|
+
id: 'txr_postal',
|
|
489
|
+
country: 'US',
|
|
490
|
+
state: 'CA',
|
|
491
|
+
postal_code: '90210',
|
|
492
|
+
tax_code: null,
|
|
493
|
+
percentage: 9.0,
|
|
494
|
+
active: true,
|
|
495
|
+
livemode: true,
|
|
496
|
+
display_name: 'CA 90210 General Tax',
|
|
497
|
+
updated_at: new Date('2024-01-01'),
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
mockFindAll.mockResolvedValueOnce([rateA, rateB]);
|
|
501
|
+
|
|
502
|
+
const result = await TaxRate.findMatchingRate({
|
|
503
|
+
country: 'US',
|
|
504
|
+
state: 'CA',
|
|
505
|
+
postalCode: '90210',
|
|
506
|
+
taxCode: 'digital',
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(result?.id).toBe('txr_postal');
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('should use state as tiebreaker when postal scores are equal', async () => {
|
|
513
|
+
const rateA = {
|
|
514
|
+
id: 'txr_no_state',
|
|
515
|
+
country: 'US',
|
|
516
|
+
state: null,
|
|
517
|
+
postal_code: null,
|
|
518
|
+
tax_code: 'digital',
|
|
519
|
+
percentage: 5.0,
|
|
520
|
+
active: true,
|
|
521
|
+
livemode: true,
|
|
522
|
+
display_name: 'US Digital Tax',
|
|
523
|
+
updated_at: new Date('2024-01-01'),
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const rateB = {
|
|
527
|
+
id: 'txr_with_state',
|
|
528
|
+
country: 'US',
|
|
529
|
+
state: 'CA',
|
|
530
|
+
postal_code: null,
|
|
531
|
+
tax_code: 'digital',
|
|
532
|
+
percentage: 7.5,
|
|
533
|
+
active: true,
|
|
534
|
+
livemode: true,
|
|
535
|
+
display_name: 'CA Digital Tax',
|
|
536
|
+
updated_at: new Date('2024-01-01'),
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
mockFindAll.mockResolvedValueOnce([rateA, rateB]);
|
|
540
|
+
|
|
541
|
+
const result = await TaxRate.findMatchingRate({
|
|
542
|
+
country: 'US',
|
|
543
|
+
state: 'CA',
|
|
544
|
+
taxCode: 'digital',
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
expect(result?.id).toBe('txr_with_state');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should use tax code as final tiebreaker', async () => {
|
|
551
|
+
const rateA = {
|
|
552
|
+
id: 'txr_general',
|
|
553
|
+
country: 'US',
|
|
554
|
+
state: 'CA',
|
|
555
|
+
postal_code: null,
|
|
556
|
+
tax_code: null,
|
|
557
|
+
percentage: 7.5,
|
|
558
|
+
active: true,
|
|
559
|
+
livemode: true,
|
|
560
|
+
display_name: 'CA General Tax',
|
|
561
|
+
updated_at: new Date('2024-01-02'),
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const rateB = {
|
|
565
|
+
id: 'txr_digital',
|
|
566
|
+
country: 'US',
|
|
567
|
+
state: 'CA',
|
|
568
|
+
postal_code: null,
|
|
569
|
+
tax_code: 'digital',
|
|
570
|
+
percentage: 9.5,
|
|
571
|
+
active: true,
|
|
572
|
+
livemode: true,
|
|
573
|
+
display_name: 'CA Digital Tax',
|
|
574
|
+
updated_at: new Date('2024-01-01'),
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
mockFindAll.mockResolvedValueOnce([rateA, rateB]);
|
|
578
|
+
|
|
579
|
+
const result = await TaxRate.findMatchingRate({
|
|
580
|
+
country: 'US',
|
|
581
|
+
state: 'CA',
|
|
582
|
+
taxCode: 'digital',
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
expect(result?.id).toBe('txr_digital');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should prefer newer rate when all scores are equal', async () => {
|
|
589
|
+
const rateA = {
|
|
590
|
+
id: 'txr_old',
|
|
591
|
+
country: 'US',
|
|
592
|
+
state: 'CA',
|
|
593
|
+
postal_code: null,
|
|
594
|
+
tax_code: 'digital',
|
|
595
|
+
percentage: 7.5,
|
|
596
|
+
active: true,
|
|
597
|
+
livemode: true,
|
|
598
|
+
display_name: 'CA Digital Tax (Old)',
|
|
599
|
+
updated_at: new Date('2024-01-01'),
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const rateB = {
|
|
603
|
+
id: 'txr_new',
|
|
604
|
+
country: 'US',
|
|
605
|
+
state: 'CA',
|
|
606
|
+
postal_code: null,
|
|
607
|
+
tax_code: 'digital',
|
|
608
|
+
percentage: 8.5,
|
|
609
|
+
active: true,
|
|
610
|
+
livemode: true,
|
|
611
|
+
display_name: 'CA Digital Tax (New)',
|
|
612
|
+
updated_at: new Date('2024-02-01'),
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
mockFindAll.mockResolvedValueOnce([rateB, rateA]);
|
|
616
|
+
|
|
617
|
+
const result = await TaxRate.findMatchingRate({
|
|
618
|
+
country: 'US',
|
|
619
|
+
state: 'CA',
|
|
620
|
+
taxCode: 'digital',
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(result?.id).toBe('txr_new');
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe('Real-world scenarios', () => {
|
|
628
|
+
it('should handle country-level fallback', async () => {
|
|
629
|
+
const rate = {
|
|
630
|
+
id: 'txr_country',
|
|
631
|
+
country: 'US',
|
|
632
|
+
state: null,
|
|
633
|
+
postal_code: null,
|
|
634
|
+
tax_code: null,
|
|
635
|
+
percentage: 5.0,
|
|
636
|
+
active: true,
|
|
637
|
+
livemode: true,
|
|
638
|
+
display_name: 'US General Tax',
|
|
639
|
+
updated_at: new Date('2024-01-01'),
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
mockFindAll.mockResolvedValueOnce([rate]);
|
|
643
|
+
|
|
644
|
+
const result = await TaxRate.findMatchingRate({
|
|
645
|
+
country: 'US',
|
|
646
|
+
state: 'TX',
|
|
647
|
+
postalCode: '75001',
|
|
648
|
+
taxCode: 'services',
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
expect(result?.id).toBe('txr_country');
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('should handle complete match hierarchy', async () => {
|
|
655
|
+
const rates = [
|
|
656
|
+
{
|
|
657
|
+
id: 'txr_exact',
|
|
658
|
+
country: 'US',
|
|
659
|
+
state: 'CA',
|
|
660
|
+
postal_code: '90210',
|
|
661
|
+
tax_code: 'digital',
|
|
662
|
+
percentage: 10.0,
|
|
663
|
+
active: true,
|
|
664
|
+
livemode: true,
|
|
665
|
+
display_name: 'Exact Match',
|
|
666
|
+
updated_at: new Date('2024-01-01'),
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
id: 'txr_postal',
|
|
670
|
+
country: 'US',
|
|
671
|
+
state: 'CA',
|
|
672
|
+
postal_code: '90210',
|
|
673
|
+
tax_code: null,
|
|
674
|
+
percentage: 9.5,
|
|
675
|
+
active: true,
|
|
676
|
+
livemode: true,
|
|
677
|
+
display_name: 'Postal Match',
|
|
678
|
+
updated_at: new Date('2024-01-01'),
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
id: 'txr_state',
|
|
682
|
+
country: 'US',
|
|
683
|
+
state: 'CA',
|
|
684
|
+
postal_code: null,
|
|
685
|
+
tax_code: 'digital',
|
|
686
|
+
percentage: 8.5,
|
|
687
|
+
active: true,
|
|
688
|
+
livemode: true,
|
|
689
|
+
display_name: 'State Match',
|
|
690
|
+
updated_at: new Date('2024-01-01'),
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
id: 'txr_country',
|
|
694
|
+
country: 'US',
|
|
695
|
+
state: null,
|
|
696
|
+
postal_code: null,
|
|
697
|
+
tax_code: null,
|
|
698
|
+
percentage: 5.0,
|
|
699
|
+
active: true,
|
|
700
|
+
livemode: true,
|
|
701
|
+
display_name: 'Country Match',
|
|
702
|
+
updated_at: new Date('2024-01-01'),
|
|
703
|
+
},
|
|
704
|
+
];
|
|
705
|
+
|
|
706
|
+
mockFindAll.mockResolvedValueOnce(rates);
|
|
707
|
+
|
|
708
|
+
const result = await TaxRate.findMatchingRate({
|
|
709
|
+
country: 'US',
|
|
710
|
+
state: 'CA',
|
|
711
|
+
postalCode: '90210',
|
|
712
|
+
taxCode: 'digital',
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
expect(result?.id).toBe('txr_exact');
|
|
716
|
+
expect(result?.percentage).toBe(10.0);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('should handle missing postal in user input', async () => {
|
|
720
|
+
const rate = {
|
|
721
|
+
id: 'txr_state',
|
|
722
|
+
country: 'US',
|
|
723
|
+
state: 'CA',
|
|
724
|
+
postal_code: null,
|
|
725
|
+
tax_code: 'digital',
|
|
726
|
+
percentage: 8.5,
|
|
727
|
+
active: true,
|
|
728
|
+
livemode: true,
|
|
729
|
+
display_name: 'CA Digital Tax',
|
|
730
|
+
updated_at: new Date('2024-01-01'),
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
mockFindAll.mockResolvedValueOnce([rate]);
|
|
734
|
+
|
|
735
|
+
const result = await TaxRate.findMatchingRate({
|
|
736
|
+
country: 'US',
|
|
737
|
+
state: 'CA',
|
|
738
|
+
taxCode: 'digital',
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
expect(result?.id).toBe('txr_state');
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
describe('Error handling', () => {
|
|
746
|
+
it('should handle database errors gracefully', async () => {
|
|
747
|
+
mockFindAll.mockRejectedValueOnce(new Error('Database connection failed'));
|
|
748
|
+
|
|
749
|
+
const result = await TaxRate.findMatchingRate({
|
|
750
|
+
country: 'US',
|
|
751
|
+
state: 'CA',
|
|
752
|
+
postalCode: '90210',
|
|
753
|
+
taxCode: 'digital',
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
expect(result).toBeNull();
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('should handle null sequelize instance', async () => {
|
|
760
|
+
const originalSequelize = TaxRate.sequelize;
|
|
761
|
+
// @ts-ignore
|
|
762
|
+
TaxRate.sequelize = null;
|
|
763
|
+
|
|
764
|
+
const result = await TaxRate.findMatchingRate({
|
|
765
|
+
country: 'US',
|
|
766
|
+
state: 'CA',
|
|
767
|
+
postalCode: '90210',
|
|
768
|
+
taxCode: 'digital',
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
expect(result).toBeNull();
|
|
772
|
+
|
|
773
|
+
// @ts-ignore
|
|
774
|
+
TaxRate.sequelize = originalSequelize;
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
});
|