@labdigital/commercetools-mock 3.0.0-beta.3 → 3.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{config-BGZiZhn4.d.mts → config-CN3DgmWu.d.mts} +4 -4
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +253 -38
- package/dist/index.mjs.map +1 -1
- package/dist/storage/sqlite.d.mts +1 -1
- package/package.json +1 -1
- package/src/lib/tax.test.ts +53 -0
- package/src/lib/tax.ts +90 -13
- package/src/repositories/cart/actions.ts +399 -10
- package/src/repositories/cart/helpers.ts +61 -18
- package/src/repositories/cart/index.test.ts +76 -0
- package/src/repositories/cart/index.ts +38 -5
- package/src/services/cart.test.ts +633 -0
- package/src/shipping.ts +21 -12
|
@@ -2160,4 +2160,637 @@ describe("Cart Update Actions", () => {
|
|
|
2160
2160
|
expect(customLineItem.taxedPrice.taxPortions).toHaveLength(1);
|
|
2161
2161
|
expect(customLineItem.taxedPrice.taxPortions[0].rate).toBe(0.21);
|
|
2162
2162
|
});
|
|
2163
|
+
|
|
2164
|
+
describe("External tax mode", () => {
|
|
2165
|
+
// €10.00 product, picked so tax math stays trivial: 1000 cents × rate.
|
|
2166
|
+
const simpleProductDraft = (sku: string) => ({
|
|
2167
|
+
...productDraft,
|
|
2168
|
+
masterVariant: {
|
|
2169
|
+
sku,
|
|
2170
|
+
prices: [
|
|
2171
|
+
{
|
|
2172
|
+
value: {
|
|
2173
|
+
type: "centPrecision" as const,
|
|
2174
|
+
currencyCode: "EUR",
|
|
2175
|
+
centAmount: 1000,
|
|
2176
|
+
fractionDigits: 2,
|
|
2177
|
+
} as CentPrecisionMoney,
|
|
2178
|
+
},
|
|
2179
|
+
],
|
|
2180
|
+
attributes: [],
|
|
2181
|
+
},
|
|
2182
|
+
variants: [],
|
|
2183
|
+
slug: { "nl-NL": `simple-${sku}` },
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
test("addLineItem applies externalTaxRate and aggregates cart taxedPrice", async () => {
|
|
2187
|
+
const product = await productFactory.create(simpleProductDraft("ext-1"));
|
|
2188
|
+
const externalCart = await cartFactory.create({
|
|
2189
|
+
currency: "EUR",
|
|
2190
|
+
country: "NL",
|
|
2191
|
+
taxMode: "External",
|
|
2192
|
+
});
|
|
2193
|
+
assert(product, "product not created");
|
|
2194
|
+
|
|
2195
|
+
// €10 × 2 = €20 net; 10% tax = €2; gross = €22.
|
|
2196
|
+
const response = await ctMock.app.inject({
|
|
2197
|
+
method: "POST",
|
|
2198
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2199
|
+
payload: {
|
|
2200
|
+
version: 1,
|
|
2201
|
+
actions: [
|
|
2202
|
+
{
|
|
2203
|
+
action: "addLineItem",
|
|
2204
|
+
sku: "ext-1",
|
|
2205
|
+
quantity: 2,
|
|
2206
|
+
externalTaxRate: {
|
|
2207
|
+
name: "External NL VAT",
|
|
2208
|
+
amount: 0.1,
|
|
2209
|
+
country: "NL",
|
|
2210
|
+
includedInPrice: false,
|
|
2211
|
+
},
|
|
2212
|
+
},
|
|
2213
|
+
],
|
|
2214
|
+
},
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
expect(response.statusCode).toBe(200);
|
|
2218
|
+
const cart = response.json();
|
|
2219
|
+
const lineItem = cart.lineItems[0];
|
|
2220
|
+
expect(lineItem.taxRate.name).toBe("External NL VAT");
|
|
2221
|
+
expect(lineItem.taxRate.amount).toBe(0.1);
|
|
2222
|
+
expect(lineItem.taxedPrice.totalNet.centAmount).toBe(2000);
|
|
2223
|
+
expect(lineItem.taxedPrice.totalGross.centAmount).toBe(2200);
|
|
2224
|
+
expect(cart.taxedPrice).toBeDefined();
|
|
2225
|
+
expect(cart.taxedPrice.totalNet.centAmount).toBe(2000);
|
|
2226
|
+
expect(cart.taxedPrice.totalGross.centAmount).toBe(2200);
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
test("addCustomLineItem applies externalTaxRate", async () => {
|
|
2230
|
+
const externalCart = await cartFactory.create({
|
|
2231
|
+
currency: "EUR",
|
|
2232
|
+
country: "NL",
|
|
2233
|
+
taxMode: "External",
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
// €10 net; 20% tax = €2; gross = €12.
|
|
2237
|
+
const response = await ctMock.app.inject({
|
|
2238
|
+
method: "POST",
|
|
2239
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2240
|
+
payload: {
|
|
2241
|
+
version: 1,
|
|
2242
|
+
actions: [
|
|
2243
|
+
{
|
|
2244
|
+
action: "addCustomLineItem",
|
|
2245
|
+
name: { en: "Service" },
|
|
2246
|
+
slug: "service",
|
|
2247
|
+
money: { currencyCode: "EUR", centAmount: 1000 },
|
|
2248
|
+
quantity: 1,
|
|
2249
|
+
externalTaxRate: {
|
|
2250
|
+
name: "Ext",
|
|
2251
|
+
amount: 0.2,
|
|
2252
|
+
country: "NL",
|
|
2253
|
+
includedInPrice: false,
|
|
2254
|
+
},
|
|
2255
|
+
},
|
|
2256
|
+
],
|
|
2257
|
+
},
|
|
2258
|
+
});
|
|
2259
|
+
|
|
2260
|
+
expect(response.statusCode).toBe(200);
|
|
2261
|
+
const customLineItem = response.json().customLineItems[0];
|
|
2262
|
+
expect(customLineItem.taxRate.amount).toBe(0.2);
|
|
2263
|
+
expect(customLineItem.taxedPrice.totalNet.centAmount).toBe(1000);
|
|
2264
|
+
expect(customLineItem.taxedPrice.totalGross.centAmount).toBe(1200);
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
test("setLineItemTaxRate updates the line item tax", async () => {
|
|
2268
|
+
const product = await productFactory.create(simpleProductDraft("ext-2"));
|
|
2269
|
+
const externalCart = await cartFactory.create({
|
|
2270
|
+
currency: "EUR",
|
|
2271
|
+
country: "NL",
|
|
2272
|
+
taxMode: "External",
|
|
2273
|
+
});
|
|
2274
|
+
assert(product, "product not created");
|
|
2275
|
+
|
|
2276
|
+
const added = await ctMock.app.inject({
|
|
2277
|
+
method: "POST",
|
|
2278
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2279
|
+
payload: {
|
|
2280
|
+
version: 1,
|
|
2281
|
+
actions: [{ action: "addLineItem", sku: "ext-2", quantity: 1 }],
|
|
2282
|
+
},
|
|
2283
|
+
});
|
|
2284
|
+
expect(added.statusCode).toBe(200);
|
|
2285
|
+
const lineItemId = added.json().lineItems[0].id;
|
|
2286
|
+
|
|
2287
|
+
// €10 net; 5% tax = €0.50; gross = €10.50.
|
|
2288
|
+
const response = await ctMock.app.inject({
|
|
2289
|
+
method: "POST",
|
|
2290
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2291
|
+
payload: {
|
|
2292
|
+
version: 2,
|
|
2293
|
+
actions: [
|
|
2294
|
+
{
|
|
2295
|
+
action: "setLineItemTaxRate",
|
|
2296
|
+
lineItemId,
|
|
2297
|
+
externalTaxRate: {
|
|
2298
|
+
name: "Late VAT",
|
|
2299
|
+
amount: 0.05,
|
|
2300
|
+
country: "NL",
|
|
2301
|
+
includedInPrice: false,
|
|
2302
|
+
},
|
|
2303
|
+
},
|
|
2304
|
+
],
|
|
2305
|
+
},
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
expect(response.statusCode).toBe(200);
|
|
2309
|
+
const lineItem = response.json().lineItems[0];
|
|
2310
|
+
expect(lineItem.taxRate.amount).toBe(0.05);
|
|
2311
|
+
expect(lineItem.taxedPrice.totalGross.centAmount).toBe(1050);
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
test("setLineItemTaxRate rejects non-External tax mode", async () => {
|
|
2315
|
+
const product = await productFactory.create(simpleProductDraft("ext-3"));
|
|
2316
|
+
assert(cart, "cart not created");
|
|
2317
|
+
assert(product, "product not created");
|
|
2318
|
+
|
|
2319
|
+
const added = await ctMock.app.inject({
|
|
2320
|
+
method: "POST",
|
|
2321
|
+
url: `/dummy/carts/${cart.id}`,
|
|
2322
|
+
payload: {
|
|
2323
|
+
version: 1,
|
|
2324
|
+
actions: [{ action: "addLineItem", sku: "ext-3", quantity: 1 }],
|
|
2325
|
+
},
|
|
2326
|
+
});
|
|
2327
|
+
const lineItemId = added.json().lineItems[0].id;
|
|
2328
|
+
|
|
2329
|
+
const response = await ctMock.app.inject({
|
|
2330
|
+
method: "POST",
|
|
2331
|
+
url: `/dummy/carts/${cart.id}`,
|
|
2332
|
+
payload: {
|
|
2333
|
+
version: 2,
|
|
2334
|
+
actions: [
|
|
2335
|
+
{
|
|
2336
|
+
action: "setLineItemTaxRate",
|
|
2337
|
+
lineItemId,
|
|
2338
|
+
externalTaxRate: {
|
|
2339
|
+
name: "Bad",
|
|
2340
|
+
amount: 0.1,
|
|
2341
|
+
country: "NL",
|
|
2342
|
+
},
|
|
2343
|
+
},
|
|
2344
|
+
],
|
|
2345
|
+
},
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
expect(response.statusCode).toBe(400);
|
|
2349
|
+
});
|
|
2350
|
+
|
|
2351
|
+
test("setCustomLineItemTaxRate updates the custom line item tax", async () => {
|
|
2352
|
+
const externalCart = await cartFactory.create({
|
|
2353
|
+
currency: "EUR",
|
|
2354
|
+
country: "NL",
|
|
2355
|
+
taxMode: "External",
|
|
2356
|
+
});
|
|
2357
|
+
|
|
2358
|
+
const added = await ctMock.app.inject({
|
|
2359
|
+
method: "POST",
|
|
2360
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2361
|
+
payload: {
|
|
2362
|
+
version: 1,
|
|
2363
|
+
actions: [
|
|
2364
|
+
{
|
|
2365
|
+
action: "addCustomLineItem",
|
|
2366
|
+
name: { en: "Svc" },
|
|
2367
|
+
slug: "svc",
|
|
2368
|
+
money: { currencyCode: "EUR", centAmount: 1000 },
|
|
2369
|
+
quantity: 1,
|
|
2370
|
+
},
|
|
2371
|
+
],
|
|
2372
|
+
},
|
|
2373
|
+
});
|
|
2374
|
+
const cliId = added.json().customLineItems[0].id;
|
|
2375
|
+
|
|
2376
|
+
const response = await ctMock.app.inject({
|
|
2377
|
+
method: "POST",
|
|
2378
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2379
|
+
payload: {
|
|
2380
|
+
version: 2,
|
|
2381
|
+
actions: [
|
|
2382
|
+
{
|
|
2383
|
+
action: "setCustomLineItemTaxRate",
|
|
2384
|
+
customLineItemId: cliId,
|
|
2385
|
+
externalTaxRate: {
|
|
2386
|
+
name: "Ext",
|
|
2387
|
+
amount: 0.25,
|
|
2388
|
+
country: "NL",
|
|
2389
|
+
includedInPrice: false,
|
|
2390
|
+
},
|
|
2391
|
+
},
|
|
2392
|
+
],
|
|
2393
|
+
},
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
expect(response.statusCode).toBe(200);
|
|
2397
|
+
const cli = response.json().customLineItems[0];
|
|
2398
|
+
expect(cli.taxRate.amount).toBe(0.25);
|
|
2399
|
+
expect(cli.taxedPrice.totalGross.centAmount).toBe(1250);
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
test("setShippingMethodTaxRate updates the shipping tax", async () => {
|
|
2403
|
+
const zone = await zoneFactory.create({
|
|
2404
|
+
name: "NL",
|
|
2405
|
+
locations: [{ country: "NL" }],
|
|
2406
|
+
});
|
|
2407
|
+
const shippingMethod = await shippingMethodFactory.create({
|
|
2408
|
+
name: "Std",
|
|
2409
|
+
taxCategory: { typeId: "tax-category", id: taxCategory.id },
|
|
2410
|
+
zoneRates: [
|
|
2411
|
+
{
|
|
2412
|
+
zone: { typeId: "zone", id: zone.id },
|
|
2413
|
+
shippingRates: [
|
|
2414
|
+
{
|
|
2415
|
+
price: { currencyCode: "EUR", centAmount: 500 },
|
|
2416
|
+
},
|
|
2417
|
+
],
|
|
2418
|
+
},
|
|
2419
|
+
],
|
|
2420
|
+
isDefault: false,
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
const externalCart = await cartFactory.create({
|
|
2424
|
+
currency: "EUR",
|
|
2425
|
+
country: "NL",
|
|
2426
|
+
taxMode: "External",
|
|
2427
|
+
shippingAddress: { country: "NL" },
|
|
2428
|
+
});
|
|
2429
|
+
|
|
2430
|
+
await ctMock.app.inject({
|
|
2431
|
+
method: "POST",
|
|
2432
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2433
|
+
payload: {
|
|
2434
|
+
version: 1,
|
|
2435
|
+
actions: [
|
|
2436
|
+
{
|
|
2437
|
+
action: "setShippingMethod",
|
|
2438
|
+
shippingMethod: {
|
|
2439
|
+
typeId: "shipping-method",
|
|
2440
|
+
id: shippingMethod.id,
|
|
2441
|
+
},
|
|
2442
|
+
externalTaxRate: {
|
|
2443
|
+
name: "Initial",
|
|
2444
|
+
amount: 0.1,
|
|
2445
|
+
country: "NL",
|
|
2446
|
+
includedInPrice: false,
|
|
2447
|
+
},
|
|
2448
|
+
},
|
|
2449
|
+
],
|
|
2450
|
+
},
|
|
2451
|
+
});
|
|
2452
|
+
|
|
2453
|
+
const response = await ctMock.app.inject({
|
|
2454
|
+
method: "POST",
|
|
2455
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2456
|
+
payload: {
|
|
2457
|
+
version: 2,
|
|
2458
|
+
actions: [
|
|
2459
|
+
{
|
|
2460
|
+
action: "setShippingMethodTaxRate",
|
|
2461
|
+
externalTaxRate: {
|
|
2462
|
+
name: "Updated",
|
|
2463
|
+
amount: 0.2,
|
|
2464
|
+
country: "NL",
|
|
2465
|
+
includedInPrice: false,
|
|
2466
|
+
},
|
|
2467
|
+
},
|
|
2468
|
+
],
|
|
2469
|
+
},
|
|
2470
|
+
});
|
|
2471
|
+
|
|
2472
|
+
expect(response.statusCode).toBe(200);
|
|
2473
|
+
const shippingInfo = response.json().shippingInfo;
|
|
2474
|
+
expect(shippingInfo.taxRate.amount).toBe(0.2);
|
|
2475
|
+
expect(shippingInfo.taxedPrice.totalGross.centAmount).toBe(600);
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
test("setCustomShippingMethod accepts externalTaxRate", async () => {
|
|
2479
|
+
const externalCart = await cartFactory.create({
|
|
2480
|
+
currency: "EUR",
|
|
2481
|
+
country: "NL",
|
|
2482
|
+
taxMode: "External",
|
|
2483
|
+
shippingAddress: { country: "NL" },
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
const response = await ctMock.app.inject({
|
|
2487
|
+
method: "POST",
|
|
2488
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2489
|
+
payload: {
|
|
2490
|
+
version: 1,
|
|
2491
|
+
actions: [
|
|
2492
|
+
{
|
|
2493
|
+
action: "setCustomShippingMethod",
|
|
2494
|
+
shippingMethodName: "Custom",
|
|
2495
|
+
shippingRate: {
|
|
2496
|
+
price: { currencyCode: "EUR", centAmount: 1000 },
|
|
2497
|
+
},
|
|
2498
|
+
externalTaxRate: {
|
|
2499
|
+
name: "Custom VAT",
|
|
2500
|
+
amount: 0.15,
|
|
2501
|
+
country: "NL",
|
|
2502
|
+
includedInPrice: false,
|
|
2503
|
+
},
|
|
2504
|
+
},
|
|
2505
|
+
],
|
|
2506
|
+
},
|
|
2507
|
+
});
|
|
2508
|
+
|
|
2509
|
+
expect(response.statusCode).toBe(200);
|
|
2510
|
+
const shippingInfo = response.json().shippingInfo;
|
|
2511
|
+
expect(shippingInfo.taxRate.amount).toBe(0.15);
|
|
2512
|
+
expect(shippingInfo.taxedPrice.totalGross.centAmount).toBe(1150);
|
|
2513
|
+
});
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
describe("ExternalAmount tax mode", () => {
|
|
2517
|
+
const simpleProductDraft = (sku: string) => ({
|
|
2518
|
+
...productDraft,
|
|
2519
|
+
masterVariant: {
|
|
2520
|
+
sku,
|
|
2521
|
+
prices: [
|
|
2522
|
+
{
|
|
2523
|
+
value: {
|
|
2524
|
+
type: "centPrecision" as const,
|
|
2525
|
+
currencyCode: "EUR",
|
|
2526
|
+
centAmount: 1000,
|
|
2527
|
+
fractionDigits: 2,
|
|
2528
|
+
} as CentPrecisionMoney,
|
|
2529
|
+
},
|
|
2530
|
+
],
|
|
2531
|
+
attributes: [],
|
|
2532
|
+
},
|
|
2533
|
+
variants: [],
|
|
2534
|
+
slug: { "nl-NL": `simple-${sku}` },
|
|
2535
|
+
});
|
|
2536
|
+
|
|
2537
|
+
test("setLineItemTaxAmount sets explicit gross and derives net", async () => {
|
|
2538
|
+
const product = await productFactory.create(simpleProductDraft("amt-1"));
|
|
2539
|
+
const externalCart = await cartFactory.create({
|
|
2540
|
+
currency: "EUR",
|
|
2541
|
+
country: "NL",
|
|
2542
|
+
taxMode: "ExternalAmount",
|
|
2543
|
+
});
|
|
2544
|
+
assert(product, "product not created");
|
|
2545
|
+
|
|
2546
|
+
const added = await ctMock.app.inject({
|
|
2547
|
+
method: "POST",
|
|
2548
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2549
|
+
payload: {
|
|
2550
|
+
version: 1,
|
|
2551
|
+
actions: [{ action: "addLineItem", sku: "amt-1", quantity: 1 }],
|
|
2552
|
+
},
|
|
2553
|
+
});
|
|
2554
|
+
const lineItemId = added.json().lineItems[0].id;
|
|
2555
|
+
|
|
2556
|
+
const response = await ctMock.app.inject({
|
|
2557
|
+
method: "POST",
|
|
2558
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2559
|
+
payload: {
|
|
2560
|
+
version: 2,
|
|
2561
|
+
actions: [
|
|
2562
|
+
{
|
|
2563
|
+
action: "setLineItemTaxAmount",
|
|
2564
|
+
lineItemId,
|
|
2565
|
+
externalTaxAmount: {
|
|
2566
|
+
totalGross: { currencyCode: "EUR", centAmount: 1210 },
|
|
2567
|
+
taxRate: {
|
|
2568
|
+
name: "Ext VAT",
|
|
2569
|
+
amount: 0.21,
|
|
2570
|
+
country: "NL",
|
|
2571
|
+
includedInPrice: true,
|
|
2572
|
+
},
|
|
2573
|
+
},
|
|
2574
|
+
},
|
|
2575
|
+
],
|
|
2576
|
+
},
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
expect(response.statusCode).toBe(200);
|
|
2580
|
+
const lineItem = response.json().lineItems[0];
|
|
2581
|
+
expect(lineItem.taxRate.amount).toBe(0.21);
|
|
2582
|
+
expect(lineItem.taxedPrice.totalGross.centAmount).toBe(1210);
|
|
2583
|
+
expect(lineItem.taxedPrice.totalNet.centAmount).toBe(1000);
|
|
2584
|
+
expect(lineItem.taxedPrice.totalTax.centAmount).toBe(210);
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2587
|
+
test("setLineItemTaxAmount rejects non-ExternalAmount tax mode", async () => {
|
|
2588
|
+
const product = await productFactory.create(simpleProductDraft("amt-2"));
|
|
2589
|
+
assert(cart, "cart not created");
|
|
2590
|
+
assert(product, "product not created");
|
|
2591
|
+
|
|
2592
|
+
const added = await ctMock.app.inject({
|
|
2593
|
+
method: "POST",
|
|
2594
|
+
url: `/dummy/carts/${cart.id}`,
|
|
2595
|
+
payload: {
|
|
2596
|
+
version: 1,
|
|
2597
|
+
actions: [{ action: "addLineItem", sku: "amt-2", quantity: 1 }],
|
|
2598
|
+
},
|
|
2599
|
+
});
|
|
2600
|
+
const lineItemId = added.json().lineItems[0].id;
|
|
2601
|
+
|
|
2602
|
+
const response = await ctMock.app.inject({
|
|
2603
|
+
method: "POST",
|
|
2604
|
+
url: `/dummy/carts/${cart.id}`,
|
|
2605
|
+
payload: {
|
|
2606
|
+
version: 2,
|
|
2607
|
+
actions: [
|
|
2608
|
+
{
|
|
2609
|
+
action: "setLineItemTaxAmount",
|
|
2610
|
+
lineItemId,
|
|
2611
|
+
externalTaxAmount: {
|
|
2612
|
+
totalGross: { currencyCode: "EUR", centAmount: 100 },
|
|
2613
|
+
taxRate: {
|
|
2614
|
+
name: "X",
|
|
2615
|
+
amount: 0.21,
|
|
2616
|
+
country: "NL",
|
|
2617
|
+
},
|
|
2618
|
+
},
|
|
2619
|
+
},
|
|
2620
|
+
],
|
|
2621
|
+
},
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
expect(response.statusCode).toBe(400);
|
|
2625
|
+
});
|
|
2626
|
+
|
|
2627
|
+
test("setCustomLineItemTaxAmount sets explicit gross", async () => {
|
|
2628
|
+
const externalCart = await cartFactory.create({
|
|
2629
|
+
currency: "EUR",
|
|
2630
|
+
country: "NL",
|
|
2631
|
+
taxMode: "ExternalAmount",
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
const added = await ctMock.app.inject({
|
|
2635
|
+
method: "POST",
|
|
2636
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2637
|
+
payload: {
|
|
2638
|
+
version: 1,
|
|
2639
|
+
actions: [
|
|
2640
|
+
{
|
|
2641
|
+
action: "addCustomLineItem",
|
|
2642
|
+
name: { en: "Svc" },
|
|
2643
|
+
slug: "svc",
|
|
2644
|
+
money: { currencyCode: "EUR", centAmount: 1000 },
|
|
2645
|
+
quantity: 1,
|
|
2646
|
+
},
|
|
2647
|
+
],
|
|
2648
|
+
},
|
|
2649
|
+
});
|
|
2650
|
+
const cliId = added.json().customLineItems[0].id;
|
|
2651
|
+
|
|
2652
|
+
const response = await ctMock.app.inject({
|
|
2653
|
+
method: "POST",
|
|
2654
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2655
|
+
payload: {
|
|
2656
|
+
version: 2,
|
|
2657
|
+
actions: [
|
|
2658
|
+
{
|
|
2659
|
+
action: "setCustomLineItemTaxAmount",
|
|
2660
|
+
customLineItemId: cliId,
|
|
2661
|
+
externalTaxAmount: {
|
|
2662
|
+
totalGross: { currencyCode: "EUR", centAmount: 1250 },
|
|
2663
|
+
taxRate: {
|
|
2664
|
+
name: "Ext",
|
|
2665
|
+
amount: 0.25,
|
|
2666
|
+
country: "NL",
|
|
2667
|
+
},
|
|
2668
|
+
},
|
|
2669
|
+
},
|
|
2670
|
+
],
|
|
2671
|
+
},
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
expect(response.statusCode).toBe(200);
|
|
2675
|
+
const cli = response.json().customLineItems[0];
|
|
2676
|
+
expect(cli.taxedPrice.totalGross.centAmount).toBe(1250);
|
|
2677
|
+
expect(cli.taxedPrice.totalNet.centAmount).toBe(1000);
|
|
2678
|
+
});
|
|
2679
|
+
|
|
2680
|
+
test("setShippingMethodTaxAmount sets explicit shipping gross", async () => {
|
|
2681
|
+
const externalCart = await cartFactory.create({
|
|
2682
|
+
currency: "EUR",
|
|
2683
|
+
country: "NL",
|
|
2684
|
+
taxMode: "ExternalAmount",
|
|
2685
|
+
shippingAddress: { country: "NL" },
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
await ctMock.app.inject({
|
|
2689
|
+
method: "POST",
|
|
2690
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2691
|
+
payload: {
|
|
2692
|
+
version: 1,
|
|
2693
|
+
actions: [
|
|
2694
|
+
{
|
|
2695
|
+
action: "setCustomShippingMethod",
|
|
2696
|
+
shippingMethodName: "Custom",
|
|
2697
|
+
shippingRate: {
|
|
2698
|
+
price: { currencyCode: "EUR", centAmount: 1000 },
|
|
2699
|
+
},
|
|
2700
|
+
},
|
|
2701
|
+
],
|
|
2702
|
+
},
|
|
2703
|
+
});
|
|
2704
|
+
|
|
2705
|
+
const response = await ctMock.app.inject({
|
|
2706
|
+
method: "POST",
|
|
2707
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2708
|
+
payload: {
|
|
2709
|
+
version: 2,
|
|
2710
|
+
actions: [
|
|
2711
|
+
{
|
|
2712
|
+
action: "setShippingMethodTaxAmount",
|
|
2713
|
+
externalTaxAmount: {
|
|
2714
|
+
totalGross: { currencyCode: "EUR", centAmount: 1200 },
|
|
2715
|
+
taxRate: {
|
|
2716
|
+
name: "Ship Tax",
|
|
2717
|
+
amount: 0.2,
|
|
2718
|
+
country: "NL",
|
|
2719
|
+
},
|
|
2720
|
+
},
|
|
2721
|
+
},
|
|
2722
|
+
],
|
|
2723
|
+
},
|
|
2724
|
+
});
|
|
2725
|
+
|
|
2726
|
+
expect(response.statusCode).toBe(200);
|
|
2727
|
+
const body = response.json();
|
|
2728
|
+
expect(body.shippingInfo.taxedPrice.totalGross.centAmount).toBe(1200);
|
|
2729
|
+
expect(body.shippingInfo.taxedPrice.totalNet.centAmount).toBe(1000);
|
|
2730
|
+
// taxedShippingPrice mirrors shippingInfo.taxedPrice even in
|
|
2731
|
+
// ExternalAmount mode (it's a subtotal, not the cart total).
|
|
2732
|
+
expect(body.taxedShippingPrice.totalGross.centAmount).toBe(1200);
|
|
2733
|
+
expect(body.taxedShippingPrice.totalNet.centAmount).toBe(1000);
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
test("setCartTotalTax overrides aggregated taxedPrice", async () => {
|
|
2737
|
+
const product = await productFactory.create(simpleProductDraft("amt-3"));
|
|
2738
|
+
const externalCart = await cartFactory.create({
|
|
2739
|
+
currency: "EUR",
|
|
2740
|
+
country: "NL",
|
|
2741
|
+
taxMode: "ExternalAmount",
|
|
2742
|
+
});
|
|
2743
|
+
assert(product, "product not created");
|
|
2744
|
+
|
|
2745
|
+
const response = await ctMock.app.inject({
|
|
2746
|
+
method: "POST",
|
|
2747
|
+
url: `/dummy/carts/${externalCart.id}`,
|
|
2748
|
+
payload: {
|
|
2749
|
+
version: 1,
|
|
2750
|
+
actions: [
|
|
2751
|
+
{ action: "addLineItem", sku: "amt-3", quantity: 1 },
|
|
2752
|
+
{
|
|
2753
|
+
action: "setCartTotalTax",
|
|
2754
|
+
externalTotalGross: { currencyCode: "EUR", centAmount: 1210 },
|
|
2755
|
+
externalTaxPortions: [
|
|
2756
|
+
{
|
|
2757
|
+
rate: 0.21,
|
|
2758
|
+
name: "NL",
|
|
2759
|
+
amount: { currencyCode: "EUR", centAmount: 210 },
|
|
2760
|
+
},
|
|
2761
|
+
],
|
|
2762
|
+
},
|
|
2763
|
+
],
|
|
2764
|
+
},
|
|
2765
|
+
});
|
|
2766
|
+
|
|
2767
|
+
expect(response.statusCode).toBe(200);
|
|
2768
|
+
const body = response.json();
|
|
2769
|
+
expect(body.taxedPrice.totalGross.centAmount).toBe(1210);
|
|
2770
|
+
expect(body.taxedPrice.totalNet.centAmount).toBe(1000);
|
|
2771
|
+
expect(body.taxedPrice.totalTax.centAmount).toBe(210);
|
|
2772
|
+
expect(body.taxedPrice.taxPortions).toHaveLength(1);
|
|
2773
|
+
expect(body.taxedPrice.taxPortions[0].rate).toBe(0.21);
|
|
2774
|
+
});
|
|
2775
|
+
|
|
2776
|
+
test("setCartTotalTax rejects non-ExternalAmount tax mode", async () => {
|
|
2777
|
+
assert(cart, "cart not created");
|
|
2778
|
+
|
|
2779
|
+
const response = await ctMock.app.inject({
|
|
2780
|
+
method: "POST",
|
|
2781
|
+
url: `/dummy/carts/${cart.id}`,
|
|
2782
|
+
payload: {
|
|
2783
|
+
version: 1,
|
|
2784
|
+
actions: [
|
|
2785
|
+
{
|
|
2786
|
+
action: "setCartTotalTax",
|
|
2787
|
+
externalTotalGross: { currencyCode: "EUR", centAmount: 100 },
|
|
2788
|
+
},
|
|
2789
|
+
],
|
|
2790
|
+
},
|
|
2791
|
+
});
|
|
2792
|
+
|
|
2793
|
+
expect(response.statusCode).toBe(400);
|
|
2794
|
+
});
|
|
2795
|
+
});
|
|
2163
2796
|
});
|
package/src/shipping.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
Cart,
|
|
3
3
|
CartValueTier,
|
|
4
4
|
CentPrecisionMoney,
|
|
5
|
+
ExternalTaxRateDraft,
|
|
5
6
|
InvalidOperationError,
|
|
6
7
|
MissingTaxRateForCountryError,
|
|
7
8
|
ShippingInfo,
|
|
@@ -10,9 +11,11 @@ import type {
|
|
|
10
11
|
ShippingRatePriceTier,
|
|
11
12
|
TaxedItemPrice,
|
|
12
13
|
TaxPortion,
|
|
14
|
+
TaxRate,
|
|
13
15
|
} from "@commercetools/platform-sdk";
|
|
14
16
|
import { Decimal } from "decimal.js";
|
|
15
17
|
import { CommercetoolsError } from "./exceptions.ts";
|
|
18
|
+
import { taxRateFromExternalDraft } from "./lib/tax.ts";
|
|
16
19
|
import type { GetParams, RepositoryContext } from "./repositories/abstract.ts";
|
|
17
20
|
import {
|
|
18
21
|
createCentPrecisionMoney,
|
|
@@ -190,6 +193,7 @@ export const createShippingInfoFromMethod = async (
|
|
|
190
193
|
storage: AbstractStorage,
|
|
191
194
|
resource: ShippingCalculationSource,
|
|
192
195
|
method: ShippingMethod,
|
|
196
|
+
externalTaxRate?: ExternalTaxRateDraft,
|
|
193
197
|
): Promise<Omit<ShippingInfo, "deliveries">> => {
|
|
194
198
|
const country = resource.shippingAddress!.country;
|
|
195
199
|
|
|
@@ -228,20 +232,25 @@ export const createShippingInfoFromMethod = async (
|
|
|
228
232
|
);
|
|
229
233
|
}
|
|
230
234
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
+
let taxRate: TaxRate | undefined;
|
|
236
|
+
if (externalTaxRate) {
|
|
237
|
+
taxRate = taxRateFromExternalDraft(externalTaxRate);
|
|
238
|
+
} else {
|
|
239
|
+
const taxCategory = await storage.getByResourceIdentifier<"tax-category">(
|
|
240
|
+
context.projectKey,
|
|
241
|
+
method.taxCategory,
|
|
242
|
+
);
|
|
235
243
|
|
|
236
|
-
|
|
237
|
-
|
|
244
|
+
// TODO: match state in addition to country
|
|
245
|
+
taxRate = taxCategory.rates.find((rate) => rate.country === country);
|
|
238
246
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
247
|
+
if (!taxRate) {
|
|
248
|
+
throw new CommercetoolsError<MissingTaxRateForCountryError>({
|
|
249
|
+
code: "MissingTaxRateForCountry",
|
|
250
|
+
message: `Tax category '${taxCategory.id}' is missing a tax rate for country '${country}'.`,
|
|
251
|
+
taxCategoryId: taxCategory.id,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
245
254
|
}
|
|
246
255
|
|
|
247
256
|
const shippingRateTier = shippingRate.tiers.find((tier) => tier.isMatching);
|