feat(dashboard,medusa): Promotion Campaign fixes (#7337)
* chore(medusa): strict zod versions in workspace * feat(dashboard): add campaign create to promotion UI * wip * fix(medusa): Missing middlewares export (#7289) * fix(docblock-generator): fix how type names created from Zod objects are inferred (#7292) * feat(api-ref): show schema of a tag (#7297) * feat: Add support for sendgrid and logger notification providers (#7290) * feat: Add support for sendgrid and logger notification providers * fix: changes based on PR review * chore: add action to automatically label docs (#7284) * chore: add action to automatically label docs * removes the paths param * docs: preparations for preview (#7267) * configured base paths + added development banner * fix typelist site url * added navbar and sidebar badges * configure algolia filters * remove AI assistant * remove unused imports * change navbar text and badge * lint fixes * fix build error * add to api reference rewrites * fix build error * fix build errors in user-guide * fix feedback component * add parent title to pagination * added breadcrumbs component * remove user-guide links * resolve todos * fix details about authentication * change documentation title * lint content * chore: fix bug with form reset * chore: address reviews * chore: fix specs * chore: loads of FE fixes + BE adds * chore: add more polishes + reorg files * chore: fixes to promotions modal * chore: cleanup * chore: cleanup * chore: fix build * chore: fkix cart spec * chore: fix module tests * chore: fix moar tests * wip * chore: templates + fixes + migrate currency * chore: fix build, add validation for max_quantity * chore: allow removing campaigns * chore: fix specs * chore: scope campaigns based on currency * remove console logs * chore: add translations + update keys * chore: move over filesfrom v2 to routes * chore(dashboard): Delete old translation files (#7423) * feat(dashboard,admin-sdk,admin-shared,admin-vite-plugin): Add support for UI extensions (#7383) * intial work * update lock * add routes and fix HMR of configs * cleanup * rm imports * rm debug from plugin * address feedback * address feedback * temp skip specs --------- Co-authored-by: Adrien de Peretti <adrien.deperetti@gmail.com> Co-authored-by: Shahed Nasser <shahednasser@gmail.com> Co-authored-by: Stevche Radevski <sradevski@live.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com>
This commit is contained in:
@@ -5,13 +5,13 @@ export const defaultCampaignsData = [
|
||||
id: "campaign-id-1",
|
||||
name: "campaign 1",
|
||||
description: "test description",
|
||||
currency: "USD",
|
||||
campaign_identifier: "test-1",
|
||||
starts_at: new Date("01/01/2023"),
|
||||
ends_at: new Date("01/01/2024"),
|
||||
budget: {
|
||||
type: CampaignBudgetType.SPEND,
|
||||
limit: 1000,
|
||||
currency_code: "USD",
|
||||
used: 0,
|
||||
},
|
||||
},
|
||||
@@ -19,13 +19,13 @@ export const defaultCampaignsData = [
|
||||
id: "campaign-id-2",
|
||||
name: "campaign 1",
|
||||
description: "test description",
|
||||
currency: "USD",
|
||||
campaign_identifier: "test-2",
|
||||
starts_at: new Date("01/01/2023"),
|
||||
ends_at: new Date("01/01/2024"),
|
||||
budget: {
|
||||
type: CampaignBudgetType.USAGE,
|
||||
limit: 1000,
|
||||
currency_code: "USD",
|
||||
used: 0,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import { CreatePromotionDTO } from "@medusajs/types"
|
||||
import { PromotionType } from "@medusajs/utils"
|
||||
|
||||
export const defaultPromotionsData = [
|
||||
export const defaultPromotionsData: CreatePromotionDTO[] = [
|
||||
{
|
||||
id: "promotion-id-1",
|
||||
code: "PROMOTION_1",
|
||||
type: PromotionType.STANDARD,
|
||||
application_method: {
|
||||
currency_code: "USD",
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "across",
|
||||
value: 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "promotion-id-2",
|
||||
code: "PROMOTION_2",
|
||||
type: PromotionType.STANDARD,
|
||||
application_method: {
|
||||
currency_code: "USD",
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "across",
|
||||
value: 1000,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { CreatePromotionDTO } from "@medusajs/types"
|
||||
import {
|
||||
CreatePromotionDTO,
|
||||
IPromotionModuleService,
|
||||
PromotionDTO,
|
||||
} from "@medusajs/types"
|
||||
import { isPresent } from "@medusajs/utils"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { Promotion } from "@models"
|
||||
import { defaultPromotionsData } from "./data"
|
||||
@@ -21,3 +26,47 @@ export async function createPromotions(
|
||||
|
||||
return promotions
|
||||
}
|
||||
|
||||
export async function createDefaultPromotions(
|
||||
service: IPromotionModuleService,
|
||||
promotionsData: Partial<CreatePromotionDTO>[] = defaultPromotionsData
|
||||
): Promise<Promotion[]> {
|
||||
const promotions: Promotion[] = []
|
||||
|
||||
for (let promotionData of promotionsData) {
|
||||
let promotion = await createDefaultPromotion(service, promotionData)
|
||||
|
||||
promotions.push(promotion)
|
||||
}
|
||||
|
||||
return promotions
|
||||
}
|
||||
|
||||
export async function createDefaultPromotion(
|
||||
service: IPromotionModuleService,
|
||||
data: Partial<CreatePromotionDTO>
|
||||
): Promise<PromotionDTO> {
|
||||
const { application_method = {}, campaign = {}, ...promotion } = data
|
||||
|
||||
return await service.create({
|
||||
code: "PROMOTION_TEST",
|
||||
type: "standard",
|
||||
campaign_id: "campaign-id-1",
|
||||
...promotion,
|
||||
application_method: {
|
||||
currency_code: "USD",
|
||||
target_type: "items",
|
||||
type: "fixed",
|
||||
allocation: "across",
|
||||
value: 1000,
|
||||
...application_method,
|
||||
},
|
||||
campaign: isPresent(campaign)
|
||||
? {
|
||||
campaign_identifier: "campaign-identifier",
|
||||
name: "new campaign",
|
||||
...campaign,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IPromotionModuleService } from "@medusajs/types"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { CampaignBudgetType } from "../../../../../../core/utils/src/promotion/index"
|
||||
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||
import { createPromotions } from "../../../__fixtures__/promotion"
|
||||
|
||||
@@ -27,7 +28,7 @@ moduleIntegrationTestRunner({
|
||||
id: "campaign-id-1",
|
||||
name: "campaign 1",
|
||||
description: "test description",
|
||||
currency: "USD",
|
||||
|
||||
campaign_identifier: "test-1",
|
||||
starts_at: expect.any(Date),
|
||||
ends_at: expect.any(Date),
|
||||
@@ -40,7 +41,7 @@ moduleIntegrationTestRunner({
|
||||
id: "campaign-id-2",
|
||||
name: "campaign 1",
|
||||
description: "test description",
|
||||
currency: "USD",
|
||||
|
||||
campaign_identifier: "test-2",
|
||||
starts_at: expect.any(Date),
|
||||
ends_at: expect.any(Date),
|
||||
@@ -92,6 +93,26 @@ moduleIntegrationTestRunner({
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when required budget params are not met", async () => {
|
||||
const error = await service
|
||||
.createCampaigns([
|
||||
{
|
||||
name: "test",
|
||||
campaign_identifier: "test",
|
||||
budget: {
|
||||
limit: 1000,
|
||||
type: "spend",
|
||||
used: 10,
|
||||
},
|
||||
},
|
||||
])
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.message).toContain(
|
||||
"Campaign Budget type is a required field"
|
||||
)
|
||||
})
|
||||
|
||||
it("should create a basic campaign successfully", async () => {
|
||||
const startsAt = new Date("01/01/2024")
|
||||
const endsAt = new Date("01/01/2025")
|
||||
@@ -174,7 +195,6 @@ moduleIntegrationTestRunner({
|
||||
{
|
||||
id: "campaign-id-1",
|
||||
description: "test description 1",
|
||||
currency: "EUR",
|
||||
campaign_identifier: "new",
|
||||
starts_at: new Date("01/01/2024"),
|
||||
ends_at: new Date("01/01/2025"),
|
||||
@@ -184,7 +204,6 @@ moduleIntegrationTestRunner({
|
||||
expect(updatedCampaign).toEqual(
|
||||
expect.objectContaining({
|
||||
description: "test description 1",
|
||||
currency: "EUR",
|
||||
campaign_identifier: "new",
|
||||
starts_at: new Date("01/01/2024"),
|
||||
ends_at: new Date("01/01/2025"),
|
||||
@@ -214,6 +233,37 @@ moduleIntegrationTestRunner({
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should create a campaign budget if not present successfully", async () => {
|
||||
await createCampaigns(MikroOrmWrapper.forkManager(), [
|
||||
{
|
||||
id: "campaign-id-new",
|
||||
name: "campaign 1",
|
||||
description: "test description",
|
||||
campaign_identifier: "test-1",
|
||||
} as any,
|
||||
])
|
||||
|
||||
const [updatedCampaign] = await service.updateCampaigns([
|
||||
{
|
||||
id: "campaign-id-new",
|
||||
budget: {
|
||||
type: CampaignBudgetType.SPEND,
|
||||
limit: 100,
|
||||
used: 100,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
expect(updatedCampaign).toEqual(
|
||||
expect.objectContaining({
|
||||
budget: expect.objectContaining({
|
||||
limit: 100,
|
||||
used: 100,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveCampaign", () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IPromotionModuleService } from "@medusajs/types"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||
import { createDefaultPromotion } from "../../../__fixtures__/promotion"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -18,11 +19,7 @@ moduleIntegrationTestRunner({
|
||||
|
||||
describe("registerUsage", () => {
|
||||
it("should register usage for type spend", async () => {
|
||||
const createdPromotion = await service.create({
|
||||
code: "TEST_PROMO_SPEND",
|
||||
type: "standard",
|
||||
campaign_id: "campaign-id-1",
|
||||
})
|
||||
const createdPromotion = await createDefaultPromotion(service, {})
|
||||
|
||||
await service.registerUsage([
|
||||
{
|
||||
@@ -53,9 +50,7 @@ moduleIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("should register usage for type usage", async () => {
|
||||
const createdPromotion = await service.create({
|
||||
code: "TEST_PROMO_USAGE",
|
||||
type: "standard",
|
||||
const createdPromotion = await createDefaultPromotion(service, {
|
||||
campaign_id: "campaign-id-2",
|
||||
})
|
||||
|
||||
@@ -103,9 +98,7 @@ moduleIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("should not register usage when limit is exceed for type usage", async () => {
|
||||
const createdPromotion = await service.create({
|
||||
code: "TEST_PROMO_USAGE",
|
||||
type: "standard",
|
||||
const createdPromotion = await createDefaultPromotion(service, {
|
||||
campaign_id: "campaign-id-2",
|
||||
})
|
||||
|
||||
@@ -144,11 +137,7 @@ moduleIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("should not register usage above limit when exceeded for type spend", async () => {
|
||||
const createdPromotion = await service.create({
|
||||
code: "TEST_PROMO_SPEND",
|
||||
type: "standard",
|
||||
campaign_id: "campaign-id-1",
|
||||
})
|
||||
const createdPromotion = await createDefaultPromotion(service, {})
|
||||
|
||||
await service.updateCampaigns({
|
||||
id: "campaign-id-1",
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { PromotionType } from "@medusajs/utils"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { createPromotions } from "../../../__fixtures__/promotion"
|
||||
import { IPromotionModuleService } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PROMOTION,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IPromotionModuleService>) => {
|
||||
describe("Promotion Service", () => {
|
||||
beforeEach(async () => {
|
||||
await createPromotions(MikroOrmWrapper.forkManager())
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should throw an error when required params are not passed", async () => {
|
||||
const error = await service
|
||||
.create([
|
||||
{
|
||||
type: PromotionType.STANDARD,
|
||||
} as any,
|
||||
])
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.message).toContain(
|
||||
"Value for Promotion.code is required, 'undefined' found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should create a promotion successfully", async () => {
|
||||
await service.create([
|
||||
{
|
||||
code: "PROMOTION_TEST",
|
||||
type: PromotionType.STANDARD,
|
||||
},
|
||||
])
|
||||
|
||||
const [promotion] = await service.list({
|
||||
code: ["PROMOTION_TEST"],
|
||||
})
|
||||
|
||||
expect(promotion).toEqual(
|
||||
expect.objectContaining({
|
||||
code: "PROMOTION_TEST",
|
||||
is_automatic: false,
|
||||
type: "standard",
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"namespaces": ["public"],
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
@@ -31,15 +33,6 @@
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"currency": {
|
||||
"name": "currency",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"campaign_identifier": {
|
||||
"name": "campaign_identifier",
|
||||
"type": "text",
|
||||
@@ -107,14 +100,18 @@
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_campaign_identifier_unique",
|
||||
"columnNames": ["campaign_identifier"],
|
||||
"columnNames": [
|
||||
"campaign_identifier"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"keyName": "promotion_campaign_pkey",
|
||||
"columnNames": ["id"],
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
@@ -141,7 +138,10 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"enumItems": ["spend", "usage"],
|
||||
"enumItems": [
|
||||
"spend",
|
||||
"usage"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"campaign_id": {
|
||||
@@ -153,6 +153,15 @@
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"currency_code": {
|
||||
"name": "currency_code",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"limit": {
|
||||
"name": "limit",
|
||||
"type": "numeric",
|
||||
@@ -177,7 +186,8 @@
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"nullable": false,
|
||||
"default": "0",
|
||||
"mappedType": "decimal"
|
||||
},
|
||||
"raw_used": {
|
||||
@@ -186,7 +196,7 @@
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"nullable": false,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
@@ -226,14 +236,18 @@
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": ["type"],
|
||||
"columnNames": [
|
||||
"type"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_campaign_budget_type",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": ["campaign_id"],
|
||||
"columnNames": [
|
||||
"campaign_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "promotion_campaign_budget_campaign_id_unique",
|
||||
"primary": false,
|
||||
@@ -241,7 +255,9 @@
|
||||
},
|
||||
{
|
||||
"keyName": "promotion_campaign_budget_pkey",
|
||||
"columnNames": ["id"],
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
@@ -251,9 +267,13 @@
|
||||
"foreignKeys": {
|
||||
"promotion_campaign_budget_campaign_id_foreign": {
|
||||
"constraintName": "promotion_campaign_budget_campaign_id_foreign",
|
||||
"columnNames": ["campaign_id"],
|
||||
"columnNames": [
|
||||
"campaign_id"
|
||||
],
|
||||
"localTableName": "public.promotion_campaign_budget",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion_campaign",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
@@ -305,7 +325,10 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"enumItems": ["standard", "buyget"],
|
||||
"enumItems": [
|
||||
"standard",
|
||||
"buyget"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"created_at": {
|
||||
@@ -345,14 +368,18 @@
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": ["code"],
|
||||
"columnNames": [
|
||||
"code"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_promotion_code",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": ["type"],
|
||||
"columnNames": [
|
||||
"type"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_promotion_type",
|
||||
"primary": false,
|
||||
@@ -360,14 +387,18 @@
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_promotion_code_unique",
|
||||
"columnNames": ["code"],
|
||||
"columnNames": [
|
||||
"code"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"keyName": "promotion_pkey",
|
||||
"columnNames": ["id"],
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
@@ -377,11 +408,16 @@
|
||||
"foreignKeys": {
|
||||
"promotion_campaign_id_foreign": {
|
||||
"constraintName": "promotion_campaign_id_foreign",
|
||||
"columnNames": ["campaign_id"],
|
||||
"columnNames": [
|
||||
"campaign_id"
|
||||
],
|
||||
"localTableName": "public.promotion",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion_campaign",
|
||||
"deleteRule": "set null"
|
||||
"deleteRule": "set null",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -402,7 +438,7 @@
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"nullable": false,
|
||||
"mappedType": "decimal"
|
||||
},
|
||||
"raw_value": {
|
||||
@@ -411,9 +447,18 @@
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"nullable": false,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"currency_code": {
|
||||
"name": "currency_code",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"max_quantity": {
|
||||
"name": "max_quantity",
|
||||
"type": "numeric",
|
||||
@@ -448,7 +493,10 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"enumItems": ["fixed", "percentage"],
|
||||
"enumItems": [
|
||||
"fixed",
|
||||
"percentage"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"target_type": {
|
||||
@@ -458,7 +506,11 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"enumItems": ["order", "shipping_methods", "items"],
|
||||
"enumItems": [
|
||||
"order",
|
||||
"shipping_methods",
|
||||
"items"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"allocation": {
|
||||
@@ -468,7 +520,10 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"enumItems": ["each", "across"],
|
||||
"enumItems": [
|
||||
"each",
|
||||
"across"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"promotion_id": {
|
||||
@@ -517,36 +572,56 @@
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": ["type"],
|
||||
"columnNames": [
|
||||
"type"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_application_method_type",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": ["target_type"],
|
||||
"columnNames": [
|
||||
"target_type"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_application_method_target_type",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": ["allocation"],
|
||||
"columnNames": [
|
||||
"allocation"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_application_method_allocation",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": ["promotion_id"],
|
||||
"columnNames": [
|
||||
"promotion_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "promotion_application_method_promotion_id_unique",
|
||||
"primary": false,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_promotion_application_method_currency_code",
|
||||
"columnNames": [
|
||||
"currency_code"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_application_method_currency_code\" ON \"promotion_application_method\" (currency_code) WHERE deleted_at IS NOT NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "promotion_application_method_pkey",
|
||||
"columnNames": ["id"],
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
@@ -556,9 +631,13 @@
|
||||
"foreignKeys": {
|
||||
"promotion_application_method_promotion_id_foreign": {
|
||||
"constraintName": "promotion_application_method_promotion_id_foreign",
|
||||
"columnNames": ["promotion_id"],
|
||||
"columnNames": [
|
||||
"promotion_id"
|
||||
],
|
||||
"localTableName": "public.promotion_application_method",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
@@ -601,7 +680,15 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"enumItems": ["gte", "lte", "gt", "lt", "eq", "ne", "in"],
|
||||
"enumItems": [
|
||||
"gte",
|
||||
"lte",
|
||||
"gt",
|
||||
"lt",
|
||||
"eq",
|
||||
"ne",
|
||||
"in"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"created_at": {
|
||||
@@ -641,14 +728,18 @@
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": ["attribute"],
|
||||
"columnNames": [
|
||||
"attribute"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_promotion_rule_attribute",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": ["operator"],
|
||||
"columnNames": [
|
||||
"operator"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_promotion_rule_operator",
|
||||
"primary": false,
|
||||
@@ -656,7 +747,9 @@
|
||||
},
|
||||
{
|
||||
"keyName": "promotion_rule_pkey",
|
||||
"columnNames": ["id"],
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
@@ -691,7 +784,10 @@
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "promotion_promotion_rule_pkey",
|
||||
"columnNames": ["promotion_id", "promotion_rule_id"],
|
||||
"columnNames": [
|
||||
"promotion_id",
|
||||
"promotion_rule_id"
|
||||
],
|
||||
"composite": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
@@ -701,18 +797,26 @@
|
||||
"foreignKeys": {
|
||||
"promotion_promotion_rule_promotion_id_foreign": {
|
||||
"constraintName": "promotion_promotion_rule_promotion_id_foreign",
|
||||
"columnNames": ["promotion_id"],
|
||||
"columnNames": [
|
||||
"promotion_id"
|
||||
],
|
||||
"localTableName": "public.promotion_promotion_rule",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"promotion_promotion_rule_promotion_rule_id_foreign": {
|
||||
"constraintName": "promotion_promotion_rule_promotion_rule_id_foreign",
|
||||
"columnNames": ["promotion_rule_id"],
|
||||
"columnNames": [
|
||||
"promotion_rule_id"
|
||||
],
|
||||
"localTableName": "public.promotion_promotion_rule",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion_rule",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
@@ -745,7 +849,10 @@
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "application_method_target_rules_pkey",
|
||||
"columnNames": ["application_method_id", "promotion_rule_id"],
|
||||
"columnNames": [
|
||||
"application_method_id",
|
||||
"promotion_rule_id"
|
||||
],
|
||||
"composite": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
@@ -755,18 +862,26 @@
|
||||
"foreignKeys": {
|
||||
"application_method_target_rules_application_method_id_foreign": {
|
||||
"constraintName": "application_method_target_rules_application_method_id_foreign",
|
||||
"columnNames": ["application_method_id"],
|
||||
"columnNames": [
|
||||
"application_method_id"
|
||||
],
|
||||
"localTableName": "public.application_method_target_rules",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion_application_method",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"application_method_target_rules_promotion_rule_id_foreign": {
|
||||
"constraintName": "application_method_target_rules_promotion_rule_id_foreign",
|
||||
"columnNames": ["promotion_rule_id"],
|
||||
"columnNames": [
|
||||
"promotion_rule_id"
|
||||
],
|
||||
"localTableName": "public.application_method_target_rules",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion_rule",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
@@ -799,7 +914,10 @@
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "application_method_buy_rules_pkey",
|
||||
"columnNames": ["application_method_id", "promotion_rule_id"],
|
||||
"columnNames": [
|
||||
"application_method_id",
|
||||
"promotion_rule_id"
|
||||
],
|
||||
"composite": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
@@ -809,18 +927,26 @@
|
||||
"foreignKeys": {
|
||||
"application_method_buy_rules_application_method_id_foreign": {
|
||||
"constraintName": "application_method_buy_rules_application_method_id_foreign",
|
||||
"columnNames": ["application_method_id"],
|
||||
"columnNames": [
|
||||
"application_method_id"
|
||||
],
|
||||
"localTableName": "public.application_method_buy_rules",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion_application_method",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"application_method_buy_rules_promotion_rule_id_foreign": {
|
||||
"constraintName": "application_method_buy_rules_promotion_rule_id_foreign",
|
||||
"columnNames": ["promotion_rule_id"],
|
||||
"columnNames": [
|
||||
"promotion_rule_id"
|
||||
],
|
||||
"localTableName": "public.application_method_buy_rules",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion_rule",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
@@ -893,7 +1019,9 @@
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": ["promotion_rule_id"],
|
||||
"columnNames": [
|
||||
"promotion_rule_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_promotion_rule_promotion_rule_value_id",
|
||||
"primary": false,
|
||||
@@ -901,7 +1029,9 @@
|
||||
},
|
||||
{
|
||||
"keyName": "promotion_rule_value_pkey",
|
||||
"columnNames": ["id"],
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
@@ -911,9 +1041,13 @@
|
||||
"foreignKeys": {
|
||||
"promotion_rule_value_promotion_rule_id_foreign": {
|
||||
"constraintName": "promotion_rule_value_promotion_rule_id_foreign",
|
||||
"columnNames": ["promotion_rule_id"],
|
||||
"columnNames": [
|
||||
"promotion_rule_id"
|
||||
],
|
||||
"localTableName": "public.promotion_rule_value",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.promotion_rule",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
|
||||
@@ -3,14 +3,14 @@ import { Migration } from "@mikro-orm/migrations"
|
||||
export class Migration20240227120221 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(
|
||||
'create table if not exists "promotion_campaign" ("id" text not null, "name" text not null, "description" text null, "currency" text null, "campaign_identifier" text not null, "starts_at" timestamptz null, "ends_at" timestamptz null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_campaign_pkey" primary key ("id"));'
|
||||
'create table if not exists "promotion_campaign" ("id" text not null, "name" text not null, "description" text null, "campaign_identifier" text not null, "starts_at" timestamptz null, "ends_at" timestamptz null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_campaign_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "promotion_campaign" add constraint "IDX_campaign_identifier_unique" unique ("campaign_identifier");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table if not exists "promotion_campaign_budget" ("id" text not null, "type" text check ("type" in (\'spend\', \'usage\')) not null, "campaign_id" text not null, "limit" numeric null, "raw_limit" jsonb null, "used" numeric null, "raw_used" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_campaign_budget_pkey" primary key ("id"));'
|
||||
'create table if not exists "promotion_campaign_budget" ("id" text not null, "type" text check ("type" in (\'spend\', \'usage\')) not null, "campaign_id" text not null, "limit" numeric null, "raw_limit" jsonb null, "used" numeric not null default 0, "raw_used" jsonb not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_campaign_budget_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index if not exists "IDX_campaign_budget_type" on "promotion_campaign_budget" ("type");'
|
||||
@@ -113,5 +113,37 @@ export class Migration20240227120221 extends Migration {
|
||||
this.addSql(
|
||||
'alter table if exists "promotion_rule_value" add constraint "promotion_rule_value_promotion_rule_id_foreign" foreign key ("promotion_rule_id") references "promotion_rule" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table if exists "promotion" drop constraint if exists "promotion_campaign_id_foreign";'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table if exists "promotion" add constraint "promotion_campaign_id_foreign" foreign key ("campaign_id") references "promotion_campaign" ("id") on update cascade on delete set null;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table if exists "promotion_application_method" add column if not exists "currency_code" text not null;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_promotion_application_method_currency_code" ON "promotion_application_method" (currency_code) WHERE deleted_at IS NOT NULL;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "promotion_application_method" alter column "value" type numeric using ("value"::numeric);'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "promotion_application_method" alter column "raw_value" type jsonb using ("raw_value"::jsonb);'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "promotion_application_method" alter column "raw_value" set not null;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table if exists "promotion_campaign_budget" add column if not exists "currency_code" text null;'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
ApplicationMethodTargetTypeValues,
|
||||
ApplicationMethodTypeValues,
|
||||
BigNumberRawValue,
|
||||
DAL,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
BigNumber,
|
||||
DALUtils,
|
||||
MikroOrmBigNumberProperty,
|
||||
PromotionUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
@@ -22,34 +22,34 @@ import {
|
||||
ManyToMany,
|
||||
OnInit,
|
||||
OneToOne,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import Promotion from "./promotion"
|
||||
import PromotionRule from "./promotion-rule"
|
||||
|
||||
type OptionalFields =
|
||||
| "value"
|
||||
| "max_quantity"
|
||||
| "apply_to_quantity"
|
||||
| "buy_rules_min_quantity"
|
||||
| "allocation"
|
||||
| DAL.SoftDeletableEntityDateColumns
|
||||
const tableName = "promotion_application_method"
|
||||
const CurrencyCodeIndex = createPsqlIndexStatementHelper({
|
||||
tableName,
|
||||
columns: "currency_code",
|
||||
where: "deleted_at IS NOT NULL",
|
||||
})
|
||||
|
||||
@Entity({ tableName: "promotion_application_method" })
|
||||
@Entity({ tableName })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
export default class ApplicationMethod {
|
||||
[OptionalProps]?: OptionalFields
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@MikroOrmBigNumberProperty({ nullable: true })
|
||||
value: BigNumber | number | null = null
|
||||
@MikroOrmBigNumberProperty()
|
||||
value: BigNumber | number | null
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
raw_value: BigNumberRawValue | null = null
|
||||
@Property({ columnType: "jsonb" })
|
||||
raw_value: BigNumberRawValue | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
@CurrencyCodeIndex.MikroORMIndex()
|
||||
currency_code: string | null = null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true, serializer: Number })
|
||||
max_quantity?: number | null = null
|
||||
|
||||
@@ -47,17 +47,20 @@ export default class CampaignBudget {
|
||||
})
|
||||
campaign: Campaign | null = null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
currency_code: string | null = null
|
||||
|
||||
@MikroOrmBigNumberProperty({ nullable: true })
|
||||
limit: BigNumber | number | null = null
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
raw_limit: BigNumberRawValue | null = null
|
||||
|
||||
@MikroOrmBigNumberProperty({ nullable: true })
|
||||
used: BigNumber | number | null = null
|
||||
@MikroOrmBigNumberProperty({ default: 0 })
|
||||
used: BigNumber | number = 0
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
raw_used: BigNumberRawValue | null = null
|
||||
@Property({ columnType: "jsonb" })
|
||||
raw_used: BigNumberRawValue
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
|
||||
@@ -19,7 +19,6 @@ import Promotion from "./promotion"
|
||||
type OptionalRelations = "budget"
|
||||
type OptionalFields =
|
||||
| "description"
|
||||
| "currency"
|
||||
| "starts_at"
|
||||
| "ends_at"
|
||||
| DAL.SoftDeletableEntityDateColumns
|
||||
@@ -40,9 +39,6 @@ export default class Campaign {
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
description: string | null = null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
currency: string | null = null
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
@Unique({
|
||||
name: "IDX_campaign_identifier_unique",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
CampaignBudgetTypeValues,
|
||||
Context,
|
||||
DAL,
|
||||
InternalModuleDeclaration,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
arrayDifference,
|
||||
deduplicate,
|
||||
isDefined,
|
||||
isPresent,
|
||||
isString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
@@ -475,6 +477,11 @@ export default class PromotionModuleService<
|
||||
const promotionsData: CreatePromotionDTO[] = []
|
||||
const applicationMethodsData: CreateApplicationMethodDTO[] = []
|
||||
const campaignsData: CreateCampaignDTO[] = []
|
||||
const existingCampaigns = await this.campaignService_.list(
|
||||
{ id: data.map((d) => d.campaign_id).filter((id) => isString(id)) },
|
||||
{ relations: ["budget"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const promotionCodeApplicationMethodDataMap = new Map<
|
||||
string,
|
||||
@@ -504,12 +511,10 @@ export default class PromotionModuleService<
|
||||
campaign_id: campaignId,
|
||||
...promotionData
|
||||
} of data) {
|
||||
if (applicationMethodData) {
|
||||
promotionCodeApplicationMethodDataMap.set(
|
||||
promotionData.code,
|
||||
applicationMethodData
|
||||
)
|
||||
}
|
||||
promotionCodeApplicationMethodDataMap.set(
|
||||
promotionData.code,
|
||||
applicationMethodData
|
||||
)
|
||||
|
||||
if (rulesData) {
|
||||
promotionCodeRulesDataMap.set(promotionData.code, rulesData)
|
||||
@@ -522,6 +527,38 @@ export default class PromotionModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
if (!campaignData && !campaignId) {
|
||||
promotionsData.push({ ...promotionData })
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const existingCampaign = existingCampaigns.find(
|
||||
(c) => c.id === campaignId
|
||||
)
|
||||
|
||||
if (campaignId && !existingCampaign) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find campaign with id - ${campaignId}`
|
||||
)
|
||||
}
|
||||
|
||||
const campaignCurrency =
|
||||
campaignData?.budget?.currency_code ||
|
||||
existingCampaigns.find((c) => c.id === campaignId)?.budget
|
||||
?.currency_code
|
||||
|
||||
if (
|
||||
campaignData?.budget?.type === CampaignBudgetType.SPEND &&
|
||||
campaignCurrency !== applicationMethodData?.currency_code
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Currency between promotion and campaigns should match`
|
||||
)
|
||||
}
|
||||
|
||||
if (campaignData) {
|
||||
promotionCodeCampaignMap.set(promotionData.code, campaignData)
|
||||
}
|
||||
@@ -536,7 +573,6 @@ export default class PromotionModuleService<
|
||||
promotionsData,
|
||||
sharedContext
|
||||
)
|
||||
const promotionsToAdd: PromotionTypes.AddPromotionsToCampaignDTO[] = []
|
||||
|
||||
for (const promotion of createdPromotions) {
|
||||
const applMethodData = promotionCodeApplicationMethodDataMap.get(
|
||||
@@ -708,6 +744,10 @@ export default class PromotionModuleService<
|
||||
{ id: promotionIds },
|
||||
{ relations: ["application_method"] }
|
||||
)
|
||||
const existingCampaigns = await this.campaignService_.list(
|
||||
{ id: data.map((d) => d.campaign_id).filter((d) => isPresent(d)) },
|
||||
{ relations: ["budget"] }
|
||||
)
|
||||
|
||||
const existingPromotionsMap = new Map<string, Promotion>(
|
||||
existingPromotions.map((promotion) => [promotion.id, promotion])
|
||||
@@ -721,20 +761,40 @@ export default class PromotionModuleService<
|
||||
campaign_id: campaignId,
|
||||
...promotionData
|
||||
} of data) {
|
||||
if (campaignId) {
|
||||
const existingCampaign = existingCampaigns.find(
|
||||
(c) => c.id === campaignId
|
||||
)
|
||||
const existingPromotion = existingPromotionsMap.get(promotionData.id)!
|
||||
const existingApplicationMethod = existingPromotion?.application_method
|
||||
const promotionCurrencyCode =
|
||||
existingApplicationMethod?.currency_code ||
|
||||
applicationMethodData?.currency_code
|
||||
|
||||
if (campaignId && !existingCampaign) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Could not find campaign with id ${campaignId}`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
campaignId &&
|
||||
existingCampaign?.budget?.type === CampaignBudgetType.SPEND &&
|
||||
existingCampaign.budget.currency_code !== promotionCurrencyCode
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Currency code doesn't match for campaign (${campaignId}) and promotion (${existingPromotion.id})`
|
||||
)
|
||||
}
|
||||
|
||||
if (isDefined(campaignId)) {
|
||||
promotionsData.push({ ...promotionData, campaign_id: campaignId })
|
||||
} else {
|
||||
promotionsData.push(promotionData)
|
||||
}
|
||||
|
||||
if (!applicationMethodData) {
|
||||
continue
|
||||
}
|
||||
|
||||
const existingPromotion = existingPromotionsMap.get(promotionData.id)
|
||||
const existingApplicationMethod = existingPromotion?.application_method
|
||||
|
||||
if (!existingApplicationMethod) {
|
||||
if (!applicationMethodData || !existingApplicationMethod) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -913,9 +973,11 @@ export default class PromotionModuleService<
|
||||
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<PromotionTypes.PromotionRuleDTO[]> {
|
||||
const promotion = await this.promotionService_.retrieve(promotionId, {
|
||||
relations: ["application_method"],
|
||||
})
|
||||
const promotion = await this.promotionService_.retrieve(
|
||||
promotionId,
|
||||
{ relations: ["application_method"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const applicationMethod = promotion.application_method
|
||||
|
||||
@@ -1153,6 +1215,8 @@ export default class PromotionModuleService<
|
||||
)
|
||||
|
||||
if (campaignBudgetData) {
|
||||
this.validateCampaignBudgetData(campaignBudgetData)
|
||||
|
||||
campaignBudgetsData.push({
|
||||
...campaignBudgetData,
|
||||
campaign: createdCampaign.id,
|
||||
@@ -1170,6 +1234,28 @@ export default class PromotionModuleService<
|
||||
return createdCampaigns
|
||||
}
|
||||
|
||||
protected validateCampaignBudgetData(data: {
|
||||
type?: CampaignBudgetTypeValues
|
||||
currency_code?: string | null
|
||||
}) {
|
||||
if (!data.type) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Campaign Budget type is a required field`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
data.type === CampaignBudgetType.SPEND &&
|
||||
!isPresent(data.currency_code)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Campaign Budget type is a required field`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async updateCampaigns(
|
||||
data: PromotionTypes.UpdateCampaignDTO,
|
||||
sharedContext?: Context
|
||||
@@ -1207,7 +1293,8 @@ export default class PromotionModuleService<
|
||||
) {
|
||||
const campaignIds = data.map((d) => d.id)
|
||||
const campaignsData: UpdateCampaignDTO[] = []
|
||||
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
||||
const updateBudgetData: UpdateCampaignBudgetDTO[] = []
|
||||
const createBudgetData: CreateCampaignBudgetDTO[] = []
|
||||
|
||||
const existingCampaigns = await this.listCampaigns(
|
||||
{ id: campaignIds },
|
||||
@@ -1220,18 +1307,38 @@ export default class PromotionModuleService<
|
||||
)
|
||||
|
||||
for (const updateCampaignData of data) {
|
||||
const { budget: campaignBudgetData, ...campaignData } = updateCampaignData
|
||||
|
||||
const existingCampaign = existingCampaignsMap.get(campaignData.id)
|
||||
const existingCampaignBudget = existingCampaign?.budget
|
||||
const { budget: budgetData, ...campaignData } = updateCampaignData
|
||||
const existingCampaign = existingCampaignsMap.get(campaignData.id)!
|
||||
|
||||
campaignsData.push(campaignData)
|
||||
|
||||
if (existingCampaignBudget && campaignBudgetData) {
|
||||
campaignBudgetsData.push({
|
||||
id: existingCampaignBudget.id,
|
||||
...campaignBudgetData,
|
||||
})
|
||||
// Type & currency code of the budget is immutable, we don't allow for it to be updated.
|
||||
// If an existing budget is present, we remove the type and currency from being updated
|
||||
if (
|
||||
(existingCampaign?.budget && budgetData?.type) ||
|
||||
budgetData?.currency_code
|
||||
) {
|
||||
delete budgetData?.type
|
||||
delete budgetData?.currency_code
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Campaign budget attributes (type, currency_code) are immutable`
|
||||
)
|
||||
}
|
||||
|
||||
if (budgetData) {
|
||||
if (existingCampaign?.budget) {
|
||||
updateBudgetData.push({
|
||||
id: existingCampaign.budget.id,
|
||||
...budgetData,
|
||||
})
|
||||
} else {
|
||||
createBudgetData.push({
|
||||
...budgetData,
|
||||
campaign: existingCampaign.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1240,11 +1347,12 @@ export default class PromotionModuleService<
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (campaignBudgetsData.length) {
|
||||
await this.campaignBudgetService_.update(
|
||||
campaignBudgetsData,
|
||||
sharedContext
|
||||
)
|
||||
if (updateBudgetData.length) {
|
||||
await this.campaignBudgetService_.update(updateBudgetData, sharedContext)
|
||||
}
|
||||
|
||||
if (createBudgetData.length) {
|
||||
await this.campaignBudgetService_.create(createBudgetData, sharedContext)
|
||||
}
|
||||
|
||||
return updatedCampaigns
|
||||
@@ -1274,7 +1382,7 @@ export default class PromotionModuleService<
|
||||
const campaign = await this.campaignService_.retrieve(id, {}, sharedContext)
|
||||
const promotionsToAdd = await this.promotionService_.list(
|
||||
{ id: promotionIds, campaign_id: null },
|
||||
{ take: null },
|
||||
{ take: null, relations: ["application_method"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
@@ -1292,6 +1400,20 @@ export default class PromotionModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
const promotionsWithInvalidCurrency = promotionsToAdd.filter(
|
||||
(promotion) =>
|
||||
campaign.budget?.type === CampaignBudgetType.SPEND &&
|
||||
promotion.application_method?.currency_code !==
|
||||
campaign?.budget?.currency_code
|
||||
)
|
||||
|
||||
if (promotionsWithInvalidCurrency.length > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Cannot add promotions to campaign where currency_code don't match.`
|
||||
)
|
||||
}
|
||||
|
||||
await this.promotionService_.update(
|
||||
promotionsToAdd.map((promotion) => ({
|
||||
id: promotion.id,
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface CreateApplicationMethodDTO {
|
||||
target_type: ApplicationMethodTargetTypeValues
|
||||
allocation?: ApplicationMethodAllocationValues
|
||||
value?: number
|
||||
currency_code: string
|
||||
promotion: Promotion | string | PromotionDTO
|
||||
max_quantity?: number | null
|
||||
buy_rules_min_quantity?: number | null
|
||||
@@ -19,11 +20,12 @@ export interface CreateApplicationMethodDTO {
|
||||
}
|
||||
|
||||
export interface UpdateApplicationMethodDTO {
|
||||
id: string
|
||||
id?: string
|
||||
type?: ApplicationMethodTypeValues
|
||||
target_type?: ApplicationMethodTargetTypeValues
|
||||
allocation?: ApplicationMethodAllocationValues
|
||||
value?: number
|
||||
currency_code?: string
|
||||
promotion?: Promotion | string | PromotionDTO
|
||||
max_quantity?: number | null
|
||||
buy_rules_min_quantity?: number | null
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Campaign } from "@models"
|
||||
|
||||
export interface CreateCampaignBudgetDTO {
|
||||
type?: CampaignBudgetTypeValues
|
||||
limit?: number
|
||||
limit?: number | null
|
||||
currency_code?: string | null
|
||||
used?: number
|
||||
campaign?: Campaign | string
|
||||
}
|
||||
@@ -11,6 +12,7 @@ export interface CreateCampaignBudgetDTO {
|
||||
export interface UpdateCampaignBudgetDTO {
|
||||
id: string
|
||||
type?: CampaignBudgetTypeValues
|
||||
limit?: number
|
||||
limit?: number | null
|
||||
currency_code?: string | null
|
||||
used?: number
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Promotion } from "@models"
|
||||
export interface CreateCampaignDTO {
|
||||
name: string
|
||||
description?: string
|
||||
currency?: string
|
||||
campaign_identifier: string
|
||||
starts_at?: Date
|
||||
ends_at?: Date
|
||||
@@ -15,7 +14,6 @@ export interface UpdateCampaignDTO {
|
||||
id: string
|
||||
name?: string
|
||||
description?: string
|
||||
currency?: string
|
||||
campaign_identifier?: string
|
||||
starts_at?: Date
|
||||
ends_at?: Date
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface CreatePromotionDTO {
|
||||
code: string
|
||||
type: PromotionTypeValues
|
||||
is_automatic?: boolean
|
||||
campaign_id?: string
|
||||
campaign_id?: string | null
|
||||
}
|
||||
|
||||
export interface UpdatePromotionDTO {
|
||||
@@ -12,5 +12,5 @@ export interface UpdatePromotionDTO {
|
||||
code?: string
|
||||
type?: PromotionTypeValues
|
||||
is_automatic?: boolean
|
||||
campaign_id?: string
|
||||
campaign_id?: string | null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user