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:
Riqwan Thamir
2024-05-23 15:28:00 +02:00
committed by GitHub
co-authored by Adrien de Peretti Shahed Nasser Stevche Radevski Oli Juhl Kasper Fabricius Kristensen
parent 4a10821bfe
commit d1d23f1e8d
72 changed files with 5380 additions and 3473 deletions
@@ -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
}