@labdigital/commercetools-mock 2.58.0 → 2.59.1

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/src/shipping.ts CHANGED
@@ -1,12 +1,23 @@
1
1
  import type {
2
2
  Cart,
3
3
  CartValueTier,
4
+ CentPrecisionMoney,
4
5
  InvalidOperationError,
6
+ MissingTaxRateForCountryError,
7
+ Order,
8
+ ShippingInfo,
9
+ ShippingMethod,
10
+ ShippingMethodDoesNotMatchCartError,
5
11
  ShippingRate,
6
12
  ShippingRatePriceTier,
13
+ TaxPortion,
14
+ TaxRate,
15
+ TaxedItemPrice,
7
16
  } from "@commercetools/platform-sdk";
17
+ import { Decimal } from "decimal.js";
8
18
  import { CommercetoolsError } from "./exceptions";
9
19
  import type { GetParams, RepositoryContext } from "./repositories/abstract";
20
+ import { createCentPrecisionMoney, roundDecimal } from "./repositories/helpers";
10
21
  import type { AbstractStorage } from "./storage/abstract";
11
22
 
12
23
  export const markMatchingShippingRate = (
@@ -145,3 +156,149 @@ export const getShippingMethodsMatchingCart = (
145
156
  results: results,
146
157
  };
147
158
  };
159
+
160
+ /**
161
+ * Interface for cart-like objects that can be used for shipping calculations
162
+ */
163
+ interface ShippingCalculationSource {
164
+ id: string;
165
+ totalPrice: CentPrecisionMoney;
166
+ shippingAddress?: {
167
+ country: string;
168
+ [key: string]: any;
169
+ };
170
+ taxRoundingMode?: string;
171
+ }
172
+
173
+ /**
174
+ * Creates shipping info from a shipping method, handling all tax calculations and pricing logic.
175
+ */
176
+ export const createShippingInfoFromMethod = (
177
+ context: RepositoryContext,
178
+ storage: AbstractStorage,
179
+ resource: ShippingCalculationSource,
180
+ method: ShippingMethod,
181
+ ): Omit<ShippingInfo, "deliveries"> => {
182
+ const country = resource.shippingAddress!.country;
183
+
184
+ // There should only be one zone rate matching the address, since
185
+ // Locations cannot be assigned to more than one zone.
186
+ // See https://docs.commercetools.com/api/projects/zones#location
187
+ const zoneRate = method.zoneRates.find((rate) =>
188
+ rate.zone.obj?.locations.some((loc) => loc.country === country),
189
+ );
190
+
191
+ if (!zoneRate) {
192
+ // This shouldn't happen because getShippingMethodsMatchingCart already
193
+ // filtered out shipping methods without any zones matching the address
194
+ throw new Error("Zone rate not found");
195
+ }
196
+
197
+ // Shipping rates are defined by currency, and getShippingMethodsMatchingCart
198
+ // also matches on currency, so there should only be one in the array.
199
+ // See https://docs.commercetools.com/api/projects/shippingMethods#zonerate
200
+ const shippingRate = zoneRate.shippingRates[0];
201
+ if (!shippingRate) {
202
+ // This shouldn't happen because getShippingMethodsMatchingCart already
203
+ // filtered out shipping methods without any matching rates
204
+ throw new Error("Shipping rate not found");
205
+ }
206
+
207
+ const taxCategory = storage.getByResourceIdentifier<"tax-category">(
208
+ context.projectKey,
209
+ method.taxCategory,
210
+ );
211
+
212
+ // TODO: match state in addition to country
213
+ const taxRate = taxCategory.rates.find((rate) => rate.country === country);
214
+
215
+ if (!taxRate) {
216
+ throw new CommercetoolsError<MissingTaxRateForCountryError>({
217
+ code: "MissingTaxRateForCountry",
218
+ message: `Tax category '${taxCategory.id}' is missing a tax rate for country '${country}'.`,
219
+ taxCategoryId: taxCategory.id,
220
+ });
221
+ }
222
+
223
+ const shippingRateTier = shippingRate.tiers.find((tier) => tier.isMatching);
224
+ if (shippingRateTier && shippingRateTier.type !== "CartValue") {
225
+ throw new Error("Non-CartValue shipping rate tier is not supported");
226
+ }
227
+
228
+ let shippingPrice = shippingRateTier
229
+ ? createCentPrecisionMoney(shippingRateTier.price)
230
+ : shippingRate.price;
231
+
232
+ // Handle freeAbove: if total is above the freeAbove threshold, shipping is free
233
+ if (
234
+ shippingRate.freeAbove &&
235
+ shippingRate.freeAbove.currencyCode === resource.totalPrice.currencyCode &&
236
+ resource.totalPrice.centAmount >= shippingRate.freeAbove.centAmount
237
+ ) {
238
+ shippingPrice = {
239
+ ...shippingPrice,
240
+ centAmount: 0,
241
+ };
242
+ }
243
+
244
+ // Calculate tax amounts
245
+ const totalGross: CentPrecisionMoney = taxRate.includedInPrice
246
+ ? shippingPrice
247
+ : {
248
+ ...shippingPrice,
249
+ centAmount: roundDecimal(
250
+ new Decimal(shippingPrice.centAmount).mul(1 + taxRate.amount),
251
+ resource.taxRoundingMode || "HalfEven",
252
+ ).toNumber(),
253
+ };
254
+
255
+ const totalNet: CentPrecisionMoney = taxRate.includedInPrice
256
+ ? {
257
+ ...shippingPrice,
258
+ centAmount: roundDecimal(
259
+ new Decimal(shippingPrice.centAmount).div(1 + taxRate.amount),
260
+ resource.taxRoundingMode || "HalfEven",
261
+ ).toNumber(),
262
+ }
263
+ : shippingPrice;
264
+
265
+ const taxPortions: TaxPortion[] = [
266
+ {
267
+ name: taxRate.name,
268
+ rate: taxRate.amount,
269
+ amount: {
270
+ ...shippingPrice,
271
+ centAmount: totalGross.centAmount - totalNet.centAmount,
272
+ },
273
+ },
274
+ ];
275
+
276
+ const totalTax: CentPrecisionMoney = {
277
+ ...shippingPrice,
278
+ centAmount: taxPortions.reduce(
279
+ (acc, portion) => acc + portion.amount.centAmount,
280
+ 0,
281
+ ),
282
+ };
283
+
284
+ const taxedPrice: TaxedItemPrice = {
285
+ totalNet,
286
+ totalGross,
287
+ taxPortions,
288
+ totalTax,
289
+ };
290
+
291
+ return {
292
+ shippingMethod: {
293
+ typeId: "shipping-method" as const,
294
+ id: method.id,
295
+ },
296
+ shippingMethodName: method.name,
297
+ price: shippingPrice,
298
+ shippingRate,
299
+ taxedPrice,
300
+ taxRate,
301
+ taxCategory: method.taxCategory,
302
+ shippingMethodState: "MatchesCart",
303
+ };
304
+ };