diff --git a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts index 68fd15fab5cf9..98b67a323b8bc 100644 --- a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts +++ b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts @@ -5009,26 +5009,31 @@ moduleIntegrationTestRunner({ context ) - expect(JSON.parse(JSON.stringify(result))).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt2", - amount: 1225, - code: "BUY50GET1000", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 1275, - code: "BUY50GET1000", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt2", - amount: 50, - code: "BUY10GET20", - }, - ]) + const serializedResult = JSON.parse(JSON.stringify(result)) + + expect(serializedResult).toHaveLength(3) + expect(serializedResult).toEqual( + expect.arrayContaining([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt2", + amount: 1225, + code: "BUY50GET1000", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 1275, + code: "BUY50GET1000", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt2", + amount: 50, + code: "BUY10GET20", + }, + ]) + ) }) it("should compute adjustment accurately across items", async () => { diff --git a/packages/modules/promotion/src/migrations/.snapshot-medusa-promotion.json b/packages/modules/promotion/src/migrations/.snapshot-medusa-promotion.json index 90fb4fddd73ce..c754f0dbbe4ee 100644 --- a/packages/modules/promotion/src/migrations/.snapshot-medusa-promotion.json +++ b/packages/modules/promotion/src/migrations/.snapshot-medusa-promotion.json @@ -102,6 +102,7 @@ "keyName": "IDX_promotion_campaign_deleted_at", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_campaign_deleted_at\" ON \"promotion_campaign\" (deleted_at) WHERE deleted_at IS NULL" @@ -110,6 +111,7 @@ "keyName": "IDX_promotion_campaign_campaign_identifier_unique", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_promotion_campaign_campaign_identifier_unique\" ON \"promotion_campaign\" (campaign_identifier) WHERE deleted_at IS NULL" @@ -120,12 +122,14 @@ "id" ], "composite": false, + "constraint": true, "primary": true, "unique": true } ], "checks": [], - "foreignKeys": {} + "foreignKeys": {}, + "nativeEnums": {} }, { "columns": { @@ -246,6 +250,7 @@ "keyName": "IDX_campaign_budget_type", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_campaign_budget_type\" ON \"promotion_campaign_budget\" (type) WHERE deleted_at IS NULL" @@ -254,6 +259,7 @@ "keyName": "IDX_promotion_campaign_budget_campaign_id_unique", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_promotion_campaign_budget_campaign_id_unique\" ON \"promotion_campaign_budget\" (campaign_id) WHERE deleted_at IS NULL" @@ -262,6 +268,7 @@ "keyName": "IDX_promotion_campaign_budget_deleted_at", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_campaign_budget_deleted_at\" ON \"promotion_campaign_budget\" (deleted_at) WHERE deleted_at IS NULL" @@ -272,6 +279,7 @@ "id" ], "composite": false, + "constraint": true, "primary": true, "unique": true } @@ -291,7 +299,8 @@ "deleteRule": "cascade", "updateRule": "cascade" } - } + }, + "nativeEnums": {} }, { "columns": { @@ -396,26 +405,11 @@ "name": "promotion", "schema": "public", "indexes": [ - { - "keyName": "IDX_promotion_code_unique", - "columnNames": [], - "composite": false, - "primary": false, - "unique": false, - "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_promotion_code_unique\" ON \"promotion\" (code) WHERE deleted_at IS NULL" - }, - { - "keyName": "IDX_promotion_code", - "columnNames": [], - "composite": false, - "primary": false, - "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_code\" ON \"promotion\" (code) WHERE deleted_at IS NULL" - }, { "keyName": "IDX_promotion_type", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_type\" ON \"promotion\" (type) WHERE deleted_at IS NULL" @@ -424,6 +418,7 @@ "keyName": "IDX_promotion_status", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_status\" ON \"promotion\" (status) WHERE deleted_at IS NULL" @@ -432,6 +427,7 @@ "keyName": "IDX_promotion_campaign_id", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_campaign_id\" ON \"promotion\" (campaign_id) WHERE deleted_at IS NULL" @@ -440,16 +436,27 @@ "keyName": "IDX_promotion_deleted_at", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_deleted_at\" ON \"promotion\" (deleted_at) WHERE deleted_at IS NULL" }, + { + "keyName": "IDX_unique_promotion_code", + "columnNames": [], + "composite": false, + "constraint": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_unique_promotion_code\" ON \"promotion\" (code) WHERE deleted_at IS NULL" + }, { "keyName": "promotion_pkey", "columnNames": [ "id" ], "composite": false, + "constraint": true, "primary": true, "unique": true } @@ -469,7 +476,8 @@ "deleteRule": "set null", "updateRule": "cascade" } - } + }, + "nativeEnums": {} }, { "columns": { @@ -625,6 +633,7 @@ "keyName": "IDX_application_method_type", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_application_method_type\" ON \"promotion_application_method\" (type) WHERE deleted_at IS NULL" @@ -633,6 +642,7 @@ "keyName": "IDX_application_method_target_type", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_application_method_target_type\" ON \"promotion_application_method\" (target_type) WHERE deleted_at IS NULL" @@ -641,6 +651,7 @@ "keyName": "IDX_application_method_allocation", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_application_method_allocation\" ON \"promotion_application_method\" (allocation) WHERE deleted_at IS NULL" @@ -649,6 +660,7 @@ "keyName": "IDX_promotion_application_method_promotion_id_unique", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_promotion_application_method_promotion_id_unique\" ON \"promotion_application_method\" (promotion_id) WHERE deleted_at IS NULL" @@ -657,6 +669,7 @@ "keyName": "IDX_promotion_application_method_deleted_at", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_application_method_deleted_at\" ON \"promotion_application_method\" (deleted_at) WHERE deleted_at IS NULL" @@ -665,6 +678,7 @@ "keyName": "IDX_promotion_application_method_currency_code", "columnNames": [], "composite": false, + "constraint": 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" @@ -675,6 +689,7 @@ "id" ], "composite": false, + "constraint": true, "primary": true, "unique": true } @@ -694,7 +709,8 @@ "deleteRule": "cascade", "updateRule": "cascade" } - } + }, + "nativeEnums": {} }, { "columns": { @@ -783,6 +799,7 @@ "keyName": "IDX_promotion_rule_attribute", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_rule_attribute\" ON \"promotion_rule\" (attribute) WHERE deleted_at IS NULL" @@ -791,6 +808,7 @@ "keyName": "IDX_promotion_rule_operator", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_rule_operator\" ON \"promotion_rule\" (operator) WHERE deleted_at IS NULL" @@ -799,6 +817,7 @@ "keyName": "IDX_promotion_rule_deleted_at", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_rule_deleted_at\" ON \"promotion_rule\" (deleted_at) WHERE deleted_at IS NULL" @@ -809,12 +828,14 @@ "id" ], "composite": false, + "constraint": true, "primary": true, "unique": true } ], "checks": [], - "foreignKeys": {} + "foreignKeys": {}, + "nativeEnums": {} }, { "columns": { @@ -847,6 +868,7 @@ "promotion_rule_id" ], "composite": true, + "constraint": true, "primary": true, "unique": true } @@ -879,7 +901,8 @@ "deleteRule": "cascade", "updateRule": "cascade" } - } + }, + "nativeEnums": {} }, { "columns": { @@ -912,6 +935,7 @@ "promotion_rule_id" ], "composite": true, + "constraint": true, "primary": true, "unique": true } @@ -944,7 +968,8 @@ "deleteRule": "cascade", "updateRule": "cascade" } - } + }, + "nativeEnums": {} }, { "columns": { @@ -977,6 +1002,7 @@ "promotion_rule_id" ], "composite": true, + "constraint": true, "primary": true, "unique": true } @@ -1009,7 +1035,8 @@ "deleteRule": "cascade", "updateRule": "cascade" } - } + }, + "nativeEnums": {} }, { "columns": { @@ -1080,6 +1107,7 @@ "keyName": "IDX_promotion_rule_value_promotion_rule_id", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_rule_value_promotion_rule_id\" ON \"promotion_rule_value\" (promotion_rule_id) WHERE deleted_at IS NULL" @@ -1088,6 +1116,7 @@ "keyName": "IDX_promotion_rule_value_deleted_at", "columnNames": [], "composite": false, + "constraint": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_rule_value_deleted_at\" ON \"promotion_rule_value\" (deleted_at) WHERE deleted_at IS NULL" @@ -1098,6 +1127,7 @@ "id" ], "composite": false, + "constraint": true, "primary": true, "unique": true } @@ -1117,7 +1147,9 @@ "deleteRule": "cascade", "updateRule": "cascade" } - } + }, + "nativeEnums": {} } - ] + ], + "nativeEnums": {} } diff --git a/packages/modules/promotion/src/migrations/Migration20250226130616.ts b/packages/modules/promotion/src/migrations/Migration20250226130616.ts new file mode 100644 index 0000000000000..17525d2c9a6f1 --- /dev/null +++ b/packages/modules/promotion/src/migrations/Migration20250226130616.ts @@ -0,0 +1,29 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20250226130616 extends Migration { + override async up(): Promise { + this.addSql( + 'alter table if exists "promotion" drop constraint if exists "IDX_promotion_code_unique";' + ) + this.addSql(`drop index if exists "IDX_promotion_code_unique";`) + this.addSql(`drop index if exists "IDX_promotion_code";`) + this.addSql( + `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_unique_promotion_code" ON "promotion" (code) WHERE deleted_at IS NULL;` + ) + } + + override async down(): Promise { + this.addSql(`drop index if exists "IDX_unique_promotion_code";`) + + this.addSql( + `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_promotion_code_unique" ON "promotion" (code) WHERE deleted_at IS NULL;` + ) + this.addSql( + `CREATE INDEX IF NOT EXISTS "IDX_promotion_code" ON "promotion" (code) WHERE deleted_at IS NULL;` + ) + + this.addSql( + 'alter table if exists "promotion" add constraint "IDX_promotion_code_unique" unique ("code");' + ) + } +} diff --git a/packages/modules/promotion/src/models/promotion.ts b/packages/modules/promotion/src/models/promotion.ts index ff10fc31518c3..679ddf3db95f9 100644 --- a/packages/modules/promotion/src/models/promotion.ts +++ b/packages/modules/promotion/src/models/promotion.ts @@ -6,11 +6,7 @@ import PromotionRule from "./promotion-rule" const Promotion = model .define("Promotion", { id: model.id({ prefix: "promo" }).primaryKey(), - code: model - .text() - .searchable() - .unique("IDX_promotion_code_unique") - .index("IDX_promotion_code"), + code: model.text().searchable(), is_automatic: model.boolean().default(false), type: model.enum(PromotionUtils.PromotionType).index("IDX_promotion_type"), status: model @@ -35,5 +31,13 @@ const Promotion = model .cascades({ delete: ["application_method"], }) + .indexes([ + { + name: "IDX_unique_promotion_code", + on: ["code"], + where: "deleted_at IS NULL", + unique: true, + }, + ]) export default Promotion diff --git a/packages/modules/promotion/src/utils/compute-actions/buy-get.ts b/packages/modules/promotion/src/utils/compute-actions/buy-get.ts index 03af6114ce79b..caaae3295ab79 100644 --- a/packages/modules/promotion/src/utils/compute-actions/buy-get.ts +++ b/packages/modules/promotion/src/utils/compute-actions/buy-get.ts @@ -242,13 +242,53 @@ export function getComputedActionsForBuyGet( export function sortByBuyGetType(a, b) { if (a.type === PromotionType.BUYGET && b.type !== PromotionType.BUYGET) { - return -1 + return -1 // BuyGet promotions come first } else if ( a.type !== PromotionType.BUYGET && b.type === PromotionType.BUYGET ) { - return 1 + return 1 // BuyGet promotions come first + } else if (a.type === b.type) { + // If types are equal, sort by application_method.value in descending order when types are equal + if (a.application_method.value < b.application_method.value) { + return 1 // Higher value comes first + } else if (a.application_method.value > b.application_method.value) { + return -1 // Lower value comes later + } + + /* + If the promotion is a BuyGet & the value is the same, we need to sort by the following criteria: + - buy_rules_min_quantity in descending order + - apply_to_quantity in descending order + */ + if (a.type === PromotionType.BUYGET) { + if ( + a.application_method.buy_rules_min_quantity < + b.application_method.buy_rules_min_quantity + ) { + return 1 + } else if ( + a.application_method.buy_rules_min_quantity > + b.application_method.buy_rules_min_quantity + ) { + return -1 + } + + if ( + a.application_method.apply_to_quantity < + b.application_method.apply_to_quantity + ) { + return 1 + } else if ( + a.application_method.apply_to_quantity > + b.application_method.apply_to_quantity + ) { + return -1 + } + } + + return 0 // If all criteria are equal, keep original order } else { - return 0 + return 0 // If types are different (and not BuyGet), keep original order } }