feat(medusa,core-flows,types): add cart <> tax integration workflows + steps (#6580)

what:

- adds tax lines to cart when item operations take place

RESOLVES CORE-1821
RESOLVES CORE-1822
RESOLVES CORE-1823
RESOLVES CORE-1824
This commit is contained in:
Riqwan Thamir
2024-03-07 16:17:43 +00:00
committed by GitHub
parent 8c57e61cb8
commit e4acde1aa2
26 changed files with 1096 additions and 180 deletions
@@ -0,0 +1,115 @@
import {
CartLineItemDTO,
CartShippingMethodDTO,
CartWorkflowDTO,
ITaxModuleService,
ItemTaxLineDTO,
ShippingTaxLineDTO,
TaxCalculationContext,
TaxableItemDTO,
TaxableShippingDTO,
} from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { ModuleRegistrationName } from "../../../../../modules-sdk/dist"
interface StepInput {
cart: CartWorkflowDTO
items: CartLineItemDTO[]
shipping_methods: CartShippingMethodDTO[]
}
function normalizeTaxModuleContext(
cart: CartWorkflowDTO
): TaxCalculationContext | null {
const address = cart.shipping_address
if (!address || !address.country_code) {
return null
}
let customer = cart.customer
? {
id: cart.customer.id,
email: cart.customer.email,
customer_groups: cart.customer.groups?.map((g) => g.id) || [],
}
: undefined
return {
address: {
country_code: address.country_code,
province_code: address.province,
address_1: address.address_1,
address_2: address.address_2,
city: address.city,
postal_code: address.postal_code,
},
customer,
// TODO: Should probably come in from order module, defaulting to false
is_return: false,
}
}
function normalizeLineItemsForTax(
cart: CartWorkflowDTO,
items: CartLineItemDTO[]
): TaxableItemDTO[] {
return items.map((item) => ({
id: item.id,
product_id: item.product_id!,
product_name: item.variant_title,
product_sku: item.variant_sku,
product_type: item.product_type,
product_type_id: item.product_type,
quantity: item.quantity,
unit_price: item.unit_price,
currency_code: cart.currency_code,
}))
}
function normalizeLineItemsForShipping(
cart: CartWorkflowDTO,
shippingMethods: CartShippingMethodDTO[]
): TaxableShippingDTO[] {
return shippingMethods.map((shippingMethod) => ({
id: shippingMethod.id,
shipping_option_id: shippingMethod.shipping_option_id!,
unit_price: shippingMethod.amount,
currency_code: cart.currency_code,
}))
}
export const getItemTaxLinesStepId = "get-item-tax-lines"
export const getItemTaxLinesStep = createStep(
getItemTaxLinesStepId,
async (data: StepInput, { container }) => {
const { cart, items, shipping_methods: shippingMethods } = data
const taxService = container.resolve<ITaxModuleService>(
ModuleRegistrationName.TAX
)
const taxContext = normalizeTaxModuleContext(cart)
if (!taxContext) {
return new StepResponse({
lineItemTaxLines: [],
shippingMethodsTaxLines: [],
})
}
const lineItemTaxLines = (await taxService.getTaxLines(
normalizeLineItemsForTax(cart, items),
taxContext
)) as ItemTaxLineDTO[]
const shippingMethodsTaxLines = (await taxService.getTaxLines(
normalizeLineItemsForShipping(cart, shippingMethods),
taxContext
)) as ShippingTaxLineDTO[]
return new StepResponse({
lineItemTaxLines,
shippingMethodsTaxLines,
})
}
)
@@ -6,12 +6,15 @@ export * from "./find-one-or-any-region"
export * from "./find-or-create-customer"
export * from "./find-sales-channel"
export * from "./get-actions-to-compute-from-promotions"
export * from "./get-item-tax-lines"
export * from "./get-variant-price-sets"
export * from "./get-variants"
export * from "./prepare-adjustments-from-promotion-actions"
export * from "./remove-line-item-adjustments"
export * from "./remove-shipping-method-adjustments"
export * from "./retrieve-cart"
export * from "./retrieve-cart-with-links"
export * from "./set-tax-lines-for-items"
export * from "./update-cart-promotions"
export * from "./update-carts"
export * from "./validate-variants-existence"
@@ -0,0 +1,32 @@
import { LinkModuleUtils, Modules } from "@medusajs/modules-sdk"
import { CartWorkflowDTO } from "@medusajs/types"
import { isObject, remoteQueryObjectFromString } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
interface StepInput {
cart_or_cart_id: string | CartWorkflowDTO
fields: string[]
}
export const retrieveCartWithLinksStepId = "retrieve-cart-with-links"
export const retrieveCartWithLinksStep = createStep(
retrieveCartWithLinksStepId,
async (data: StepInput, { container }) => {
const { cart_or_cart_id: cartOrCartId, fields } = data
if (isObject(cartOrCartId)) {
return new StepResponse(cartOrCartId)
}
const id = cartOrCartId
const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY)
const query = remoteQueryObjectFromString({
entryPoint: Modules.CART,
fields,
})
const [cart] = await remoteQuery(query, { cart: { id } })
return new StepResponse(cart)
}
)
@@ -0,0 +1,128 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
CartWorkflowDTO,
CreateLineItemTaxLineDTO,
CreateShippingMethodTaxLineDTO,
ICartModuleService,
ItemTaxLineDTO,
ShippingTaxLineDTO,
} from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
interface StepInput {
cart: CartWorkflowDTO
item_tax_lines: ItemTaxLineDTO[]
shipping_tax_lines: ShippingTaxLineDTO[]
}
export const setTaxLinesForItemsStepId = "set-tax-lines-for-items"
export const setTaxLinesForItemsStep = createStep(
setTaxLinesForItemsStepId,
async (data: StepInput, { container }) => {
const { cart, item_tax_lines, shipping_tax_lines } = data
const cartService = container.resolve<ICartModuleService>(
ModuleRegistrationName.CART
)
const getShippingTaxLinesPromise =
await cartService.listShippingMethodTaxLines({
shipping_method_id: shipping_tax_lines.map((t) => t.shipping_line_id),
})
const getItemTaxLinesPromise = await cartService.listLineItemTaxLines({
item_id: item_tax_lines.map((t) => t.line_item_id),
})
const itemsTaxLinesData = normalizeItemTaxLinesForCart(item_tax_lines)
const setItemTaxLinesPromise = itemsTaxLinesData.length
? cartService.setLineItemTaxLines(cart.id, itemsTaxLinesData)
: 0
const shippingTaxLinesData =
normalizeShippingTaxLinesForCart(shipping_tax_lines)
const setShippingTaxLinesPromise = shippingTaxLinesData.length
? await cartService.setShippingMethodTaxLines(
cart.id,
shippingTaxLinesData
)
: 0
const [existingShippingMethodTaxLines, existingLineItemTaxLines] =
await Promise.all([
getShippingTaxLinesPromise,
getItemTaxLinesPromise,
setItemTaxLinesPromise,
setShippingTaxLinesPromise,
])
return new StepResponse(null, {
cart,
existingLineItemTaxLines,
existingShippingMethodTaxLines,
})
},
async (revertData, { container }) => {
if (!revertData) {
return
}
const { cart, existingLineItemTaxLines, existingShippingMethodTaxLines } =
revertData
const cartService = container.resolve<ICartModuleService>(
ModuleRegistrationName.CART
)
if (existingLineItemTaxLines) {
await cartService.setLineItemTaxLines(
cart.id,
existingLineItemTaxLines.map((taxLine) => ({
description: taxLine.description,
tax_rate_id: taxLine.tax_rate_id,
code: taxLine.code,
rate: taxLine.rate,
provider_id: taxLine.provider_id,
item_id: taxLine.item_id,
}))
)
}
await cartService.setShippingMethodTaxLines(
cart.id,
existingShippingMethodTaxLines.map((taxLine) => ({
description: taxLine.description,
tax_rate_id: taxLine.tax_rate_id,
code: taxLine.code,
rate: taxLine.rate,
provider_id: taxLine.provider_id,
shipping_method_id: taxLine.shipping_method_id,
}))
)
}
)
function normalizeItemTaxLinesForCart(
taxLines: ItemTaxLineDTO[]
): CreateLineItemTaxLineDTO[] {
return taxLines.map((taxLine) => ({
description: taxLine.name,
tax_rate_id: taxLine.rate_id,
code: taxLine.code!,
rate: taxLine.rate!,
provider_id: taxLine.provider_id,
item_id: taxLine.line_item_id,
}))
}
function normalizeShippingTaxLinesForCart(
taxLines: ShippingTaxLineDTO[]
): CreateShippingMethodTaxLineDTO[] {
return taxLines.map((taxLine) => ({
description: taxLine.name,
tax_rate_id: taxLine.rate_id,
code: taxLine.code!,
rate: taxLine.rate!,
provider_id: taxLine.provider_id,
shipping_method_id: taxLine.shipping_line_id,
}))
}
@@ -48,6 +48,6 @@ export const updateCartsStep = createStep(
})
}
await cartModule.update(dataToUpdate)
return await cartModule.update(dataToUpdate)
}
)
@@ -0,0 +1,24 @@
import {
CartLineItemDTO,
CartShippingMethodDTO,
CartWorkflowDTO,
} from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { updateTaxLinesWorkflow } from "../workflows"
interface StepInput {
cart_or_cart_id: CartWorkflowDTO | string
items?: CartLineItemDTO[]
shipping_methods?: CartShippingMethodDTO[]
}
export const updateTaxLinesStepId = "update-tax-lines-step"
export const updateTaxLinesStep = createStep(
updateTaxLinesStepId,
async (input: StepInput, { container }) => {
// TODO: manually trigger rollback on workflow when step fails
await updateTaxLinesWorkflow(container).run({ input })
return new StepResponse(null)
}
)
@@ -16,12 +16,8 @@ export const validateVariantsExistStep = createStep(
)
const variants = await productModuleService.listVariants(
{
id: data.variantIds,
},
{
select: ["id"],
}
{ id: data.variantIds },
{ select: ["id"] }
)
const variantIdToData = new Set(variants.map((v) => v.id))
@@ -10,6 +10,7 @@ interface Input {
export function prepareLineItemData(data: Input) {
const { variant, unitPrice, quantity, metadata, cartId } = data
const lineItem: any = {
quantity,
title: variant.title,
@@ -14,6 +14,7 @@ import {
validateVariantsExistStep,
} from "../steps"
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
import { updateTaxLinesStep } from "../steps/update-tax-lines"
import { prepareLineItemData } from "../utils/prepare-line-item-data"
// TODO: The AddToCartWorkflow are missing the following steps:
@@ -25,12 +26,12 @@ export const addToCartWorkflowId = "add-to-cart"
export const addToCartWorkflow = createWorkflow(
addToCartWorkflowId,
(input: WorkflowData<AddToCartWorkflowInputDTO>) => {
const variantIds = transform({ input }, (data) => {
return (data.input.items ?? []).map((i) => i.variant_id)
const variantIds = validateVariantsExistStep({
variantIds: transform({ input }, (data) => {
return (data.input.items ?? []).map((i) => i.variant_id)
}),
})
validateVariantsExistStep({ variantIds })
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
const pricingContext = transform({ cart: input.cart }, (data) => {
return {
@@ -45,31 +46,55 @@ export const addToCartWorkflow = createWorkflow(
context: pricingContext,
})
const variants = getVariantsStep({
filter: { id: variantIds },
})
const lineItems = transform(
{ priceSets, input, variants, cart: input.cart },
(data) => {
const items = (data.input.items ?? []).map((item) => {
const variant = data.variants.find((v) => v.id === item.variant_id)!
return prepareLineItemData({
variant: variant,
unitPrice: data.priceSets[item.variant_id].calculated_amount,
quantity: item.quantity,
metadata: item?.metadata ?? {},
cartId: data.cart.id,
}) as CreateLineItemForCartDTO
})
return items
}
const variants = getVariantsStep(
transform({ variantIds }, (data) => {
return {
filter: { id: data.variantIds },
config: {
select: [
"id",
"title",
"sku",
"barcode",
"product.id",
"product.title",
"product.description",
"product.subtitle",
"product.thumbnail",
"product.type",
"product.collection",
"product.handle",
],
relations: ["product"],
},
}
})
)
const lineItems = transform({ priceSets, input, variants }, (data) => {
const items = (data.input.items ?? []).map((item) => {
const variant = data.variants.find((v) => v.id === item.variant_id)!
return prepareLineItemData({
variant: variant,
unitPrice: data.priceSets[item.variant_id].calculated_amount,
quantity: item.quantity,
metadata: item?.metadata ?? {},
cartId: data.input.cart.id,
}) as CreateLineItemForCartDTO
})
return items
})
const items = addToCartStep({ items: lineItems })
updateTaxLinesStep({
cart_or_cart_id: input.cart,
items,
// TODO: add shipping methods here when its ready
})
refreshCartPromotionsStep({ id: input.cart.id })
return items
@@ -14,6 +14,7 @@ import {
getVariantsStep,
validateVariantsExistStep,
} from "../steps"
import { updateTaxLinesStep } from "../steps/update-tax-lines"
import { prepareLineItemData } from "../utils/prepare-line-item-data"
// TODO: The UpdateLineItemsWorkflow are missing the following steps:
@@ -83,26 +84,30 @@ export const createCartWorkflow = createWorkflow(
}
)
const variants = getVariantsStep({
filter: { id: variantIds },
config: {
select: [
"id",
"title",
"sku",
"barcode",
"product.id",
"product.title",
"product.description",
"product.subtitle",
"product.thumbnail",
"product.type",
"product.collection",
"product.handle",
],
relations: ["product"],
},
})
const variants = getVariantsStep(
transform({ variantIds }, (data) => {
return {
filter: { id: data.variantIds },
config: {
select: [
"id",
"title",
"sku",
"barcode",
"product.id",
"product.title",
"product.description",
"product.subtitle",
"product.thumbnail",
"product.type",
"product.collection",
"product.handle",
],
relations: ["product"],
},
}
})
)
const lineItems = transform({ priceSets, input, variants }, (data) => {
const items = (data.input.items ?? []).map((item) => {
@@ -127,9 +132,10 @@ export const createCartWorkflow = createWorkflow(
})
const carts = createCartsStep([cartToCreate])
const cart = transform({ carts }, (data) => data.carts?.[0])
updateTaxLinesStep({ cart_or_cart_id: cart.id })
return cart
}
)
@@ -5,3 +5,4 @@ export * from "./refresh-payment-collection"
export * from "./update-cart"
export * from "./update-cart-promotions"
export * from "./update-line-item-in-cart"
export * from "./update-tax-lines"
@@ -1,4 +1,4 @@
import { CartDTO, UpdateCartWorkflowInputDTO } from "@medusajs/types"
import { UpdateCartWorkflowInputDTO } from "@medusajs/types"
import { PromotionActions, isPresent } from "@medusajs/utils"
import {
WorkflowData,
@@ -10,16 +10,16 @@ import {
findOneOrAnyRegionStep,
findOrCreateCustomerStep,
findSalesChannelStep,
retrieveCartStep,
updateCartsStep,
} from "../steps"
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
import { updateTaxLinesStep } from "../steps/update-tax-lines"
import { refreshPaymentCollectionForCartStep } from "./refresh-payment-collection"
export const updateCartWorkflowId = "update-cart"
export const updateCartWorkflow = createWorkflow(
updateCartWorkflowId,
(input: WorkflowData<UpdateCartWorkflowInputDTO>): WorkflowData<CartDTO> => {
(input: WorkflowData<UpdateCartWorkflowInputDTO>): WorkflowData<void> => {
const [salesChannel, region, customerData] = parallelize(
findSalesChannelStep({
salesChannelId: input.sales_channel_id,
@@ -61,8 +61,9 @@ export const updateCartWorkflow = createWorkflow(
}
)
updateCartsStep([cartInput])
const carts = updateCartsStep([cartInput])
updateTaxLinesStep({ cart_or_cart_id: carts[0].id })
refreshCartPromotionsStep({
id: input.id,
promo_codes: input.promo_codes,
@@ -72,19 +73,5 @@ export const updateCartWorkflow = createWorkflow(
refreshPaymentCollectionForCartStep({
cart_id: input.id,
})
const retrieveCartInput = {
id: input.id,
config: {
relations: [
"items",
"items.adjustments",
"shipping_methods",
"shipping_methods.adjustments",
],
},
}
return retrieveCartStep(retrieveCartInput)
}
)
@@ -0,0 +1,91 @@
import {
CartLineItemDTO,
CartShippingMethodDTO,
CartWorkflowDTO,
} from "@medusajs/types"
import {
WorkflowData,
createWorkflow,
transform,
} from "@medusajs/workflows-sdk"
import {
getItemTaxLinesStep,
retrieveCartWithLinksStep,
setTaxLinesForItemsStep,
} from "../steps"
const cartFields = [
"id",
"currency_code",
"email",
"items.id",
"items.variant_id",
"items.product_id",
"items.product_title",
"items.product_description",
"items.product_subtitle",
"items.product_type",
"items.product_collection",
"items.product_handle",
"items.variant_sku",
"items.variant_barcode",
"items.variant_title",
"items.title",
"items.quantity",
"items.unit_price",
"items.tax_lines.id",
"items.tax_lines.description",
"items.tax_lines.code",
"items.tax_lines.rate",
"items.tax_lines.provider_id",
"shipping_methods.tax_lines.id",
"shipping_methods.tax_lines.description",
"shipping_methods.tax_lines.code",
"shipping_methods.tax_lines.rate",
"shipping_methods.tax_lines.provider_id",
"shipping_methods.shipping_option_id",
"shipping_methods.amount",
"customer.id",
"customer.email",
"customer.groups.id",
"shipping_address.id",
"shipping_address.address_1",
"shipping_address.address_2",
"shipping_address.city",
"shipping_address.postal_code",
"shipping_address.country_code",
"shipping_address.region_code",
"shipping_address.province",
]
type WorkflowInput = {
cart_or_cart_id: string | CartWorkflowDTO
items?: CartLineItemDTO[]
shipping_methods?: CartShippingMethodDTO[]
}
export const updateTaxLinesWorkflowId = "update-tax-lines"
export const updateTaxLinesWorkflow = createWorkflow(
updateTaxLinesWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
const cart = retrieveCartWithLinksStep({
cart_or_cart_id: input.cart_or_cart_id,
fields: cartFields,
})
const taxLineItems = getItemTaxLinesStep(
transform({ input, cart }, (data) => ({
cart: data.cart,
items: data.input.items || data.cart.items,
shipping_methods:
data.input.shipping_methods || data.cart.shipping_methods,
}))
)
setTaxLinesForItemsStep({
cart,
item_tax_lines: taxLineItems.lineItemTaxLines,
shipping_tax_lines: taxLineItems.shippingMethodsTaxLines,
})
}
)