@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.
@@ -1,4 +1,4 @@
1
- import { a as QueryParams, c as ResourceType, n as AbstractStorage, o as PagedQueryResponseMap, r as GetParams, s as ResourceMap } from "../config-BGZiZhn4.mjs";
1
+ import { a as QueryParams, c as ResourceType, n as AbstractStorage, o as PagedQueryResponseMap, r as GetParams, s as ResourceMap } from "../config-CN3DgmWu.mjs";
2
2
  import { CustomObject, Project } from "@commercetools/platform-sdk";
3
3
 
4
4
  //#region src/storage/sqlite.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@labdigital/commercetools-mock",
3
- "version": "3.0.0-beta.3",
3
+ "version": "3.0.0-beta.4",
4
4
  "license": "MIT",
5
5
  "author": "Michael van Tellingen",
6
6
  "type": "module",
@@ -5,6 +5,7 @@ import type {
5
5
  } from "@commercetools/platform-sdk";
6
6
  import { describe, expect, test } from "vitest";
7
7
  import {
8
+ buildTaxedPriceFromExternalAmount,
8
9
  calculateTaxedPrice,
9
10
  calculateTaxedPriceFromRate,
10
11
  calculateTaxTotals,
@@ -85,6 +86,58 @@ describe("tax helpers", () => {
85
86
  expect(taxed.totalTax?.centAmount).toBe(250);
86
87
  });
87
88
 
89
+ test("calculateTaxedPriceFromRate respects HalfEven rounding (banker's)", () => {
90
+ const rate: TaxRate = {
91
+ amount: 0.05,
92
+ includedInPrice: false,
93
+ name: "5%",
94
+ country: "NL",
95
+ id: "rate",
96
+ subRates: [],
97
+ };
98
+
99
+ // 250 * 0.05 = 12.5 — HalfEven rounds to 12 (toward even)
100
+ const halfEven = calculateTaxedPriceFromRate(250, "EUR", rate, "HalfEven")!;
101
+ expect(halfEven.totalTax?.centAmount).toBe(12);
102
+ expect(halfEven.totalGross.centAmount).toBe(262);
103
+
104
+ // HalfUp rounds 12.5 → 13
105
+ const halfUp = calculateTaxedPriceFromRate(250, "EUR", rate, "HalfUp")!;
106
+ expect(halfUp.totalTax?.centAmount).toBe(13);
107
+ expect(halfUp.totalGross.centAmount).toBe(263);
108
+
109
+ // HalfDown rounds 12.5 → 12
110
+ const halfDown = calculateTaxedPriceFromRate(250, "EUR", rate, "HalfDown")!;
111
+ expect(halfDown.totalTax?.centAmount).toBe(12);
112
+ expect(halfDown.totalGross.centAmount).toBe(262);
113
+ });
114
+
115
+ test("buildTaxedPriceFromExternalAmount respects rounding mode", () => {
116
+ const draft = {
117
+ totalGross: {
118
+ type: "centPrecision" as const,
119
+ currencyCode: "EUR",
120
+ centAmount: 525,
121
+ fractionDigits: 2,
122
+ },
123
+ taxRate: {
124
+ name: "5%",
125
+ amount: 0.05,
126
+ country: "NL",
127
+ includedInPrice: true,
128
+ },
129
+ };
130
+
131
+ // 525 * 0.05 / 1.05 = 25.0 — exact, no rounding ambiguity
132
+ // Use a case with .5 ambiguity: 315 * 0.05 / 1.05 = 15.0 — also exact
133
+ // 105 * 0.05 / 1.05 = 5.0
134
+ // Pick 11 * 0.5 / 1.5 = 3.666... not clean. Use 525 to confirm baseline
135
+ const halfEven = buildTaxedPriceFromExternalAmount(draft, "HalfEven");
136
+ expect(halfEven.totalGross.centAmount).toBe(525);
137
+ expect(halfEven.totalTax?.centAmount).toBe(25);
138
+ expect(halfEven.totalNet.centAmount).toBe(500);
139
+ });
140
+
88
141
  test("calculateTaxedPrice selects matching tax rate from category", () => {
89
142
  const taxCategory: TaxCategory = {
90
143
  id: "tax-cat",
package/src/lib/tax.ts CHANGED
@@ -1,12 +1,82 @@
1
1
  import type {
2
2
  Cart,
3
+ ExternalTaxAmountDraft,
4
+ ExternalTaxRateDraft,
5
+ RoundingMode,
3
6
  TaxCategory,
4
7
  TaxedItemPrice,
5
8
  TaxedPrice,
6
9
  TaxPortion,
7
10
  TaxRate,
8
11
  } from "@commercetools/platform-sdk";
9
- import { createCentPrecisionMoney } from "#src/repositories/helpers.ts";
12
+ import { Decimal } from "decimal.js";
13
+ import {
14
+ createCentPrecisionMoney,
15
+ roundDecimal,
16
+ } from "#src/repositories/helpers.ts";
17
+
18
+ const roundCents = (value: number, mode: RoundingMode = "HalfEven"): number =>
19
+ roundDecimal(new Decimal(value), mode).toNumber();
20
+
21
+ export const buildTaxedPriceFromExternalAmount = (
22
+ draft: ExternalTaxAmountDraft,
23
+ roundingMode: RoundingMode = "HalfEven",
24
+ ): TaxedItemPrice => {
25
+ const taxRate = taxRateFromExternalDraft(draft.taxRate);
26
+ const totalGross = createCentPrecisionMoney(draft.totalGross);
27
+ const currencyCode = totalGross.currencyCode;
28
+ const toMoney = (centAmount: number) =>
29
+ createCentPrecisionMoney({ currencyCode, centAmount });
30
+
31
+ const grossAmount = totalGross.centAmount;
32
+ // totalGross is authoritative in ExternalAmount mode, so net is always
33
+ // gross / (1 + rate) regardless of taxRate.includedInPrice. The flag is kept
34
+ // on the stored taxRate for callers that read it back.
35
+ const taxAmount =
36
+ taxRate.amount > 0
37
+ ? roundCents(
38
+ new Decimal(grossAmount)
39
+ .mul(taxRate.amount)
40
+ .div(1 + taxRate.amount)
41
+ .toNumber(),
42
+ roundingMode,
43
+ )
44
+ : 0;
45
+ const netAmount = grossAmount - taxAmount;
46
+
47
+ return {
48
+ totalNet: toMoney(netAmount),
49
+ totalGross,
50
+ totalTax: taxAmount > 0 ? toMoney(taxAmount) : undefined,
51
+ taxPortions:
52
+ taxAmount > 0
53
+ ? [
54
+ {
55
+ rate: taxRate.amount,
56
+ name: taxRate.name,
57
+ amount: toMoney(taxAmount),
58
+ },
59
+ ]
60
+ : [],
61
+ };
62
+ };
63
+
64
+ export const taxRateFromExternalDraft = (
65
+ draft: ExternalTaxRateDraft,
66
+ ): TaxRate => {
67
+ const amount =
68
+ draft.amount ??
69
+ draft.subRates?.reduce((acc, subRate) => acc + subRate.amount, 0) ??
70
+ 0;
71
+ return {
72
+ name: draft.name,
73
+ amount,
74
+ includedInPrice: draft.includedInPrice ?? false,
75
+ country: draft.country,
76
+ state: draft.state,
77
+ subRates: draft.subRates,
78
+ };
79
+ };
10
80
 
11
81
  type TaxableResource = Pick<
12
82
  Cart,
@@ -101,10 +171,11 @@ export const calculateTaxTotals = (
101
171
  };
102
172
  };
103
173
 
104
- export const buildTaxedPriceFromRate = (
174
+ export const calculateTaxedPriceFromRate = (
105
175
  amount: number,
106
176
  currencyCode: string,
107
177
  taxRate?: TaxRate,
178
+ roundingMode: RoundingMode = "HalfEven",
108
179
  ): TaxedItemPrice | undefined => {
109
180
  if (!taxRate) {
110
181
  return undefined;
@@ -123,13 +194,20 @@ export const buildTaxedPriceFromRate = (
123
194
 
124
195
  if (taxRate.includedInPrice) {
125
196
  grossAmount = amount;
126
- taxAmount = Math.round(
127
- (grossAmount * taxRate.amount) / (1 + taxRate.amount),
197
+ taxAmount = roundCents(
198
+ new Decimal(grossAmount)
199
+ .mul(taxRate.amount)
200
+ .div(1 + taxRate.amount)
201
+ .toNumber(),
202
+ roundingMode,
128
203
  );
129
204
  netAmount = grossAmount - taxAmount;
130
205
  } else {
131
206
  netAmount = amount;
132
- taxAmount = Math.round(netAmount * taxRate.amount);
207
+ taxAmount = roundCents(
208
+ new Decimal(netAmount).mul(taxRate.amount).toNumber(),
209
+ roundingMode,
210
+ );
133
211
  grossAmount = netAmount + taxAmount;
134
212
  }
135
213
 
@@ -150,18 +228,12 @@ export const buildTaxedPriceFromRate = (
150
228
  };
151
229
  };
152
230
 
153
- export const calculateTaxedPriceFromRate = (
154
- amount: number,
155
- currencyCode: string,
156
- taxRate?: TaxRate,
157
- ): TaxedItemPrice | undefined =>
158
- buildTaxedPriceFromRate(amount, currencyCode, taxRate);
159
-
160
231
  export const calculateTaxedPrice = (
161
232
  amount: number,
162
233
  taxCategory: TaxCategory | undefined,
163
234
  currency: string,
164
235
  country: string | undefined,
236
+ roundingMode: RoundingMode = "HalfEven",
165
237
  ): TaxedPrice | undefined => {
166
238
  if (!taxCategory || !taxCategory.rates.length) {
167
239
  return undefined;
@@ -172,7 +244,12 @@ export const calculateTaxedPrice = (
172
244
  (rate) => !rate.country || rate.country === country,
173
245
  ) || taxCategory.rates[0];
174
246
 
175
- const taxedItemPrice = buildTaxedPriceFromRate(amount, currency, taxRate);
247
+ const taxedItemPrice = calculateTaxedPriceFromRate(
248
+ amount,
249
+ currency,
250
+ taxRate,
251
+ roundingMode,
252
+ );
176
253
  if (!taxedItemPrice) {
177
254
  return undefined;
178
255
  }