import type { Address, AddressDraft, BaseResource, Cart, CartAddCustomLineItemAction, CartAddItemShippingAddressAction, CartAddLineItemAction, CartAddPaymentAction, CartChangeCustomLineItemMoneyAction, CartChangeCustomLineItemQuantityAction, CartChangeLineItemQuantityAction, CartChangeTaxRoundingModeAction, CartFreezeCartAction, CartRemoveCustomLineItemAction, CartRemoveDiscountCodeAction, CartRemoveLineItemAction, CartRemovePaymentAction, CartRemoveShippingMethodAction, CartSetAnonymousIdAction, CartSetBillingAddressAction, CartSetBillingAddressCustomTypeAction, CartSetCartTotalTaxAction, CartSetCountryAction, CartSetCustomerEmailAction, CartSetCustomerIdAction, CartSetCustomFieldAction, CartSetCustomLineItemTaxAmountAction, CartSetCustomLineItemTaxRateAction, CartSetCustomShippingMethodAction, CartSetCustomTypeAction, CartSetDirectDiscountsAction, CartSetLineItemCustomFieldAction, CartSetLineItemCustomTypeAction, CartSetLineItemPriceAction, CartSetLineItemShippingDetailsAction, CartSetLineItemTaxAmountAction, CartSetLineItemTaxRateAction, CartSetLocaleAction, CartSetPurchaseOrderNumberAction, CartSetShippingAddressAction, CartSetShippingAddressCustomFieldAction, CartSetShippingAddressCustomTypeAction, CartSetShippingMethodAction, CartSetShippingMethodTaxAmountAction, CartSetShippingMethodTaxRateAction, CartUnfreezeCartAction, CartUpdateAction, CustomFields, GeneralError, InvalidOperationError, ItemShippingDetails, LineItem, Product, ProductVariant, Project, ReferencedResourceNotFoundError, TaxRate, UpdateAction, } from "@commercetools/platform-sdk"; import type { CartAddDiscountCodeAction, CustomLineItem, DirectDiscount, } from "@commercetools/platform-sdk/dist/declarations/src/generated/models/cart"; import type { ShippingMethodResourceIdentifier } from "@commercetools/platform-sdk/dist/declarations/src/generated/models/shipping-method"; import { v4 as uuidv4 } from "uuid"; import { CommercetoolsError } from "#src/exceptions.ts"; import { buildTaxedPriceFromExternalAmount, calculateTaxedPriceFromRate, calculateTaxTotals, taxRateFromExternalDraft, } from "#src/lib/tax.ts"; import type { Writable } from "#src/types.ts"; import type { UpdateHandlerInterface } from "../abstract.ts"; import { AbstractUpdateHandler, type RepositoryContext } from "../abstract.ts"; import { calculateMoneyTotalCentAmount, createAddress, createCentPrecisionMoney, createCustomFields, createTypedMoney, } from "../helpers.ts"; import { calculateCartTotalPrice, calculateLineItemTotalPrice, computeItemTaxedPrice, createCustomLineItemFromDraft, createDiscountCodeInfoFromCode, selectPrice, } from "./helpers.ts"; import type { CartRepository } from "./index.ts"; /** * Freezing a cart locks in its prices, so the actions that would change what * the cart costs are rejected while it is frozen. * See https://docs.commercetools.com/api/projects/carts#freeze-cart */ const PRICE_CHANGING_ACTIONS = new Set([ "addCustomLineItem", "addDiscountCode", "addLineItem", "changeCustomLineItemMoney", "changeCustomLineItemQuantity", "changeLineItemQuantity", "changeTaxRoundingMode", "recalculate", "removeCustomLineItem", "removeDiscountCode", "removeLineItem", "removeShippingMethod", "setCartTotalTax", "setCountry", "setCustomLineItemTaxAmount", "setCustomLineItemTaxRate", "setCustomShippingMethod", "setDirectDiscounts", "setLineItemPrice", "setLineItemTaxAmount", "setLineItemTaxRate", "setShippingMethod", "setShippingMethodTaxAmount", "setShippingMethodTaxRate", ]); export class CartUpdateHandler extends AbstractUpdateHandler implements Partial> { private repository: CartRepository; constructor(storage: any, repository: CartRepository) { super(storage); this.repository = repository; } async apply( context: RepositoryContext, resource: R, version: number, actions: UpdateAction[], ): Promise { const updated = await super.apply(context, resource, version, actions); if (updated.version !== resource.version) { const cart = updated as unknown as Writable; if (cart.taxMode === "ExternalAmount") { // Cart total tax is set explicitly via setCartTotalTax — never // aggregate from line items. Shipping subtotal is just a mirror of // shippingInfo.taxedPrice and should still be kept in sync. cart.taxedShippingPrice = cart.shippingInfo?.taxedPrice; } else { const { taxedPrice, taxedShippingPrice } = calculateTaxTotals(cart); cart.taxedPrice = taxedPrice; cart.taxedShippingPrice = taxedShippingPrice; } } return updated; } protected beforeAction( resource: BaseResource | Project, action: UpdateAction, ) { const cart = resource as unknown as Cart; if ( cart.cartState === "Frozen" && PRICE_CHANGING_ACTIONS.has(action.action) ) { throw new CommercetoolsError({ code: "InvalidOperation", message: `The cart with ID '${cart.id}' is frozen and cannot be modified by the action '${action.action}'.`, }); } } freezeCart( _context: RepositoryContext, resource: Writable, _action: CartFreezeCartAction, ) { if (resource.cartState !== "Active") { throw new CommercetoolsError({ code: "InvalidOperation", message: `The cart with ID '${resource.id}' cannot be frozen because it is in state '${resource.cartState}'.`, }); } resource.cartState = "Frozen"; } unfreezeCart( _context: RepositoryContext, resource: Writable, _action: CartUnfreezeCartAction, ) { if (resource.cartState !== "Frozen") { throw new CommercetoolsError({ code: "InvalidOperation", message: `The cart with ID '${resource.id}' cannot be unfrozen because it is in state '${resource.cartState}'.`, }); } resource.cartState = "Active"; } addItemShippingAddress( context: RepositoryContext, resource: Writable, { action, address }: CartAddItemShippingAddressAction, ) { const newAddress = createAddress( address, context.projectKey, this._storage, ); if (newAddress) { resource.itemShippingAddresses.push(newAddress); } } async addLineItem( context: RepositoryContext, resource: Writable, { productId, variantId, sku, custom, quantity = 1, addedAt, key, externalTaxRate, }: CartAddLineItemAction, ) { let product: Product | null = null; if (productId && variantId) { // Fetch product and variant by ID product = await this._storage.get( context.projectKey, "product", productId, {}, ); } else if (sku) { // Fetch product and variant by SKU const items = await this._storage.query(context.projectKey, "product", { where: [ `masterData(current(masterVariant(sku="${sku}"))) or masterData(current(variants(sku="${sku}")))`, ], }); if (items.count === 1) { product = items.results[0]; } } if (!product) { // Check if product is found throw new CommercetoolsError({ code: "General", message: sku ? `A product containing a variant with SKU '${sku}' not found.` : `A product with ID '${productId}' not found.`, }); } // Find matching variant const variant: ProductVariant | undefined = [ product.masterData.current.masterVariant, ...product.masterData.current.variants, ].find((x) => { if (sku) return x.sku === sku; if (variantId) return x.id === variantId; return false; }); if (!variant) { // Check if variant is found throw new CommercetoolsError({ code: "General", message: sku ? `A variant with SKU '${sku}' for product '${product.id}' not found.` : `A variant with ID '${variantId}' for product '${product.id}' not found.`, }); } const alreadyAdded = resource.lineItems.some( (x) => x.productId === product?.id && x.variant.id === variant?.id, ); if (alreadyAdded) { // increase quantity and update total price resource.lineItems.forEach((x) => { if (x.productId === product?.id && x.variant.id === variant?.id) { x.quantity += quantity; x.totalPrice.centAmount = calculateLineItemTotalPrice(x); x.taxedPrice = computeItemTaxedPrice(x, resource.taxRoundingMode); } }); } else { // add line item if (!variant.prices?.length) { throw new CommercetoolsError({ code: "General", message: `A product with ID '${productId}' doesn't have any prices.`, }); } const currency = resource.totalPrice.currencyCode; const price = selectPrice({ prices: variant.prices, currency, country: resource.country, }); if (!price) { throw new CommercetoolsError({ code: "InvalidOperation", message: `No valid price found for ${productId} for country ${resource.country} and currency ${currency}`, }); } const totalPrice = createCentPrecisionMoney({ currencyCode: price.value.currencyCode, centAmount: calculateMoneyTotalCentAmount(price.value, quantity), }); const taxRate = resource.taxMode === "External" && externalTaxRate ? taxRateFromExternalDraft(externalTaxRate) : undefined; const taxedPrice = taxRate ? calculateTaxedPriceFromRate( totalPrice.centAmount, totalPrice.currencyCode, taxRate, resource.taxRoundingMode, ) : undefined; resource.lineItems.push({ id: uuidv4(), key, addedAt: addedAt ? addedAt : new Date().toISOString(), productId: product.id, productKey: product.key, productSlug: product.masterData.current.slug, productType: product.productType, name: product.masterData.current.name, variant, price: price, taxRate, taxedPrice, taxedPricePortions: [], perMethodTaxRate: [], totalPrice, quantity, discountedPricePerQuantity: [], lineItemMode: "Standard", priceMode: "Platform", state: [], custom: await createCustomFields( custom, context.projectKey, this._storage, ), }); } // Update cart total price resource.totalPrice.centAmount = calculateCartTotalPrice(resource); } async addPayment( context: RepositoryContext, resource: Writable, { payment }: CartAddPaymentAction, ) { const resolvedPayment = await this._storage.getByResourceIdentifier( context.projectKey, payment, ); if (!resolvedPayment) { throw new CommercetoolsError({ code: "ReferencedResourceNotFound", message: `Payment ${payment.id} not found`, typeId: "payment", }); } if (!resource.paymentInfo) { resource.paymentInfo = { payments: [], }; } resource.paymentInfo.payments.push({ typeId: "payment", id: resolvedPayment.id, }); } async removePayment( context: RepositoryContext, resource: Writable, { payment }: CartRemovePaymentAction, ) { const resolvedPayment = await this._storage.getByResourceIdentifier( context.projectKey, payment, ); if (!resolvedPayment) { throw new CommercetoolsError({ code: "ReferencedResourceNotFound", message: `Payment ${payment.id} not found`, typeId: "payment", }); } if (!resource.paymentInfo) { return; } resource.paymentInfo.payments = resource.paymentInfo.payments.filter( (reference) => reference.id !== resolvedPayment.id, ); if (resource.paymentInfo.payments.length === 0) { resource.paymentInfo = undefined; } } changeLineItemQuantity( context: RepositoryContext, resource: Writable, { lineItemId, lineItemKey, quantity }: CartChangeLineItemQuantityAction, ) { let lineItem: Writable | undefined; if (lineItemId) { lineItem = resource.lineItems.find((x) => x.id === lineItemId); if (!lineItem) { throw new CommercetoolsError({ code: "General", message: `A line item with ID '${lineItemId}' not found.`, }); } } else if (lineItemKey) { lineItem = resource.lineItems.find((x) => x.id === lineItemId); if (!lineItem) { throw new CommercetoolsError({ code: "General", message: `A line item with Key '${lineItemKey}' not found.`, }); } } else { throw new CommercetoolsError({ code: "General", message: "Either lineItemid or lineItemKey needs to be provided.", }); } if (quantity === 0) { // delete line item resource.lineItems = resource.lineItems.filter( (x) => x.id !== lineItemId, ); } else { resource.lineItems.forEach((x) => { if (x.id === lineItemId && quantity) { x.quantity = quantity; x.totalPrice.centAmount = calculateLineItemTotalPrice(x); x.taxedPrice = computeItemTaxedPrice(x, resource.taxRoundingMode); } }); } // Update cart total price resource.totalPrice.centAmount = calculateCartTotalPrice(resource); } changeTaxRoundingMode( _context: RepositoryContext, resource: Writable, { taxRoundingMode }: CartChangeTaxRoundingModeAction, ) { resource.taxRoundingMode = taxRoundingMode; } recalculate() { // Dummy action when triggering a recalculation of the cart // // From commercetools documentation: // This update action does not set any Cart field in particular, // but it triggers several Cart updates to bring prices and discounts to the latest state. // Those can become stale over time when no Cart updates have been performed for a while // and prices on related Products have changed in the meanwhile. } async addDiscountCode( context: RepositoryContext, resource: Writable, { code }: CartAddDiscountCodeAction, ) { const info = await createDiscountCodeInfoFromCode( context.projectKey, this._storage, code, ); if ( !resource.discountCodes .map((dc) => dc.discountCode.id) .includes(info.discountCode.id) ) { resource.discountCodes.push(info); } } removeDiscountCode( context: RepositoryContext, resource: Writable, { discountCode }: CartRemoveDiscountCodeAction, ) { resource.discountCodes = resource.discountCodes.filter( (code) => code.discountCode.id !== discountCode.id, ); } removeLineItem( context: RepositoryContext, resource: Writable, { lineItemId, quantity }: CartRemoveLineItemAction, ) { const lineItem = resource.lineItems.find((x) => x.id === lineItemId); if (!lineItem) { // Check if product is found throw new CommercetoolsError({ code: "General", message: `A line item with ID '${lineItemId}' not found.`, }); } const shouldDelete = !quantity || quantity >= lineItem.quantity; if (shouldDelete) { // delete line item resource.lineItems = resource.lineItems.filter( (x) => x.id !== lineItemId, ); } else { // decrease quantity and update total price resource.lineItems.forEach((x) => { if (x.id === lineItemId && quantity) { x.quantity -= quantity; x.totalPrice.centAmount = calculateLineItemTotalPrice(x); x.taxedPrice = computeItemTaxedPrice(x, resource.taxRoundingMode); } }); } // Update cart total price resource.totalPrice.centAmount = calculateCartTotalPrice(resource); } async addCustomLineItem( context: RepositoryContext, resource: Writable, { money, name, slug, quantity = 1, taxCategory, externalTaxRate, custom, priceMode = "Standard", key, }: CartAddCustomLineItemAction, ) { const customLineItem = await createCustomLineItemFromDraft( context.projectKey, { money, name, slug, quantity, taxCategory, externalTaxRate, custom, priceMode, key, }, this._storage, resource.shippingAddress?.country ?? resource.country, resource.taxMode, resource.taxRoundingMode, ); resource.customLineItems.push(customLineItem); resource.totalPrice.centAmount = calculateCartTotalPrice(resource); } removeCustomLineItem( context: RepositoryContext, resource: Writable, { customLineItemId, customLineItemKey }: CartRemoveCustomLineItemAction, ) { let customLineItem; if (!customLineItemId && !customLineItemKey) { throw new CommercetoolsError({ code: "General", message: "Either customLineItemId or customLineItemKey needs to be provided.", }); } if (customLineItemId) { customLineItem = resource.customLineItems.find( (x) => x.id === customLineItemId, ); if (!customLineItem) { throw new CommercetoolsError({ code: "General", message: `A custom line item with ID '${customLineItemId}' not found.`, }); } resource.customLineItems = resource.customLineItems.filter( (x) => x.id !== customLineItemId, ); } if (customLineItemKey) { customLineItem = resource.customLineItems.find( (x) => x.key === customLineItemKey, ); if (!customLineItem) { throw new CommercetoolsError({ code: "General", message: `A custom line item with key '${customLineItemKey}' not found.`, }); } resource.customLineItems = resource.customLineItems.filter( (x) => x.key !== customLineItemKey, ); } resource.totalPrice.centAmount = calculateCartTotalPrice(resource); } changeCustomLineItemQuantity( context: RepositoryContext, resource: Writable, { customLineItemId, customLineItemKey, quantity, }: CartChangeCustomLineItemQuantityAction, ) { let customLineItem; if (!customLineItemId && !customLineItemKey) { throw new CommercetoolsError({ code: "General", message: "Either customLineItemId or customLineItemKey needs to be provided.", }); } const setQuantity = ( customLineItem: Writable | undefined, ) => { if (!customLineItem) { throw new CommercetoolsError({ code: "General", message: `A custom line item with ${customLineItemId ? `ID '${customLineItemId}'` : `key '${customLineItemKey}'`} not found.`, }); } customLineItem.quantity = quantity; customLineItem.totalPrice = createCentPrecisionMoney({ currencyCode: customLineItem.money.currencyCode, centAmount: calculateMoneyTotalCentAmount( customLineItem.money, quantity, ), }); customLineItem.taxedPrice = computeItemTaxedPrice( customLineItem, resource.taxRoundingMode, ); }; if (customLineItemId) { customLineItem = resource.customLineItems.find( (x) => x.id === customLineItemId, ); setQuantity(customLineItem); } if (customLineItemKey) { customLineItem = resource.customLineItems.find( (x) => x.key === customLineItemKey, ); setQuantity(customLineItem); } // Update cart total price resource.totalPrice.centAmount = calculateCartTotalPrice(resource); } changeCustomLineItemMoney( context: RepositoryContext, resource: Writable, { customLineItemId, customLineItemKey, money, }: CartChangeCustomLineItemMoneyAction, ) { let customLineItem; const setMoney = (customLineItem: Writable | undefined) => { if (!customLineItem) { throw new CommercetoolsError({ code: "General", message: `A custom line item with ${customLineItemId ? `ID '${customLineItemId}'` : `key '${customLineItemKey}'`} not found.`, }); } customLineItem.money = createTypedMoney(money); customLineItem.totalPrice = createCentPrecisionMoney({ currencyCode: money.currencyCode, centAmount: calculateMoneyTotalCentAmount( money, customLineItem.quantity, ), }); customLineItem.taxedPrice = computeItemTaxedPrice( customLineItem, resource.taxRoundingMode, ); }; if (customLineItemId) { customLineItem = resource.customLineItems.find( (x) => x.id === customLineItemId, ); setMoney(customLineItem); } if (customLineItemKey) { customLineItem = resource.customLineItems.find( (x) => x.key === customLineItemKey, ); setMoney(customLineItem); } // Update cart total price resource.totalPrice.centAmount = calculateCartTotalPrice(resource); } setAnonymousId( _context: RepositoryContext, resource: Writable, { anonymousId }: CartSetAnonymousIdAction, ) { resource.anonymousId = anonymousId; resource.customerId = undefined; } setBillingAddress( context: RepositoryContext, resource: Writable, { address }: CartSetBillingAddressAction, ) { resource.billingAddress = createAddress( address, context.projectKey, this._storage, ); } async setBillingAddressCustomType( context: RepositoryContext, resource: Writable, custom: CartSetBillingAddressCustomTypeAction, ) { if (!resource.billingAddress) { throw new CommercetoolsError({ code: "InvalidOperation", message: "Resource has no billing address", }); } if (!custom.type) { resource.billingAddress.custom = undefined; return; } const resolvedType = await this._storage.getByResourceIdentifier<"type">( context.projectKey, custom.type, ); if (!resolvedType) { throw new CommercetoolsError({ code: "ReferencedResourceNotFound", message: `Type ${custom.type} not found`, typeId: "type", id: custom.type?.id, key: custom.type?.key, }); } resource.billingAddress.custom = { type: { typeId: "type", id: resolvedType.id, }, fields: custom.fields || {}, }; } setCountry( context: RepositoryContext, resource: Writable, { country }: CartSetCountryAction, ) { resource.country = country; } setCustomerEmail( context: RepositoryContext, resource: Writable, { email }: CartSetCustomerEmailAction, ) { resource.customerEmail = email; } setCustomerId( _context: RepositoryContext, resource: Writable, { customerId }: CartSetCustomerIdAction, ) { resource.anonymousId = undefined; resource.customerId = customerId; } setCustomField( context: RepositoryContext, resource: Cart, { name, value }: CartSetCustomFieldAction, ) { this._setCustomFieldValues(resource, { name, value }); } async setCustomShippingMethod( context: RepositoryContext, resource: Writable, { shippingMethodName, shippingRate, taxCategory, externalTaxRate, }: CartSetCustomShippingMethodAction, ) { const isTaxExternal = resource.taxMode === "External"; if (externalTaxRate && !isTaxExternal) { throw new CommercetoolsError({ code: "InvalidOperation", message: "An external tax rate can only be set for a Cart with External tax mode.", }); } const tax = !isTaxExternal && taxCategory ? await this._storage.getByResourceIdentifier<"tax-category">( context.projectKey, taxCategory, ) : undefined; const taxRate = isTaxExternal && externalTaxRate ? taxRateFromExternalDraft(externalTaxRate) : undefined; const price = createCentPrecisionMoney(shippingRate.price); const taxedPrice = taxRate ? calculateTaxedPriceFromRate( price.centAmount, price.currencyCode, taxRate, resource.taxRoundingMode, ) : undefined; resource.shippingInfo = { shippingMethodName, price, shippingRate: { price, tiers: [], }, taxCategory: tax ? { typeId: "tax-category", id: tax?.id, } : undefined, taxRate, taxedPrice, shippingMethodState: "MatchesCart", }; } async setCustomType( context: RepositoryContext, resource: Writable, { type, fields }: CartSetCustomTypeAction, ) { await this._setCustomType(context, resource, { type, fields }); } setDirectDiscounts( context: RepositoryContext, resource: Writable, { discounts }: CartSetDirectDiscountsAction, ) { // Doesn't apply any discounts logic, just sets the directDiscounts field resource.directDiscounts = discounts.map( (discount) => ({ ...discount, id: uuidv4(), }) as DirectDiscount, ); } setLineItemCustomField( context: RepositoryContext, resource: Writable, { lineItemId, lineItemKey, name, value, action, }: CartSetLineItemCustomFieldAction, ) { const lineItem = resource.lineItems.find( (x) => (lineItemId && x.id === lineItemId) || (lineItemKey && x.key === lineItemKey), ); if (!lineItem) { // Check if line item is found throw new CommercetoolsError({ code: "General", message: lineItemKey ? `A line item with key '${lineItemKey}' not found.` : `A line item with ID '${lineItemId}' not found.`, }); } if (!lineItem.custom) { throw new CommercetoolsError({ code: "InvalidOperation", message: "Resource has no custom field", }); } lineItem.custom.fields[name] = value; } async setLineItemCustomType( context: RepositoryContext, resource: Writable, { lineItemId, lineItemKey, type, fields }: CartSetLineItemCustomTypeAction, ) { const lineItem = resource.lineItems.find( (x) => (lineItemId && x.id === lineItemId) || (lineItemKey && x.key === lineItemKey), ); if (!lineItem) { // Check if line item is found throw new CommercetoolsError({ code: "General", message: lineItemKey ? `A line item with key '${lineItemKey}' not found.` : `A line item with ID '${lineItemId}' not found.`, }); } if (!type) { lineItem.custom = undefined; } else { const resolvedType = await this._storage.getByResourceIdentifier( context.projectKey, type, ); if (!resolvedType) { throw new CommercetoolsError({ code: "ReferencedResourceNotFound", message: `Type ${type} not found`, typeId: "type", id: type?.id, key: type?.key, }); } lineItem.custom = { type: { typeId: "type", id: resolvedType.id, }, fields: fields || {}, }; } } setLineItemPrice( context: RepositoryContext, resource: Writable, { lineItemId, lineItemKey, externalPrice }: CartSetLineItemPriceAction, ) { const lineItem = resource.lineItems.find( (x) => (lineItemId && x.id === lineItemId) || (lineItemKey && x.key === lineItemKey), ); if (!lineItem) { throw new CommercetoolsError({ code: "General", message: lineItemKey ? `A line item with key '${lineItemKey}' not found.` : `A line item with ID '${lineItemId}' not found.`, }); } if (!externalPrice && lineItem.priceMode !== "ExternalPrice") { return; } if ( externalPrice && externalPrice.currencyCode !== resource.totalPrice.currencyCode ) { throw new CommercetoolsError({ code: "General", message: `Currency mismatch. Expected '${resource.totalPrice.currencyCode}' but got '${externalPrice.currencyCode}'.`, }); } if (externalPrice) { lineItem.priceMode = "ExternalPrice"; const priceValue = createTypedMoney(externalPrice); lineItem.price = lineItem.price ?? { id: uuidv4() }; lineItem.price.value = priceValue; } else { lineItem.priceMode = "Platform"; const price = selectPrice({ prices: lineItem.variant.prices, currency: resource.totalPrice.currencyCode, country: resource.country, }); if (!price) { throw new CommercetoolsError({ code: "InvalidOperation", message: `No valid price found for ${lineItem.productId} for country ${resource.country} and currency ${resource.totalPrice.currencyCode}`, }); } lineItem.price = price; } const lineItemTotal = calculateLineItemTotalPrice(lineItem); lineItem.totalPrice = createCentPrecisionMoney({ currencyCode: lineItem.price!.value.currencyCode, centAmount: lineItemTotal, }); lineItem.taxedPrice = computeItemTaxedPrice( lineItem, resource.taxRoundingMode, ); resource.totalPrice.centAmount = calculateCartTotalPrice(resource); } setLineItemShippingDetails( context: RepositoryContext, resource: Writable, { action, shippingDetails, lineItemId, lineItemKey, }: CartSetLineItemShippingDetailsAction, ) { const lineItem = resource.lineItems.find( (x) => (lineItemId && x.id === lineItemId) || (lineItemKey && x.key === lineItemKey), ); if (!lineItem) { // Check if line item is found throw new CommercetoolsError({ code: "General", message: lineItemKey ? `A line item with key '${lineItemKey}' not found.` : `A line item with ID '${lineItemId}' not found.`, }); } lineItem.shippingDetails = { ...shippingDetails, valid: true, } as ItemShippingDetails; } setPurchaseOrderNumber( _context: RepositoryContext, resource: Writable, { purchaseOrderNumber }: CartSetPurchaseOrderNumberAction, ) { resource.purchaseOrderNumber = purchaseOrderNumber; } setLocale( context: RepositoryContext, resource: Writable, { locale }: CartSetLocaleAction, ) { resource.locale = locale; } async setShippingAddress( context: RepositoryContext, resource: Writable, { address }: CartSetShippingAddressAction, ) { if (!address) { resource.shippingAddress = undefined; return; } let custom: CustomFields | undefined; if ((address as Address & AddressDraft).custom) { custom = await createCustomFields( (address as Address & AddressDraft).custom, context.projectKey, this._storage, ); } resource.shippingAddress = { ...address, custom: custom, }; } async setShippingAddressCustomType( context: RepositoryContext, resource: Writable, custom: CartSetShippingAddressCustomTypeAction, ) { if (!resource.shippingAddress) { throw new CommercetoolsError({ code: "InvalidOperation", message: "Resource has no shipping address", }); } if (!custom.type) { resource.shippingAddress.custom = undefined; return; } const resolvedType = await this._storage.getByResourceIdentifier<"type">( context.projectKey, custom.type, ); if (!resolvedType) { throw new CommercetoolsError({ code: "ReferencedResourceNotFound", message: `Type ${custom.type} not found`, typeId: "type", id: custom.type?.id, key: custom.type?.key, }); } resource.shippingAddress.custom = { type: { typeId: "type", id: resolvedType.id, }, fields: custom.fields || {}, }; } async setShippingMethod( context: RepositoryContext, resource: Writable, { shippingMethod, externalTaxRate }: CartSetShippingMethodAction, ) { if (shippingMethod) { resource.shippingInfo = await this.repository.createShippingInfo( context, resource, shippingMethod, externalTaxRate ?? undefined, ); } else { resource.shippingInfo = undefined; } } setLineItemTaxRate( _context: RepositoryContext, resource: Writable, { lineItemId, lineItemKey, externalTaxRate }: CartSetLineItemTaxRateAction, ) { if (resource.taxMode !== "External") { throw new CommercetoolsError({ code: "InvalidOperation", message: "A line item tax rate can only be set for a Cart with External tax mode.", }); } const lineItem = resource.lineItems.find( (x) => (lineItemId && x.id === lineItemId) || (lineItemKey && x.key === lineItemKey), ); if (!lineItem) { throw new CommercetoolsError({ code: "General", message: lineItemKey ? `A line item with key '${lineItemKey}' not found.` : `A line item with ID '${lineItemId}' not found.`, }); } const writable = lineItem as Writable; writable.taxRate = externalTaxRate ? taxRateFromExternalDraft(externalTaxRate) : undefined; writable.taxedPrice = computeItemTaxedPrice( writable, resource.taxRoundingMode, ); } setCustomLineItemTaxRate( _context: RepositoryContext, resource: Writable, { customLineItemId, customLineItemKey, externalTaxRate, }: CartSetCustomLineItemTaxRateAction, ) { if (resource.taxMode !== "External") { throw new CommercetoolsError({ code: "InvalidOperation", message: "A custom line item tax rate can only be set for a Cart with External tax mode.", }); } const customLineItem = resource.customLineItems.find( (x) => (customLineItemId && x.id === customLineItemId) || (customLineItemKey && x.key === customLineItemKey), ); if (!customLineItem) { throw new CommercetoolsError({ code: "General", message: customLineItemKey ? `A custom line item with key '${customLineItemKey}' not found.` : `A custom line item with ID '${customLineItemId}' not found.`, }); } const writable = customLineItem as Writable; writable.taxRate = externalTaxRate ? taxRateFromExternalDraft(externalTaxRate) : undefined; writable.taxedPrice = computeItemTaxedPrice( writable, resource.taxRoundingMode, ); } setShippingMethodTaxRate( _context: RepositoryContext, resource: Writable, { externalTaxRate }: CartSetShippingMethodTaxRateAction, ) { if (resource.taxMode !== "External") { throw new CommercetoolsError({ code: "InvalidOperation", message: "A shipping method tax rate can only be set for a Cart with External tax mode.", }); } if (!resource.shippingInfo) { throw new CommercetoolsError({ code: "InvalidOperation", message: "Cart has no shipping method.", }); } const shippingInfo = resource.shippingInfo as Writable< typeof resource.shippingInfo >; const taxRate: TaxRate | undefined = externalTaxRate ? taxRateFromExternalDraft(externalTaxRate) : undefined; shippingInfo.taxRate = taxRate; shippingInfo.taxedPrice = taxRate ? calculateTaxedPriceFromRate( shippingInfo.price.centAmount, shippingInfo.price.currencyCode, taxRate, resource.taxRoundingMode, ) : undefined; } setLineItemTaxAmount( _context: RepositoryContext, resource: Writable, { lineItemId, lineItemKey, externalTaxAmount, }: CartSetLineItemTaxAmountAction, ) { if (resource.taxMode !== "ExternalAmount") { throw new CommercetoolsError({ code: "InvalidOperation", message: "A line item tax amount can only be set for a Cart with ExternalAmount tax mode.", }); } const lineItem = resource.lineItems.find( (x) => (lineItemId && x.id === lineItemId) || (lineItemKey && x.key === lineItemKey), ); if (!lineItem) { throw new CommercetoolsError({ code: "General", message: lineItemKey ? `A line item with key '${lineItemKey}' not found.` : `A line item with ID '${lineItemId}' not found.`, }); } const writable = lineItem as Writable; if (externalTaxAmount) { writable.taxRate = taxRateFromExternalDraft(externalTaxAmount.taxRate); writable.taxedPrice = buildTaxedPriceFromExternalAmount( externalTaxAmount, resource.taxRoundingMode, ); } else { writable.taxRate = undefined; writable.taxedPrice = undefined; } } setCustomLineItemTaxAmount( _context: RepositoryContext, resource: Writable, { customLineItemId, customLineItemKey, externalTaxAmount, }: CartSetCustomLineItemTaxAmountAction, ) { if (resource.taxMode !== "ExternalAmount") { throw new CommercetoolsError({ code: "InvalidOperation", message: "A custom line item tax amount can only be set for a Cart with ExternalAmount tax mode.", }); } const customLineItem = resource.customLineItems.find( (x) => (customLineItemId && x.id === customLineItemId) || (customLineItemKey && x.key === customLineItemKey), ); if (!customLineItem) { throw new CommercetoolsError({ code: "General", message: customLineItemKey ? `A custom line item with key '${customLineItemKey}' not found.` : `A custom line item with ID '${customLineItemId}' not found.`, }); } const writable = customLineItem as Writable; if (externalTaxAmount) { writable.taxRate = taxRateFromExternalDraft(externalTaxAmount.taxRate); writable.taxedPrice = buildTaxedPriceFromExternalAmount( externalTaxAmount, resource.taxRoundingMode, ); } else { writable.taxRate = undefined; writable.taxedPrice = undefined; } } setShippingMethodTaxAmount( _context: RepositoryContext, resource: Writable, { externalTaxAmount }: CartSetShippingMethodTaxAmountAction, ) { if (resource.taxMode !== "ExternalAmount") { throw new CommercetoolsError({ code: "InvalidOperation", message: "A shipping method tax amount can only be set for a Cart with ExternalAmount tax mode.", }); } if (!resource.shippingInfo) { throw new CommercetoolsError({ code: "InvalidOperation", message: "Cart has no shipping method.", }); } const shippingInfo = resource.shippingInfo as Writable< typeof resource.shippingInfo >; if (externalTaxAmount) { shippingInfo.taxRate = taxRateFromExternalDraft( externalTaxAmount.taxRate, ); shippingInfo.taxedPrice = buildTaxedPriceFromExternalAmount( externalTaxAmount, resource.taxRoundingMode, ); } else { shippingInfo.taxRate = undefined; shippingInfo.taxedPrice = undefined; } } setCartTotalTax( _context: RepositoryContext, resource: Writable, { externalTotalGross, externalTaxPortions }: CartSetCartTotalTaxAction, ) { if (resource.taxMode !== "ExternalAmount") { throw new CommercetoolsError({ code: "InvalidOperation", message: "A cart total tax can only be set for a Cart with ExternalAmount tax mode.", }); } const totalGross = createCentPrecisionMoney(externalTotalGross); const currencyCode = totalGross.currencyCode; const portions = externalTaxPortions?.map((portion) => ({ name: portion.name, rate: portion.rate, amount: createCentPrecisionMoney(portion.amount), })) ?? []; const totalTaxCentAmount = portions.reduce( (acc, portion) => acc + portion.amount.centAmount, 0, ); const totalNet = createCentPrecisionMoney({ currencyCode, centAmount: totalGross.centAmount - totalTaxCentAmount, }); resource.taxedPrice = { totalNet, totalGross, totalTax: totalTaxCentAmount > 0 ? createCentPrecisionMoney({ currencyCode, centAmount: totalTaxCentAmount, }) : undefined, taxPortions: portions, }; } setShippingAddressCustomField( context: RepositoryContext, resource: Writable, { name, value }: CartSetShippingAddressCustomFieldAction, ) { if (!resource.shippingAddress) { throw new CommercetoolsError({ code: "InvalidOperation", message: "Resource has no shipping address", }); } if (!resource.shippingAddress.custom) { throw new CommercetoolsError({ code: "InvalidOperation", message: "Resource has no custom field", }); } resource.shippingAddress.custom.fields[name] = value; } async removeShippingMethod( context: RepositoryContext, resource: Writable, { shippingKey }: CartRemoveShippingMethodAction, ) { if (!resource.shippingInfo) { return; } const shippingMethod = await this._storage.getByResourceIdentifier<"shipping-method">( context.projectKey, { typeId: "shipping-method", key: shippingKey, } as ShippingMethodResourceIdentifier, ); if (resource.shippingInfo?.shippingMethod?.id !== shippingMethod.id) { throw new CommercetoolsError({ code: "ReferencedResourceNotFound", message: "Shipping method with key not found", typeId: "shipping-method", key: shippingKey, }); } resource.shippingInfo = undefined; } }