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 21:47:43 +05:30
committed by GitHub
parent 8c57e61cb8
commit e4acde1aa2
26 changed files with 1096 additions and 180 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/core-flows": patch
"@medusajs/medusa": patch
"@medusajs/types": patch
---
feat(medusa,core-flows,types): add cart <> tax integration workflows + steps

View File

@@ -12,11 +12,13 @@ import {
IPromotionModuleService,
IRegionModuleService,
ISalesChannelModuleService,
ITaxModuleService,
} from "@medusajs/types"
import { PromotionRuleOperator, PromotionType } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import adminSeeder from "../../../../helpers/admin-seeder"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { setupTaxStructure } from "../../fixtures"
jest.setTimeout(50000)
@@ -27,38 +29,36 @@ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
describe("Store Carts API", () => {
let appContainer
let cartModuleService: ICartModuleService
let regionModuleService: IRegionModuleService
let scModuleService: ISalesChannelModuleService
let cartModule: ICartModuleService
let regionModule: IRegionModuleService
let scModule: ISalesChannelModuleService
let customerModule: ICustomerModuleService
let productModule: IProductModuleService
let pricingModule: IPricingModuleService
let remoteLink: RemoteLink
let promotionModule: IPromotionModuleService
let taxModule: ITaxModuleService
let defaultRegion
beforeAll(async () => {
appContainer = getContainer()
cartModuleService = appContainer.resolve(ModuleRegistrationName.CART)
regionModuleService = appContainer.resolve(
ModuleRegistrationName.REGION
)
scModuleService = appContainer.resolve(
ModuleRegistrationName.SALES_CHANNEL
)
cartModule = appContainer.resolve(ModuleRegistrationName.CART)
regionModule = appContainer.resolve(ModuleRegistrationName.REGION)
scModule = appContainer.resolve(ModuleRegistrationName.SALES_CHANNEL)
customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER)
productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT)
pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING)
remoteLink = appContainer.resolve(LinkModuleUtils.REMOTE_LINK)
promotionModule = appContainer.resolve(ModuleRegistrationName.PROMOTION)
taxModule = appContainer.resolve(ModuleRegistrationName.TAX)
})
beforeEach(async () => {
await adminSeeder(dbConnection)
// Here, so we don't have to create a region for each test
defaultRegion = await regionModuleService.create({
defaultRegion = await regionModule.create({
name: "Default Region",
currency_code: "dkk",
})
@@ -66,12 +66,12 @@ medusaIntegrationTestRunner({
describe("POST /store/carts", () => {
it("should create a cart", async () => {
const region = await regionModuleService.create({
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
const salesChannel = await scModuleService.create({
const salesChannel = await scModule.create({
name: "Webshop",
})
@@ -172,10 +172,44 @@ medusaIntegrationTestRunner({
)
})
it("should create cart with customer from email", async () => {
it("should create cart with customer from email and tax lines", async () => {
await setupTaxStructure(taxModule)
const [product] = await productModule.create([
{
title: "Test product default tax",
variants: [{ title: "Test variant default tax" }],
},
])
const [priceSet] = await pricingModule.create([
{ prices: [{ amount: 3000, currency_code: "usd" }] },
])
await remoteLink.create([
{
productService: { variant_id: product.variants[0].id },
pricingService: { price_set_id: priceSet.id },
},
])
const created = await api.post(`/store/carts`, {
currency_code: "usd",
email: "tony@stark-industries.com",
shipping_address: {
address_1: "test address 1",
address_2: "test address 2",
city: "NY",
country_code: "US",
province: "NY",
postal_code: "94016",
},
items: [
{
quantity: 1,
variant_id: product.variants[0].id,
},
],
})
expect(created.status).toEqual(200)
@@ -188,12 +222,25 @@ medusaIntegrationTestRunner({
id: expect.any(String),
email: "tony@stark-industries.com",
}),
items: [
expect.objectContaining({
tax_lines: [
expect.objectContaining({
description: "NY Default Rate",
code: "NYDEFAULT",
rate: 6,
provider_id: "system",
}),
],
adjustments: [],
}),
],
})
)
})
it("should create cart with any region", async () => {
await regionModuleService.create({
await regionModule.create({
name: "US",
currency_code: "usd",
})
@@ -217,7 +264,7 @@ medusaIntegrationTestRunner({
})
it("should create cart with region currency code", async () => {
const region = await regionModuleService.create({
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
@@ -278,6 +325,8 @@ medusaIntegrationTestRunner({
describe("POST /store/carts/:id", () => {
it("should update a cart with promo codes with a replace action", async () => {
await setupTaxStructure(taxModule)
const targetRules = [
{
attribute: "product_id",
@@ -293,7 +342,7 @@ medusaIntegrationTestRunner({
type: "fixed",
target_type: "items",
allocation: "each",
value: "300",
value: 300,
apply_to_quantity: 1,
max_quantity: 1,
target_rules: targetRules,
@@ -307,15 +356,23 @@ medusaIntegrationTestRunner({
type: "fixed",
target_type: "items",
allocation: "across",
value: "1000",
value: 1000,
apply_to_quantity: 1,
target_rules: targetRules,
},
})
const cart = await cartModuleService.create({
const cart = await cartModule.create({
currency_code: "usd",
email: "tony@stark.com",
shipping_address: {
address_1: "test address 1",
address_2: "test address 2",
city: "NY",
country_code: "US",
province: "NY",
postal_code: "94016",
},
items: [
{
id: "item-1",
@@ -327,7 +384,7 @@ medusaIntegrationTestRunner({
],
})
const [adjustment] = await cartModuleService.addLineItemAdjustments([
const [adjustment] = await cartModule.addLineItemAdjustments([
{
code: appliedPromotion.code!,
amount: 300,
@@ -353,6 +410,14 @@ medusaIntegrationTestRunner({
items: [
expect.objectContaining({
id: "item-1",
tax_lines: [
expect.objectContaining({
description: "NY Default Rate",
code: "NYDEFAULT",
rate: 6,
provider_id: "system",
}),
],
adjustments: [
expect.objectContaining({
id: expect.not.stringContaining(adjustment.id),
@@ -363,7 +428,6 @@ medusaIntegrationTestRunner({
],
})
)
// Should remove all adjustments from other promo codes
updated = await api.post(`/store/carts/${cart.id}`, {
promo_codes: [],
@@ -377,26 +441,61 @@ medusaIntegrationTestRunner({
expect.objectContaining({
id: "item-1",
adjustments: [],
tax_lines: [
expect.objectContaining({
description: "NY Default Rate",
code: "NYDEFAULT",
rate: 6,
provider_id: "system",
}),
],
}),
],
})
)
})
it("should update a cart's region, sales channel and customer data", async () => {
const region = await regionModuleService.create({
it("should update a cart's region, sales channel, customer data and tax lines", async () => {
await setupTaxStructure(taxModule)
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
const salesChannel = await scModuleService.create({
const salesChannel = await scModule.create({
name: "Webshop",
})
const cart = await cartModuleService.create({
const cart = await cartModule.create({
currency_code: "eur",
email: "tony@stark.com",
shipping_address: {
address_1: "test address 1",
address_2: "test address 2",
city: "NY",
country_code: "US",
province: "NY",
postal_code: "94016",
},
items: [
{
id: "item-1",
unit_price: 2000,
quantity: 1,
title: "Test item",
product_id: "prod_tshirt",
} as any,
],
})
// Manually inserting shipping methods here since the cart does not
// currently support it. Move to API when ready.
await cartModule.addShippingMethods(cart.id, [
{ amount: 500, name: "express" },
{ amount: 500, name: "standard" },
])
let updated = await api.post(`/store/carts/${cart.id}`, {
region_id: region.id,
email: "tony@stark.com",
@@ -417,6 +516,53 @@ medusaIntegrationTestRunner({
email: "tony@stark.com",
}),
sales_channel_id: salesChannel.id,
shipping_address: expect.objectContaining({
city: "NY",
country_code: "US",
province: "NY",
}),
shipping_methods: expect.arrayContaining([
expect.objectContaining({
shipping_option_id: null,
amount: 500,
tax_lines: [
expect.objectContaining({
description: "NY Default Rate",
code: "NYDEFAULT",
rate: 6,
provider_id: "system",
}),
],
adjustments: [],
}),
expect.objectContaining({
shipping_option_id: null,
amount: 500,
tax_lines: [
expect.objectContaining({
description: "NY Default Rate",
code: "NYDEFAULT",
rate: 6,
provider_id: "system",
}),
],
adjustments: [],
}),
]),
items: [
expect.objectContaining({
id: "item-1",
tax_lines: [
expect.objectContaining({
description: "NY Default Rate",
code: "NYDEFAULT",
rate: 6,
provider_id: "system",
}),
],
adjustments: [],
}),
],
})
)
@@ -432,11 +578,21 @@ medusaIntegrationTestRunner({
currency_code: "usd",
email: null,
customer_id: null,
region: expect.objectContaining({
id: region.id,
currency_code: "usd",
}),
sales_channel_id: null,
items: [
expect.objectContaining({
id: "item-1",
tax_lines: [
expect.objectContaining({
description: "NY Default Rate",
code: "NYDEFAULT",
rate: 6,
provider_id: "system",
}),
],
adjustments: [],
}),
],
})
)
})
@@ -444,12 +600,12 @@ medusaIntegrationTestRunner({
describe("GET /store/carts/:id", () => {
it("should create and update a cart", async () => {
const region = await regionModuleService.create({
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
const salesChannel = await scModuleService.create({
const salesChannel = await scModule.create({
name: "Webshop",
})
@@ -494,16 +650,16 @@ medusaIntegrationTestRunner({
describe("GET /store/carts", () => {
it("should get cart", async () => {
const region = await regionModuleService.create({
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
const salesChannel = await scModuleService.create({
const salesChannel = await scModule.create({
name: "Webshop",
})
const cart = await cartModuleService.create({
const cart = await cartModule.create({
currency_code: "usd",
items: [
{
@@ -542,19 +698,40 @@ medusaIntegrationTestRunner({
describe("POST /store/carts/:id/line-items", () => {
it("should add item to cart", async () => {
const [product] = await productModule.create([
await setupTaxStructure(taxModule)
const customer = await customerModule.create({
email: "tony@stark-industries.com",
})
const [productWithSpecialTax] = await productModule.create([
{
// This product ID is setup in the tax structure fixture (setupTaxStructure)
id: "product_id_1",
title: "Test product",
variants: [
{
title: "Test variant",
},
],
variants: [{ title: "Test variant" }],
} as any,
])
const [productWithDefaultTax] = await productModule.create([
{
title: "Test product default tax",
variants: [{ title: "Test variant default tax" }],
},
])
const cart = await cartModuleService.create({
const cart = await cartModule.create({
currency_code: "usd",
customer_id: customer.id,
shipping_address: {
customer_id: customer.id,
address_1: "test address 1",
address_2: "test address 2",
city: "SF",
country_code: "US",
province: "CA",
postal_code: "94016",
},
items: [
{
id: "item-1",
@@ -573,61 +750,60 @@ medusaIntegrationTestRunner({
type: "fixed",
target_type: "items",
allocation: "across",
value: "300",
value: 300,
apply_to_quantity: 2,
target_rules: [
{
attribute: "product_id",
operator: "in",
values: ["prod_mat", product.id],
values: ["prod_mat", productWithSpecialTax.id],
},
],
},
})
const [lineItemAdjustment] =
await cartModuleService.addLineItemAdjustments([
{
code: appliedPromotion.code!,
amount: 300,
item_id: "item-1",
promotion_id: appliedPromotion.id,
},
])
const [lineItemAdjustment] = await cartModule.addLineItemAdjustments([
{
code: appliedPromotion.code!,
amount: 300,
item_id: "item-1",
promotion_id: appliedPromotion.id,
},
])
const priceSet = await pricingModule.create({
prices: [
{
amount: 3000,
currency_code: "usd",
},
],
})
const [priceSet, priceSetDefaultTax] = await pricingModule.create([
{
prices: [{ amount: 3000, currency_code: "usd" }],
},
{
prices: [{ amount: 2000, currency_code: "usd" }],
},
])
await remoteLink.create([
{
productService: {
variant_id: product.variants[0].id,
variant_id: productWithSpecialTax.variants[0].id,
},
pricingService: {
price_set_id: priceSet.id,
pricingService: { price_set_id: priceSet.id },
},
{
productService: {
variant_id: productWithDefaultTax.variants[0].id,
},
pricingService: { price_set_id: priceSetDefaultTax.id },
},
{
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
},
])
await remoteLink.create({
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
let response = await api.post(`/store/carts/${cart.id}/line-items`, {
variant_id: productWithSpecialTax.variants[0].id,
quantity: 1,
})
const response = await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: product.variants[0].id,
quantity: 1,
}
)
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
@@ -638,6 +814,14 @@ medusaIntegrationTestRunner({
unit_price: 3000,
quantity: 1,
title: "Test variant",
tax_lines: [
expect.objectContaining({
description: "CA Reduced Rate for Products",
code: "CAREDUCE_PROD",
rate: 3,
provider_id: "system",
}),
],
adjustments: [
expect.objectContaining({
code: "PROMOTION_APPLIED",
@@ -649,6 +833,7 @@ medusaIntegrationTestRunner({
unit_price: 2000,
quantity: 1,
title: "Test item",
tax_lines: [],
adjustments: [
expect.objectContaining({
id: expect.not.stringContaining(lineItemAdjustment.id),
@@ -660,17 +845,45 @@ medusaIntegrationTestRunner({
]),
})
)
response = await api.post(`/store/carts/${cart.id}/line-items`, {
variant_id: productWithDefaultTax.variants[0].id,
quantity: 1,
})
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
currency_code: "usd",
items: expect.arrayContaining([
expect.objectContaining({
unit_price: 2000,
quantity: 1,
title: "Test variant default tax",
tax_lines: [
// Uses the california default rate
expect.objectContaining({
description: "CA Default Rate",
code: "CADEFAULT",
rate: 5,
provider_id: "system",
}),
],
}),
]),
})
)
})
})
describe("POST /store/carts/:id/payment-collections", () => {
it("should create a payment collection for the cart", async () => {
const region = await regionModuleService.create({
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
const cart = await cartModuleService.create({
const cart = await cartModule.create({
currency_code: "usd",
region_id: region.id,
})

View File

@@ -0,0 +1 @@
export * from "./tax"

View File

@@ -0,0 +1,245 @@
import { ITaxModuleService } from "@medusajs/types"
export const setupTaxStructure = async (service: ITaxModuleService) => {
// Setup for this specific test
//
// Using the following structure to setup tests.
// US - default 2%
// - Region: CA - default 5%
// - Override: Reduced rate (for 3 product ids): 3%
// - Override: Reduced rate (for product type): 1%
// - Region: NY - default: 6%
// - Region: FL - default: 4%
//
// Denmark - default 25%
//
// Germany - default 19%
// - Override: Reduced Rate (for product type) - 7%
//
// Canada - default 5%
// - Override: Reduced rate (for product id) - 3%
// - Override: Reduced rate (for product type) - 3.5%
// - Region: QC - default 2%
// - Override: Reduced rate (for same product type as country reduced rate): 1%
// - Region: BC - default 2%
//
const [us, dk, de, ca] = await service.createTaxRegions([
{
country_code: "US",
default_tax_rate: { name: "US Default Rate", rate: 2, code: "US_DEF" },
},
{
country_code: "DK",
default_tax_rate: {
name: "Denmark Default Rate",
rate: 25,
code: "DK_DEF",
},
},
{
country_code: "DE",
default_tax_rate: {
code: "DE19",
name: "Germany Default Rate",
rate: 19,
},
},
{
country_code: "CA",
default_tax_rate: { name: "Canada Default Rate", rate: 5 },
},
])
// Create province regions within the US
const [cal, ny, fl, qc, bc] = await service.createTaxRegions([
{
country_code: "US",
province_code: "CA",
parent_id: us.id,
default_tax_rate: {
rate: 5,
name: "CA Default Rate",
code: "CADEFAULT",
},
},
{
country_code: "US",
province_code: "NY",
parent_id: us.id,
default_tax_rate: {
rate: 6,
name: "NY Default Rate",
code: "NYDEFAULT",
},
},
{
country_code: "US",
province_code: "FL",
parent_id: us.id,
default_tax_rate: {
rate: 4,
name: "FL Default Rate",
code: "FLDEFAULT",
},
},
{
country_code: "CA",
province_code: "QC",
parent_id: ca.id,
default_tax_rate: {
rate: 2,
name: "QC Default Rate",
code: "QCDEFAULT",
},
},
{
country_code: "CA",
province_code: "BC",
parent_id: ca.id,
default_tax_rate: {
rate: 2,
name: "BC Default Rate",
code: "BCDEFAULT",
},
},
])
const [calProd, calType, deType, canProd, canType, qcType] =
await service.create([
{
tax_region_id: cal.id,
name: "CA Reduced Rate for Products",
rate: 3,
code: "CAREDUCE_PROD",
},
{
tax_region_id: cal.id,
name: "CA Reduced Rate for Product Type",
rate: 1,
code: "CAREDUCE_TYPE",
},
{
tax_region_id: de.id,
name: "Germany Reduced Rate for Product Type",
rate: 7,
code: "DEREDUCE_TYPE",
},
{
tax_region_id: ca.id,
name: "Canada Reduced Rate for Product",
rate: 3,
code: "CAREDUCE_PROD_CA",
},
{
tax_region_id: ca.id,
name: "Canada Reduced Rate for Product Type",
rate: 3.5,
code: "CAREDUCE_TYPE_CA",
},
{
tax_region_id: qc.id,
name: "QC Reduced Rate for Product Type",
rate: 1,
code: "QCREDUCE_TYPE",
},
])
// Create tax rate rules for specific products and product types
await service.createTaxRateRules([
{
reference: "product",
reference_id: "product_id_1",
tax_rate_id: calProd.id,
},
{
reference: "product",
reference_id: "product_id_2",
tax_rate_id: calProd.id,
},
{
reference: "product",
reference_id: "product_id_3",
tax_rate_id: calProd.id,
},
{
reference: "product_type",
reference_id: "product_type_id_1",
tax_rate_id: calType.id,
},
{
reference: "product_type",
reference_id: "product_type_id_2",
tax_rate_id: deType.id,
},
{
reference: "product",
reference_id: "product_id_4",
tax_rate_id: canProd.id,
},
{
reference: "product_type",
reference_id: "product_type_id_3",
tax_rate_id: canType.id,
},
{
reference: "product_type",
reference_id: "product_type_id_3",
tax_rate_id: qcType.id,
},
])
return {
us: {
country: us,
children: {
cal: {
province: cal,
overrides: {
calProd,
calType,
},
},
ny: {
province: ny,
overrides: {},
},
fl: {
province: fl,
overrides: {},
},
},
overrides: {},
},
dk: {
country: dk,
children: {},
overrides: {},
},
de: {
country: de,
children: {},
overrides: {
deType,
},
},
ca: {
country: ca,
children: {
qc: {
province: qc,
overrides: {
qcType,
},
},
bc: {
province: bc,
overrides: {},
},
},
overrides: {
canProd,
canType,
},
},
}
}

View File

@@ -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,
})
}
)

View File

@@ -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"

View File

@@ -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)
}
)

View File

@@ -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,
}))
}

View File

@@ -48,6 +48,6 @@ export const updateCartsStep = createStep(
})
}
await cartModule.update(dataToUpdate)
return await cartModule.update(dataToUpdate)
}
)

View File

@@ -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)
}
)

View File

@@ -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))

View File

@@ -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,

View File

@@ -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

View File

@@ -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
}
)

View File

@@ -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"

View File

@@ -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)
}
)

View File

@@ -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,
})
}
)

View File

@@ -1,18 +1,20 @@
import { addToCartWorkflow } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICartModuleService } from "@medusajs/types"
import { LinkModuleUtils, Modules } from "@medusajs/modules-sdk"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
import { defaultStoreCartFields } from "../../query-config"
import { StorePostCartsCartLineItemsReq } from "./validators"
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
const cartModuleService = req.scope.resolve<ICartModuleService>(
ModuleRegistrationName.CART
)
const remoteQuery = req.scope.resolve(LinkModuleUtils.REMOTE_QUERY)
const cart = await cartModuleService.retrieve(req.params.id, {
select: ["id", "region_id", "currency_code"],
const query = remoteQueryObjectFromString({
entryPoint: Modules.CART,
fields: defaultStoreCartFields,
})
const [cart] = await remoteQuery(query, {
cart: { id: req.params.id },
})
const workflowInput = {
@@ -29,13 +31,6 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
throw errors[0].error
}
const remoteQuery = req.scope.resolve("remoteQuery")
const query = remoteQueryObjectFromString({
entryPoint: "cart",
fields: defaultStoreCartFields,
})
const [updatedCart] = await remoteQuery(query, {
cart: { id: req.params.id },
})

View File

@@ -1,4 +1,5 @@
import { updateCartWorkflow } from "@medusajs/core-flows"
import { LinkModuleUtils, Modules } from "@medusajs/modules-sdk"
import { UpdateCartDataDTO } from "@medusajs/types"
import { remoteQueryObjectFromString } from "@medusajs/utils"
@@ -6,12 +7,11 @@ import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { defaultStoreCartFields } from "../query-config"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const remoteQuery = req.scope.resolve(LinkModuleUtils.REMOTE_QUERY)
const variables = { id: req.params.id }
const query = remoteQueryObjectFromString({
entryPoint: "cart",
entryPoint: Modules.CART,
fields: defaultStoreCartFields,
})
@@ -38,16 +38,16 @@ export const POST = async (
throw errors[0].error
}
const remoteQuery = req.scope.resolve("remoteQuery")
const remoteQuery = req.scope.resolve(LinkModuleUtils.REMOTE_QUERY)
const query = remoteQueryObjectFromString({
entryPoint: "cart",
entryPoint: Modules.CART,
fields: defaultStoreCartFields,
})
const [updatedCart] = await remoteQuery(query, {
const [cart] = await remoteQuery(query, {
cart: { id: req.params.id },
})
res.status(200).json({ cart: updatedCart })
res.status(200).json({ cart })
}

View File

@@ -5,16 +5,40 @@ export const defaultStoreCartFields = [
"created_at",
"updated_at",
"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.created_at",
"items.updated_at",
"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",
"items.adjustments.id",
"items.adjustments.code",
"items.adjustments.amount",
"customer.id",
"customer.email",
"customer.groups.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",
"shipping_methods.adjustments.id",
"shipping_methods.adjustments.code",
"shipping_methods.adjustments.amount",
@@ -27,6 +51,7 @@ export const defaultStoreCartFields = [
"shipping_address.postal_code",
"shipping_address.country_code",
"shipping_address.region_code",
"shipping_address.province",
"shipping_address.phone",
"billing_address.id",
"billing_address.first_name",
@@ -51,23 +76,29 @@ export const defaultStoreCartFields = [
export const defaultStoreCartRelations = [
"items",
"items.tax_lines",
"items.adjustments",
"region",
"customer",
"customer.groups",
"shipping_address",
"billing_address",
"shipping_methods",
"shipping_methods.tax_lines",
"shipping_methods.adjustments",
]
export const allowedRelations = [
"items",
"items.tax_lines",
"items.adjustments",
"region",
"customer",
"customer.groups",
"shipping_address",
"billing_address",
"shipping_methods",
"shipping_methods.tax_lines",
"shipping_methods.adjustments",
"sales_channel",
]

View File

@@ -1,4 +1,5 @@
import { createCartWorkflow } from "@medusajs/core-flows"
import { LinkModuleUtils, Modules } from "@medusajs/modules-sdk"
import { CreateCartWorkflowInputDTO } from "@medusajs/types"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import {
@@ -25,16 +26,13 @@ export const POST = async (
throw errors[0].error
}
const remoteQuery = req.scope.resolve("remoteQuery")
const variables = { id: result.id }
const remoteQuery = req.scope.resolve(LinkModuleUtils.REMOTE_QUERY)
const query = remoteQueryObjectFromString({
entryPoint: "cart",
entryPoint: Modules.CART,
fields: defaultStoreCartFields,
})
const [cart] = await remoteQuery(query, { cart: variables })
const [cart] = await remoteQuery(query, { cart: { id: result.id } })
res.status(200).json({ cart })
}

View File

@@ -29,6 +29,14 @@ export class StorePostCartReq {
@IsString()
region_id?: string
@IsOptional()
@IsType([AddressPayload, String])
shipping_address?: AddressPayload | string
@IsOptional()
@IsType([AddressPayload, String])
billing_address?: AddressPayload | string
@IsOptional()
@IsString()
email?: string

View File

@@ -20,6 +20,7 @@ export default class SystemTaxService implements ITaxProvider {
name: r.name,
code: r.code,
line_item_id: l.line_item.id,
provider_id: this.getIdentifier(),
}))
})
@@ -31,6 +32,7 @@ export default class SystemTaxService implements ITaxProvider {
name: r.name,
code: r.code,
shipping_line_id: l.shipping_line.id,
provider_id: this.getIdentifier(),
}))
})
)

View File

@@ -6,6 +6,7 @@ import {
InternalModuleDeclaration,
ModuleJoinerConfig,
ModulesSdkTypes,
TaxRegionDTO,
TaxTypes,
} from "@medusajs/types"
import {
@@ -14,16 +15,14 @@ import {
MedusaContext,
MedusaError,
ModulesSdkUtils,
arrayDifference,
isDefined,
isString,
promiseAll,
} from "@medusajs/utils"
import { TaxProvider, TaxRate, TaxRegion, TaxRateRule } from "@models"
import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { TaxRegionDTO } from "@medusajs/types"
import { uniqueRateReferenceIndexName } from "../models/tax-rate-rule"
import { singleDefaultRegionIndexName } from "../models/tax-rate"
import { uniqueRateReferenceIndexName } from "../models/tax-rate-rule"
import { countryCodeProvinceIndexName } from "../models/tax-region"
type InjectedDependencies = {

View File

@@ -1,3 +1,5 @@
import { CustomerDTO } from "../customer"
import { ProductDTO } from "../product"
import { CartDTO, CartLineItemDTO } from "./common"
import { UpdateLineItemDTO } from "./mutations"
@@ -70,7 +72,7 @@ export interface CreateCartWorkflowInputDTO {
export interface AddToCartWorkflowInputDTO {
items: CreateCartCreateLineItemDTO[]
cart: CartDTO
cart: CartWorkflowDTO
}
export interface UpdateCartWorkflowInputDTO {
@@ -91,3 +93,8 @@ export interface CreatePaymentCollectionForCartWorkflowInputDTO {
amount: number
metadata?: Record<string, unknown>
}
export interface CartWorkflowDTO extends CartDTO {
customer?: CustomerDTO
product?: ProductDTO
}

View File

@@ -165,6 +165,7 @@ interface TaxLineDTO {
rate: number | null
code: string | null
name: string
provider_id: string
}
export interface ItemTaxLineDTO extends TaxLineDTO {