fix(core-flows, medusa): Prevent cart addresses duplication on update (#13841)
* Allow id field in addresses properties for cart update validator * Update cart addresses in update step where id is provided, both reference and nested fields * Add tests * Add changeset * Remove unnecessary map step * Review changes
This commit is contained in:
1 parent
4dbf46f2cb
commit
fdc2b722d9
4 files changed
+337
-5
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(medusa): Allow 'id' in shipping_address and billing_address in validator of store udpate cart route
|
||||||
|
fix(core-flows): Update addresses nested fields when 'id' is provided in payload for update-carts step
|
||||||
@@ -1444,6 +1444,290 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
expect(cart.items?.length).toEqual(1)
|
expect(cart.items?.length).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should update cart shipping address fields", async () => {
|
||||||
|
const salesChannel = await scModuleService.createSalesChannels({
|
||||||
|
name: "Webshop",
|
||||||
|
})
|
||||||
|
|
||||||
|
const regions = await regionModuleService.createRegions([
|
||||||
|
{
|
||||||
|
name: "US",
|
||||||
|
currency_code: "usd",
|
||||||
|
countries: ["us"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
let cart = await cartModuleService.createCarts({
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: regions[0].id,
|
||||||
|
shipping_address: {
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Doe",
|
||||||
|
address_1: "123 Main St",
|
||||||
|
city: "New York",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "10001",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const shippingAddressId = cart.shipping_address?.id
|
||||||
|
|
||||||
|
await updateCartWorkflow(appContainer).run({
|
||||||
|
input: {
|
||||||
|
id: cart.id,
|
||||||
|
shipping_address: {
|
||||||
|
id: shippingAddressId,
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
address_1: "456 Oak Ave",
|
||||||
|
city: "Los Angeles",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "90001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cart = await cartModuleService.retrieveCart(cart.id, {
|
||||||
|
relations: ["shipping_address"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(cart.shipping_address).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: shippingAddressId,
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
address_1: "456 Oak Ave",
|
||||||
|
city: "Los Angeles",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "90001",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update cart billing address fields", async () => {
|
||||||
|
const salesChannel = await scModuleService.createSalesChannels({
|
||||||
|
name: "Webshop",
|
||||||
|
})
|
||||||
|
|
||||||
|
const regions = await regionModuleService.createRegions([
|
||||||
|
{
|
||||||
|
name: "US",
|
||||||
|
currency_code: "usd",
|
||||||
|
countries: ["us"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
let cart = await cartModuleService.createCarts({
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: regions[0].id,
|
||||||
|
billing_address: {
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Doe",
|
||||||
|
address_1: "123 Main St",
|
||||||
|
city: "New York",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "10001",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const billingAddressId = cart.billing_address?.id
|
||||||
|
|
||||||
|
await updateCartWorkflow(appContainer).run({
|
||||||
|
input: {
|
||||||
|
id: cart.id,
|
||||||
|
billing_address: {
|
||||||
|
id: billingAddressId,
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
address_1: "456 Oak Ave",
|
||||||
|
city: "Los Angeles",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "90001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cart = await cartModuleService.retrieveCart(cart.id, {
|
||||||
|
relations: ["billing_address"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(cart.billing_address).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: billingAddressId,
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
address_1: "456 Oak Ave",
|
||||||
|
city: "Los Angeles",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "90001",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update both shipping and billing addresses simultaneously", async () => {
|
||||||
|
const salesChannel = await scModuleService.createSalesChannels({
|
||||||
|
name: "Webshop",
|
||||||
|
})
|
||||||
|
|
||||||
|
const regions = await regionModuleService.createRegions([
|
||||||
|
{
|
||||||
|
name: "US",
|
||||||
|
currency_code: "usd",
|
||||||
|
countries: ["us"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
let cart = await cartModuleService.createCarts({
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: regions[0].id,
|
||||||
|
shipping_address: {
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Doe",
|
||||||
|
address_1: "123 Main St",
|
||||||
|
city: "New York",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "10001",
|
||||||
|
},
|
||||||
|
billing_address: {
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Doe",
|
||||||
|
address_1: "789 Business Blvd",
|
||||||
|
city: "Chicago",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "60601",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const shippingAddressId = cart.shipping_address?.id
|
||||||
|
const billingAddressId = cart.billing_address?.id
|
||||||
|
|
||||||
|
await updateCartWorkflow(appContainer).run({
|
||||||
|
input: {
|
||||||
|
id: cart.id,
|
||||||
|
shipping_address: {
|
||||||
|
id: shippingAddressId,
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
address_1: "456 Oak Ave",
|
||||||
|
city: "Los Angeles",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "90001",
|
||||||
|
},
|
||||||
|
billing_address: {
|
||||||
|
id: billingAddressId,
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
address_1: "321 Corporate Dr",
|
||||||
|
city: "San Francisco",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "94102",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cart = await cartModuleService.retrieveCart(cart.id, {
|
||||||
|
relations: ["shipping_address", "billing_address"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(cart.shipping_address).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: shippingAddressId,
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
address_1: "456 Oak Ave",
|
||||||
|
city: "Los Angeles",
|
||||||
|
postal_code: "90001",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(cart.billing_address).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: billingAddressId,
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
address_1: "321 Corporate Dr",
|
||||||
|
city: "San Francisco",
|
||||||
|
postal_code: "94102",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should rollback address updates on workflow failure", async () => {
|
||||||
|
const salesChannel = await scModuleService.createSalesChannels({
|
||||||
|
name: "Webshop",
|
||||||
|
})
|
||||||
|
|
||||||
|
const regions = await regionModuleService.createRegions([
|
||||||
|
{
|
||||||
|
name: "US",
|
||||||
|
currency_code: "usd",
|
||||||
|
countries: ["us"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
let cart = await cartModuleService.createCarts({
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: regions[0].id,
|
||||||
|
shipping_address: {
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Doe",
|
||||||
|
address_1: "123 Main St",
|
||||||
|
city: "New York",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "10001",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const originalShippingAddress = { ...cart.shipping_address }
|
||||||
|
const shippingAddressId = cart.shipping_address?.id
|
||||||
|
|
||||||
|
const workflow = updateCartWorkflow(appContainer)
|
||||||
|
|
||||||
|
workflow.appendAction("throw", "update-carts", {
|
||||||
|
invoke: async function failStep() {
|
||||||
|
throw new Error("Simulated failure")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { errors } = await workflow.run({
|
||||||
|
input: {
|
||||||
|
id: cart.id,
|
||||||
|
shipping_address: {
|
||||||
|
id: shippingAddressId,
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
address_1: "456 Oak Ave",
|
||||||
|
city: "Los Angeles",
|
||||||
|
country_code: "us",
|
||||||
|
postal_code: "90001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toBeDefined()
|
||||||
|
expect(errors?.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
cart = await cartModuleService.retrieveCart(cart.id, {
|
||||||
|
relations: ["shipping_address"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(cart.shipping_address).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: shippingAddressId,
|
||||||
|
first_name: originalShippingAddress.first_name,
|
||||||
|
last_name: originalShippingAddress.last_name,
|
||||||
|
address_1: originalShippingAddress.address_1,
|
||||||
|
city: originalShippingAddress.city,
|
||||||
|
postal_code: originalShippingAddress.postal_code,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("AddToCartWorkflow", () => {
|
describe("AddToCartWorkflow", () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ICartModuleService,
|
ICartModuleService,
|
||||||
|
UpdateAddressDTO,
|
||||||
UpdateCartDTO,
|
UpdateCartDTO,
|
||||||
UpdateCartWorkflowInputDTO,
|
UpdateCartWorkflowInputDTO,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
@@ -46,17 +47,53 @@ export const updateCartsStep = createStep(
|
|||||||
{ select: selects, relations }
|
{ select: selects, relations }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Since service factory udpate method will correctly keep the reference to the addresses,
|
||||||
|
// but won't update its fields, we do this separately
|
||||||
|
const addressesInput = data
|
||||||
|
.flatMap((cart) => [cart.shipping_address, cart.billing_address])
|
||||||
|
.filter((address) => !!address)
|
||||||
|
let addressesToUpdateIds: string[] = []
|
||||||
|
const addressesToUpdate = addressesInput.filter(
|
||||||
|
(address): address is UpdateAddressDTO => {
|
||||||
|
if ("id" in address && !!address.id) {
|
||||||
|
addressesToUpdateIds.push(address.id as string)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const addressesBeforeUpdate = await cartModule.listAddresses({
|
||||||
|
id: addressesToUpdate.map((address) => address.id),
|
||||||
|
})
|
||||||
|
if (addressesToUpdate.length) {
|
||||||
|
await cartModule.updateAddresses(addressesToUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
const updatedCart = await cartModule.updateCarts(data)
|
const updatedCart = await cartModule.updateCarts(data)
|
||||||
|
|
||||||
return new StepResponse(updatedCart, cartsBeforeUpdate)
|
return new StepResponse(updatedCart, {
|
||||||
|
cartsBeforeUpdate,
|
||||||
|
addressesBeforeUpdate,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async (cartsBeforeUpdate, { container }) => {
|
async (dataToCompensate, { container }) => {
|
||||||
if (!cartsBeforeUpdate) {
|
if (!dataToCompensate) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { cartsBeforeUpdate, addressesBeforeUpdate } = dataToCompensate
|
||||||
|
|
||||||
const cartModule = container.resolve<ICartModuleService>(Modules.CART)
|
const cartModule = container.resolve<ICartModuleService>(Modules.CART)
|
||||||
|
|
||||||
|
const addressesToUpdate: UpdateAddressDTO[] = []
|
||||||
|
for (const address of addressesBeforeUpdate) {
|
||||||
|
addressesToUpdate.push({
|
||||||
|
...address,
|
||||||
|
metadata: address.metadata ?? undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await cartModule.updateAddresses(addressesToUpdate)
|
||||||
|
|
||||||
const dataToUpdate: UpdateCartDTO[] = []
|
const dataToUpdate: UpdateCartDTO[] = []
|
||||||
|
|
||||||
for (const cart of cartsBeforeUpdate) {
|
for (const cart of cartsBeforeUpdate) {
|
||||||
|
|||||||
@@ -43,13 +43,17 @@ export const StoreRemoveCartPromotions = z
|
|||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
|
const StoreCartUpsertAddress = AddressPayload.merge(z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
}))
|
||||||
|
|
||||||
export type StoreUpdateCartType = z.infer<typeof UpdateCart>
|
export type StoreUpdateCartType = z.infer<typeof UpdateCart>
|
||||||
export const UpdateCart = z
|
export const UpdateCart = z
|
||||||
.object({
|
.object({
|
||||||
region_id: z.string().optional(),
|
region_id: z.string().optional(),
|
||||||
email: z.string().email().nullish(),
|
email: z.string().email().nullish(),
|
||||||
billing_address: z.union([AddressPayload, z.string()]).optional(),
|
billing_address: z.union([StoreCartUpsertAddress, z.string()]).optional(),
|
||||||
shipping_address: z.union([AddressPayload, z.string()]).optional(),
|
shipping_address: z.union([StoreCartUpsertAddress, z.string()]).optional(),
|
||||||
sales_channel_id: z.string().nullish(),
|
sales_channel_id: z.string().nullish(),
|
||||||
metadata: z.record(z.unknown()).nullish(),
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
promo_codes: z.array(z.string()).optional(),
|
promo_codes: z.array(z.string()).optional(),
|
||||||
|
|||||||
Reference in new issue
Block a user