feat(promotion,dashboard,types,utils,medusa): Add statuses to promotions (#10950)

what:

- adds a status column to promotion table
- introduce active promotion query
- scope revert, register and compute actions to active promotions
- admin to create and update promotion with statuses

RESOLVES CMRC-845
RESOLVES CMRC-846
RESOLVES CMRC-847
RESOLVES CMRC-848
RESOLVES CMRC-849
RESOLVES CMRC-850
This commit is contained in:
Riqwan Thamir authored and GitHub committed 2025-01-16 19:17:22 +00:00
1 parent effee5c8bb
commit 5eab9e7399
35 files changed
+1208 -1743

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
---
"@medusajs/promotion": patch
"@medusajs/dashboard": patch
"@medusajs/types": patch
"@medusajs/utils": patch
"@medusajs/medusa": patch
---
feat(promotion,dashboard,types,utils,medusa): Add statuses to promotions
@@ -1,5 +1,9 @@
import { CampaignBudgetType, PromotionType } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
CampaignBudgetType,
PromotionStatus,
PromotionType,
} from "@medusajs/utils"
import { createAdminUser } from "../../../../helpers/create-admin-user" import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000) jest.setTimeout(50000)
@@ -46,6 +50,7 @@ export const campaignsData = [
const promotionData = { const promotionData = {
code: "TEST", code: "TEST",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
is_automatic: true, is_automatic: true,
application_method: { application_method: {
target_type: "items", target_type: "items",
@@ -108,6 +113,7 @@ medusaIntegrationTestRunner({
return { return {
code, code,
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
is_automatic: true, is_automatic: true,
application_method: { application_method: {
target_type: "items", target_type: "items",
@@ -5,6 +5,7 @@ import {
PriceListType, PriceListType,
ProductStatus, ProductStatus,
PromotionRuleOperator, PromotionRuleOperator,
PromotionStatus,
PromotionType, PromotionType,
} from "@medusajs/utils" } from "@medusajs/utils"
import { import {
@@ -176,6 +177,7 @@ medusaIntegrationTestRunner({
{ {
code: "PROMOTION_APPLIED", code: "PROMOTION_APPLIED",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "items", target_type: "items",
@@ -488,6 +490,65 @@ medusaIntegrationTestRunner({
) )
}) })
it("should remove promotions when promotion is no longer in active state", async () => {
let responseBeforePromotionUpdate = await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: product.variants[0].id,
quantity: 1,
},
storeHeaders
)
expect(responseBeforePromotionUpdate.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
items: expect.arrayContaining([
expect.objectContaining({
adjustments: [
{
id: expect.any(String),
code: "PROMOTION_APPLIED",
promotion_id: promotion.id,
amount: 100,
},
],
}),
]),
})
)
await api.post(
`/admin/promotions/${promotion.id}`,
{ status: PromotionStatus.INACTIVE },
adminHeaders
)
let responseAfterPromotionUpdate = await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: product.variants[0].id,
quantity: 1,
},
storeHeaders
)
expect(responseAfterPromotionUpdate.status).toEqual(200)
expect(responseAfterPromotionUpdate.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
items: expect.arrayContaining([
expect.objectContaining({
adjustments: [],
}),
expect.objectContaining({
adjustments: [],
}),
]),
})
)
})
describe("with custom shipping options prices", () => { describe("with custom shipping options prices", () => {
beforeEach(async () => { beforeEach(async () => {
cart = ( cart = (
@@ -1205,6 +1266,7 @@ medusaIntegrationTestRunner({
{ {
code: "PROMOTION_TEST", code: "PROMOTION_TEST",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "items", target_type: "items",
@@ -1,5 +1,5 @@
import { PromotionType } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { PromotionStatus, PromotionType } from "@medusajs/utils"
import { createAdminUser } from "../../../../helpers/create-admin-user" import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000) jest.setTimeout(50000)
@@ -8,6 +8,43 @@ const adminHeaders = {
headers: { "x-medusa-access-token": "test_token" }, headers: { "x-medusa-access-token": "test_token" },
} }
const standardPromotionPayload = {
code: "TEST",
type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
is_automatic: true,
campaign: {
name: "test",
campaign_identifier: "test-1",
budget: {
type: "usage",
limit: 100,
},
},
application_method: {
target_type: "items",
type: "fixed",
allocation: "each",
currency_code: "USD",
value: 100,
max_quantity: 100,
target_rules: [
{
attribute: "test.test",
operator: "eq",
values: ["test1", "test2"],
},
],
},
rules: [
{
attribute: "test.test",
operator: "eq",
values: ["test1", "test2"],
},
],
}
medusaIntegrationTestRunner({ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
describe("Admin Promotions API", () => { describe("Admin Promotions API", () => {
@@ -34,6 +71,7 @@ medusaIntegrationTestRunner({
{ {
code: "TEST_ACROSS", code: "TEST_ACROSS",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
application_method: { application_method: {
type: "fixed", type: "fixed",
allocation: "across", allocation: "across",
@@ -120,6 +158,7 @@ medusaIntegrationTestRunner({
{ {
code: "first", code: "first",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "order", target_type: "order",
@@ -183,41 +222,7 @@ medusaIntegrationTestRunner({
it("should create a standard promotion successfully", async () => { it("should create a standard promotion successfully", async () => {
const response = await api.post( const response = await api.post(
`/admin/promotions`, `/admin/promotions`,
{ standardPromotionPayload,
code: "TEST",
type: PromotionType.STANDARD,
is_automatic: true,
campaign: {
name: "test",
campaign_identifier: "test-1",
budget: {
type: "usage",
limit: 100,
},
},
application_method: {
target_type: "items",
type: "fixed",
allocation: "each",
currency_code: "USD",
value: 100,
max_quantity: 100,
target_rules: [
{
attribute: "test.test",
operator: "eq",
values: ["test1", "test2"],
},
],
},
rules: [
{
attribute: "test.test",
operator: "eq",
values: ["test1", "test2"],
},
],
},
adminHeaders adminHeaders
) )
@@ -275,6 +280,7 @@ medusaIntegrationTestRunner({
{ {
code: "TEST", code: "TEST",
type: PromotionType.BUYGET, type: PromotionType.BUYGET,
status: PromotionStatus.ACTIVE,
is_automatic: true, is_automatic: true,
application_method: { application_method: {
target_type: "items", target_type: "items",
@@ -316,6 +322,7 @@ medusaIntegrationTestRunner({
{ {
code: "TEST", code: "TEST",
type: PromotionType.BUYGET, type: PromotionType.BUYGET,
status: PromotionStatus.ACTIVE,
is_automatic: true, is_automatic: true,
application_method: { application_method: {
target_type: "items", target_type: "items",
@@ -356,6 +363,7 @@ medusaIntegrationTestRunner({
{ {
code: "TEST", code: "TEST",
type: PromotionType.BUYGET, type: PromotionType.BUYGET,
status: PromotionStatus.ACTIVE,
is_automatic: true, is_automatic: true,
campaign: { campaign: {
name: "test", name: "test",
@@ -457,6 +465,23 @@ medusaIntegrationTestRunner({
}) })
) )
}) })
it("should throw error when an incorrect status is passed", async () => {
const { response } = await api
.post(
`/admin/promotions`,
{ ...standardPromotionPayload, status: "does-not-exist" },
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message:
"Invalid request: Expected: 'draft, active, inactive' for field 'status', but got: 'does-not-exist'",
})
})
}) })
describe("DELETE /admin/promotions/:id", () => { describe("DELETE /admin/promotions/:id", () => {
@@ -491,7 +516,7 @@ medusaIntegrationTestRunner({
expect(response.status).toEqual(404) expect(response.status).toEqual(404)
expect(response.data.message).toEqual( expect(response.data.message).toEqual(
`Promotion with id "does-not-exist" not found` `Promotion id not found: does-not-exist`
) )
}) })
@@ -1,11 +1,12 @@
import { RemoteLink } from "@medusajs/modules-sdk" import { RemoteLink } from "@medusajs/modules-sdk"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { ICartModuleService, IPromotionModuleService } from "@medusajs/types" import { ICartModuleService, IPromotionModuleService } from "@medusajs/types"
import { import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
Modules, Modules,
PromotionStatus,
PromotionType, PromotionType,
} from "@medusajs/utils" } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { import {
adminHeaders, adminHeaders,
createAdminUser, createAdminUser,
@@ -48,11 +49,12 @@ medusaIntegrationTestRunner({
await promotionModuleService.createPromotions({ await promotionModuleService.createPromotions({
code: "PROMOTION_APPLIED", code: "PROMOTION_APPLIED",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "items", target_type: "items",
allocation: "each", allocation: "each",
value: "300", value: 300,
apply_to_quantity: 1, apply_to_quantity: 1,
currency_code: "usd", currency_code: "usd",
max_quantity: 1, max_quantity: 1,
@@ -70,11 +72,12 @@ medusaIntegrationTestRunner({
await promotionModuleService.createPromotions({ await promotionModuleService.createPromotions({
code: "PROMOTION_TEST", code: "PROMOTION_TEST",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "items", target_type: "items",
allocation: "across", allocation: "across",
value: "1000", value: 1000,
apply_to_quantity: 1, apply_to_quantity: 1,
currency_code: "usd", currency_code: "usd",
target_rules: [ target_rules: [
@@ -165,6 +168,7 @@ medusaIntegrationTestRunner({
{ {
code: "PROMOTION_APPLIED", code: "PROMOTION_APPLIED",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
rules: [ rules: [
{ {
attribute: "customer_id", attribute: "customer_id",
@@ -181,7 +185,7 @@ medusaIntegrationTestRunner({
type: "fixed", type: "fixed",
target_type: "shipping_methods", target_type: "shipping_methods",
allocation: "each", allocation: "each",
value: "100", value: 100,
max_quantity: 1, max_quantity: 1,
currency_code: "usd", currency_code: "usd",
target_rules: [ target_rules: [
@@ -199,6 +203,7 @@ medusaIntegrationTestRunner({
{ {
code: "PROMOTION_NEW", code: "PROMOTION_NEW",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
rules: [ rules: [
{ {
attribute: "customer_id", attribute: "customer_id",
@@ -215,7 +220,7 @@ medusaIntegrationTestRunner({
type: "fixed", type: "fixed",
target_type: "shipping_methods", target_type: "shipping_methods",
allocation: "each", allocation: "each",
value: "200", value: 200,
max_quantity: 1, max_quantity: 1,
currency_code: "usd", currency_code: "usd",
target_rules: [ target_rules: [
@@ -19,6 +19,7 @@ import {
MedusaError, MedusaError,
Modules, Modules,
ProductStatus, ProductStatus,
PromotionStatus,
PromotionType, PromotionType,
} from "@medusajs/utils" } from "@medusajs/utils"
import { import {
@@ -1266,6 +1267,7 @@ medusaIntegrationTestRunner({
{ {
code: "TEST", code: "TEST",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
is_automatic: true, is_automatic: true,
campaign: { campaign: {
campaign_identifier: "test", campaign_identifier: "test",
@@ -1,11 +1,12 @@
import { RemoteLink } from "@medusajs/modules-sdk" import { RemoteLink } from "@medusajs/modules-sdk"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { ICartModuleService, IPromotionModuleService } from "@medusajs/types" import { ICartModuleService, IPromotionModuleService } from "@medusajs/types"
import { import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
Modules, Modules,
PromotionStatus,
PromotionType, PromotionType,
} from "@medusajs/utils" } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { import {
adminHeaders, adminHeaders,
createAdminUser, createAdminUser,
@@ -48,11 +49,12 @@ medusaIntegrationTestRunner({
await promotionModuleService.createPromotions({ await promotionModuleService.createPromotions({
code: "PROMOTION_APPLIED", code: "PROMOTION_APPLIED",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "items", target_type: "items",
allocation: "each", allocation: "each",
value: "300", value: 300,
apply_to_quantity: 1, apply_to_quantity: 1,
currency_code: "usd", currency_code: "usd",
max_quantity: 1, max_quantity: 1,
@@ -70,11 +72,12 @@ medusaIntegrationTestRunner({
await promotionModuleService.createPromotions({ await promotionModuleService.createPromotions({
code: "PROMOTION_APPLIED_TO_REMOVE", code: "PROMOTION_APPLIED_TO_REMOVE",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "items", target_type: "items",
allocation: "each", allocation: "each",
value: "300", value: 300,
apply_to_quantity: 1, apply_to_quantity: 1,
currency_code: "usd", currency_code: "usd",
max_quantity: 1, max_quantity: 1,
@@ -180,6 +183,7 @@ medusaIntegrationTestRunner({
const appliedPromotion = const appliedPromotion =
await promotionModuleService.createPromotions({ await promotionModuleService.createPromotions({
code: "PROMOTION_APPLIED", code: "PROMOTION_APPLIED",
status: PromotionStatus.ACTIVE,
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
rules: [ rules: [
{ {
@@ -197,7 +201,7 @@ medusaIntegrationTestRunner({
type: "fixed", type: "fixed",
target_type: "shipping_methods", target_type: "shipping_methods",
allocation: "each", allocation: "each",
value: "100", value: 100,
max_quantity: 1, max_quantity: 1,
currency_code: "usd", currency_code: "usd",
target_rules: [ target_rules: [
@@ -214,6 +218,7 @@ medusaIntegrationTestRunner({
await promotionModuleService.createPromotions({ await promotionModuleService.createPromotions({
code: "PROMOTION_APPLIED_TO_REMOVE", code: "PROMOTION_APPLIED_TO_REMOVE",
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
rules: [ rules: [
{ {
attribute: "customer_id", attribute: "customer_id",
@@ -230,7 +235,7 @@ medusaIntegrationTestRunner({
type: "fixed", type: "fixed",
target_type: "shipping_methods", target_type: "shipping_methods",
allocation: "each", allocation: "each",
value: "100", value: 100,
max_quantity: 1, max_quantity: 1,
currency_code: "usd", currency_code: "usd",
target_rules: [ target_rules: [
@@ -202,7 +202,7 @@ const useDynamicSearchResults = (
{ {
q: debouncedSearch, q: debouncedSearch,
limit, limit,
fields: "id,code", fields: "id,code,status",
}, },
{ {
enabled: isAreaEnabled(currentArea, "promotion"), enabled: isAreaEnabled(currentArea, "promotion"),
@@ -1,28 +1,13 @@
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { useTranslation } from "react-i18next" import { getPromotionStatus } from "../../../../../lib/promotions"
import {
getPromotionStatus,
PromotionStatus,
} from "../../../../../lib/promotions"
import { StatusCell as StatusCell_ } from "../../common/status-cell" import { StatusCell as StatusCell_ } from "../../common/status-cell"
type PromotionCellProps = { type PromotionCellProps = {
promotion: HttpTypes.AdminPromotion promotion: HttpTypes.AdminPromotion
} }
type StatusColors = "grey" | "orange" | "green" | "red"
type StatusMap = Record<string, [StatusColors, string]>
export const StatusCell = ({ promotion }: PromotionCellProps) => { export const StatusCell = ({ promotion }: PromotionCellProps) => {
const { t } = useTranslation() const [color, text] = getPromotionStatus(promotion)
const statusMap: StatusMap = {
[PromotionStatus.DISABLED]: ["grey", t("statuses.disabled")],
[PromotionStatus.ACTIVE]: ["green", t("statuses.active")],
[PromotionStatus.SCHEDULED]: ["orange", t("statuses.scheduled")],
[PromotionStatus.EXPIRED]: ["red", t("statuses.expired")],
}
const [color, text] = statusMap[getPromotionStatus(promotion)]
return <StatusCell_ color={color}>{text}</StatusCell_> return <StatusCell_ color={color}>{text}</StatusCell_>
} }
@@ -1,3 +1,4 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes, LinkMethodRequest } from "@medusajs/types" import { HttpTypes, LinkMethodRequest } from "@medusajs/types"
import { import {
QueryKey, QueryKey,
@@ -10,7 +11,6 @@ import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client" import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory" import { queryKeysFactory } from "../../lib/query-key-factory"
import { promotionsQueryKeys } from "./promotions" import { promotionsQueryKeys } from "./promotions"
import { FetchError } from "@medusajs/js-sdk"
const REGIONS_QUERY_KEY = "campaigns" as const const REGIONS_QUERY_KEY = "campaigns" as const
export const campaignsQueryKeys = queryKeysFactory(REGIONS_QUERY_KEY) export const campaignsQueryKeys = queryKeysFactory(REGIONS_QUERY_KEY)
@@ -88,6 +88,8 @@ export const useUpdateCampaign = (
onSuccess: (data, variables, context) => { onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() }) queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.details() }) queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.details() })
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.details() })
options?.onSuccess?.(data, variables, context) options?.onSuccess?.(data, variables, context)
}, },
File diff suppressed because it is too large. Load diff
@@ -1933,7 +1933,19 @@
} }
}, },
"status": { "status": {
"title": "Status" "label": "Status",
"draft": {
"title": "Draft",
"description": "Customers will not be able to use the code yet"
},
"active": {
"title": "Active",
"description": "Customers will be able to use the code"
},
"inactive": {
"title": "Inactive",
"description": "Customers will no longer be able to use the code"
}
}, },
"method": { "method": {
"label": "Method", "label": "Method",
@@ -2676,6 +2688,8 @@
"scheduled": "Scheduled", "scheduled": "Scheduled",
"expired": "Expired", "expired": "Expired",
"active": "Active", "active": "Active",
"inactive": "Inactive",
"draft": "Draft",
"enabled": "Enabled", "enabled": "Enabled",
"disabled": "Disabled" "disabled": "Disabled"
}, },
+24 -6
View File
@@ -1,10 +1,28 @@
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { i18n } from "../components/utilities/i18n"
export enum PromotionStatus { export enum PromotionStatus {
SCHEDULED = "SCHEDULED", SCHEDULED = "SCHEDULED",
EXPIRED = "EXPIRED", EXPIRED = "EXPIRED",
ACTIVE = "ACTIVE", ACTIVE = "ACTIVE",
DISABLED = "DISABLED", INACTIVE = "INACTIVE",
DRAFT = "DRAFT",
}
export type StatusColors = "grey" | "orange" | "green" | "red" | "grey"
export type StatusMap = Record<string, [StatusColors, string]>
export const promotionStatusMap: StatusMap = {
[PromotionStatus.ACTIVE]: ["green", i18n.t("statuses.active")],
[PromotionStatus.INACTIVE]: ["red", i18n.t("statuses.inactive")],
[PromotionStatus.DRAFT]: ["grey", i18n.t("statuses.draft")],
[PromotionStatus.SCHEDULED]: [
"orange",
`${i18n.t("promotions.fields.campaign")} ${i18n.t("statuses.scheduled").toLowerCase()}`,
],
[PromotionStatus.EXPIRED]: [
"red",
`${i18n.t("promotions.fields.campaign")} ${i18n.t("statuses.expired").toLowerCase()}`,
],
} }
export const getPromotionStatus = (promotion: HttpTypes.AdminPromotion) => { export const getPromotionStatus = (promotion: HttpTypes.AdminPromotion) => {
@@ -12,11 +30,11 @@ export const getPromotionStatus = (promotion: HttpTypes.AdminPromotion) => {
const campaign = promotion.campaign const campaign = promotion.campaign
if (!campaign) { if (!campaign) {
return PromotionStatus.ACTIVE return promotionStatusMap[promotion.status!.toUpperCase()]
} }
if (new Date(campaign.starts_at!) > date) { if (campaign.starts_at && new Date(campaign.starts_at!) > date) {
return PromotionStatus.SCHEDULED return promotionStatusMap[PromotionStatus.SCHEDULED]
} }
const campaignBudget = campaign.budget const campaignBudget = campaign.budget
@@ -24,8 +42,8 @@ export const getPromotionStatus = (promotion: HttpTypes.AdminPromotion) => {
campaignBudget && campaignBudget.used! > campaignBudget.limit! campaignBudget && campaignBudget.used! > campaignBudget.limit!
if ((campaign.ends_at && new Date(campaign.ends_at) < date) || overBudget) { if ((campaign.ends_at && new Date(campaign.ends_at) < date) || overBudget) {
return PromotionStatus.EXPIRED return promotionStatusMap[PromotionStatus.EXPIRED]
} }
return PromotionStatus.ACTIVE return promotionStatusMap[promotion.status!.toUpperCase()]
} }
@@ -1,4 +1,12 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import {
ApplicationMethodAllocationValues,
ApplicationMethodTargetTypeValues,
ApplicationMethodTypeValues,
PromotionRuleOperatorValues,
PromotionStatusValues,
PromotionTypeValues,
} from "@medusajs/types"
import { import {
Alert, Alert,
Badge, Badge,
@@ -17,14 +25,6 @@ import { useEffect, useMemo, useState } from "react"
import { useForm, useWatch } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next" import { Trans, useTranslation } from "react-i18next"
import { z } from "zod" import { z } from "zod"
import {
ApplicationMethodAllocationValues,
ApplicationMethodTargetTypeValues,
ApplicationMethodTypeValues,
PromotionRuleOperatorValues,
PromotionTypeValues,
} from "@medusajs/types"
import { Divider } from "../../../../../components/common/divider" import { Divider } from "../../../../../components/common/divider"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import { DeprecatedPercentageInput } from "../../../../../components/inputs/percentage-input" import { DeprecatedPercentageInput } from "../../../../../components/inputs/percentage-input"
@@ -50,6 +50,7 @@ const defaultValues = {
is_automatic: "false", is_automatic: "false",
code: "", code: "",
type: "standard" as PromotionTypeValues, type: "standard" as PromotionTypeValues,
status: "draft" as PromotionStatusValues,
rules: [], rules: [],
application_method: { application_method: {
allocation: "each" as ApplicationMethodAllocationValues, allocation: "each" as ApplicationMethodAllocationValues,
@@ -508,6 +509,48 @@ export const CreatePromotionForm = () => {
}} }}
/> />
<Form.Field
control={form.control}
name="status"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.form.status.label")}
</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
value={field.value}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"draft"}
label={t("promotions.form.status.draft.title")}
description={t(
"promotions.form.status.draft.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"active"}
label={t("promotions.form.status.active.title")}
description={t(
"promotions.form.status.active.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div className="flex gap-y-4"> <div className="flex gap-y-4">
<Form.Field <Form.Field
control={form.control} control={form.control}
@@ -25,6 +25,7 @@ export const CreatePromotionSchema = z
is_automatic: z.string().toLowerCase(), is_automatic: z.string().toLowerCase(),
code: z.string().min(1), code: z.string().min(1),
type: z.enum(["buyget", "standard"]), type: z.enum(["buyget", "standard"]),
status: z.enum(["draft", "active", "inactive"]),
rules: RuleSchema, rules: RuleSchema,
application_method: z.object({ application_method: z.object({
allocation: z.enum(["each", "across"]), allocation: z.enum(["each", "across"]),
@@ -16,10 +16,7 @@ import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeletePromotion } from "../../../../../hooks/api/promotions" import { useDeletePromotion } from "../../../../../hooks/api/promotions"
import { formatCurrency } from "../../../../../lib/format-currency" import { formatCurrency } from "../../../../../lib/format-currency"
import { formatPercentage } from "../../../../../lib/percentage-helpers" import { formatPercentage } from "../../../../../lib/percentage-helpers"
import { import { getPromotionStatus } from "../../../../../lib/promotions"
getPromotionStatus,
PromotionStatus,
} from "../../../../../lib/promotions"
type PromotionGeneralSectionProps = { type PromotionGeneralSectionProps = {
promotion: HttpTypes.AdminPromotion promotion: HttpTypes.AdminPromotion
@@ -78,16 +75,7 @@ export const PromotionGeneralSection = ({
}) })
} }
const [color, text] = { const [color, text] = getPromotionStatus(promotion)
[PromotionStatus.DISABLED]: ["grey", t("statuses.disabled")],
[PromotionStatus.ACTIVE]: ["green", t("statuses.active")],
[PromotionStatus.SCHEDULED]: ["orange", t("statuses.scheduled")],
[PromotionStatus.EXPIRED]: ["red", t("statuses.expired")],
}[getPromotionStatus(promotion)] as [
"grey" | "orange" | "green" | "red",
string
]
const displayValue = getDisplayValue(promotion) const displayValue = getDisplayValue(promotion)
return ( return (
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { PromotionDTO } from "@medusajs/types" import { AdminPromotion } from "@medusajs/types"
import { Button, CurrencyInput, Input, RadioGroup, Text } from "@medusajs/ui" import { Button, CurrencyInput, Input, RadioGroup, Text } from "@medusajs/ui"
import { useForm, useWatch } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next" import { Trans, useTranslation } from "react-i18next"
@@ -13,12 +13,13 @@ import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
import { getCurrencySymbol } from "../../../../../lib/data/currencies" import { getCurrencySymbol } from "../../../../../lib/data/currencies"
type EditPromotionFormProps = { type EditPromotionFormProps = {
promotion: PromotionDTO promotion: AdminPromotion
} }
const EditPromotionSchema = zod.object({ const EditPromotionSchema = zod.object({
is_automatic: zod.string().toLowerCase(), is_automatic: zod.string().toLowerCase(),
code: zod.string().min(1), code: zod.string().min(1),
status: zod.enum(["active", "inactive", "draft"]),
value_type: zod.enum(["fixed", "percentage"]), value_type: zod.enum(["fixed", "percentage"]),
value: zod.number(), value: zod.number(),
allocation: zod.enum(["each", "across"]), allocation: zod.enum(["each", "across"]),
@@ -34,6 +35,7 @@ export const EditPromotionDetailsForm = ({
defaultValues: { defaultValues: {
is_automatic: promotion.is_automatic!.toString(), is_automatic: promotion.is_automatic!.toString(),
code: promotion.code, code: promotion.code,
status: promotion.status,
value: promotion.application_method!.value, value: promotion.application_method!.value,
allocation: promotion.application_method!.allocation, allocation: promotion.application_method!.allocation,
value_type: promotion.application_method!.type, value_type: promotion.application_method!.type,
@@ -55,6 +57,7 @@ export const EditPromotionDetailsForm = ({
{ {
is_automatic: data.is_automatic === "true", is_automatic: data.is_automatic === "true",
code: data.code, code: data.code,
status: data.status,
application_method: { application_method: {
value: data.value, value: data.value,
type: data.value_type as any, type: data.value_type as any,
@@ -77,6 +80,51 @@ export const EditPromotionDetailsForm = ({
> >
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-y-auto"> <RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-y-auto">
<div className="flex flex-col gap-y-8"> <div className="flex flex-col gap-y-8">
<Form.Field
control={form.control}
name="status"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("promotions.form.status.label")}</Form.Label>
<Form.Control>
<RadioGroup
className="flex-col gap-y-3"
{...field}
value={field.value}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"draft"}
label={t("promotions.form.status.draft.title")}
description={t(
"promotions.form.status.draft.description"
)}
/>
<RadioGroup.ChoiceBox
value={"active"}
label={t("promotions.form.status.active.title")}
description={t(
"promotions.form.status.active.description"
)}
/>
<RadioGroup.ChoiceBox
value={"inactive"}
label={t("promotions.form.status.inactive.title")}
description={t(
"promotions.form.status.inactive.description"
)}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field <Form.Field
control={form.control} control={form.control}
name="is_automatic" name="is_automatic"
@@ -9,3 +9,4 @@ export * from "./delete-promotions"
export * from "./update-campaigns" export * from "./update-campaigns"
export * from "./update-promotion-rules" export * from "./update-promotion-rules"
export * from "./update-promotions" export * from "./update-promotions"
export * from "./update-promotions-status"
@@ -0,0 +1,55 @@
import {
AdditionalData,
PromotionStatusValues,
} from "@medusajs/framework/types"
import { MedusaError, PromotionStatus } from "@medusajs/framework/utils"
import {
WorkflowResponse,
createHook,
createStep,
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { updatePromotionsStep } from "../steps"
export type UpdatePromotionsStatusWorkflowInput = {
promotionsData: {
id: string
status: PromotionStatusValues
}[]
} & AdditionalData
export const updatePromotionsValidationStep = createStep(
"update-promotions-validation",
async function ({ promotionsData }: UpdatePromotionsStatusWorkflowInput) {
for (const promotionData of promotionsData) {
const allowedStatuses: PromotionStatusValues[] =
Object.values(PromotionStatus)
if (!allowedStatuses.includes(promotionData.status)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`promotion's status should be one of - ${allowedStatuses.join(", ")}`
)
}
}
}
)
export const updatePromotionsStatusWorkflowId = "update-promotions-status"
export const updatePromotionsStatusWorkflow = createWorkflow(
updatePromotionsStatusWorkflowId,
(input: UpdatePromotionsStatusWorkflowInput) => {
updatePromotionsValidationStep(input)
const updatedPromotions = updatePromotionsStep(input.promotionsData)
const promotionStatusUpdated = createHook("promotionStatusUpdated", {
promotions: updatedPromotions,
additional_data: input.additional_data,
})
return new WorkflowResponse(updatedPromotions, {
hooks: [promotionStatusUpdated],
})
}
)
@@ -1,11 +1,21 @@
import { AdditionalData, UpdatePromotionDTO } from "@medusajs/framework/types"
import { import {
WorkflowData, AdditionalData,
WorkflowResponse, PromotionDTO,
PromotionStatusValues,
UpdatePromotionDTO,
} from "@medusajs/framework/types"
import { isString } from "@medusajs/framework/utils"
import {
createHook, createHook,
createWorkflow, createWorkflow,
transform,
when,
WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useRemoteQueryStep } from "../../common"
import { updatePromotionsStep } from "../steps" import { updatePromotionsStep } from "../steps"
import { updatePromotionsStatusWorkflow } from "./update-promotions-status"
/** /**
* The data to update one or more promotions, along with custom data that's passed to the workflow's hooks. * The data to update one or more promotions, along with custom data that's passed to the workflow's hooks.
@@ -20,12 +30,12 @@ export type UpdatePromotionsWorkflowInput = {
export const updatePromotionsWorkflowId = "update-promotions" export const updatePromotionsWorkflowId = "update-promotions"
/** /**
* This workflow updates one or more promotions. It's used by the [Update Promotion Admin API Route](https://docs.medusajs.com/api/admin#promotions_postpromotionsid). * This workflow updates one or more promotions. It's used by the [Update Promotion Admin API Route](https://docs.medusajs.com/api/admin#promotions_postpromotionsid).
* *
* This workflow has a hook that allows you to perform custom actions on the updated promotion. For example, you can pass under `additional_data` custom data that * This workflow has a hook that allows you to perform custom actions on the updated promotion. For example, you can pass under `additional_data` custom data that
* allows you to update custom data models linked to the promotions. * allows you to update custom data models linked to the promotions.
* *
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around updating promotions. * You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around updating promotions.
* *
* @example * @example
* const { result } = await updatePromotionsWorkflow(container) * const { result } = await updatePromotionsWorkflow(container)
* .run({ * .run({
@@ -41,17 +51,78 @@ export const updatePromotionsWorkflowId = "update-promotions"
* } * }
* } * }
* }) * })
* *
* @summary * @summary
* *
* Update one or more promotions. * Update one or more promotions.
* *
* @property hooks.promotionsUpdated - This hook is executed after the promotions are updated. You can consume this hook to perform custom actions on the updated promotions. * @property hooks.promotionsUpdated - This hook is executed after the promotions are updated. You can consume this hook to perform custom actions on the updated promotions.
*/ */
export const updatePromotionsWorkflow = createWorkflow( export const updatePromotionsWorkflow = createWorkflow(
updatePromotionsWorkflowId, updatePromotionsWorkflowId,
(input: WorkflowData<UpdatePromotionsWorkflowInput>) => { (input: WorkflowData<UpdatePromotionsWorkflowInput>) => {
const updatedPromotions = updatePromotionsStep(input.promotionsData) const promotionIds = transform({ input }, ({ input }) =>
input.promotionsData.map((pd) => pd.id)
)
const promotions = useRemoteQueryStep({
entry_point: "promotion",
variables: { id: promotionIds },
fields: ["id", "status"],
list: true,
throw_if_key_not_found: true,
}).config({ name: "get-promotions" })
const promotionInputs = transform(
{ promotions, input },
({ promotions, input }) => {
const promotionMap: Record<string, PromotionDTO> = {}
const promotionsUpdateInput: UpdatePromotionsWorkflowInput["promotionsData"] =
[]
const promotionsStatusUpdateInput: {
id: string
status: PromotionStatusValues
}[] = []
for (const promotion of promotions) {
promotionMap[promotion.id] = promotion
}
for (const promotionUpdateData of input.promotionsData) {
const promotion = promotionMap[promotionUpdateData.id]
const { status, ...rest } = promotionUpdateData
promotionsUpdateInput.push(rest)
if (
isString(status) &&
promotionUpdateData.status !== promotion.status
) {
promotionsStatusUpdateInput.push({
id: promotionUpdateData.id,
status,
})
}
}
return { promotionsUpdateInput, promotionsStatusUpdateInput }
}
)
const updatedPromotions = updatePromotionsStep(
promotionInputs.promotionsUpdateInput
)
when({ promotionInputs }, ({ promotionInputs }) => {
return !!promotionInputs.promotionsStatusUpdateInput?.length
}).then(() => {
updatePromotionsStatusWorkflow.runAsStep({
input: {
promotionsData: promotionInputs.promotionsStatusUpdateInput,
},
})
})
const promotionsUpdated = createHook("promotionsUpdated", { const promotionsUpdated = createHook("promotionsUpdated", {
promotions: updatedPromotions, promotions: updatedPromotions,
additional_data: input.additional_data, additional_data: input.additional_data,
@@ -3,6 +3,7 @@ import {
ApplicationMethodTargetTypeValues, ApplicationMethodTargetTypeValues,
ApplicationMethodTypeValues, ApplicationMethodTypeValues,
PromotionRuleOperatorValues, PromotionRuleOperatorValues,
PromotionStatusValues,
PromotionTypeValues, PromotionTypeValues,
} from "../../../promotion" } from "../../../promotion"
import { AdminCreateCampaign } from "../../campaign" import { AdminCreateCampaign } from "../../campaign"
@@ -64,6 +65,7 @@ export interface AdminUpdatePromotion {
code?: string code?: string
is_automatic?: boolean is_automatic?: boolean
type?: PromotionTypeValues type?: PromotionTypeValues
status?: PromotionStatusValues
campaign_id?: string | null campaign_id?: string | null
campaign?: AdminCreateCampaign campaign?: AdminCreateCampaign
application_method?: AdminUpdateApplicationMethod application_method?: AdminUpdateApplicationMethod
@@ -3,6 +3,7 @@ import {
ApplicationMethodTargetTypeValues, ApplicationMethodTargetTypeValues,
ApplicationMethodTypeValues, ApplicationMethodTypeValues,
PromotionRuleOperatorValues, PromotionRuleOperatorValues,
PromotionStatusValues,
PromotionTypeValues, PromotionTypeValues,
} from "../../promotion" } from "../../promotion"
import { AdminCampaign } from "../campaign" import { AdminCampaign } from "../campaign"
@@ -37,6 +38,7 @@ export interface BasePromotion {
is_automatic?: boolean is_automatic?: boolean
application_method?: BaseApplicationMethod application_method?: BaseApplicationMethod
rules?: BasePromotionRule[] rules?: BasePromotionRule[]
status?: PromotionStatusValues
campaign_id?: string campaign_id?: string
campaign?: AdminCampaign campaign?: AdminCampaign
created_at: string created_at: string
@@ -5,7 +5,7 @@ import {
CreateApplicationMethodDTO, CreateApplicationMethodDTO,
UpdateApplicationMethodDTO, UpdateApplicationMethodDTO,
} from "./application-method" } from "./application-method"
import { CampaignDTO } from "./campaign" import { CampaignDTO, FilterableCampaignProps } from "./campaign"
import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule" import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule"
/** /**
@@ -13,6 +13,11 @@ import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule"
*/ */
export type PromotionTypeValues = "standard" | "buyget" export type PromotionTypeValues = "standard" | "buyget"
/**
* The promotion's possible types.
*/
export type PromotionStatusValues = "draft" | "active" | "inactive"
/** /**
* The promotion's possible rule types. * The promotion's possible rule types.
*/ */
@@ -41,6 +46,15 @@ export interface PromotionDTO {
*/ */
type?: PromotionTypeValues type?: PromotionTypeValues
/**
* The status of the promotion:
*
* - `active` promotion is available for user to consume
* - `inactive` promotion is no longer available to the user
* - `draft` promotion is currently being prepared
*/
status?: PromotionStatusValues
/** /**
* Whether the promotion is applied automatically. * Whether the promotion is applied automatically.
*/ */
@@ -85,6 +99,15 @@ export interface CreatePromotionDTO {
*/ */
type: PromotionTypeValues type: PromotionTypeValues
/**
* The status of the promotion:
*
* - `draft` indicates that a promotion is currently being prepared
* - `active` indicates that a promotion is active
* - `inactive` indicates that a promotion is no longer active
*/
status: PromotionStatusValues
/** /**
* Whether the promotion is applied automatically. * Whether the promotion is applied automatically.
*/ */
@@ -135,6 +158,15 @@ export interface UpdatePromotionDTO {
*/ */
type?: PromotionTypeValues type?: PromotionTypeValues
/**
* The status of the promotion:
*
* - `draft` indicates that a promotion is currently being prepared
* - `active` indicates that a promotion is active
* - `inactive` indicates that a promotion is no longer active
*/
status?: PromotionStatusValues
/** /**
* The associated application method. * The associated application method.
*/ */
@@ -180,4 +212,14 @@ export interface FilterablePromotionProps
* Filter promotions by their type. * Filter promotions by their type.
*/ */
type?: PromotionTypeValues[] type?: PromotionTypeValues[]
/**
* Filter promotions by their status.
*/
status?: PromotionStatusValues[]
/**
* Filter promotions by their campaign
*/
campaign?: FilterableCampaignProps
} }
@@ -3,6 +3,12 @@ export enum PromotionType {
BUYGET = "buyget", BUYGET = "buyget",
} }
export enum PromotionStatus {
DRAFT = "draft",
ACTIVE = "active",
INACTIVE = "inactive",
}
export enum ApplicationMethodType { export enum ApplicationMethodType {
FIXED = "fixed", FIXED = "fixed",
PERCENTAGE = "percentage", PERCENTAGE = "percentage",
@@ -3,6 +3,7 @@ export const defaultAdminPromotionFields = [
"code", "code",
"is_automatic", "is_automatic",
"type", "type",
"status",
"created_at", "created_at",
"updated_at", "updated_at",
"deleted_at", "deleted_at",
@@ -3,9 +3,11 @@ import {
ApplicationMethodTargetType, ApplicationMethodTargetType,
ApplicationMethodType, ApplicationMethodType,
PromotionRuleOperator, PromotionRuleOperator,
PromotionStatus,
PromotionType, PromotionType,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { z } from "zod" import { z } from "zod"
import { applyAndAndOrOperators } from "../../utils/common-validators"
import { import {
createFindParams, createFindParams,
createOperatorMap, createOperatorMap,
@@ -13,7 +15,6 @@ import {
WithAdditionalData, WithAdditionalData,
} from "../../utils/validators" } from "../../utils/validators"
import { CreateCampaign } from "../campaigns/validators" import { CreateCampaign } from "../campaigns/validators"
import { applyAndAndOrOperators } from "../../utils/common-validators"
export type AdminGetPromotionParamsType = z.infer< export type AdminGetPromotionParamsType = z.infer<
typeof AdminGetPromotionParams typeof AdminGetPromotionParams
@@ -160,6 +161,7 @@ export const CreatePromotion = z
code: z.string(), code: z.string(),
is_automatic: z.boolean().optional(), is_automatic: z.boolean().optional(),
type: z.nativeEnum(PromotionType), type: z.nativeEnum(PromotionType),
status: z.nativeEnum(PromotionStatus).default(PromotionStatus.DRAFT),
campaign_id: z.string().nullish(), campaign_id: z.string().nullish(),
campaign: CreateCampaign.optional(), campaign: CreateCampaign.optional(),
application_method: AdminCreateApplicationMethod, application_method: AdminCreateApplicationMethod,
@@ -183,6 +185,7 @@ export const UpdatePromotion = z
code: z.string().optional(), code: z.string().optional(),
is_automatic: z.boolean().optional(), is_automatic: z.boolean().optional(),
type: z.nativeEnum(PromotionType).optional(), type: z.nativeEnum(PromotionType).optional(),
status: z.nativeEnum(PromotionStatus).optional(),
campaign_id: z.string().nullish(), campaign_id: z.string().nullish(),
application_method: AdminUpdateApplicationMethod.optional(), application_method: AdminUpdateApplicationMethod.optional(),
}) })
@@ -3,7 +3,11 @@ import {
IPromotionModuleService, IPromotionModuleService,
PromotionDTO, PromotionDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { isPresent, toMikroORMEntity } from "@medusajs/framework/utils" import {
isPresent,
PromotionStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Promotion } from "@models" import { Promotion } from "@models"
import { defaultPromotionsData } from "./data" import { defaultPromotionsData } from "./data"
@@ -51,6 +55,7 @@ export async function createDefaultPromotion(
return await service.createPromotions({ return await service.createPromotions({
code: "PROMOTION_TEST", code: "PROMOTION_TEST",
type: "standard", type: "standard",
status: PromotionStatus.ACTIVE,
campaign_id: "campaign-id-1", campaign_id: "campaign-id-1",
...promotion, ...promotion,
application_method: { application_method: {
@@ -2,6 +2,7 @@ import { IPromotionModuleService } from "@medusajs/framework/types"
import { import {
ApplicationMethodType, ApplicationMethodType,
Modules, Modules,
PromotionStatus,
PromotionType, PromotionType,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner, SuiteOptions } from "@medusajs/test-utils" import { moduleIntegrationTestRunner, SuiteOptions } from "@medusajs/test-utils"
@@ -17,13 +18,85 @@ moduleIntegrationTestRunner({
service, service,
}: SuiteOptions<IPromotionModuleService>) => { }: SuiteOptions<IPromotionModuleService>) => {
describe("Promotion Service: computeActions", () => { describe("Promotion Service: computeActions", () => {
beforeAll(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date("02/02/2023"))
})
afterAll(() => {
jest.useRealTimers()
})
beforeEach(async () => { beforeEach(async () => {
await createCampaigns(MikroOrmWrapper.forkManager()) await createCampaigns(MikroOrmWrapper.forkManager())
}) })
it("should return empty array when promotion is not active (draft or inactive)", async () => {
const promotion = await createDefaultPromotion(service, {
status: PromotionStatus.DRAFT,
rules: [
{
attribute: "customer.customer_group.id",
operator: "in",
values: ["VIP", "top100"],
},
],
application_method: {
type: "fixed",
target_type: "items",
allocation: "each",
value: 200,
max_quantity: 1,
target_rules: [
{
attribute: "product_category.id",
operator: "eq",
values: ["catg_cotton"],
},
],
},
})
const result = await service.computeActions([promotion.code!], {
currency_code: "usd",
customer: {
customer_group: {
id: "VIP",
},
},
items: [
{
id: "item_cotton_tshirt",
quantity: 1,
subtotal: 100,
product_category: {
id: "catg_cotton",
},
product: {
id: "prod_tshirt",
},
},
{
id: "item_cotton_sweater",
quantity: 5,
subtotal: 750,
product_category: {
id: "catg_cotton",
},
product: {
id: "prod_sweater",
},
},
],
})
expect(result).toEqual([])
})
describe("when code is not present in database", () => { describe("when code is not present in database", () => {
it("should return empty array when promotion does not exist", async () => { it("should return empty array when promotion does not exist", async () => {
const response = await service.computeActions(["DOES_NOT_EXIST"], { const response = await service.computeActions(["DOES_NOT_EXIST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -87,6 +160,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -136,6 +210,7 @@ moduleIntegrationTestRunner({
const resultWithoutCustomer = await service.computeActions( const resultWithoutCustomer = await service.computeActions(
["PROMOTION_TEST"], ["PROMOTION_TEST"],
{ {
currency_code: "usd",
items: [ items: [
{ {
id: "item_cotton_tshirt", id: "item_cotton_tshirt",
@@ -221,6 +296,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -257,19 +333,7 @@ moduleIntegrationTestRunner({
{ {
action: "addItemAdjustment", action: "addItemAdjustment",
item_id: "item_cotton_tshirt", item_id: "item_cotton_tshirt",
amount: 30, amount: 50,
code: "PROMOTION_TEST",
},
{
action: "addItemAdjustment",
item_id: "item_cotton_sweater",
amount: 30,
code: "PROMOTION_TEST",
},
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 20,
code: "PROMOTION_TEST_2", code: "PROMOTION_TEST_2",
}, },
{ {
@@ -278,6 +342,12 @@ moduleIntegrationTestRunner({
amount: 50, amount: 50,
code: "PROMOTION_TEST_2", code: "PROMOTION_TEST_2",
}, },
{
action: "addItemAdjustment",
item_id: "item_cotton_sweater",
amount: 30,
code: "PROMOTION_TEST",
},
]) ])
}) })
@@ -334,6 +404,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -408,6 +479,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -469,6 +541,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -522,6 +595,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -622,6 +696,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -735,6 +810,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -809,6 +885,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -866,6 +943,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -921,6 +999,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -994,6 +1073,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions([], { const result = await service.computeActions([], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1093,6 +1173,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1126,18 +1207,6 @@ moduleIntegrationTestRunner({
) )
expect(JSON.parse(JSON.stringify(result))).toEqual([ expect(JSON.parse(JSON.stringify(result))).toEqual([
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 7.5,
code: "PROMOTION_TEST",
},
{
action: "addItemAdjustment",
item_id: "item_cotton_sweater",
amount: 22.5,
code: "PROMOTION_TEST",
},
{ {
action: "addItemAdjustment", action: "addItemAdjustment",
item_id: "item_cotton_tshirt", item_id: "item_cotton_tshirt",
@@ -1150,6 +1219,18 @@ moduleIntegrationTestRunner({
amount: 37.5, amount: 37.5,
code: "PROMOTION_TEST_2", code: "PROMOTION_TEST_2",
}, },
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 7.5,
code: "PROMOTION_TEST",
},
{
action: "addItemAdjustment",
item_id: "item_cotton_sweater",
amount: 22.5,
code: "PROMOTION_TEST",
},
]) ])
}) })
@@ -1205,6 +1286,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1278,6 +1360,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1334,6 +1417,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1386,6 +1470,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1459,6 +1544,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions([], { const result = await service.computeActions([], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1557,6 +1643,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1668,6 +1755,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1753,6 +1841,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1809,6 +1898,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1864,6 +1954,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -1937,6 +2028,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions([], { const result = await service.computeActions([], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -2012,6 +2104,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
[], [],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -2100,6 +2193,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -2206,6 +2300,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -2279,6 +2374,7 @@ moduleIntegrationTestRunner({
}) })
const result = await service.computeActions(["PROMOTION_TEST"], { const result = await service.computeActions(["PROMOTION_TEST"], {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -3974,6 +4070,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -4007,18 +4104,6 @@ moduleIntegrationTestRunner({
) )
expect(JSON.parse(JSON.stringify(result))).toEqual([ expect(JSON.parse(JSON.stringify(result))).toEqual([
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 7.5,
code: "PROMOTION_TEST",
},
{
action: "addItemAdjustment",
item_id: "item_cotton_sweater",
amount: 22.5,
code: "PROMOTION_TEST",
},
{ {
action: "addItemAdjustment", action: "addItemAdjustment",
item_id: "item_cotton_tshirt", item_id: "item_cotton_tshirt",
@@ -4031,6 +4116,18 @@ moduleIntegrationTestRunner({
amount: 37.5, amount: 37.5,
code: "PROMOTION_TEST_2", code: "PROMOTION_TEST_2",
}, },
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 7.5,
code: "PROMOTION_TEST",
},
{
action: "addItemAdjustment",
item_id: "item_cotton_sweater",
amount: 22.5,
code: "PROMOTION_TEST",
},
]) ])
}) })
@@ -4073,6 +4170,7 @@ moduleIntegrationTestRunner({
const result = await service.computeActions( const result = await service.computeActions(
["PROMOTION_TEST", "PROMOTION_TEST_2"], ["PROMOTION_TEST", "PROMOTION_TEST_2"],
{ {
currency_code: "usd",
customer: { customer: {
customer_group: { customer_group: {
id: "VIP", id: "VIP",
@@ -6,8 +6,8 @@ import {
Modules, Modules,
PromotionType, PromotionType,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { PromotionModuleService } from "@services"
import { SuiteOptions, moduleIntegrationTestRunner } from "@medusajs/test-utils" import { SuiteOptions, moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { PromotionModuleService } from "@services"
import { createCampaigns } from "../../../__fixtures__/campaigns" import { createCampaigns } from "../../../__fixtures__/campaigns"
import { import {
createDefaultPromotion, createDefaultPromotion,
@@ -24,6 +24,15 @@ moduleIntegrationTestRunner({
service, service,
}: SuiteOptions<IPromotionModuleService>) => { }: SuiteOptions<IPromotionModuleService>) => {
describe("Promotion Service", () => { describe("Promotion Service", () => {
beforeAll(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date("02/02/2023"))
})
afterAll(() => {
jest.useRealTimers()
})
beforeEach(async () => { beforeEach(async () => {
await createCampaigns(MikroOrmWrapper.forkManager()) await createCampaigns(MikroOrmWrapper.forkManager())
}) })
@@ -809,30 +818,32 @@ moduleIntegrationTestRunner({
expect(count).toEqual(2) expect(count).toEqual(2)
expect(promotions).toEqual([ expect(promotions).toEqual([
{ expect.objectContaining({
id: "promotion-id-1", id: "promotion-id-1",
code: "PROMOTION_1", code: "PROMOTION_1",
campaign_id: null, campaign_id: null,
campaign: null, campaign: null,
status: "draft",
is_automatic: false, is_automatic: false,
type: "standard", type: "standard",
application_method: expect.any(Object), application_method: expect.any(Object),
created_at: expect.any(Date), created_at: expect.any(Date),
updated_at: expect.any(Date), updated_at: expect.any(Date),
deleted_at: null, deleted_at: null,
}, }),
{ expect.objectContaining({
id: "promotion-id-2", id: "promotion-id-2",
code: "PROMOTION_2", code: "PROMOTION_2",
campaign_id: null, campaign_id: null,
campaign: null, campaign: null,
status: "draft",
is_automatic: false, is_automatic: false,
type: "standard", type: "standard",
application_method: null, application_method: null,
created_at: expect.any(Date), created_at: expect.any(Date),
updated_at: expect.any(Date), updated_at: expect.any(Date),
deleted_at: null, deleted_at: null,
}, }),
]) ])
}) })
@@ -1,8 +1,8 @@
import { IPromotionModuleService } from "@medusajs/framework/types" import { IPromotionModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner, SuiteOptions } from "@medusajs/test-utils" import { moduleIntegrationTestRunner, SuiteOptions } from "@medusajs/test-utils"
import { createCampaigns } from "../../../__fixtures__/campaigns" import { createCampaigns } from "../../../__fixtures__/campaigns"
import { createDefaultPromotion } from "../../../__fixtures__/promotion" import { createDefaultPromotion } from "../../../__fixtures__/promotion"
import { Modules } from "@medusajs/framework/utils"
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -13,6 +13,15 @@ moduleIntegrationTestRunner({
service, service,
}: SuiteOptions<IPromotionModuleService>) => { }: SuiteOptions<IPromotionModuleService>) => {
describe("Promotion Service: campaign usage", () => { describe("Promotion Service: campaign usage", () => {
beforeAll(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date("02/02/2023"))
})
afterAll(() => {
jest.useRealTimers()
})
beforeEach(async () => { beforeEach(async () => {
await createCampaigns(MikroOrmWrapper.forkManager()) await createCampaigns(MikroOrmWrapper.forkManager())
}) })
@@ -13,6 +13,15 @@ moduleIntegrationTestRunner({
service, service,
}: SuiteOptions<IPromotionModuleService>) => { }: SuiteOptions<IPromotionModuleService>) => {
describe("Promotion Service: campaign usage", () => { describe("Promotion Service: campaign usage", () => {
beforeAll(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date("02/02/2023"))
})
afterAll(() => {
jest.useRealTimers()
})
beforeEach(async () => { beforeEach(async () => {
await createCampaigns(MikroOrmWrapper.forkManager()) await createCampaigns(MikroOrmWrapper.forkManager())
}) })
@@ -345,6 +345,21 @@
], ],
"mappedType": "enum" "mappedType": "enum"
}, },
"status": {
"name": "status",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "'draft'",
"enumItems": [
"draft",
"active",
"inactive"
],
"mappedType": "enum"
},
"campaign_id": { "campaign_id": {
"name": "campaign_id", "name": "campaign_id",
"type": "text", "type": "text",
@@ -414,6 +429,14 @@
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_type\" ON \"promotion\" (type) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_type\" ON \"promotion\" (type) WHERE deleted_at IS NULL"
}, },
{
"keyName": "IDX_promotion_status",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_status\" ON \"promotion\" (status) WHERE deleted_at IS NULL"
},
{ {
"keyName": "IDX_promotion_campaign_id", "keyName": "IDX_promotion_campaign_id",
"columnNames": [], "columnNames": [],
@@ -0,0 +1,23 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20250113094144 extends Migration {
async up(): Promise<void> {
this.addSql(
"alter table if exists \"promotion\" add column if not exists \"status\" text check (\"status\" in ('draft', 'active', 'inactive')) not null default 'draft';"
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_promotion_status" ON "promotion" (status) WHERE deleted_at IS NULL;'
)
// Data Migration
this.addSql(`UPDATE promotion SET status = 'active';`)
}
async down(): Promise<void> {
this.addSql('drop index if exists "IDX_promotion_status";')
this.addSql(
'alter table if exists "promotion" drop column if exists "status";'
)
}
}
@@ -13,6 +13,10 @@ const Promotion = model
.index("IDX_promotion_code"), .index("IDX_promotion_code"),
is_automatic: model.boolean().default(false), is_automatic: model.boolean().default(false),
type: model.enum(PromotionUtils.PromotionType).index("IDX_promotion_type"), type: model.enum(PromotionUtils.PromotionType).index("IDX_promotion_type"),
status: model
.enum(PromotionUtils.PromotionStatus)
.index("IDX_promotion_status")
.default(PromotionUtils.PromotionStatus.DRAFT),
campaign: model campaign: model
.belongsTo(() => Campaign, { .belongsTo(() => Campaign, {
mappedBy: "promotions", mappedBy: "promotions",
@@ -2,10 +2,13 @@ import {
CampaignBudgetTypeValues, CampaignBudgetTypeValues,
Context, Context,
DAL, DAL,
FilterablePromotionProps,
FindConfig,
InferEntityType, InferEntityType,
InternalModuleDeclaration, InternalModuleDeclaration,
ModuleJoinerConfig, ModuleJoinerConfig,
ModulesSdkTypes, ModulesSdkTypes,
PromotionDTO,
PromotionTypes, PromotionTypes,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
@@ -24,6 +27,7 @@ import {
MedusaContext, MedusaContext,
MedusaError, MedusaError,
MedusaService, MedusaService,
PromotionStatus,
PromotionType, PromotionType,
toMikroORMEntity, toMikroORMEntity,
transformPropertiesToBigNumber, transformPropertiesToBigNumber,
@@ -134,6 +138,40 @@ export default class PromotionModuleService
return joinerConfig return joinerConfig
} }
@InjectManager()
listActivePromotions(
filters?: FilterablePromotionProps,
config?: FindConfig<PromotionDTO>,
sharedContext?: Context
): Promise<PromotionDTO[]> {
const activeFilters = {
$or: [
{
status: PromotionStatus.ACTIVE,
campaign_id: null,
...filters,
},
{
status: PromotionStatus.ACTIVE,
...filters,
campaign: {
...filters?.campaign,
$and: [
{
$or: [{ starts_at: null }, { starts_at: { $lte: new Date() } }],
},
{
$or: [{ ends_at: null }, { ends_at: { $gt: new Date() } }],
},
],
},
},
],
}
return this.listPromotions(activeFilters, config, sharedContext)
}
@InjectManager() @InjectManager()
async registerUsage( async registerUsage(
computedActions: PromotionTypes.UsageComputedActions[], computedActions: PromotionTypes.UsageComputedActions[],
@@ -148,11 +186,9 @@ export default class PromotionModuleService
const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>() const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>()
const promotionCodeUsageMap = new Map<string, boolean>() const promotionCodeUsageMap = new Map<string, boolean>()
const existingPromotions = await this.listPromotions( const existingPromotions = await this.listActivePromotions(
{ code: promotionCodes }, { code: promotionCodes },
{ { relations: ["campaign", "campaign.budget"] },
relations: ["campaign", "campaign.budget"],
},
sharedContext sharedContext
) )
@@ -250,15 +286,13 @@ export default class PromotionModuleService
const promotionCodeUsageMap = new Map<string, boolean>() const promotionCodeUsageMap = new Map<string, boolean>()
const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>() const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>()
const existingPromotions = await this.listPromotions( const existingPromotions = await this.listActivePromotions(
{ {
code: computedActions code: computedActions
.map((computedAction) => computedAction.code) .map((computedAction) => computedAction.code)
.filter(Boolean), .filter(Boolean),
}, },
{ { relations: ["campaign", "campaign.budget"] },
relations: ["campaign", "campaign.budget"],
},
sharedContext sharedContext
) )
@@ -362,7 +396,7 @@ export default class PromotionModuleService
>() >()
const automaticPromotions = preventAutoPromotions const automaticPromotions = preventAutoPromotions
? [] ? []
: await this.listPromotions( : await this.listActivePromotions(
{ is_automatic: true }, { is_automatic: true },
{ select: ["code"] }, { select: ["code"] },
sharedContext sharedContext
@@ -402,7 +436,7 @@ export default class PromotionModuleService
}) })
}) })
const promotions = await this.listPromotions( const promotions = await this.listActivePromotions(
{ {
code: [ code: [
...promotionCodesToApply, ...promotionCodesToApply,
@@ -412,6 +446,7 @@ export default class PromotionModuleService
}, },
{ {
take: null, take: null,
order: { application_method: { value: "DESC" } },
relations: [ relations: [
"application_method", "application_method",
"application_method.target_rules", "application_method.target_rules",