feat: promotion usage limit (#13760)
* feat: promotion usage limit * fix: update, refactor tests, parallel case * fix: batch update, cleanup unused map * feat: paralel campaign and promotion tests * chore: changesets, fix i18 schema * fix: ui tweaks * chore: refactor --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,980 @@
|
||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import { Modules, PromotionStatus, PromotionType } from "@medusajs/utils"
|
||||
import {
|
||||
createAdminUser,
|
||||
generatePublishableKey,
|
||||
generateStoreHeaders,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures/tax"
|
||||
import { medusaTshirtProduct } from "../../../__fixtures__/product"
|
||||
|
||||
jest.setTimeout(500000)
|
||||
|
||||
const adminHeaders = {
|
||||
headers: { "x-medusa-access-token": "test_token" },
|
||||
}
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
describe("Admin Promotions API - Promotion Limits", () => {
|
||||
let appContainer
|
||||
let promotion
|
||||
let product
|
||||
let region
|
||||
let salesChannel
|
||||
let storeHeaders
|
||||
let shippingProfile
|
||||
let stockLocation
|
||||
let fulfillmentSet
|
||||
let shippingOption
|
||||
|
||||
beforeAll(async () => {
|
||||
appContainer = getContainer()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
await setupTaxStructure(appContainer.resolve(Modules.TAX))
|
||||
|
||||
shippingProfile = (
|
||||
await api.post(
|
||||
`/admin/shipping-profiles`,
|
||||
{ name: "default", type: "default" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.shipping_profile
|
||||
|
||||
stockLocation = (
|
||||
await api.post(
|
||||
`/admin/stock-locations`,
|
||||
{ name: "test location" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.stock_location
|
||||
|
||||
const fulfillmentSets = (
|
||||
await api.post(
|
||||
`/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`,
|
||||
{
|
||||
name: `Test-inventory`,
|
||||
type: "test-type",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.stock_location.fulfillment_sets
|
||||
|
||||
fulfillmentSet = (
|
||||
await api.post(
|
||||
`/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`,
|
||||
{
|
||||
name: `Test-inventory`,
|
||||
geo_zones: [{ type: "country", country_code: "US" }],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.fulfillment_set
|
||||
|
||||
await api.post(
|
||||
`/admin/stock-locations/${stockLocation.id}/fulfillment-providers`,
|
||||
{ add: ["manual_test-provider"] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
shippingOption = (
|
||||
await api.post(
|
||||
`/admin/shipping-options`,
|
||||
{
|
||||
name: `Test shipping option ${fulfillmentSet.id}`,
|
||||
service_zone_id: fulfillmentSet.service_zones[0].id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
provider_id: "manual_test-provider",
|
||||
price_type: "flat",
|
||||
type: {
|
||||
label: "Test type",
|
||||
description: "Test description",
|
||||
code: "test-code",
|
||||
},
|
||||
prices: [{ currency_code: "usd", amount: 1000 }],
|
||||
rules: [],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.shipping_option
|
||||
|
||||
product = (
|
||||
await api.post(
|
||||
`/admin/products`,
|
||||
{ ...medusaTshirtProduct, shipping_profile_id: shippingProfile.id },
|
||||
adminHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
region = (
|
||||
await api.post(
|
||||
`/admin/regions`,
|
||||
{
|
||||
name: "Test Region",
|
||||
currency_code: "usd",
|
||||
countries: ["us"],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.region
|
||||
|
||||
salesChannel = (
|
||||
await api.post(
|
||||
`/admin/sales-channels`,
|
||||
{ name: "Test Sales Channel" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.sales_channel
|
||||
|
||||
await api.post(
|
||||
`/admin/stock-locations/${stockLocation.id}/sales-channels`,
|
||||
{ add: [salesChannel.id] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
})
|
||||
|
||||
describe("Create promotion with limit", () => {
|
||||
it("should create a promotion with a usage limit", async () => {
|
||||
const response = await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "LIMITED_PROMO",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
limit: 5,
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "across",
|
||||
value: 100,
|
||||
currency_code: "usd",
|
||||
},
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.data.promotion).toEqual(
|
||||
expect.objectContaining({
|
||||
code: "LIMITED_PROMO",
|
||||
limit: 5,
|
||||
used: 0,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should create a promotion without a limit (unlimited)", async () => {
|
||||
const response = await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "UNLIMITED_PROMO",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "across",
|
||||
value: 100,
|
||||
currency_code: "usd",
|
||||
},
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.data.promotion).toEqual(
|
||||
expect.objectContaining({
|
||||
code: "UNLIMITED_PROMO",
|
||||
limit: null,
|
||||
used: 0,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should prevent creating automatic promotion with limit", async () => {
|
||||
const response = await api
|
||||
.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "AUTO_PROMO",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
is_automatic: true,
|
||||
limit: 5,
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "across",
|
||||
value: 100,
|
||||
currency_code: "usd",
|
||||
},
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((err) => {
|
||||
return err.response
|
||||
})
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(response.data.message).toContain(
|
||||
"Automatic promotions cannot have a usage limit"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Complete order increments usage", () => {
|
||||
beforeEach(async () => {
|
||||
promotion = (
|
||||
await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "TEST_LIMIT",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
limit: 3,
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "across",
|
||||
value: 100,
|
||||
currency_code: "usd",
|
||||
},
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.promotion
|
||||
})
|
||||
|
||||
it("should increment used count when order is completed", async () => {
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cart.promotions).toHaveLength(1)
|
||||
expect(cart.promotions[0].code).toBe(promotion.code)
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/shipping-methods`,
|
||||
{ option_id: shippingOption.id },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const paymentCollection = (
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{ cart_id: cart.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const order = (
|
||||
await api.post(`/store/carts/${cart.id}/complete`, {}, storeHeaders)
|
||||
).data.order
|
||||
|
||||
expect(order).toBeDefined()
|
||||
|
||||
const updatedPromotion = (
|
||||
await api.get(`/admin/promotions/${promotion.id}`, adminHeaders)
|
||||
).data.promotion
|
||||
|
||||
expect(updatedPromotion.used).toBe(1)
|
||||
})
|
||||
|
||||
it("should not increment used count when promotion is only added to cart", async () => {
|
||||
// Create cart with promotion but don't complete
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cart.promotions).toHaveLength(1)
|
||||
|
||||
// Check promotion usage was NOT incremented
|
||||
const updatedPromotion = (
|
||||
await api.get(`/admin/promotions/${promotion.id}`, adminHeaders)
|
||||
).data.promotion
|
||||
|
||||
expect(updatedPromotion.used).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Limit enforcement on cart completion", () => {
|
||||
beforeEach(async () => {
|
||||
promotion = (
|
||||
await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "LIMIT_2",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
limit: 2,
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "across",
|
||||
value: 100,
|
||||
currency_code: "usd",
|
||||
},
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.promotion
|
||||
})
|
||||
|
||||
it("should allow completing 2 orders successfully", async () => {
|
||||
// Complete first cart
|
||||
const cart1 = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
// Setup first cart
|
||||
await api.post(
|
||||
`/store/carts/${cart1.id}/shipping-methods`,
|
||||
{ option_id: shippingOption.id },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const paymentCollection1 = (
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{ cart_id: cart1.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection1.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
await api.post(`/store/carts/${cart1.id}/complete`, {}, storeHeaders)
|
||||
|
||||
// Complete second cart
|
||||
const cart2 = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
// Setup second cart
|
||||
await api.post(
|
||||
`/store/carts/${cart2.id}/shipping-methods`,
|
||||
{ option_id: shippingOption.id },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const paymentCollection2 = (
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{ cart_id: cart2.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection2.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
await api.post(`/store/carts/${cart2.id}/complete`, {}, storeHeaders)
|
||||
|
||||
const updatedPromotion = (
|
||||
await api.get(`/admin/promotions/${promotion.id}`, adminHeaders)
|
||||
).data.promotion
|
||||
|
||||
expect(updatedPromotion.used).toBe(2)
|
||||
})
|
||||
|
||||
it("should not add promotion to the third cart when limit is exceeded", async () => {
|
||||
// Complete first two orders
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/shipping-methods`,
|
||||
{ option_id: shippingOption.id },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const paymentCollection = (
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{ cart_id: cart.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
await api.post(`/store/carts/${cart.id}/complete`, {}, storeHeaders)
|
||||
}
|
||||
|
||||
// Third cart should fail
|
||||
const cart3 = (
|
||||
await api.post(
|
||||
`/store/carts?fields=*promotions.*`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cart3.promotions).toHaveLength(0) // promotion cannot be appleied since action "PROMOTION EXCEEDED LIMIT" is returned
|
||||
})
|
||||
|
||||
it("should fail third cart completion with limit exceeded", async () => {
|
||||
const carts = [] as any[]
|
||||
// Complete first two orders
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/shipping-methods`,
|
||||
{ option_id: shippingOption.id },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const paymentCollection = (
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{ cart_id: cart.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
carts.push(cart)
|
||||
}
|
||||
|
||||
// complete first 2 carts
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await api.post(
|
||||
`/store/carts/${carts[i].id}/complete`,
|
||||
{},
|
||||
storeHeaders
|
||||
)
|
||||
}
|
||||
|
||||
// Third cart should fail
|
||||
const cart3 = carts[2]
|
||||
|
||||
const response = await api
|
||||
.post(`/store/carts/${cart3.id}/complete`, {}, storeHeaders)
|
||||
.catch((err) => {
|
||||
return err.response
|
||||
})
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(response.data.message).toContain(
|
||||
"Promotion usage exceeds the limit"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Update limit validation", () => {
|
||||
beforeEach(async () => {
|
||||
promotion = (
|
||||
await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "UPDATE_LIMIT",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
limit: 10,
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "across",
|
||||
value: 100,
|
||||
currency_code: "usd",
|
||||
},
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.promotion
|
||||
})
|
||||
|
||||
it("should allow updating limit to higher value", async () => {
|
||||
const response = await api.post(
|
||||
`/admin/promotions/${promotion.id}`,
|
||||
{
|
||||
limit: 20,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.data.promotion.limit).toBe(20)
|
||||
})
|
||||
|
||||
it("should prevent updating limit to less than current usage", async () => {
|
||||
// Complete two order to set used = 2
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/shipping-methods`,
|
||||
{ option_id: shippingOption.id },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const paymentCollection = (
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{ cart_id: cart.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
await api.post(`/store/carts/${cart.id}/complete`, {}, storeHeaders)
|
||||
}
|
||||
|
||||
// Try to update limit to 0 (less than used = 2)
|
||||
const response = await api
|
||||
.post(
|
||||
`/admin/promotions/${promotion.id}`,
|
||||
{
|
||||
limit: 1,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((err) => {
|
||||
return err.response
|
||||
})
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(response.data.message).toContain(
|
||||
"cannot be less than current usage"
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow updating limit to 2 when used is 1", async () => {
|
||||
// Complete one order to set used = 1
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [promotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/shipping-methods`,
|
||||
{ option_id: shippingOption.id },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const paymentCollection = (
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{ cart_id: cart.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
await api.post(`/store/carts/${cart.id}/complete`, {}, storeHeaders)
|
||||
|
||||
// Update limit to 2
|
||||
const response = await api.post(
|
||||
`/admin/promotions/${promotion.id}`,
|
||||
{
|
||||
limit: 2,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.data.promotion.limit).toBe(2)
|
||||
expect(response.data.promotion.used).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Both campaign and promotion limits", () => {
|
||||
let campaign
|
||||
let campaignPromotion
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create campaign with budget limit of 3
|
||||
campaign = (
|
||||
await api.post(
|
||||
`/admin/campaigns`,
|
||||
{
|
||||
name: "Test Campaign",
|
||||
campaign_identifier: "test-campaign",
|
||||
budget: {
|
||||
type: "usage",
|
||||
limit: 3,
|
||||
},
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.campaign
|
||||
|
||||
// Create promotion with limit of 2
|
||||
campaignPromotion = (
|
||||
await api.post(
|
||||
`/admin/promotions`,
|
||||
{
|
||||
code: "CAMPAIGN_LIMIT",
|
||||
type: PromotionType.STANDARD,
|
||||
status: PromotionStatus.ACTIVE,
|
||||
limit: 2,
|
||||
campaign_id: campaign.id,
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "across",
|
||||
value: 100,
|
||||
currency_code: "usd",
|
||||
},
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.promotion
|
||||
})
|
||||
|
||||
it("should hit promotion limit first ", async () => {
|
||||
// Complete 2 orders - should hit promotion limit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [campaignPromotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/shipping-methods`,
|
||||
{ option_id: shippingOption.id },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const paymentCollection = (
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{ cart_id: cart.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
await api.post(`/store/carts/${cart.id}/complete`, {}, storeHeaders)
|
||||
}
|
||||
|
||||
// Third order should fail with promotion limit exceeded
|
||||
const cart3 = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [campaignPromotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cart3.promotions).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should hit campaign limit first", async () => {
|
||||
await api.post(
|
||||
`/admin/promotions/${campaignPromotion.id}`,
|
||||
{
|
||||
limit: 5,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
// Complete 3 orders - should hit campaign limit
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const cart = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [campaignPromotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cart.id}/shipping-methods`,
|
||||
{ option_id: shippingOption.id },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const paymentCollection = (
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{ cart_id: cart.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
await api.post(`/store/carts/${cart.id}/complete`, {}, storeHeaders)
|
||||
}
|
||||
|
||||
const cart4 = (
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
sales_channel_id: salesChannel.id,
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "SF",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "94016",
|
||||
},
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
promo_codes: [campaignPromotion.code],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
expect(cart4.promotions).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user