chore: idempotent cart operations (#13236)
* chore(core-flows): idempotent cart operations * changeset * add tests * revert * revert route * promo test * skip bugs * fix test * tests * avoid workflow name conflict * prevent nested workflow from being deleted until the top level parent finishes * remove unused setTimeout * update changeset * rm comments --------- Co-authored-by: adrien2p <adrien.deperetti@gmail.com>
This commit is contained in:
co-authored by
adrien2p
parent
b111d01898
commit
9412669e65
@@ -8,7 +8,7 @@ import {
|
||||
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures/tax"
|
||||
import { medusaTshirtProduct } from "../../../__fixtures__/product"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
jest.setTimeout(50000000)
|
||||
|
||||
const adminHeaders = {
|
||||
headers: { "x-medusa-access-token": "test_token" },
|
||||
@@ -609,6 +609,11 @@ medusaIntegrationTestRunner({
|
||||
|
||||
expect(cartWithPromotion1).toEqual(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
adjustments: [],
|
||||
}),
|
||||
],
|
||||
promotions: [],
|
||||
})
|
||||
)
|
||||
@@ -621,6 +626,140 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cartWithPromotion2).toEqual(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
adjustments: [
|
||||
expect.objectContaining({
|
||||
code: response.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
code: response.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should add promotion and remove it from cart using delete", async () => {
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
const salesChannel = (
|
||||
await api.post(
|
||||
"/admin/sales-channels",
|
||||
{ name: "Webshop", description: "channel" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.sales_channel
|
||||
|
||||
const region = (
|
||||
await api.post(
|
||||
"/admin/regions",
|
||||
{ name: "US", currency_code: "usd", countries: ["us"] },
|
||||
adminHeaders
|
||||
)
|
||||
).data.region
|
||||
|
||||
const product = (
|
||||
await api.post(
|
||||
"/admin/products",
|
||||
{
|
||||
...medusaTshirtProduct,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "TEST",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
is_automatic: false,
|
||||
application_method: {
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "each",
|
||||
currency_code: "usd",
|
||||
value: 100,
|
||||
max_quantity: 100,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "subtotal",
|
||||
operator: "gte",
|
||||
values: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// Simulate concurrent requests
|
||||
await Promise.all([
|
||||
api
|
||||
.post(
|
||||
`/store/carts/${cart.id}`,
|
||||
{
|
||||
promo_codes: [response.data.promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
.catch(() => {}),
|
||||
api
|
||||
.post(
|
||||
`/store/carts/${cart.id}`,
|
||||
{
|
||||
promo_codes: [response.data.promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
.catch(() => {}),
|
||||
])
|
||||
|
||||
const cartAfterPromotion = (
|
||||
await api.get(`/store/carts/${cart.id}`, storeHeaders)
|
||||
).data.cart
|
||||
|
||||
expect(cartAfterPromotion).toEqual(
|
||||
expect.objectContaining({
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
code: response.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const cartWithPromotion2 = (
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/line-items`,
|
||||
{ variant_id: product.variants[0].id, quantity: 40 },
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cartWithPromotion2).toEqual(
|
||||
expect.objectContaining({
|
||||
promotions: [
|
||||
@@ -630,6 +769,547 @@ medusaIntegrationTestRunner({
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
await api.delete(`/store/carts/${cart.id}/promotions`, {
|
||||
data: {
|
||||
promo_codes: [response.data.promotion.code],
|
||||
},
|
||||
...storeHeaders,
|
||||
})
|
||||
|
||||
const cartWithoutPromotion = (
|
||||
await api.get(`/store/carts/${cart.id}`, storeHeaders)
|
||||
).data.cart
|
||||
|
||||
expect(cartWithoutPromotion).toEqual(
|
||||
expect.objectContaining({
|
||||
promotions: [],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should add promotion and remove it from cart using update", async () => {
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
const salesChannel = (
|
||||
await api.post(
|
||||
"/admin/sales-channels",
|
||||
{ name: "Webshop", description: "channel" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.sales_channel
|
||||
|
||||
const region = (
|
||||
await api.post(
|
||||
"/admin/regions",
|
||||
{ name: "US", currency_code: "usd", countries: ["us"] },
|
||||
adminHeaders
|
||||
)
|
||||
).data.region
|
||||
|
||||
const product = (
|
||||
await api.post(
|
||||
"/admin/products",
|
||||
{
|
||||
...medusaTshirtProduct,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "TEST",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
is_automatic: false,
|
||||
application_method: {
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "each",
|
||||
currency_code: "usd",
|
||||
value: 100,
|
||||
max_quantity: 100,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "subtotal",
|
||||
operator: "gte",
|
||||
values: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}`,
|
||||
{
|
||||
promo_codes: [response.data.promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const cartAfterPromotion = (
|
||||
await api.get(`/store/carts/${cart.id}`, storeHeaders)
|
||||
).data.cart
|
||||
|
||||
expect(cartAfterPromotion).toEqual(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
adjustments: [
|
||||
expect.objectContaining({
|
||||
code: response.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
code: response.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const cartWithPromotion2 = (
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/line-items`,
|
||||
{ variant_id: product.variants[0].id, quantity: 40 },
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cartWithPromotion2).toEqual(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
adjustments: [
|
||||
expect.objectContaining({
|
||||
code: response.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
code: response.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}`,
|
||||
{
|
||||
promo_codes: [],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const cartWithoutPromotion = (
|
||||
await api.get(`/store/carts/${cart.id}`, storeHeaders)
|
||||
).data.cart
|
||||
|
||||
expect(cartWithoutPromotion).toEqual(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
adjustments: [],
|
||||
}),
|
||||
],
|
||||
promotions: [],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it.skip("should add two promotions and remove one from cart using delete", async () => {
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
const salesChannel = (
|
||||
await api.post(
|
||||
"/admin/sales-channels",
|
||||
{ name: "Webshop", description: "channel" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.sales_channel
|
||||
|
||||
const region = (
|
||||
await api.post(
|
||||
"/admin/regions",
|
||||
{ name: "US", currency_code: "usd", countries: ["us"] },
|
||||
adminHeaders
|
||||
)
|
||||
).data.region
|
||||
|
||||
const product = (
|
||||
await api.post(
|
||||
"/admin/products",
|
||||
{
|
||||
...medusaTshirtProduct,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
const promo1 = await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "TEST",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
is_automatic: false,
|
||||
application_method: {
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "each",
|
||||
currency_code: "usd",
|
||||
value: 100,
|
||||
max_quantity: 100,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "subtotal",
|
||||
operator: "gte",
|
||||
values: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
const promo2 = await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "TEST2",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
is_automatic: false,
|
||||
application_method: {
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "each",
|
||||
currency_code: "usd",
|
||||
value: 100,
|
||||
max_quantity: 100,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "subtotal",
|
||||
operator: "gte",
|
||||
values: "2000",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
const cartWithPromotion2 = (
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/line-items`,
|
||||
{ variant_id: product.variants[0].id, quantity: 40 },
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cartWithPromotion2).toEqual(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
adjustments: [
|
||||
expect.objectContaining({
|
||||
code: promo1.data.promotion.code,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: promo2.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
code: promo1.data.promotion.code,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: promo2.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
await api.delete(`/store/carts/${cart.id}/promotions`, {
|
||||
data: {
|
||||
promo_codes: [promo1.data.promotion.code],
|
||||
},
|
||||
...storeHeaders,
|
||||
})
|
||||
|
||||
const cartWithoutPromotion1 = (
|
||||
await api.get(`/store/carts/${cart.id}`, storeHeaders)
|
||||
).data.cart
|
||||
|
||||
expect(cartWithoutPromotion1).toEqual(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
adjustments: [
|
||||
expect.objectContaining({
|
||||
code: promo2.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
code: promo2.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it.skip("should add two promotions and remove one from cart using update", async () => {
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
const salesChannel = (
|
||||
await api.post(
|
||||
"/admin/sales-channels",
|
||||
{ name: "Webshop", description: "channel" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.sales_channel
|
||||
|
||||
const region = (
|
||||
await api.post(
|
||||
"/admin/regions",
|
||||
{ name: "US", currency_code: "usd", countries: ["us"] },
|
||||
adminHeaders
|
||||
)
|
||||
).data.region
|
||||
|
||||
const product = (
|
||||
await api.post(
|
||||
"/admin/products",
|
||||
{
|
||||
...medusaTshirtProduct,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
const [promo1, promo2, promoAutomatic] = await Promise.all([
|
||||
api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "TEST",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
is_automatic: false,
|
||||
application_method: {
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "each",
|
||||
currency_code: "usd",
|
||||
value: 50,
|
||||
max_quantity: 100,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "subtotal",
|
||||
operator: "gte",
|
||||
values: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
),
|
||||
api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "TEST_CODE_123",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
is_automatic: false,
|
||||
application_method: {
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "each",
|
||||
currency_code: "usd",
|
||||
value: 10,
|
||||
max_quantity: 100,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "subtotal",
|
||||
operator: "gte",
|
||||
values: "2000",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
),
|
||||
api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "AUTOMATIC_PROMO",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
is_automatic: true,
|
||||
application_method: {
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "each",
|
||||
currency_code: "usd",
|
||||
value: 5,
|
||||
max_quantity: 100,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "subtotal",
|
||||
operator: "gte",
|
||||
values: "500",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
),
|
||||
])
|
||||
|
||||
// apply promotions
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}`,
|
||||
{
|
||||
promo_codes: [
|
||||
promo1.data.promotion.code,
|
||||
promo2.data.promotion.code,
|
||||
],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const cartWithPromotion2 = (
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/line-items`,
|
||||
{
|
||||
variant_id: product.variants[0].id,
|
||||
quantity: 40,
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cartWithPromotion2).toEqual(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
adjustments: [
|
||||
expect.objectContaining({
|
||||
code: promo1.data.promotion.code,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: promo2.data.promotion.code,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: promoAutomatic.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
code: promo1.data.promotion.code,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: promo2.data.promotion.code,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: promoAutomatic.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}`,
|
||||
{
|
||||
promo_codes: [promo2.data.promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const cartWithoutPromotion1 = (
|
||||
await api.get(`/store/carts/${cart.id}`, storeHeaders)
|
||||
).data.cart
|
||||
|
||||
expect(cartWithoutPromotion1).toEqual(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
adjustments: [
|
||||
expect.objectContaining({
|
||||
code: promo2.data.promotion.code,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: promoAutomatic.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
code: promo2.data.promotion.code,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: promoAutomatic.data.promotion.code,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user