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:
72 files changed
+5380
-3473
No files matched your search
@@ -49,6 +49,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: "300",
|
value: "300",
|
||||||
apply_to_quantity: 1,
|
apply_to_quantity: 1,
|
||||||
|
currency_code: "usd",
|
||||||
max_quantity: 1,
|
max_quantity: 1,
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
@@ -69,6 +70,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "1000",
|
value: "1000",
|
||||||
apply_to_quantity: 1,
|
apply_to_quantity: 1,
|
||||||
|
currency_code: "usd",
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "product_id",
|
attribute: "product_id",
|
||||||
@@ -172,6 +174,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: "100",
|
value: "100",
|
||||||
max_quantity: 1,
|
max_quantity: 1,
|
||||||
|
currency_code: "usd",
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "name",
|
attribute: "name",
|
||||||
@@ -205,6 +208,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: "200",
|
value: "200",
|
||||||
max_quantity: 1,
|
max_quantity: 1,
|
||||||
|
currency_code: "usd",
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "name",
|
attribute: "name",
|
||||||
|
|||||||
@@ -534,6 +534,7 @@ medusaIntegrationTestRunner({
|
|||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: 300,
|
value: 300,
|
||||||
|
currency_code: "usd",
|
||||||
apply_to_quantity: 1,
|
apply_to_quantity: 1,
|
||||||
max_quantity: 1,
|
max_quantity: 1,
|
||||||
target_rules: targetRules,
|
target_rules: targetRules,
|
||||||
@@ -547,6 +548,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
|
currency_code: "usd",
|
||||||
value: 1000,
|
value: 1000,
|
||||||
apply_to_quantity: 1,
|
apply_to_quantity: 1,
|
||||||
target_rules: targetRules,
|
target_rules: targetRules,
|
||||||
@@ -1232,6 +1234,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: 300,
|
value: 300,
|
||||||
apply_to_quantity: 2,
|
apply_to_quantity: 2,
|
||||||
|
currency_code: "usd",
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "product_id",
|
attribute: "product_id",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: "300",
|
value: "300",
|
||||||
apply_to_quantity: 1,
|
apply_to_quantity: 1,
|
||||||
|
currency_code: "usd",
|
||||||
max_quantity: 1,
|
max_quantity: 1,
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
@@ -69,6 +70,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: "300",
|
value: "300",
|
||||||
apply_to_quantity: 1,
|
apply_to_quantity: 1,
|
||||||
|
currency_code: "usd",
|
||||||
max_quantity: 1,
|
max_quantity: 1,
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
@@ -189,6 +191,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: "100",
|
value: "100",
|
||||||
max_quantity: 1,
|
max_quantity: 1,
|
||||||
|
currency_code: "usd",
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "name",
|
attribute: "name",
|
||||||
@@ -220,6 +223,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: "100",
|
value: "100",
|
||||||
max_quantity: 1,
|
max_quantity: 1,
|
||||||
|
currency_code: "usd",
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "name",
|
attribute: "name",
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ jest.setTimeout(50000)
|
|||||||
export const campaignData = {
|
export const campaignData = {
|
||||||
name: "campaign 1",
|
name: "campaign 1",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-1",
|
campaign_identifier: "test-1",
|
||||||
starts_at: new Date("01/01/2023").toISOString(),
|
starts_at: new Date("01/01/2023").toISOString(),
|
||||||
ends_at: new Date("01/01/2024").toISOString(),
|
ends_at: new Date("01/01/2024").toISOString(),
|
||||||
budget: {
|
budget: {
|
||||||
type: CampaignBudgetType.SPEND,
|
type: CampaignBudgetType.SPEND,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
used: 0,
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,32 +24,57 @@ export const campaignsData = [
|
|||||||
id: "campaign-id-1",
|
id: "campaign-id-1",
|
||||||
name: "campaign 1",
|
name: "campaign 1",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-1",
|
campaign_identifier: "test-1",
|
||||||
starts_at: new Date("01/01/2023"),
|
starts_at: new Date("01/01/2023"),
|
||||||
ends_at: new Date("01/01/2024"),
|
ends_at: new Date("01/01/2024"),
|
||||||
budget: {
|
budget: {
|
||||||
type: CampaignBudgetType.SPEND,
|
type: CampaignBudgetType.SPEND,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
used: 0,
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "campaign-id-2",
|
id: "campaign-id-2",
|
||||||
name: "campaign 2",
|
name: "campaign 2",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-2",
|
campaign_identifier: "test-2",
|
||||||
starts_at: new Date("01/01/2023"),
|
starts_at: new Date("01/01/2023"),
|
||||||
ends_at: new Date("01/01/2024"),
|
ends_at: new Date("01/01/2024"),
|
||||||
budget: {
|
budget: {
|
||||||
type: CampaignBudgetType.USAGE,
|
type: CampaignBudgetType.USAGE,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
used: 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const promotionData = {
|
||||||
|
code: "TEST",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: {
|
||||||
|
target_type: "items",
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "each",
|
||||||
|
currency_code: "USD",
|
||||||
|
value: 100,
|
||||||
|
max_quantity: 100,
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "test.test",
|
||||||
|
operator: "eq",
|
||||||
|
values: ["test1", "test2"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
attribute: "test.test",
|
||||||
|
operator: "eq",
|
||||||
|
values: ["test1", "test2"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||||
const adminHeaders = {
|
const adminHeaders = {
|
||||||
headers: { "x-medusa-access-token": "test_token" },
|
headers: { "x-medusa-access-token": "test_token" },
|
||||||
@@ -88,6 +112,7 @@ medusaIntegrationTestRunner({
|
|||||||
value: 100,
|
value: 100,
|
||||||
max_quantity: 100,
|
max_quantity: 100,
|
||||||
target_rules: [],
|
target_rules: [],
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
rules: [],
|
rules: [],
|
||||||
}
|
}
|
||||||
@@ -109,13 +134,13 @@ medusaIntegrationTestRunner({
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "campaign 1",
|
name: "campaign 1",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-1",
|
campaign_identifier: "test-1",
|
||||||
starts_at: expect.any(String),
|
starts_at: expect.any(String),
|
||||||
ends_at: expect.any(String),
|
ends_at: expect.any(String),
|
||||||
budget: {
|
budget: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
type: "spend",
|
type: "spend",
|
||||||
|
currency_code: "USD",
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
used: 0,
|
used: 0,
|
||||||
raw_limit: {
|
raw_limit: {
|
||||||
@@ -138,7 +163,6 @@ medusaIntegrationTestRunner({
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "campaign 2",
|
name: "campaign 2",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-2",
|
campaign_identifier: "test-2",
|
||||||
starts_at: expect.any(String),
|
starts_at: expect.any(String),
|
||||||
ends_at: expect.any(String),
|
ends_at: expect.any(String),
|
||||||
@@ -147,6 +171,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: "usage",
|
type: "usage",
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
used: 0,
|
used: 0,
|
||||||
|
currency_code: null,
|
||||||
raw_limit: {
|
raw_limit: {
|
||||||
precision: 20,
|
precision: 20,
|
||||||
value: "1000",
|
value: "1000",
|
||||||
@@ -239,7 +264,6 @@ medusaIntegrationTestRunner({
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "campaign 1",
|
name: "campaign 1",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-1",
|
campaign_identifier: "test-1",
|
||||||
starts_at: expect.any(String),
|
starts_at: expect.any(String),
|
||||||
ends_at: expect.any(String),
|
ends_at: expect.any(String),
|
||||||
@@ -247,6 +271,7 @@ medusaIntegrationTestRunner({
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
type: "spend",
|
type: "spend",
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
|
currency_code: "USD",
|
||||||
raw_limit: {
|
raw_limit: {
|
||||||
precision: 20,
|
precision: 20,
|
||||||
value: "1000",
|
value: "1000",
|
||||||
@@ -297,11 +322,6 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should create a campaign successfully", async () => {
|
it("should create a campaign successfully", async () => {
|
||||||
const createdPromotion = await promotionModuleService.create({
|
|
||||||
code: "TEST",
|
|
||||||
type: "standard",
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await api.post(
|
const response = await api.post(
|
||||||
`/admin/campaigns?fields=*promotions`,
|
`/admin/campaigns?fields=*promotions`,
|
||||||
{
|
{
|
||||||
@@ -334,10 +354,11 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should create 3 campaigns in parallel and have the context passed as argument when calling createCampaigns with different transactionId", async () => {
|
it("should create 3 campaigns in parallel and have the context passed as argument when calling createCampaigns with different transactionId", async () => {
|
||||||
const parallelPromotion = await promotionModuleService.create({
|
await api.post(
|
||||||
code: "PARALLEL",
|
`/admin/promotions`,
|
||||||
type: "standard",
|
{ ...promotionData, code: "PARALLEL" },
|
||||||
})
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
const spyCreateCampaigns = jest.spyOn(
|
const spyCreateCampaigns = jest.spyOn(
|
||||||
promotionModuleService.constructor.prototype,
|
promotionModuleService.constructor.prototype,
|
||||||
@@ -438,22 +459,26 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should update a campaign successfully", async () => {
|
it("should update a campaign successfully", async () => {
|
||||||
const createdPromotion = await promotionModuleService.create({
|
const createdPromotion = (
|
||||||
code: "TEST",
|
await api.post(`/admin/promotions`, promotionData, adminHeaders)
|
||||||
type: "standard",
|
).data.promotion
|
||||||
})
|
|
||||||
|
|
||||||
const createdCampaign = await promotionModuleService.createCampaigns({
|
const createdCampaign = (
|
||||||
name: "test",
|
await api.post(
|
||||||
campaign_identifier: "test",
|
`/admin/campaigns`,
|
||||||
starts_at: new Date("01/01/2024").toISOString(),
|
{
|
||||||
ends_at: new Date("01/01/2029").toISOString(),
|
name: "test",
|
||||||
budget: {
|
campaign_identifier: "test",
|
||||||
limit: 1000,
|
starts_at: new Date("01/01/2024").toISOString(),
|
||||||
type: "usage",
|
ends_at: new Date("01/01/2029").toISOString(),
|
||||||
used: 10,
|
budget: {
|
||||||
},
|
limit: 1000,
|
||||||
})
|
type: "usage",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.campaign
|
||||||
|
|
||||||
await promotionModuleService.addPromotionsToCampaign({
|
await promotionModuleService.addPromotionsToCampaign({
|
||||||
id: createdCampaign.id,
|
id: createdCampaign.id,
|
||||||
@@ -481,7 +506,6 @@ medusaIntegrationTestRunner({
|
|||||||
budget: expect.objectContaining({
|
budget: expect.objectContaining({
|
||||||
limit: 2000,
|
limit: 2000,
|
||||||
type: "usage",
|
type: "usage",
|
||||||
used: 10,
|
|
||||||
}),
|
}),
|
||||||
promotions: [
|
promotions: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ medusaIntegrationTestRunner({
|
|||||||
target_type: "items",
|
target_type: "items",
|
||||||
type: "fixed",
|
type: "fixed",
|
||||||
allocation: "each",
|
allocation: "each",
|
||||||
|
currency_code: "USD",
|
||||||
value: 100,
|
value: 100,
|
||||||
max_quantity: 100,
|
max_quantity: 100,
|
||||||
target_rules: [
|
target_rules: [
|
||||||
@@ -97,6 +98,7 @@ medusaIntegrationTestRunner({
|
|||||||
name: "test",
|
name: "test",
|
||||||
campaign_identifier: "test-1",
|
campaign_identifier: "test-1",
|
||||||
budget: expect.objectContaining({
|
budget: expect.objectContaining({
|
||||||
|
currency_code: null,
|
||||||
type: "usage",
|
type: "usage",
|
||||||
limit: 100,
|
limit: 100,
|
||||||
}),
|
}),
|
||||||
@@ -146,6 +148,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: 100,
|
value: 100,
|
||||||
max_quantity: 100,
|
max_quantity: 100,
|
||||||
|
currency_code: "USD",
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "test.test",
|
attribute: "test.test",
|
||||||
@@ -186,6 +189,7 @@ medusaIntegrationTestRunner({
|
|||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: 100,
|
value: 100,
|
||||||
max_quantity: 100,
|
max_quantity: 100,
|
||||||
|
currency_code: "USD",
|
||||||
buy_rules: [
|
buy_rules: [
|
||||||
{
|
{
|
||||||
attribute: "test.test",
|
attribute: "test.test",
|
||||||
@@ -235,6 +239,7 @@ medusaIntegrationTestRunner({
|
|||||||
max_quantity: 100,
|
max_quantity: 100,
|
||||||
apply_to_quantity: 1,
|
apply_to_quantity: 1,
|
||||||
buy_rules_min_quantity: 1,
|
buy_rules_min_quantity: 1,
|
||||||
|
currency_code: "USD",
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "test.test",
|
attribute: "test.test",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IPromotionModuleService } from "@medusajs/types"
|
|
||||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
import { IPromotionModuleService } from "@medusajs/types"
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "order",
|
target_type: "order",
|
||||||
value: "100",
|
value: "100",
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "order",
|
target_type: "order",
|
||||||
value: 100,
|
value: 100,
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -76,6 +77,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "order",
|
target_type: "order",
|
||||||
value: 100,
|
value: 100,
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,6 +87,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "order",
|
target_type: "order",
|
||||||
value: 100,
|
value: 100,
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -108,6 +111,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "order",
|
target_type: "order",
|
||||||
value: 100,
|
value: 100,
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } }
|
|||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
env,
|
env,
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
describe("Admin: Promotion Rules API", () => {
|
describe.skip("Admin: Promotion Rules API", () => {
|
||||||
let appContainer
|
let appContainer
|
||||||
let standardPromotion
|
let standardPromotion
|
||||||
let promotionModule: IPromotionModuleService
|
let promotionModule: IPromotionModuleService
|
||||||
@@ -56,6 +56,7 @@ medusaIntegrationTestRunner({
|
|||||||
target_type: "items",
|
target_type: "items",
|
||||||
value: 100,
|
value: 100,
|
||||||
target_rules: [promotionRule],
|
target_rules: [promotionRule],
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
rules: [promotionRule],
|
rules: [promotionRule],
|
||||||
})
|
})
|
||||||
@@ -202,7 +203,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it.only("should add target rules to a promotion successfully", async () => {
|
it("should add target rules to a promotion successfully", async () => {
|
||||||
const response = await api.post(
|
const response = await api.post(
|
||||||
`/admin/promotions/${standardPromotion.id}/target-rules/batch`,
|
`/admin/promotions/${standardPromotion.id}/target-rules/batch`,
|
||||||
{
|
{
|
||||||
@@ -333,6 +334,7 @@ medusaIntegrationTestRunner({
|
|||||||
buy_rules_min_quantity: 1,
|
buy_rules_min_quantity: 1,
|
||||||
buy_rules: [promotionRule],
|
buy_rules: [promotionRule],
|
||||||
target_rules: [promotionRule],
|
target_rules: [promotionRule],
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
rules: [promotionRule],
|
rules: [promotionRule],
|
||||||
})
|
})
|
||||||
@@ -355,10 +357,11 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const promotion = (
|
const promotion = (
|
||||||
await api.get(
|
await api.get(
|
||||||
`/admin/promotions/${standardPromotion.id}`,
|
`/admin/promotions/${buyGetPromotion.id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
).data.promotion
|
).data.promotion
|
||||||
|
|
||||||
expect(promotion).toEqual(
|
expect(promotion).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: buyGetPromotion.id,
|
id: buyGetPromotion.id,
|
||||||
@@ -382,23 +385,6 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/promotions/:id/rules/batch", () => {
|
describe("POST /admin/promotions/:id/rules/batch", () => {
|
||||||
it("should throw error when required params are missing", async () => {
|
|
||||||
const { response } = await api
|
|
||||||
.post(
|
|
||||||
`/admin/promotions/${standardPromotion.id}/rules/batch`,
|
|
||||||
{},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((e) => e)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(400)
|
|
||||||
// expect(response.data).toEqual({
|
|
||||||
// type: "invalid_data",
|
|
||||||
// message:
|
|
||||||
// "each value in rule_ids must be a string, rule_ids should not be empty",
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw error when promotion does not exist", async () => {
|
it("should throw error when promotion does not exist", async () => {
|
||||||
const { response } = await api
|
const { response } = await api
|
||||||
.post(
|
.post(
|
||||||
@@ -438,23 +424,6 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/promotions/:id/target-rules/batch", () => {
|
describe("POST /admin/promotions/:id/target-rules/batch", () => {
|
||||||
it("should throw error when required params are missing", async () => {
|
|
||||||
const { response } = await api
|
|
||||||
.post(
|
|
||||||
`/admin/promotions/${standardPromotion.id}/target-rules/batch`,
|
|
||||||
{},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((e) => e)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(400)
|
|
||||||
// expect(response.data).toEqual({
|
|
||||||
// type: "invalid_data",
|
|
||||||
// message:
|
|
||||||
// "each value in rule_ids must be a string, rule_ids should not be empty",
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw error when promotion does not exist", async () => {
|
it("should throw error when promotion does not exist", async () => {
|
||||||
const { response } = await api
|
const { response } = await api
|
||||||
.post(
|
.post(
|
||||||
@@ -496,23 +465,6 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/promotions/:id/buy-rules/batch", () => {
|
describe("POST /admin/promotions/:id/buy-rules/batch", () => {
|
||||||
it("should throw error when required params are missing", async () => {
|
|
||||||
const { response } = await api
|
|
||||||
.post(
|
|
||||||
`/admin/promotions/${standardPromotion.id}/buy-rules/batch`,
|
|
||||||
{},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((e) => e)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(400)
|
|
||||||
// expect(response.data).toEqual({
|
|
||||||
// type: "invalid_data",
|
|
||||||
// message:
|
|
||||||
// "each value in rule_ids must be a string, rule_ids should not be empty",
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw error when promotion does not exist", async () => {
|
it("should throw error when promotion does not exist", async () => {
|
||||||
const { response } = await api
|
const { response } = await api
|
||||||
.post(
|
.post(
|
||||||
@@ -535,6 +487,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: PromotionType.BUYGET,
|
type: PromotionType.BUYGET,
|
||||||
application_method: {
|
application_method: {
|
||||||
type: "fixed",
|
type: "fixed",
|
||||||
|
currency_code: "USD",
|
||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: 100,
|
value: 100,
|
||||||
@@ -569,30 +522,6 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/promotions/:id/rules/batch", () => {
|
describe("POST /admin/promotions/:id/rules/batch", () => {
|
||||||
it("should throw error when required params are missing", async () => {
|
|
||||||
const { response } = await api
|
|
||||||
.post(
|
|
||||||
`/admin/promotions/${standardPromotion.id}/rules/batch`,
|
|
||||||
{
|
|
||||||
update: [
|
|
||||||
{
|
|
||||||
attribute: "test",
|
|
||||||
operator: "eq",
|
|
||||||
values: ["new value"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((e) => e)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(400)
|
|
||||||
// expect(response.data).toEqual({
|
|
||||||
// type: "invalid_data",
|
|
||||||
// message: "id must be a string, id should not be empty",
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw error when promotion does not exist", async () => {
|
it("should throw error when promotion does not exist", async () => {
|
||||||
const { response } = await api
|
const { response } = await api
|
||||||
.post(
|
.post(
|
||||||
@@ -705,38 +634,40 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.attributes).toEqual([
|
expect(response.data.attributes).toEqual(
|
||||||
{
|
expect.arrayContaining([
|
||||||
id: "currency",
|
expect.objectContaining({
|
||||||
label: "Currency code",
|
id: "currency_code",
|
||||||
required: true,
|
label: "Currency Code",
|
||||||
value: "currency_code",
|
required: true,
|
||||||
},
|
value: "currency_code",
|
||||||
{
|
}),
|
||||||
id: "customer_group",
|
expect.objectContaining({
|
||||||
label: "Customer Group",
|
id: "customer_group",
|
||||||
required: false,
|
label: "Customer Group",
|
||||||
value: "customer_group.id",
|
required: false,
|
||||||
},
|
value: "customer_group.id",
|
||||||
{
|
}),
|
||||||
id: "region",
|
expect.objectContaining({
|
||||||
label: "Region",
|
id: "region",
|
||||||
required: false,
|
label: "Region",
|
||||||
value: "region.id",
|
required: false,
|
||||||
},
|
value: "region.id",
|
||||||
{
|
}),
|
||||||
id: "country",
|
expect.objectContaining({
|
||||||
label: "Country",
|
id: "country",
|
||||||
required: false,
|
label: "Country",
|
||||||
value: "shipping_address.country_code",
|
required: false,
|
||||||
},
|
value: "shipping_address.country_code",
|
||||||
{
|
}),
|
||||||
id: "sales_channel",
|
expect.objectContaining({
|
||||||
label: "Sales Channel",
|
id: "sales_channel",
|
||||||
required: false,
|
label: "Sales Channel",
|
||||||
value: "sales_channel.id",
|
required: false,
|
||||||
},
|
value: "sales_channel.id",
|
||||||
])
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -826,7 +757,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = await api.get(
|
response = await api.get(
|
||||||
`/admin/promotions/rule-value-options/rules/currency?limit=2&order=name`,
|
`/admin/promotions/rule-value-options/rules/currency_code?limit=2&order=name`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -834,8 +765,8 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response.data.values.length).toEqual(2)
|
expect(response.data.values.length).toEqual(2)
|
||||||
expect(response.data.values).toEqual(
|
expect(response.data.values).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
{ label: "afn", value: "afn" },
|
{ label: "Afghan Afghani", value: "afn" },
|
||||||
{ label: "all", value: "all" },
|
{ label: "Albanian Lek", value: "all" },
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "order",
|
target_type: "order",
|
||||||
value: "100",
|
value: "100",
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ medusaIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "order",
|
target_type: "order",
|
||||||
value: "100",
|
value: "100",
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { IPromotionModuleService } from "@medusajs/types"
|
|
||||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { IPromotionModuleService } from "@medusajs/types"
|
||||||
import { PromotionType } from "@medusajs/utils"
|
import { PromotionType } from "@medusajs/utils"
|
||||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
@@ -53,8 +53,9 @@ medusaIntegrationTestRunner({
|
|||||||
target_type: "items",
|
target_type: "items",
|
||||||
type: "fixed",
|
type: "fixed",
|
||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: "100",
|
value: 100,
|
||||||
max_quantity: 100,
|
max_quantity: 100,
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -86,8 +87,9 @@ medusaIntegrationTestRunner({
|
|||||||
target_type: "items",
|
target_type: "items",
|
||||||
type: "fixed",
|
type: "fixed",
|
||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: "100",
|
value: 100,
|
||||||
max_quantity: 100,
|
max_quantity: 100,
|
||||||
|
currency_code: "USD",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -96,7 +98,7 @@ medusaIntegrationTestRunner({
|
|||||||
{
|
{
|
||||||
code: "TEST_TWO",
|
code: "TEST_TWO",
|
||||||
application_method: {
|
application_method: {
|
||||||
value: "200",
|
value: 200,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
@@ -122,9 +124,10 @@ medusaIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "100",
|
value: 100,
|
||||||
apply_to_quantity: 1,
|
apply_to_quantity: 1,
|
||||||
buy_rules_min_quantity: 1,
|
buy_rules_min_quantity: 1,
|
||||||
|
currency_code: "USD",
|
||||||
buy_rules: [
|
buy_rules: [
|
||||||
{
|
{
|
||||||
attribute: "product_collection.id",
|
attribute: "product_collection.id",
|
||||||
@@ -147,7 +150,7 @@ medusaIntegrationTestRunner({
|
|||||||
{
|
{
|
||||||
code: "TEST_TWO",
|
code: "TEST_TWO",
|
||||||
application_method: {
|
application_method: {
|
||||||
value: "200",
|
value: 200,
|
||||||
buy_rules_min_quantity: 6,
|
buy_rules_min_quantity: 6,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
|||||||
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
||||||
|
import { AdminRuleValueOptionsListResponse } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
QueryKey,
|
QueryKey,
|
||||||
useMutation,
|
useMutation,
|
||||||
@@ -23,7 +24,6 @@ import {
|
|||||||
PromotionRuleAttributesListRes,
|
PromotionRuleAttributesListRes,
|
||||||
PromotionRuleOperatorsListRes,
|
PromotionRuleOperatorsListRes,
|
||||||
PromotionRulesListRes,
|
PromotionRulesListRes,
|
||||||
PromotionRuleValuesListRes,
|
|
||||||
} from "../../types/api-responses"
|
} from "../../types/api-responses"
|
||||||
import { campaignsQueryKeys } from "./campaigns"
|
import { campaignsQueryKeys } from "./campaigns"
|
||||||
|
|
||||||
@@ -36,10 +36,11 @@ export const promotionsQueryKeys = {
|
|||||||
ruleType,
|
ruleType,
|
||||||
],
|
],
|
||||||
listRuleAttributes: (ruleType: string) => [PROMOTIONS_QUERY_KEY, ruleType],
|
listRuleAttributes: (ruleType: string) => [PROMOTIONS_QUERY_KEY, ruleType],
|
||||||
listRuleValues: (ruleType: string, ruleValue: string) => [
|
listRuleValues: (ruleType: string, ruleValue: string, query: object) => [
|
||||||
PROMOTIONS_QUERY_KEY,
|
PROMOTIONS_QUERY_KEY,
|
||||||
ruleType,
|
ruleType,
|
||||||
ruleValue,
|
ruleValue,
|
||||||
|
query,
|
||||||
],
|
],
|
||||||
listRuleOperators: () => [PROMOTIONS_QUERY_KEY],
|
listRuleOperators: () => [PROMOTIONS_QUERY_KEY],
|
||||||
}
|
}
|
||||||
@@ -142,19 +143,25 @@ export const usePromotionRuleAttributes = (
|
|||||||
export const usePromotionRuleValues = (
|
export const usePromotionRuleValues = (
|
||||||
ruleType: string,
|
ruleType: string,
|
||||||
ruleValue: string,
|
ruleValue: string,
|
||||||
|
query?: Record<string, any>,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<
|
UseQueryOptions<
|
||||||
PromotionListRes,
|
AdminRuleValueOptionsListResponse,
|
||||||
Error,
|
Error,
|
||||||
PromotionRuleValuesListRes,
|
AdminRuleValueOptionsListResponse,
|
||||||
QueryKey
|
QueryKey
|
||||||
>,
|
>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryKey: promotionsQueryKeys.listRuleValues(ruleType, ruleValue),
|
queryKey: promotionsQueryKeys.listRuleValues(
|
||||||
queryFn: async () => client.promotions.listRuleValues(ruleType, ruleValue),
|
ruleType,
|
||||||
|
ruleValue,
|
||||||
|
query || {}
|
||||||
|
),
|
||||||
|
queryFn: async () =>
|
||||||
|
client.promotions.listRuleValues(ruleType, ruleValue, query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -936,6 +936,9 @@
|
|||||||
},
|
},
|
||||||
"promotions": {
|
"promotions": {
|
||||||
"domain": "Promotions",
|
"domain": "Promotions",
|
||||||
|
"sections": {
|
||||||
|
"details": "Promotion Details"
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"method": "Method",
|
"method": "Method",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
@@ -945,6 +948,9 @@
|
|||||||
"allocation": "Allocation",
|
"allocation": "Allocation",
|
||||||
"addCondition": "Add condition",
|
"addCondition": "Add condition",
|
||||||
"clearAll": "Clear all",
|
"clearAll": "Clear all",
|
||||||
|
"amount": {
|
||||||
|
"tooltip": "Select the currency code to enable setting the amount"
|
||||||
|
},
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"title": "Who can use this code?",
|
"title": "Who can use this code?",
|
||||||
@@ -979,6 +985,9 @@
|
|||||||
"addToCampaign": {
|
"addToCampaign": {
|
||||||
"title": "Add Promotion To Campaign"
|
"title": "Add Promotion To Campaign"
|
||||||
},
|
},
|
||||||
|
"campaign_currency": {
|
||||||
|
"tooltip": "Currency is carried over from the promotion. Change it on the promotions tab."
|
||||||
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
"and": "AND",
|
"and": "AND",
|
||||||
@@ -1083,8 +1092,12 @@
|
|||||||
"identifier": "Identifier",
|
"identifier": "Identifier",
|
||||||
"start_date": "Start date",
|
"start_date": "Start date",
|
||||||
"end_date": "End date",
|
"end_date": "End date",
|
||||||
"total_spend": "Total spend",
|
"total_spend": "Budget spent",
|
||||||
"budget_limit": "Budget limit"
|
"total_used": "Budget used",
|
||||||
|
"budget_limit": "Budget limit",
|
||||||
|
"campaign_id": {
|
||||||
|
"hint": "A list of campaigns with the same currency code as the promotion"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"budget": {
|
"budget": {
|
||||||
"create": {
|
"create": {
|
||||||
@@ -1115,6 +1128,8 @@
|
|||||||
"description": "You are about to remove {{count}} promotion(s) from the campaign. This action cannot be undone."
|
"description": "You are about to remove {{count}} promotion(s) from the campaign. This action cannot be undone."
|
||||||
},
|
},
|
||||||
"alreadyAdded": "This promotion has already been added to the campaign.",
|
"alreadyAdded": "This promotion has already been added to the campaign.",
|
||||||
|
"alreadyAddedDiffCampaign": "This promotion has already been added to a different campaign ({{name}}).",
|
||||||
|
"currencyMismatch": "Currency of the promotion and campaign doesn't match",
|
||||||
"toast": {
|
"toast": {
|
||||||
"success": "Successfully added {{count}} promotion(s) to campaign"
|
"success": "Successfully added {{count}} promotion(s) to campaign"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
import {
|
||||||
|
AdminGetPromotionsParams,
|
||||||
|
AdminGetPromotionsRuleValueParams,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import { AdminRuleValueOptionsListResponse } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
BatchAddPromotionRulesReq,
|
BatchAddPromotionRulesReq,
|
||||||
BatchRemovePromotionRulesReq,
|
BatchRemovePromotionRulesReq,
|
||||||
@@ -13,7 +16,6 @@ import {
|
|||||||
PromotionRes,
|
PromotionRes,
|
||||||
PromotionRuleAttributesListRes,
|
PromotionRuleAttributesListRes,
|
||||||
PromotionRuleOperatorsListRes,
|
PromotionRuleOperatorsListRes,
|
||||||
PromotionRuleValuesListRes,
|
|
||||||
} from "../../types/api-responses"
|
} from "../../types/api-responses"
|
||||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ async function removePromotionRules(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listPromotionRules(id: string, ruleType: string) {
|
async function listPromotionRules(id: string | null, ruleType: string) {
|
||||||
return getRequest<PromotionRuleAttributesListRes>(
|
return getRequest<PromotionRuleAttributesListRes>(
|
||||||
`/admin/promotions/${id}/${ruleType}`
|
`/admin/promotions/${id}/${ruleType}`
|
||||||
)
|
)
|
||||||
@@ -91,9 +93,14 @@ async function listPromotionRuleAttributes(ruleType: string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listPromotionRuleValues(ruleType: string, ruleValue: string) {
|
async function listPromotionRuleValues(
|
||||||
return getRequest<PromotionRuleValuesListRes>(
|
ruleType: string,
|
||||||
`/admin/promotions/rule-value-options/${ruleType}/${ruleValue}`
|
ruleValue: string,
|
||||||
|
query?: AdminGetPromotionsRuleValueParams
|
||||||
|
) {
|
||||||
|
return getRequest<AdminRuleValueOptionsListResponse>(
|
||||||
|
`/admin/promotions/rule-value-options/${ruleType}/${ruleValue}`,
|
||||||
|
query
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-13
@@ -67,12 +67,7 @@ export const AddCampaignPromotionsForm = ({
|
|||||||
promotions,
|
promotions,
|
||||||
count,
|
count,
|
||||||
isPending: isLoading,
|
isPending: isLoading,
|
||||||
isError,
|
} = usePromotions({ ...searchParams }, { placeholderData: keepPreviousData })
|
||||||
error,
|
|
||||||
} = usePromotions(
|
|
||||||
{ ...searchParams, campaign_id: "null" },
|
|
||||||
{ placeholderData: keepPreviousData }
|
|
||||||
)
|
|
||||||
|
|
||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
const filters = usePromotionTableFilters()
|
const filters = usePromotionTableFilters()
|
||||||
@@ -91,7 +86,11 @@ export const AddCampaignPromotionsForm = ({
|
|||||||
state: rowSelection,
|
state: rowSelection,
|
||||||
updater,
|
updater,
|
||||||
},
|
},
|
||||||
meta: { campaignId: campaign.id },
|
meta: {
|
||||||
|
campaignId: campaign.id,
|
||||||
|
currencyCode: campaign?.budget?.currency_code,
|
||||||
|
budgetType: campaign?.budget?.type,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
@@ -177,24 +176,33 @@ const useColumns = () => {
|
|||||||
? "indeterminate"
|
? "indeterminate"
|
||||||
: table.getIsAllPageRowsSelected()
|
: table.getIsAllPageRowsSelected()
|
||||||
}
|
}
|
||||||
onCheckedChange={(value) =>
|
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
|
||||||
table.toggleAllPageRowsSelected(!!value)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
cell: ({ row, table }) => {
|
cell: ({ row, table }) => {
|
||||||
const { campaignId } = table.options.meta as {
|
const { campaignId, currencyCode, budgetType } = table.options
|
||||||
|
.meta as {
|
||||||
campaignId: string
|
campaignId: string
|
||||||
|
currencyCode: string
|
||||||
|
budgetType: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isTypeSpend = budgetType === "spend"
|
||||||
const isAdded = row.original.campaign_id === campaignId
|
const isAdded = row.original.campaign_id === campaignId
|
||||||
|
const isAddedToADiffCampaign =
|
||||||
|
!!row.original.campaign_id &&
|
||||||
|
row.original.campaign_id !== campaignId
|
||||||
|
const currencyMismatch =
|
||||||
|
isTypeSpend &&
|
||||||
|
row.original.application_method?.currency_code !== currencyCode
|
||||||
const isSelected = row.getIsSelected() || isAdded
|
const isSelected = row.getIsSelected() || isAdded
|
||||||
|
const isIndeterminate = currencyMismatch || isAddedToADiffCampaign
|
||||||
|
|
||||||
const Component = (
|
const Component = (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isIndeterminate ? "indeterminate" : isSelected}
|
||||||
disabled={isAdded}
|
disabled={isAdded || isAddedToADiffCampaign || currencyMismatch}
|
||||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -202,6 +210,30 @@ const useColumns = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isAddedToADiffCampaign) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={t("campaigns.promotions.alreadyAddedDiffCampaign", {
|
||||||
|
name: row.original?.campaign?.name!,
|
||||||
|
})}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
{Component}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currencyMismatch) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={t("campaigns.promotions.currencyMismatch")}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
{Component}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
+16
-72
@@ -1,14 +1,7 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { CampaignResponse } from "@medusajs/types"
|
import { CampaignResponse } from "@medusajs/types"
|
||||||
import {
|
import { Button, CurrencyInput, Input, toast } from "@medusajs/ui"
|
||||||
Button,
|
import { useForm } from "react-hook-form"
|
||||||
clx,
|
|
||||||
CurrencyInput,
|
|
||||||
Input,
|
|
||||||
RadioGroup,
|
|
||||||
toast,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { useForm, useWatch } from "react-hook-form"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
@@ -24,8 +17,7 @@ type EditCampaignBudgetFormProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EditCampaignSchema = zod.object({
|
const EditCampaignSchema = zod.object({
|
||||||
limit: zod.number().min(0),
|
limit: zod.number().min(0).optional().nullable(),
|
||||||
type: zod.enum(["spend", "usage"]).optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const EditCampaignBudgetForm = ({
|
export const EditCampaignBudgetForm = ({
|
||||||
@@ -36,8 +28,7 @@ export const EditCampaignBudgetForm = ({
|
|||||||
|
|
||||||
const form = useForm<zod.infer<typeof EditCampaignSchema>>({
|
const form = useForm<zod.infer<typeof EditCampaignSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
limit: campaign?.budget?.limit,
|
limit: campaign?.budget?.limit || undefined,
|
||||||
type: campaign?.budget?.type || "spend",
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(EditCampaignSchema),
|
resolver: zodResolver(EditCampaignSchema),
|
||||||
})
|
})
|
||||||
@@ -49,8 +40,7 @@ export const EditCampaignBudgetForm = ({
|
|||||||
{
|
{
|
||||||
id: campaign.id,
|
id: campaign.id,
|
||||||
budget: {
|
budget: {
|
||||||
limit: data.limit,
|
limit: data.limit ? data.limit : null,
|
||||||
type: data.type,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -74,63 +64,11 @@ export const EditCampaignBudgetForm = ({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const watchValueType = useWatch({
|
|
||||||
control: form.control,
|
|
||||||
name: "type",
|
|
||||||
})
|
|
||||||
|
|
||||||
const isTypeSpend = watchValueType === "spend"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteDrawer.Form form={form}>
|
<RouteDrawer.Form form={form}>
|
||||||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||||
<RouteDrawer.Body>
|
<RouteDrawer.Body>
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="type"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>{t("campaigns.budget.fields.type")}</Form.Label>
|
|
||||||
|
|
||||||
<Form.Control>
|
|
||||||
<RadioGroup
|
|
||||||
className="flex-col gap-y-3"
|
|
||||||
{...field}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
>
|
|
||||||
<RadioGroup.ChoiceBox
|
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-2 border-ui-border-interactive":
|
|
||||||
"spend" === field.value,
|
|
||||||
})}
|
|
||||||
value={"spend"}
|
|
||||||
label={t("campaigns.budget.type.spend.title")}
|
|
||||||
description={t(
|
|
||||||
"campaigns.budget.type.spend.description"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioGroup.ChoiceBox
|
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-2 border-ui-border-interactive":
|
|
||||||
"usage" === field.value,
|
|
||||||
})}
|
|
||||||
value={"usage"}
|
|
||||||
label={t("campaigns.budget.type.usage.title")}
|
|
||||||
description={t(
|
|
||||||
"campaigns.budget.type.usage.description"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="limit"
|
name="limit"
|
||||||
@@ -142,16 +80,22 @@ export const EditCampaignBudgetForm = ({
|
|||||||
</Form.Label>
|
</Form.Label>
|
||||||
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{isTypeSpend ? (
|
{campaign.budget?.type === "spend" ? (
|
||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
min={0}
|
min={0}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
onChange(value ? parseInt(value) : "")
|
onChange(value ? parseInt(value) : null)
|
||||||
|
}
|
||||||
|
code={campaign.budget?.currency_code}
|
||||||
|
symbol={
|
||||||
|
campaign.budget?.currency_code
|
||||||
|
? getCurrencySymbol(
|
||||||
|
campaign.budget?.currency_code
|
||||||
|
)
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
code={campaign.currency}
|
|
||||||
symbol={getCurrencySymbol(campaign.currency)}
|
|
||||||
{...field}
|
{...field}
|
||||||
value={value}
|
value={value || undefined}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
+14
-11
@@ -15,26 +15,29 @@ import { CreateCampaignFormFields } from "../../../common/components/create-camp
|
|||||||
export const CreateCampaignSchema = zod.object({
|
export const CreateCampaignSchema = zod.object({
|
||||||
name: zod.string().min(1),
|
name: zod.string().min(1),
|
||||||
description: zod.string().optional(),
|
description: zod.string().optional(),
|
||||||
currency: zod.string().min(1),
|
|
||||||
campaign_identifier: zod.string().min(1),
|
campaign_identifier: zod.string().min(1),
|
||||||
starts_at: zod.date().optional(),
|
starts_at: zod.date().optional(),
|
||||||
ends_at: zod.date().optional(),
|
ends_at: zod.date().optional(),
|
||||||
budget: zod.object({
|
budget: zod
|
||||||
limit: zod.number().min(0),
|
.object({
|
||||||
type: zod.enum(["spend", "usage"]),
|
limit: zod.number().min(0).optional().nullable(),
|
||||||
}),
|
type: zod.enum(["spend", "usage"]),
|
||||||
|
currency_code: zod.string().optional().nullable(),
|
||||||
|
})
|
||||||
|
.refine((data) => data.type !== "spend" || data.currency_code, {
|
||||||
|
path: ["currency_code"],
|
||||||
|
message: `required field`,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const defaultCampaignValues = {
|
export const defaultCampaignValues = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
currency: "",
|
|
||||||
campaign_identifier: "",
|
campaign_identifier: "",
|
||||||
starts_at: undefined,
|
|
||||||
ends_at: undefined,
|
|
||||||
budget: {
|
budget: {
|
||||||
type: "spend" as CampaignBudgetTypeValues,
|
type: "spend" as CampaignBudgetTypeValues,
|
||||||
limit: undefined,
|
currency_code: null,
|
||||||
|
limit: null,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +56,13 @@ export const CreateCampaignForm = () => {
|
|||||||
{
|
{
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
currency: data.currency,
|
|
||||||
campaign_identifier: data.campaign_identifier,
|
campaign_identifier: data.campaign_identifier,
|
||||||
starts_at: data.starts_at,
|
starts_at: data.starts_at,
|
||||||
ends_at: data.ends_at,
|
ends_at: data.ends_at,
|
||||||
budget: {
|
budget: {
|
||||||
type: data.budget.type,
|
type: data.budget.type,
|
||||||
limit: data.budget.limit,
|
limit: data.budget.limit ? data.budget.limit : undefined,
|
||||||
|
currency_code: data.budget.currency_code,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+4
-2
@@ -53,9 +53,11 @@ export const CampaignBudget = ({ campaign }: CampaignBudgetProps) => {
|
|||||||
<Trans
|
<Trans
|
||||||
i18nKey="campaigns.totalSpend"
|
i18nKey="campaigns.totalSpend"
|
||||||
values={{
|
values={{
|
||||||
amount: campaign?.budget?.limit || 0,
|
amount: campaign?.budget?.limit || "no limit",
|
||||||
currency:
|
currency:
|
||||||
campaign?.budget?.type === "spend" ? campaign.currency : "",
|
campaign?.budget?.type === "spend" && campaign?.budget.limit
|
||||||
|
? campaign.budget?.currency_code
|
||||||
|
: "",
|
||||||
}}
|
}}
|
||||||
components={[
|
components={[
|
||||||
<span
|
<span
|
||||||
|
|||||||
+12
-11
@@ -30,7 +30,6 @@ export const CampaignGeneralSection = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { mutateAsync } = useDeleteCampaign(campaign.id)
|
const { mutateAsync } = useDeleteCampaign(campaign.id)
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
@@ -124,18 +123,20 @@ export const CampaignGeneralSection = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
{campaign?.budget && campaign.budget.type === "spend" && (
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
{t("fields.currency")}
|
<Text size="small" leading="compact" weight="plus">
|
||||||
</Text>
|
{t("fields.currency")}
|
||||||
|
|
||||||
<div>
|
|
||||||
<Badge size="xsmall">{campaign.currency}</Badge>
|
|
||||||
<Text className="inline pl-3" size="small" leading="compact">
|
|
||||||
{currencies[campaign.currency]?.name}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Badge size="xsmall">{campaign?.budget.currency_code}</Badge>
|
||||||
|
<Text className="inline pl-3" size="small" leading="compact">
|
||||||
|
{currencies[campaign?.budget.currency_code?.toUpperCase()]?.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
|||||||
+6
-2
@@ -20,7 +20,9 @@ export const CampaignSpend = ({ campaign }: CampaignSpendProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Heading level="h3" className="font-normal text-ui-fg-subtle">
|
<Heading level="h3" className="font-normal text-ui-fg-subtle">
|
||||||
{t("campaigns.fields.total_spend")}
|
{campaign.budget?.type === "spend"
|
||||||
|
? t("campaigns.fields.total_spend")
|
||||||
|
: t("campaigns.fields.total_used")}
|
||||||
</Heading>
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,7 +37,9 @@ export const CampaignSpend = ({ campaign }: CampaignSpendProps) => {
|
|||||||
values={{
|
values={{
|
||||||
amount: campaign?.budget?.used || 0,
|
amount: campaign?.budget?.used || 0,
|
||||||
currency:
|
currency:
|
||||||
campaign?.budget?.type === "spend" ? campaign.currency : "",
|
campaign?.budget?.type === "spend"
|
||||||
|
? campaign?.budget?.currency_code
|
||||||
|
: "",
|
||||||
}}
|
}}
|
||||||
components={[
|
components={[
|
||||||
<span
|
<span
|
||||||
|
|||||||
+1
-36
@@ -1,6 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { CampaignResponse } from "@medusajs/types"
|
import { CampaignResponse } from "@medusajs/types"
|
||||||
import { Button, DatePicker, Input, Select, toast } from "@medusajs/ui"
|
import { Button, DatePicker, Input, toast } from "@medusajs/ui"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { useUpdateCampaign } from "../../../../../hooks/api/campaigns"
|
import { useUpdateCampaign } from "../../../../../hooks/api/campaigns"
|
||||||
import { currencies } from "../../../../../lib/currencies"
|
|
||||||
|
|
||||||
type EditCampaignFormProps = {
|
type EditCampaignFormProps = {
|
||||||
campaign: CampaignResponse
|
campaign: CampaignResponse
|
||||||
@@ -19,7 +18,6 @@ type EditCampaignFormProps = {
|
|||||||
const EditCampaignSchema = zod.object({
|
const EditCampaignSchema = zod.object({
|
||||||
name: zod.string(),
|
name: zod.string(),
|
||||||
description: zod.string().optional(),
|
description: zod.string().optional(),
|
||||||
currency: zod.string().optional(),
|
|
||||||
campaign_identifier: zod.string().optional(),
|
campaign_identifier: zod.string().optional(),
|
||||||
starts_at: zod.date().optional(),
|
starts_at: zod.date().optional(),
|
||||||
ends_at: zod.date().optional(),
|
ends_at: zod.date().optional(),
|
||||||
@@ -33,7 +31,6 @@ export const EditCampaignForm = ({ campaign }: EditCampaignFormProps) => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: campaign.name || "",
|
name: campaign.name || "",
|
||||||
description: campaign.description || "",
|
description: campaign.description || "",
|
||||||
currency: campaign.currency || "",
|
|
||||||
campaign_identifier: campaign.campaign_identifier || "",
|
campaign_identifier: campaign.campaign_identifier || "",
|
||||||
starts_at: campaign.starts_at ? new Date(campaign.starts_at) : undefined,
|
starts_at: campaign.starts_at ? new Date(campaign.starts_at) : undefined,
|
||||||
ends_at: campaign.ends_at ? new Date(campaign.ends_at) : undefined,
|
ends_at: campaign.ends_at ? new Date(campaign.ends_at) : undefined,
|
||||||
@@ -49,7 +46,6 @@ export const EditCampaignForm = ({ campaign }: EditCampaignFormProps) => {
|
|||||||
id: campaign.id,
|
id: campaign.id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
currency: data.currency,
|
|
||||||
campaign_identifier: data.campaign_identifier,
|
campaign_identifier: data.campaign_identifier,
|
||||||
starts_at: data.starts_at,
|
starts_at: data.starts_at,
|
||||||
ends_at: data.ends_at,
|
ends_at: data.ends_at,
|
||||||
@@ -134,37 +130,6 @@ export const EditCampaignForm = ({ campaign }: EditCampaignFormProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="currency"
|
|
||||||
render={({ field: { onChange, ref, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>{t("fields.currency")}</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Select {...field} onValueChange={onChange}>
|
|
||||||
<Select.Trigger ref={ref}>
|
|
||||||
<Select.Value />
|
|
||||||
</Select.Trigger>
|
|
||||||
|
|
||||||
<Select.Content>
|
|
||||||
{Object.values(currencies).map((currency) => (
|
|
||||||
<Select.Item
|
|
||||||
value={currency.code}
|
|
||||||
key={currency.code}
|
|
||||||
>
|
|
||||||
{currency.name}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="starts_at"
|
name="starts_at"
|
||||||
|
|||||||
+96
-35
@@ -12,10 +12,12 @@ import { useEffect } from "react"
|
|||||||
import { useWatch } from "react-hook-form"
|
import { useWatch } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { useStore } from "../../../../../hooks/api/store"
|
||||||
import { currencies, getCurrencySymbol } from "../../../../../lib/currencies"
|
import { currencies, getCurrencySymbol } from "../../../../../lib/currencies"
|
||||||
|
|
||||||
export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { store } = useStore()
|
||||||
|
|
||||||
const watchValueType = useWatch({
|
const watchValueType = useWatch({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
@@ -26,13 +28,38 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
|||||||
|
|
||||||
const currencyValue = useWatch({
|
const currencyValue = useWatch({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
name: `${fieldScope}currency`,
|
name: `${fieldScope}budget.currency_code`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchPromotionCurrencyCode = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "application_method.currency_code",
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.setValue(`${fieldScope}budget.limit`, undefined)
|
form.setValue(`${fieldScope}budget.limit`, null)
|
||||||
|
|
||||||
|
if (watchValueType === "spend") {
|
||||||
|
form.setValue(`campaign.budget.currency_code`, watchPromotionCurrencyCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watchValueType === "usage") {
|
||||||
|
form.setValue(`campaign.budget.currency_code`, null)
|
||||||
|
}
|
||||||
}, [watchValueType])
|
}, [watchValueType])
|
||||||
|
|
||||||
|
if (watchPromotionCurrencyCode) {
|
||||||
|
const formCampaignBudget = form.getValues().campaign?.budget
|
||||||
|
const formCampaignCurrency = formCampaignBudget?.currency_code
|
||||||
|
|
||||||
|
if (
|
||||||
|
formCampaignBudget?.type === "spend" &&
|
||||||
|
formCampaignCurrency !== watchPromotionCurrencyCode
|
||||||
|
) {
|
||||||
|
form.setValue("campaign.budget.currency_code", watchPromotionCurrencyCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
<div>
|
<div>
|
||||||
@@ -98,33 +125,7 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Field
|
<div></div>
|
||||||
control={form.control}
|
|
||||||
name={`${fieldScope}currency`}
|
|
||||||
render={({ field: { onChange, ref, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>{t("fields.currency")}</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Select {...field} onValueChange={onChange}>
|
|
||||||
<Select.Trigger ref={ref}>
|
|
||||||
<Select.Value />
|
|
||||||
</Select.Trigger>
|
|
||||||
|
|
||||||
<Select.Content>
|
|
||||||
{Object.values(currencies).map((currency) => (
|
|
||||||
<Select.Item value={currency.code} key={currency.code}>
|
|
||||||
{currency.name}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -197,8 +198,8 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
>
|
>
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2 border", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border border-ui-border-interactive":
|
||||||
"spend" === field.value,
|
"spend" === field.value,
|
||||||
})}
|
})}
|
||||||
value={"spend"}
|
value={"spend"}
|
||||||
@@ -207,8 +208,8 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2 border", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border border-ui-border-interactive":
|
||||||
"usage" === field.value,
|
"usage" === field.value,
|
||||||
})}
|
})}
|
||||||
value={"usage"}
|
value={"usage"}
|
||||||
@@ -224,13 +225,72 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
{isTypeSpend && (
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name={`${fieldScope}budget.currency_code`}
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label
|
||||||
|
tooltip={
|
||||||
|
fieldScope.length
|
||||||
|
? t("promotions.campaign_currency.tooltip")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("fields.currency")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={onChange}
|
||||||
|
disabled={!!fieldScope.length}
|
||||||
|
>
|
||||||
|
<Select.Trigger ref={ref}>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
{Object.values(currencies)
|
||||||
|
.filter((currency) =>
|
||||||
|
store?.supported_currency_codes?.includes(
|
||||||
|
currency.code.toLocaleLowerCase()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((currency) => (
|
||||||
|
<Select.Item
|
||||||
|
value={currency.code.toLowerCase()}
|
||||||
|
key={currency.code}
|
||||||
|
>
|
||||||
|
{currency.name}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`${fieldScope}budget.limit`}
|
name={`${fieldScope}budget.limit`}
|
||||||
render={({ field: { onChange, value, ...field } }) => {
|
render={({ field: { onChange, value, ...field } }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item className="basis-1/2">
|
<Form.Item className="basis-1/2">
|
||||||
<Form.Label>{t("campaigns.budget.fields.limit")}</Form.Label>
|
<Form.Label
|
||||||
|
tooltip={
|
||||||
|
currencyValue
|
||||||
|
? undefined
|
||||||
|
: t("promotions.fields.amount.tooltip")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("campaigns.budget.fields.limit")}
|
||||||
|
</Form.Label>
|
||||||
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{isTypeSpend ? (
|
{isTypeSpend ? (
|
||||||
@@ -245,13 +305,14 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
|||||||
}
|
}
|
||||||
{...field}
|
{...field}
|
||||||
value={value}
|
value={value}
|
||||||
|
disabled={!currencyValue}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
key="usage"
|
key="usage"
|
||||||
min={0}
|
|
||||||
{...field}
|
{...field}
|
||||||
|
min={0}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onChange(
|
onChange(
|
||||||
|
|||||||
+6
-309
@@ -1,17 +1,14 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { XMarkMini } from "@medusajs/icons"
|
|
||||||
import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
|
import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
|
||||||
import { Badge, Button, Heading, Input, Select, Text } from "@medusajs/ui"
|
import { Button } from "@medusajs/ui"
|
||||||
import i18n from "i18next"
|
import i18n from "i18next"
|
||||||
import { Fragment, useState } from "react"
|
import { useState } from "react"
|
||||||
import { useFieldArray, useForm } from "react-hook-form"
|
import { useFieldArray, useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { Form } from "../../../../../../components/common/form"
|
|
||||||
import { Combobox } from "../../../../../../components/inputs/combobox"
|
|
||||||
import { RouteDrawer } from "../../../../../../components/route-modal"
|
import { RouteDrawer } from "../../../../../../components/route-modal"
|
||||||
import { usePromotionRuleValues } from "../../../../../../hooks/api/promotions"
|
|
||||||
import { RuleTypeValues } from "../../edit-rules"
|
import { RuleTypeValues } from "../../edit-rules"
|
||||||
|
import { RulesFormField } from "../rules-form-field"
|
||||||
import { getDisguisedRules } from "./utils"
|
import { getDisguisedRules } from "./utils"
|
||||||
|
|
||||||
type EditPromotionFormProps = {
|
type EditPromotionFormProps = {
|
||||||
@@ -47,307 +44,6 @@ const EditRules = zod.object({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const RuleValueFormField = ({
|
|
||||||
identifier,
|
|
||||||
scope,
|
|
||||||
valuesFields,
|
|
||||||
valuesRef,
|
|
||||||
fieldRule,
|
|
||||||
attributes,
|
|
||||||
ruleType,
|
|
||||||
}) => {
|
|
||||||
const attribute = attributes?.find(
|
|
||||||
(attr) => attr.value === fieldRule.attribute
|
|
||||||
)
|
|
||||||
const { values: options = [] } = usePromotionRuleValues(
|
|
||||||
ruleType,
|
|
||||||
attribute?.id,
|
|
||||||
{
|
|
||||||
enabled: !!attribute?.id && !attribute.disguised,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Field
|
|
||||||
key={`${identifier}.${scope}.${valuesFields.name}-${fieldRule.attribute}`}
|
|
||||||
{...valuesFields}
|
|
||||||
render={({ field: { onChange, ref, ...field } }) => {
|
|
||||||
if (fieldRule.field_type === "number") {
|
|
||||||
return (
|
|
||||||
<Form.Item className="basis-1/2">
|
|
||||||
<Form.Control>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type="number"
|
|
||||||
onChange={onChange}
|
|
||||||
className="bg-ui-bg-base"
|
|
||||||
ref={valuesRef}
|
|
||||||
min={1}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
} else if (fieldRule.field_type === "text") {
|
|
||||||
return (
|
|
||||||
<Form.Item className="basis-1/2">
|
|
||||||
<Form.Control>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
onChange={onChange}
|
|
||||||
className="bg-ui-bg-base"
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Form.Item className="basis-1/2">
|
|
||||||
<Form.Control>
|
|
||||||
<Combobox
|
|
||||||
{...field}
|
|
||||||
placeholder="Select Values"
|
|
||||||
options={options}
|
|
||||||
onChange={onChange}
|
|
||||||
className="bg-ui-bg-base"
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RulesFormField = ({
|
|
||||||
form,
|
|
||||||
ruleType,
|
|
||||||
fields,
|
|
||||||
attributes,
|
|
||||||
operators,
|
|
||||||
removeRule,
|
|
||||||
updateRule,
|
|
||||||
appendRule,
|
|
||||||
setRulesToRemove,
|
|
||||||
rulesToRemove,
|
|
||||||
scope = "rules",
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<Heading level="h2" className="mb-2">
|
|
||||||
{t(`promotions.fields.conditions.${ruleType}.title`)}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Text className="text-ui-fg-subtle txt-small mb-10">
|
|
||||||
{t(`promotions.fields.conditions.${ruleType}.description`)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{fields.map((fieldRule, index) => {
|
|
||||||
const identifier = fieldRule.id
|
|
||||||
const { ref: attributeRef, ...attributeFields } = form.register(
|
|
||||||
`${scope}.${index}.attribute`
|
|
||||||
)
|
|
||||||
const { ref: operatorRef, ...operatorFields } = form.register(
|
|
||||||
`${scope}.${index}.operator`
|
|
||||||
)
|
|
||||||
const { ref: valuesRef, ...valuesFields } = form.register(
|
|
||||||
`${scope}.${index}.values`
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={`${fieldRule.id}.${index}`}>
|
|
||||||
<div className="bg-ui-bg-subtle border-ui-border-base flex flex-row gap-2 rounded-xl border px-2 py-2">
|
|
||||||
<div className="grow">
|
|
||||||
<Form.Field
|
|
||||||
key={`${identifier}.${scope}.${attributeFields.name}`}
|
|
||||||
{...attributeFields}
|
|
||||||
render={({ field: { onChange, ref, ...field } }) => {
|
|
||||||
const existingAttributes =
|
|
||||||
fields?.map((field) => field.attribute) || []
|
|
||||||
const attributeOptions =
|
|
||||||
attributes?.filter((attr) => {
|
|
||||||
if (attr.value === fieldRule.attribute) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return !existingAttributes.includes(attr.value)
|
|
||||||
}) || []
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Item className="mb-2">
|
|
||||||
{fieldRule.required && (
|
|
||||||
<p className="text text-ui-fg-muted txt-small">
|
|
||||||
{t("promotions.form.required")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Control>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => {
|
|
||||||
updateRule(index, { ...fieldRule, values: [] })
|
|
||||||
onChange(e)
|
|
||||||
}}
|
|
||||||
disabled={fieldRule.required}
|
|
||||||
>
|
|
||||||
<Select.Trigger
|
|
||||||
ref={attributeRef}
|
|
||||||
className="bg-ui-bg-base"
|
|
||||||
>
|
|
||||||
<Select.Value
|
|
||||||
placeholder={t(
|
|
||||||
"promotions.form.selectAttribute"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Select.Trigger>
|
|
||||||
|
|
||||||
<Select.Content>
|
|
||||||
{attributeOptions?.map((c, i) => (
|
|
||||||
<Select.Item
|
|
||||||
key={`${identifier}-attribute-option-${i}`}
|
|
||||||
value={c.value}
|
|
||||||
>
|
|
||||||
<span className="text-ui-fg-subtle">
|
|
||||||
{c.label}
|
|
||||||
</span>
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Form.Field
|
|
||||||
key={`${identifier}.${scope}.${operatorFields.name}`}
|
|
||||||
{...operatorFields}
|
|
||||||
render={({ field: { onChange, ref, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item className="basis-1/2">
|
|
||||||
<Form.Control>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
onValueChange={onChange}
|
|
||||||
disabled={fieldRule.required}
|
|
||||||
>
|
|
||||||
<Select.Trigger
|
|
||||||
ref={operatorRef}
|
|
||||||
className="bg-ui-bg-base"
|
|
||||||
>
|
|
||||||
<Select.Value placeholder="Select Operator" />
|
|
||||||
</Select.Trigger>
|
|
||||||
|
|
||||||
<Select.Content>
|
|
||||||
{operators?.map((c, i) => (
|
|
||||||
<Select.Item
|
|
||||||
key={`${identifier}-operator-option-${i}`}
|
|
||||||
value={c.value}
|
|
||||||
>
|
|
||||||
<span className="text-ui-fg-subtle">
|
|
||||||
{c.label}
|
|
||||||
</span>
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RuleValueFormField
|
|
||||||
identifier={identifier}
|
|
||||||
scope={scope}
|
|
||||||
valuesFields={valuesFields}
|
|
||||||
valuesRef={valuesRef}
|
|
||||||
fieldRule={fieldRule}
|
|
||||||
attributes={attributes}
|
|
||||||
ruleType={ruleType}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-none self-center px-1">
|
|
||||||
<XMarkMini
|
|
||||||
className={`text-ui-fg-muted cursor-pointer ${
|
|
||||||
fieldRule.required ? "invisible" : "visible"
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (!fieldRule.required) {
|
|
||||||
fieldRule.id &&
|
|
||||||
setRulesToRemove &&
|
|
||||||
setRulesToRemove([...rulesToRemove, fieldRule])
|
|
||||||
|
|
||||||
removeRule(index)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{index < fields.length - 1 && (
|
|
||||||
<div className="relative px-6 py-3">
|
|
||||||
<div className="border-ui-border-strong absolute bottom-0 left-[40px] top-0 z-[-1] w-px bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-repeat-y"></div>
|
|
||||||
|
|
||||||
<Badge size="2xsmall" className=" text-xs">
|
|
||||||
{t("promotions.form.and")}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div className="mt-8">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
className="inline-block"
|
|
||||||
onClick={() => {
|
|
||||||
appendRule({
|
|
||||||
attribute: "",
|
|
||||||
operator: "",
|
|
||||||
values: [],
|
|
||||||
required: false,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("promotions.fields.addCondition")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="transparent"
|
|
||||||
className="text-ui-fg-muted hover:text-ui-fg-subtle ml-2 inline-block"
|
|
||||||
onClick={() => {
|
|
||||||
const indicesToRemove = fields
|
|
||||||
.map((field, index) => (field.required ? null : index))
|
|
||||||
.filter((f) => f !== null)
|
|
||||||
|
|
||||||
setRulesToRemove &&
|
|
||||||
setRulesToRemove(fields.filter((f) => !f.required))
|
|
||||||
removeRule(indicesToRemove)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("promotions.fields.clearAll")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditRulesForm = ({
|
export const EditRulesForm = ({
|
||||||
promotion,
|
promotion,
|
||||||
rules,
|
rules,
|
||||||
@@ -369,10 +65,11 @@ export const EditRulesForm = ({
|
|||||||
rules: [...disguisedRules, ...rules].map((rule) => ({
|
rules: [...disguisedRules, ...rules].map((rule) => ({
|
||||||
id: rule.id,
|
id: rule.id,
|
||||||
required: requiredAttributeValues.includes(rule.attribute),
|
required: requiredAttributeValues.includes(rule.attribute),
|
||||||
field_type: rule.field_type,
|
|
||||||
attribute: rule.attribute!,
|
attribute: rule.attribute!,
|
||||||
operator: rule.operator!,
|
operator: rule.operator!,
|
||||||
values: rule?.values?.map((v: { value: string }) => v.value!),
|
values: Array.isArray(rule?.values)
|
||||||
|
? rule?.values?.map((v: any) => v.value!)
|
||||||
|
: rule.values!,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
resolver: zodResolver(EditRules),
|
resolver: zodResolver(EditRules),
|
||||||
|
|||||||
+17
-3
@@ -22,6 +22,22 @@ export function getDisguisedRules(
|
|||||||
(attr) => attr.id === "buy_rules_min_quantity"
|
(attr) => attr.id === "buy_rules_min_quantity"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const currencyCodeRule = requiredAttributes.find(
|
||||||
|
(attr) => attr.id === "currency_code"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ruleType === RuleType.RULES) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "currency_code",
|
||||||
|
attribute: "currency_code",
|
||||||
|
operator: "eq",
|
||||||
|
required: currencyCodeRule?.required,
|
||||||
|
values: promotion?.application_method?.currency_code?.toLowerCase(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
if (ruleType === RuleType.TARGET_RULES) {
|
if (ruleType === RuleType.TARGET_RULES) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -29,8 +45,7 @@ export function getDisguisedRules(
|
|||||||
attribute: "apply_to_quantity",
|
attribute: "apply_to_quantity",
|
||||||
operator: "eq",
|
operator: "eq",
|
||||||
required: applyToQuantityRule?.required,
|
required: applyToQuantityRule?.required,
|
||||||
field_type: applyToQuantityRule?.field_type,
|
values: promotion?.application_method?.apply_to_quantity,
|
||||||
values: [{ value: promotion?.application_method?.apply_to_quantity }],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -42,7 +57,6 @@ export function getDisguisedRules(
|
|||||||
attribute: "buy_rules_min_quantity",
|
attribute: "buy_rules_min_quantity",
|
||||||
operator: "eq",
|
operator: "eq",
|
||||||
required: buyRulesMinQuantityRule?.required,
|
required: buyRulesMinQuantityRule?.required,
|
||||||
field_type: buyRulesMinQuantityRule?.field_type,
|
|
||||||
values: [
|
values: [
|
||||||
{ value: promotion?.application_method?.buy_rules_min_quantity },
|
{ value: promotion?.application_method?.buy_rules_min_quantity },
|
||||||
],
|
],
|
||||||
|
|||||||
+26
-12
@@ -1,4 +1,8 @@
|
|||||||
import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
|
import {
|
||||||
|
CreatePromotionRuleDTO,
|
||||||
|
PromotionDTO,
|
||||||
|
PromotionRuleDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
import { useRouteModal } from "../../../../../../components/route-modal"
|
import { useRouteModal } from "../../../../../../components/route-modal"
|
||||||
import {
|
import {
|
||||||
usePromotionAddRules,
|
usePromotionAddRules,
|
||||||
@@ -58,11 +62,10 @@ export const EditRulesWrapper = ({
|
|||||||
const { mutateAsync: updatePromotionRules, isPending } =
|
const { mutateAsync: updatePromotionRules, isPending } =
|
||||||
usePromotionUpdateRules(promotion.id, ruleType)
|
usePromotionUpdateRules(promotion.id, ruleType)
|
||||||
|
|
||||||
const handleSubmit = (rulesToRemove?: any[]) => {
|
const handleSubmit = (rulesToRemove?: { id: string }[]) => {
|
||||||
return async function (data) {
|
return async function (data: { rules: PromotionRuleDTO[] }) {
|
||||||
const applicationMethodData: Record<any, any> = {}
|
const applicationMethodData: Record<any, any> = {}
|
||||||
const { rules: allRules = [] } = data
|
const { rules: allRules = [] } = data
|
||||||
|
|
||||||
const disguisedRulesData = allRules.filter((rule) =>
|
const disguisedRulesData = allRules.filter((rule) =>
|
||||||
disguisedRules.map((rule) => rule.id).includes(rule.id!)
|
disguisedRules.map((rule) => rule.id).includes(rule.id!)
|
||||||
)
|
)
|
||||||
@@ -71,7 +74,14 @@ export const EditRulesWrapper = ({
|
|||||||
// database, they are currently all under application_method. If more of these are coming
|
// database, they are currently all under application_method. If more of these are coming
|
||||||
// up, abstract this away.
|
// up, abstract this away.
|
||||||
for (const rule of disguisedRulesData) {
|
for (const rule of disguisedRulesData) {
|
||||||
applicationMethodData[rule.id!] = parseInt(rule.values as string)
|
const currentAttribute = attributes?.find(
|
||||||
|
(attr) => attr.value === rule.attribute
|
||||||
|
)
|
||||||
|
|
||||||
|
applicationMethodData[rule.id!] =
|
||||||
|
currentAttribute?.field_type === "number"
|
||||||
|
? parseInt(rule.values as unknown as string)
|
||||||
|
: rule.values
|
||||||
}
|
}
|
||||||
|
|
||||||
// This variable will contain the rules that are actual rule objects, without the disguised
|
// This variable will contain the rules that are actual rule objects, without the disguised
|
||||||
@@ -80,13 +90,17 @@ export const EditRulesWrapper = ({
|
|||||||
(rule) => !disguisedRules.map((rule) => rule.id).includes(rule.id!)
|
(rule) => !disguisedRules.map((rule) => rule.id).includes(rule.id!)
|
||||||
)
|
)
|
||||||
|
|
||||||
const rulesToCreate = rulesData.filter((rule) => !("id" in rule))
|
const rulesToCreate: CreatePromotionRuleDTO[] = rulesData.filter(
|
||||||
|
(rule) => !("id" in rule)
|
||||||
|
)
|
||||||
const rulesToUpdate = rulesData.filter(
|
const rulesToUpdate = rulesData.filter(
|
||||||
(rule) => typeof rule.id === "string"
|
(rule: { id: string }) => typeof rule.id === "string"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (Object.keys(applicationMethodData).length) {
|
if (Object.keys(applicationMethodData).length) {
|
||||||
await updatePromotion({ application_method: applicationMethodData })
|
await updatePromotion({
|
||||||
|
application_method: applicationMethodData,
|
||||||
|
} as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
rulesToCreate.length &&
|
rulesToCreate.length &&
|
||||||
@@ -95,7 +109,7 @@ export const EditRulesWrapper = ({
|
|||||||
return {
|
return {
|
||||||
attribute: rule.attribute,
|
attribute: rule.attribute,
|
||||||
operator: rule.operator,
|
operator: rule.operator,
|
||||||
values: rule.values,
|
values: rule.operator === "eq" ? rule.values[0] : rule.values,
|
||||||
} as any
|
} as any
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
@@ -107,13 +121,13 @@ export const EditRulesWrapper = ({
|
|||||||
|
|
||||||
rulesToUpdate.length &&
|
rulesToUpdate.length &&
|
||||||
(await updatePromotionRules({
|
(await updatePromotionRules({
|
||||||
rules: rulesToUpdate.map((rule) => {
|
rules: rulesToUpdate.map((rule: PromotionRuleDTO) => {
|
||||||
return {
|
return {
|
||||||
id: rule.id!,
|
id: rule.id!,
|
||||||
attribute: rule.attribute,
|
attribute: rule.attribute,
|
||||||
operator: rule.operator,
|
operator: rule.operator,
|
||||||
values: rule.values,
|
values: rule.values as unknown as string | string[],
|
||||||
} as any
|
}
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./rule-value-form-field"
|
||||||
+157
@@ -0,0 +1,157 @@
|
|||||||
|
import { RuleAttributeOptionsResponse, StoreDTO } from "@medusajs/types"
|
||||||
|
import { Input, Select } from "@medusajs/ui"
|
||||||
|
import { RefCallBack, useWatch } from "react-hook-form"
|
||||||
|
import { Form } from "../../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../../components/inputs/combobox"
|
||||||
|
import { usePromotionRuleValues } from "../../../../../../hooks/api/promotions"
|
||||||
|
import { useStore } from "../../../../../../hooks/api/store"
|
||||||
|
|
||||||
|
type RuleValueFormFieldType = {
|
||||||
|
form: any
|
||||||
|
identifier: string
|
||||||
|
scope:
|
||||||
|
| "application_method.buy_rules"
|
||||||
|
| "rules"
|
||||||
|
| "application_method.target_rules"
|
||||||
|
valuesField: any
|
||||||
|
operatorsField: any
|
||||||
|
valuesRef: RefCallBack
|
||||||
|
fieldRule: any
|
||||||
|
attributes: RuleAttributeOptionsResponse[]
|
||||||
|
ruleType: "rules" | "target-rules" | "buy-rules"
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildFilters = (attribute?: string, store?: StoreDTO) => {
|
||||||
|
if (!attribute || !store) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribute === "currency_code") {
|
||||||
|
return {
|
||||||
|
value: store.supported_currency_codes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RuleValueFormField = ({
|
||||||
|
form,
|
||||||
|
identifier,
|
||||||
|
scope,
|
||||||
|
valuesField,
|
||||||
|
operatorsField,
|
||||||
|
valuesRef,
|
||||||
|
fieldRule,
|
||||||
|
attributes,
|
||||||
|
ruleType,
|
||||||
|
}: RuleValueFormFieldType) => {
|
||||||
|
const attribute = attributes?.find(
|
||||||
|
(attr) => attr.value === fieldRule.attribute
|
||||||
|
)
|
||||||
|
|
||||||
|
const { store, isLoading: isStoreLoading } = useStore()
|
||||||
|
const { values: options = [] } = usePromotionRuleValues(
|
||||||
|
ruleType,
|
||||||
|
attribute?.id!,
|
||||||
|
buildFilters(attribute?.id, store),
|
||||||
|
{
|
||||||
|
enabled:
|
||||||
|
!!attribute?.id &&
|
||||||
|
["select", "multiselect"].includes(attribute.field_type) &&
|
||||||
|
!isStoreLoading,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const watchOperator = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: operatorsField.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Field
|
||||||
|
key={`${identifier}.${scope}.${valuesField.name}-${fieldRule.attribute}`}
|
||||||
|
{...valuesField}
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
if (attribute?.field_type === "number") {
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-1/2">
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
onChange={onChange}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
ref={valuesRef}
|
||||||
|
min={1}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
} else if (attribute?.field_type === "text") {
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-1/2">
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
onChange={onChange}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
} else if (watchOperator === "eq") {
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-1/2">
|
||||||
|
<Form.Control>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
value={
|
||||||
|
Array.isArray(field.value) ? field.value[0] : field.value
|
||||||
|
}
|
||||||
|
onValueChange={onChange}
|
||||||
|
>
|
||||||
|
<Select.Trigger ref={ref} className="bg-ui-bg-base">
|
||||||
|
<Select.Value placeholder="Select Value" />
|
||||||
|
</Select.Trigger>
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
{options?.map((option, i) => (
|
||||||
|
<Select.Item
|
||||||
|
key={`${identifier}-value-option-${i}`}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
<span className="text-ui-fg-subtle">
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-1/2">
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
{...field}
|
||||||
|
placeholder="Select Values"
|
||||||
|
options={options}
|
||||||
|
onChange={onChange}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./rules-form-field"
|
||||||
+262
@@ -0,0 +1,262 @@
|
|||||||
|
import { XMarkMini } from "@medusajs/icons"
|
||||||
|
import {
|
||||||
|
RuleAttributeOptionsResponse,
|
||||||
|
RuleOperatorOptionsResponse,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { Badge, Button, Heading, Select, Text } from "@medusajs/ui"
|
||||||
|
import { Fragment } from "react"
|
||||||
|
import {
|
||||||
|
FieldValues,
|
||||||
|
Path,
|
||||||
|
UseFieldArrayAppend,
|
||||||
|
UseFieldArrayRemove,
|
||||||
|
UseFieldArrayUpdate,
|
||||||
|
UseFormReturn,
|
||||||
|
} from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Form } from "../../../../../../components/common/form"
|
||||||
|
import { RuleValueFormField } from "../rule-value-form-field"
|
||||||
|
|
||||||
|
type RulesFormFieldType<TSchema extends FieldValues> = {
|
||||||
|
form: UseFormReturn<TSchema>
|
||||||
|
ruleType: "rules" | "target-rules" | "buy-rules"
|
||||||
|
fields: any[]
|
||||||
|
attributes: RuleAttributeOptionsResponse[]
|
||||||
|
operators: RuleOperatorOptionsResponse[]
|
||||||
|
removeRule: UseFieldArrayRemove
|
||||||
|
updateRule: UseFieldArrayUpdate<TSchema>
|
||||||
|
appendRule: UseFieldArrayAppend<TSchema>
|
||||||
|
setRulesToRemove?: any
|
||||||
|
rulesToRemove?: any
|
||||||
|
scope?:
|
||||||
|
| "application_method.buy_rules"
|
||||||
|
| "rules"
|
||||||
|
| "application_method.target_rules"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RulesFormField = <TSchema extends FieldValues>({
|
||||||
|
form,
|
||||||
|
ruleType,
|
||||||
|
fields,
|
||||||
|
attributes,
|
||||||
|
operators,
|
||||||
|
removeRule,
|
||||||
|
updateRule,
|
||||||
|
appendRule,
|
||||||
|
setRulesToRemove,
|
||||||
|
rulesToRemove,
|
||||||
|
scope = "rules",
|
||||||
|
}: RulesFormFieldType<TSchema>) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Heading level="h2" className="mb-2">
|
||||||
|
{t(`promotions.fields.conditions.${ruleType}.title`)}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Text className="text-ui-fg-subtle txt-small mb-10">
|
||||||
|
{t(`promotions.fields.conditions.${ruleType}.description`)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{fields.map((fieldRule: any, index) => {
|
||||||
|
const identifier = fieldRule.id
|
||||||
|
const { ref: attributeRef, ...attributeField } = form.register(
|
||||||
|
`${scope}.${index}.attribute` as Path<TSchema>
|
||||||
|
)
|
||||||
|
const { ref: operatorRef, ...operatorsField } = form.register(
|
||||||
|
`${scope}.${index}.operator` as Path<TSchema>
|
||||||
|
)
|
||||||
|
const { ref: valuesRef, ...valuesField } = form.register(
|
||||||
|
`${scope}.${index}.values` as Path<TSchema>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={`${fieldRule.id}.${index}`}>
|
||||||
|
<div className="bg-ui-bg-subtle border-ui-border-base flex flex-row gap-2 rounded-xl border px-2 py-2">
|
||||||
|
<div className="grow">
|
||||||
|
<Form.Field
|
||||||
|
key={`${identifier}.${scope}.${attributeField.name}`}
|
||||||
|
{...attributeField}
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
const existingAttributes =
|
||||||
|
fields?.map((field: any) => field.attribute) || []
|
||||||
|
const attributeOptions =
|
||||||
|
attributes?.filter((attr) => {
|
||||||
|
if (attr.value === fieldRule.attribute) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !existingAttributes.includes(attr.value)
|
||||||
|
}) || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item className="mb-2">
|
||||||
|
{fieldRule.required && (
|
||||||
|
<p className="text text-ui-fg-muted txt-small">
|
||||||
|
{t("promotions.form.required")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Control>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
updateRule(index, { ...fieldRule, values: [] })
|
||||||
|
onChange(e)
|
||||||
|
}}
|
||||||
|
disabled={fieldRule.required}
|
||||||
|
>
|
||||||
|
<Select.Trigger
|
||||||
|
ref={attributeRef}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
>
|
||||||
|
<Select.Value
|
||||||
|
placeholder={t(
|
||||||
|
"promotions.form.selectAttribute"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Select.Trigger>
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
{attributeOptions?.map((c, i) => (
|
||||||
|
<Select.Item
|
||||||
|
key={`${identifier}-attribute-option-${i}`}
|
||||||
|
value={c.value}
|
||||||
|
>
|
||||||
|
<span className="text-ui-fg-subtle">
|
||||||
|
{c.label}
|
||||||
|
</span>
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Form.Field
|
||||||
|
key={`${identifier}.${scope}.${operatorsField.name}`}
|
||||||
|
{...operatorsField}
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-1/2">
|
||||||
|
<Form.Control>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={onChange}
|
||||||
|
disabled={fieldRule.required}
|
||||||
|
>
|
||||||
|
<Select.Trigger
|
||||||
|
ref={operatorRef}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
>
|
||||||
|
<Select.Value placeholder="Select Operator" />
|
||||||
|
</Select.Trigger>
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
{operators?.map((c, i) => (
|
||||||
|
<Select.Item
|
||||||
|
key={`${identifier}-operator-option-${i}`}
|
||||||
|
value={c.value}
|
||||||
|
>
|
||||||
|
<span className="text-ui-fg-subtle">
|
||||||
|
{c.label}
|
||||||
|
</span>
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RuleValueFormField
|
||||||
|
form={form}
|
||||||
|
identifier={identifier}
|
||||||
|
scope={scope}
|
||||||
|
valuesField={valuesField}
|
||||||
|
operatorsField={operatorsField}
|
||||||
|
valuesRef={valuesRef}
|
||||||
|
fieldRule={fieldRule}
|
||||||
|
attributes={attributes}
|
||||||
|
ruleType={ruleType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-none self-center px-1">
|
||||||
|
<XMarkMini
|
||||||
|
className={`text-ui-fg-muted cursor-pointer ${
|
||||||
|
fieldRule.required ? "invisible" : "visible"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!fieldRule.required) {
|
||||||
|
fieldRule.id &&
|
||||||
|
setRulesToRemove &&
|
||||||
|
setRulesToRemove([...rulesToRemove, fieldRule])
|
||||||
|
|
||||||
|
removeRule(index)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{index < fields.length - 1 && (
|
||||||
|
<div className="relative px-6 py-3">
|
||||||
|
<div className="border-ui-border-strong absolute bottom-0 left-[40px] top-0 z-[-1] w-px bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-repeat-y"></div>
|
||||||
|
|
||||||
|
<Badge size="2xsmall" className=" text-xs">
|
||||||
|
{t("promotions.form.and")}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
className="inline-block"
|
||||||
|
onClick={() => {
|
||||||
|
appendRule({
|
||||||
|
attribute: "",
|
||||||
|
operator: "",
|
||||||
|
values: [],
|
||||||
|
required: false,
|
||||||
|
} as any)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("promotions.fields.addCondition")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="transparent"
|
||||||
|
className="text-ui-fg-muted hover:text-ui-fg-subtle ml-2 inline-block"
|
||||||
|
onClick={() => {
|
||||||
|
const indicesToRemove = fields
|
||||||
|
.map((field: any, index) => (field.required ? null : index))
|
||||||
|
.filter((f) => f !== null)
|
||||||
|
|
||||||
|
setRulesToRemove &&
|
||||||
|
setRulesToRemove(fields.filter((field: any) => !field.required))
|
||||||
|
removeRule(indicesToRemove)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("promotions.fields.clearAll")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+14
-2
@@ -1,9 +1,9 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { CampaignResponse, PromotionDTO } from "@medusajs/types"
|
import { CampaignResponse, PromotionDTO } from "@medusajs/types"
|
||||||
import { Button, clx, RadioGroup, Select } from "@medusajs/ui"
|
import { Button, clx, RadioGroup, Select, Text } from "@medusajs/ui"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { useForm, useWatch } from "react-hook-form"
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import {
|
import {
|
||||||
@@ -133,6 +133,18 @@ export const AddCampaignPromotionFields = ({
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
<Trans
|
||||||
|
t={t}
|
||||||
|
i18nKey="campaigns.fields.campaign_id.hint"
|
||||||
|
components={[<br key="break" />]}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-1
@@ -87,7 +87,9 @@ export const CampaignDetails = ({ campaign }: CampaignDetailsProps) => {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Text className="txt-small">{campaign.currency || "-"}</Text>
|
<Text className="txt-small">
|
||||||
|
{campaign?.budget?.currency_code || "-"}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -11,13 +11,23 @@ export const PromotionAddCampaign = () => {
|
|||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { promotion, isPending, isError, error } = usePromotion(id!)
|
const { promotion, isPending, isError, error } = usePromotion(id!)
|
||||||
|
|
||||||
|
let campaignQuery = {}
|
||||||
|
|
||||||
|
if (promotion?.application_method?.currency_code) {
|
||||||
|
campaignQuery = {
|
||||||
|
budget: {
|
||||||
|
currency_code: promotion?.application_method?.currency_code,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
campaigns,
|
campaigns,
|
||||||
isPending: areCampaignsLoading,
|
isPending: areCampaignsLoading,
|
||||||
isError: isCampaignError,
|
isError: isCampaignError,
|
||||||
error: campaignError,
|
error: campaignError,
|
||||||
} = useCampaigns()
|
} = useCampaigns(campaignQuery)
|
||||||
|
|
||||||
if (isError || isCampaignError) {
|
if (isError || isCampaignError) {
|
||||||
throw error || campaignError
|
throw error || campaignError
|
||||||
}
|
}
|
||||||
|
|||||||
+181
-110
@@ -4,6 +4,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
clx,
|
clx,
|
||||||
CurrencyInput,
|
CurrencyInput,
|
||||||
|
Heading,
|
||||||
Input,
|
Input,
|
||||||
ProgressTabs,
|
ProgressTabs,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
@@ -15,21 +16,23 @@ import { Trans, useTranslation } from "react-i18next"
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CampaignResponse,
|
PromotionRuleOperatorValues,
|
||||||
PromotionRuleResponse,
|
PromotionRuleResponse,
|
||||||
RuleAttributeOptionsResponse,
|
RuleAttributeOptionsResponse,
|
||||||
RuleOperatorOptionsResponse,
|
RuleOperatorOptionsResponse,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useCampaigns } from "../../../../../hooks/api/campaigns"
|
||||||
import { useCreatePromotion } from "../../../../../hooks/api/promotions"
|
import { useCreatePromotion } from "../../../../../hooks/api/promotions"
|
||||||
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||||
import { defaultCampaignValues } from "../../../../campaigns/campaign-create/components/create-campaign-form"
|
import { defaultCampaignValues } from "../../../../campaigns/campaign-create/components/create-campaign-form"
|
||||||
import { RulesFormField } from "../../../common/edit-rules/components/edit-rules-form"
|
import { RulesFormField } from "../../../common/edit-rules/components/rules-form-field"
|
||||||
import { AddCampaignPromotionFields } from "../../../promotion-add-campaign/components/add-campaign-promotion-form"
|
import { AddCampaignPromotionFields } from "../../../promotion-add-campaign/components/add-campaign-promotion-form"
|
||||||
import { Tab } from "./constants"
|
import { Tab } from "./constants"
|
||||||
import { CreatePromotionSchema } from "./form-schema"
|
import { CreatePromotionSchema } from "./form-schema"
|
||||||
@@ -43,7 +46,6 @@ type CreatePromotionFormProps = {
|
|||||||
rules: PromotionRuleResponse[]
|
rules: PromotionRuleResponse[]
|
||||||
targetRules: PromotionRuleResponse[]
|
targetRules: PromotionRuleResponse[]
|
||||||
buyRules: PromotionRuleResponse[]
|
buyRules: PromotionRuleResponse[]
|
||||||
campaigns: CampaignResponse[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreatePromotionForm = ({
|
export const CreatePromotionForm = ({
|
||||||
@@ -54,7 +56,6 @@ export const CreatePromotionForm = ({
|
|||||||
rules,
|
rules,
|
||||||
targetRules,
|
targetRules,
|
||||||
buyRules,
|
buyRules,
|
||||||
campaigns,
|
|
||||||
}: CreatePromotionFormProps) => {
|
}: CreatePromotionFormProps) => {
|
||||||
const [tab, setTab] = useState<Tab>(Tab.TYPE)
|
const [tab, setTab] = useState<Tab>(Tab.TYPE)
|
||||||
const [detailsValidated, setDetailsValidated] = useState(false)
|
const [detailsValidated, setDetailsValidated] = useState(false)
|
||||||
@@ -146,46 +147,46 @@ export const CreatePromotionForm = ({
|
|||||||
...applicationMethodData
|
...applicationMethodData
|
||||||
} = application_method
|
} = application_method
|
||||||
|
|
||||||
const disguisedRuleAttributes = [
|
const disguisedRules = [
|
||||||
...targetRules.filter((r) => !!r.disguised),
|
...targetRulesData.filter((r) => !!r.disguised),
|
||||||
...buyRules.filter((r) => !!r.disguised),
|
...buyRulesData.filter((r) => !!r.disguised),
|
||||||
].map((r) => r.attribute)
|
...rules.filter((r) => !!r.disguised),
|
||||||
|
]
|
||||||
|
|
||||||
const attr: Record<any, any> = {}
|
const applicationMethodRuleData: Record<any, any> = {}
|
||||||
|
|
||||||
for (const rule of [...targetRulesData, ...buyRulesData]) {
|
for (const rule of disguisedRules) {
|
||||||
if (disguisedRuleAttributes.includes(rule.attribute)) {
|
applicationMethodRuleData[rule.attribute] =
|
||||||
attr[rule.attribute] =
|
rule.field_type === "number"
|
||||||
rule.field_type === "number"
|
? parseInt(rule.values as string)
|
||||||
? parseInt(rule.values as string)
|
: rule.values
|
||||||
: rule.values
|
}
|
||||||
}
|
|
||||||
|
const buildRulesData = (
|
||||||
|
rules: {
|
||||||
|
operator: string
|
||||||
|
attribute: string
|
||||||
|
values: any[] | any
|
||||||
|
disguised?: boolean
|
||||||
|
}[]
|
||||||
|
) => {
|
||||||
|
return rules
|
||||||
|
.filter((r) => !r.disguised)
|
||||||
|
.map((rule) => ({
|
||||||
|
operator: rule.operator as PromotionRuleOperatorValues,
|
||||||
|
attribute: rule.attribute,
|
||||||
|
values: rule.values,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
createPromotion({
|
createPromotion({
|
||||||
...promotionData,
|
...promotionData,
|
||||||
rules: rules.map((rule) => ({
|
rules: buildRulesData(rules),
|
||||||
operator: rule.operator,
|
|
||||||
attribute: rule.attribute,
|
|
||||||
values: rule.values,
|
|
||||||
})),
|
|
||||||
application_method: {
|
application_method: {
|
||||||
...applicationMethodData,
|
...applicationMethodData,
|
||||||
...attr,
|
...applicationMethodRuleData,
|
||||||
target_rules: targetRulesData
|
target_rules: buildRulesData(targetRulesData),
|
||||||
.filter((r) => !disguisedRuleAttributes.includes(r.attribute))
|
buy_rules: buildRulesData(buyRulesData),
|
||||||
.map((rule) => ({
|
|
||||||
operator: rule.operator,
|
|
||||||
attribute: rule.attribute,
|
|
||||||
values: rule.values,
|
|
||||||
})),
|
|
||||||
buy_rules: buyRulesData
|
|
||||||
.filter((r) => !disguisedRuleAttributes.includes(r.attribute))
|
|
||||||
.map((rule) => ({
|
|
||||||
operator: rule.operator,
|
|
||||||
attribute: rule.attribute,
|
|
||||||
values: rule.values,
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
is_automatic: is_automatic === "true",
|
is_automatic: is_automatic === "true",
|
||||||
}).then(() => handleSuccess())
|
}).then(() => handleSuccess())
|
||||||
@@ -272,12 +273,28 @@ export const CreatePromotionForm = ({
|
|||||||
|
|
||||||
const isAllocationEach = watchAllocation === "each"
|
const isAllocationEach = watchAllocation === "each"
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (watchAllocation === "across") {
|
||||||
|
form.setValue("application_method.max_quantity", null)
|
||||||
|
}
|
||||||
|
}, [watchAllocation])
|
||||||
|
|
||||||
const watchType = useWatch({
|
const watchType = useWatch({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
name: "type",
|
name: "type",
|
||||||
})
|
})
|
||||||
|
|
||||||
const isTypeStandard = watchType === "standard"
|
const isTypeStandard = watchType === "standard"
|
||||||
|
const formData = form.getValues()
|
||||||
|
let campaignQuery: object = {}
|
||||||
|
|
||||||
|
if (isFixedValueType && formData.application_method.currency_code) {
|
||||||
|
campaignQuery = {
|
||||||
|
budget: { currency_code: formData.application_method.currency_code },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { campaigns } = useCampaigns(campaignQuery)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTypeStandard) {
|
if (isTypeStandard) {
|
||||||
@@ -316,11 +333,36 @@ export const CreatePromotionForm = ({
|
|||||||
|
|
||||||
if (watchCampaignChoice === "new") {
|
if (watchCampaignChoice === "new") {
|
||||||
if (!formData.campaign || !formData.campaign?.budget?.type) {
|
if (!formData.campaign || !formData.campaign?.budget?.type) {
|
||||||
form.setValue("campaign", defaultCampaignValues)
|
form.setValue("campaign", {
|
||||||
|
...defaultCampaignValues,
|
||||||
|
budget: {
|
||||||
|
...defaultCampaignValues.budget,
|
||||||
|
currency_code: formData.application_method.currency_code,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [watchCampaignChoice])
|
}, [watchCampaignChoice])
|
||||||
|
|
||||||
|
const watchRules = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "rules",
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchCurrencyRule = watchRules.find(
|
||||||
|
(rule) => rule.attribute === "currency_code"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (watchCurrencyRule) {
|
||||||
|
const formData = form.getValues()
|
||||||
|
const currencyCode = formData.application_method.currency_code
|
||||||
|
const ruleValue = watchCurrencyRule.values
|
||||||
|
|
||||||
|
if (!Array.isArray(ruleValue) && currencyCode !== ruleValue) {
|
||||||
|
form.setValue("application_method.currency_code", ruleValue as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
<form
|
<form
|
||||||
@@ -410,8 +452,8 @@ export const CreatePromotionForm = ({
|
|||||||
value={template.id}
|
value={template.id}
|
||||||
label={template.title}
|
label={template.title}
|
||||||
description={template.description}
|
description={template.description}
|
||||||
className={clx("", {
|
className={clx("border", {
|
||||||
"border-ui-border-interactive border-2":
|
"border border-ui-border-interactive":
|
||||||
template.id === field.value,
|
template.id === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -430,6 +472,8 @@ export const CreatePromotionForm = ({
|
|||||||
value={Tab.PROMOTION}
|
value={Tab.PROMOTION}
|
||||||
className="flex flex-1 flex-col gap-10"
|
className="flex flex-1 flex-col gap-10"
|
||||||
>
|
>
|
||||||
|
<Heading level="h2">{t(`promotions.sections.details`)}</Heading>
|
||||||
|
|
||||||
{form.formState.errors.root && (
|
{form.formState.errors.root && (
|
||||||
<Alert
|
<Alert
|
||||||
variant="error"
|
variant="error"
|
||||||
@@ -461,19 +505,20 @@ export const CreatePromotionForm = ({
|
|||||||
description={t(
|
description={t(
|
||||||
"promotions.form.method.code.description"
|
"promotions.form.method.code.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2 border", {
|
||||||
"border-ui-border-interactive border-2":
|
"border border-ui-border-interactive":
|
||||||
"false" === field.value,
|
"false" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
value={"true"}
|
value={"true"}
|
||||||
label={t("promotions.form.method.automatic.title")}
|
label={t("promotions.form.method.automatic.title")}
|
||||||
description={t(
|
description={t(
|
||||||
"promotions.form.method.automatic.description"
|
"promotions.form.method.automatic.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2 border", {
|
||||||
"border-ui-border-interactive border-2":
|
"border border-ui-border-interactive":
|
||||||
"true" === field.value,
|
"true" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -517,6 +562,65 @@ export const CreatePromotionForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="type"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("promotions.fields.type")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup
|
||||||
|
className="flex gap-y-3"
|
||||||
|
{...field}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"standard"}
|
||||||
|
label={t("promotions.form.type.standard.title")}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.type.standard.description"
|
||||||
|
)}
|
||||||
|
className={clx("basis-1/2 border", {
|
||||||
|
"border border-ui-border-interactive":
|
||||||
|
"standard" === field.value,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"buyget"}
|
||||||
|
label={t("promotions.form.type.buyget.title")}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.type.buyget.description"
|
||||||
|
)}
|
||||||
|
className={clx("basis-1/2 border", {
|
||||||
|
"border border-ui-border-interactive":
|
||||||
|
"buyget" === field.value,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<RulesFormField
|
||||||
|
form={form}
|
||||||
|
ruleType={"rules"}
|
||||||
|
attributes={ruleAttributes}
|
||||||
|
operators={operators}
|
||||||
|
fields={ruleFields}
|
||||||
|
appendRule={appendRule}
|
||||||
|
removeRule={removeRule}
|
||||||
|
updateRule={updateRule}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="application_method.type"
|
name="application_method.type"
|
||||||
@@ -538,8 +642,8 @@ export const CreatePromotionForm = ({
|
|||||||
description={t(
|
description={t(
|
||||||
"promotions.form.value_type.fixed.description"
|
"promotions.form.value_type.fixed.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2 border", {
|
||||||
"border-ui-border-interactive border-2":
|
"border border-ui-border-interactive":
|
||||||
"fixed" === field.value,
|
"fixed" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -552,8 +656,8 @@ export const CreatePromotionForm = ({
|
|||||||
description={t(
|
description={t(
|
||||||
"promotions.form.value_type.percentage.description"
|
"promotions.form.value_type.percentage.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2 border", {
|
||||||
"border-ui-border-interactive border-2":
|
"border border-ui-border-interactive":
|
||||||
"percentage" === field.value,
|
"percentage" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -570,24 +674,39 @@ export const CreatePromotionForm = ({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="application_method.value"
|
name="application_method.value"
|
||||||
render={({ field: { onChange, value, ...field } }) => {
|
render={({ field: { onChange, value, ...field } }) => {
|
||||||
|
const currencyCode =
|
||||||
|
form.getValues().application_method.currency_code
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item className="basis-1/2">
|
<Form.Item className="basis-1/2">
|
||||||
<Form.Label>
|
<Form.Label
|
||||||
|
tooltip={
|
||||||
|
currencyCode || !isFixedValueType
|
||||||
|
? undefined
|
||||||
|
: t("promotions.fields.amount.tooltip")
|
||||||
|
}
|
||||||
|
>
|
||||||
{isFixedValueType
|
{isFixedValueType
|
||||||
? t("fields.amount")
|
? t("fields.amount")
|
||||||
: t("fields.percentage")}
|
: t("fields.percentage")}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{isFixedValueType ? (
|
{isFixedValueType ? (
|
||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
|
{...field}
|
||||||
min={0}
|
min={0}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
onChange(value ? parseInt(value) : "")
|
onChange(value ? parseInt(value) : "")
|
||||||
}}
|
}}
|
||||||
code={"USD"}
|
code={currencyCode}
|
||||||
symbol={getCurrencySymbol("USD")}
|
symbol={
|
||||||
{...field}
|
currencyCode
|
||||||
|
? getCurrencySymbol(currencyCode)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
value={value}
|
value={value}
|
||||||
|
disabled={!currencyCode}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PercentageInput
|
<PercentageInput
|
||||||
@@ -614,50 +733,6 @@ export const CreatePromotionForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="type"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>{t("promotions.fields.type")}</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<RadioGroup
|
|
||||||
className="flex gap-y-3"
|
|
||||||
{...field}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
>
|
|
||||||
<RadioGroup.ChoiceBox
|
|
||||||
value={"standard"}
|
|
||||||
label={t("promotions.form.type.standard.title")}
|
|
||||||
description={t(
|
|
||||||
"promotions.form.type.standard.description"
|
|
||||||
)}
|
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-ui-border-interactive border-2":
|
|
||||||
"standard" === field.value,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioGroup.ChoiceBox
|
|
||||||
value={"buyget"}
|
|
||||||
label={t("promotions.form.type.buyget.title")}
|
|
||||||
description={t(
|
|
||||||
"promotions.form.type.buyget.description"
|
|
||||||
)}
|
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-ui-border-interactive border-2":
|
|
||||||
"buyget" === field.value,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isTypeStandard && (
|
{isTypeStandard && (
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -681,8 +756,8 @@ export const CreatePromotionForm = ({
|
|||||||
description={t(
|
description={t(
|
||||||
"promotions.form.allocation.each.description"
|
"promotions.form.allocation.each.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2 border", {
|
||||||
"border-ui-border-interactive border-2":
|
"border border-ui-border-interactive":
|
||||||
"each" === field.value,
|
"each" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -695,8 +770,8 @@ export const CreatePromotionForm = ({
|
|||||||
description={t(
|
description={t(
|
||||||
"promotions.form.allocation.across.description"
|
"promotions.form.allocation.across.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2 border", {
|
||||||
"border-ui-border-interactive border-2":
|
"border border-ui-border-interactive":
|
||||||
"across" === field.value,
|
"across" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -751,16 +826,7 @@ export const CreatePromotionForm = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<RulesFormField
|
<Divider />
|
||||||
form={form}
|
|
||||||
ruleType={"rules"}
|
|
||||||
attributes={ruleAttributes}
|
|
||||||
operators={operators}
|
|
||||||
fields={ruleFields}
|
|
||||||
appendRule={appendRule}
|
|
||||||
removeRule={removeRule}
|
|
||||||
updateRule={updateRule}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RulesFormField
|
<RulesFormField
|
||||||
form={form}
|
form={form}
|
||||||
@@ -774,6 +840,8 @@ export const CreatePromotionForm = ({
|
|||||||
scope="application_method.target_rules"
|
scope="application_method.target_rules"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
{!isTypeStandard && (
|
{!isTypeStandard && (
|
||||||
<RulesFormField
|
<RulesFormField
|
||||||
form={form}
|
form={form}
|
||||||
@@ -793,7 +861,10 @@ export const CreatePromotionForm = ({
|
|||||||
value={Tab.CAMPAIGN}
|
value={Tab.CAMPAIGN}
|
||||||
className="flex flex-col items-center"
|
className="flex flex-col items-center"
|
||||||
>
|
>
|
||||||
<AddCampaignPromotionFields form={form} campaigns={campaigns} />
|
<AddCampaignPromotionFields
|
||||||
|
form={form}
|
||||||
|
campaigns={campaigns || []}
|
||||||
|
/>
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
</ProgressTabs>
|
</ProgressTabs>
|
||||||
|
|||||||
+37
-19
@@ -17,22 +17,40 @@ const RuleSchema = z.array(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
export const CreatePromotionSchema = z.object({
|
export const CreatePromotionSchema = z
|
||||||
template_id: z.string().optional(),
|
.object({
|
||||||
campaign_id: z.string().optional(),
|
template_id: z.string().optional(),
|
||||||
campaign_choice: z.enum(["none", "existing", "new"]).optional(),
|
campaign_id: z.string().optional(),
|
||||||
is_automatic: z.string().toLowerCase(),
|
campaign_choice: z.enum(["none", "existing", "new"]).optional(),
|
||||||
code: z.string().min(1),
|
is_automatic: z.string().toLowerCase(),
|
||||||
type: z.enum(["buyget", "standard"]),
|
code: z.string().min(1),
|
||||||
rules: RuleSchema,
|
type: z.enum(["buyget", "standard"]),
|
||||||
application_method: z.object({
|
rules: RuleSchema,
|
||||||
allocation: z.enum(["each", "across"]),
|
application_method: z.object({
|
||||||
value: z.number().min(0),
|
allocation: z.enum(["each", "across"]),
|
||||||
max_quantity: z.number().optional(),
|
value: z.number().min(0),
|
||||||
target_rules: RuleSchema,
|
currency_code: z.string(),
|
||||||
buy_rules: RuleSchema.min(2).optional(),
|
max_quantity: z.number().optional().nullable(),
|
||||||
type: z.enum(["fixed", "percentage"]),
|
target_rules: RuleSchema,
|
||||||
target_type: z.enum(["order", "shipping_methods", "items"]),
|
buy_rules: RuleSchema.min(2).optional(),
|
||||||
}),
|
type: z.enum(["fixed", "percentage"]),
|
||||||
campaign: CreateCampaignSchema.optional(),
|
target_type: z.enum(["order", "shipping_methods", "items"]),
|
||||||
})
|
}),
|
||||||
|
campaign: CreateCampaignSchema.optional(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.application_method.allocation === "across") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
data.application_method.allocation === "each" &&
|
||||||
|
typeof data.application_method.max_quantity === "number"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["application_method.max_quantity"],
|
||||||
|
message: `required field`,
|
||||||
|
}
|
||||||
|
)
|
||||||
+16
@@ -59,4 +59,20 @@ export const templates = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "buy_get",
|
||||||
|
type: "buy_get",
|
||||||
|
title: "Buy X Get Y",
|
||||||
|
description: "Buy X product(s), get Y product(s)",
|
||||||
|
defaults: {
|
||||||
|
is_automatic: "false",
|
||||||
|
type: "buyget",
|
||||||
|
application_method: {
|
||||||
|
type: "percentage",
|
||||||
|
value: 100,
|
||||||
|
apply_to_quantity: 1,
|
||||||
|
max_quantity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
-4
@@ -1,5 +1,4 @@
|
|||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { useCampaigns } from "../../../hooks/api/campaigns"
|
|
||||||
import {
|
import {
|
||||||
usePromotionRuleAttributes,
|
usePromotionRuleAttributes,
|
||||||
usePromotionRuleOperators,
|
usePromotionRuleOperators,
|
||||||
@@ -18,14 +17,12 @@ export const PromotionCreate = () => {
|
|||||||
const { rules: targetRules } = usePromotionRules(null, "target-rules")
|
const { rules: targetRules } = usePromotionRules(null, "target-rules")
|
||||||
const { rules: buyRules } = usePromotionRules(null, "buy-rules")
|
const { rules: buyRules } = usePromotionRules(null, "buy-rules")
|
||||||
const { operators } = usePromotionRuleOperators()
|
const { operators } = usePromotionRuleOperators()
|
||||||
const { campaigns } = useCampaigns()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
{rules &&
|
{rules &&
|
||||||
buyRules &&
|
buyRules &&
|
||||||
targetRules &&
|
targetRules &&
|
||||||
campaigns &&
|
|
||||||
operators &&
|
operators &&
|
||||||
ruleAttributes &&
|
ruleAttributes &&
|
||||||
targetRuleAttributes &&
|
targetRuleAttributes &&
|
||||||
@@ -38,7 +35,6 @@ export const PromotionCreate = () => {
|
|||||||
rules={rules}
|
rules={rules}
|
||||||
targetRules={targetRules}
|
targetRules={targetRules}
|
||||||
buyRules={buyRules}
|
buyRules={buyRules}
|
||||||
campaigns={campaigns}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
|
|||||||
+10
-1
@@ -1,6 +1,7 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { PromotionDTO } from "@medusajs/types"
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Container,
|
Container,
|
||||||
Copy,
|
Copy,
|
||||||
Heading,
|
Heading,
|
||||||
@@ -142,7 +143,15 @@ export const PromotionGeneralSection = ({
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
{promotion.application_method?.value}
|
<Text className="inline pr-3" size="small" leading="compact">
|
||||||
|
{promotion.application_method?.value}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{promotion?.application_method?.type === "fixed" && (
|
||||||
|
<Badge size="xsmall">
|
||||||
|
{promotion?.application_method?.currency_code}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -15,8 +15,8 @@ import * as zod from "zod"
|
|||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
||||||
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||||
@@ -29,7 +29,7 @@ const EditPromotionSchema = zod.object({
|
|||||||
is_automatic: zod.string().toLowerCase(),
|
is_automatic: zod.string().toLowerCase(),
|
||||||
code: zod.string().min(1),
|
code: zod.string().min(1),
|
||||||
value_type: zod.enum(["fixed", "percentage"]),
|
value_type: zod.enum(["fixed", "percentage"]),
|
||||||
value: zod.string(),
|
value: zod.number(),
|
||||||
allocation: zod.enum(["each", "across"]),
|
allocation: zod.enum(["each", "across"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ export const EditPromotionDetailsForm = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
is_automatic: promotion.is_automatic!.toString(),
|
is_automatic: promotion.is_automatic!.toString(),
|
||||||
code: promotion.code,
|
code: promotion.code,
|
||||||
value: promotion.application_method!.value?.toString(),
|
value: promotion.application_method!.value,
|
||||||
allocation: promotion.application_method!.allocation,
|
allocation: promotion.application_method!.allocation,
|
||||||
value_type: promotion.application_method!.type,
|
value_type: promotion.application_method!.type,
|
||||||
},
|
},
|
||||||
@@ -218,11 +218,13 @@ export const EditPromotionDetailsForm = ({
|
|||||||
{isFixedValueType ? (
|
{isFixedValueType ? (
|
||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
min={0}
|
min={0}
|
||||||
onValueChange={onChange}
|
onValueChange={(val) =>
|
||||||
|
onChange(val ? parseInt(val) : null)
|
||||||
|
}
|
||||||
code={"USD"}
|
code={"USD"}
|
||||||
symbol={getCurrencySymbol("USD")}
|
symbol={getCurrencySymbol("USD")}
|
||||||
{...field}
|
{...field}
|
||||||
value={Number(field.value)}
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PercentageInput
|
<PercentageInput
|
||||||
@@ -233,7 +235,9 @@ export const EditPromotionDetailsForm = ({
|
|||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onChange(
|
onChange(
|
||||||
e.target.value === "" ? null : e.target.value
|
e.target.value === ""
|
||||||
|
? null
|
||||||
|
: parseInt(e.target.value)
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ProductTypeDTO,
|
ProductTypeDTO,
|
||||||
ProductVariantDTO,
|
ProductVariantDTO,
|
||||||
PromotionDTO,
|
PromotionDTO,
|
||||||
|
PromotionRuleDTO,
|
||||||
SalesChannelDTO,
|
SalesChannelDTO,
|
||||||
ShippingOptionDTO,
|
ShippingOptionDTO,
|
||||||
ShippingProfileDTO,
|
ShippingProfileDTO,
|
||||||
@@ -52,7 +53,7 @@ export type PromotionListRes = { promotions: PromotionDTO[] } & ListRes
|
|||||||
export type PromotionRuleAttributesListRes = { attributes: Record<any, any>[] }
|
export type PromotionRuleAttributesListRes = { attributes: Record<any, any>[] }
|
||||||
export type PromotionRuleOperatorsListRes = { operators: Record<any, any>[] }
|
export type PromotionRuleOperatorsListRes = { operators: Record<any, any>[] }
|
||||||
export type PromotionRuleValuesListRes = { values: Record<any, any>[] }
|
export type PromotionRuleValuesListRes = { values: Record<any, any>[] }
|
||||||
export type PromotionRulesListRes = { rules: Record<any, any>[] }
|
export type PromotionRulesListRes = { rules: PromotionRuleDTO[] }
|
||||||
export type PromotionDeleteRes = DeleteRes
|
export type PromotionDeleteRes = DeleteRes
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface CampaignResponse {
|
|||||||
budget: {
|
budget: {
|
||||||
id: string
|
id: string
|
||||||
type: CampaignBudgetTypeValues
|
type: CampaignBudgetTypeValues
|
||||||
|
currency_code: string
|
||||||
limit: number
|
limit: number
|
||||||
used: number
|
used: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./promotion-rule"
|
export * from "./promotion-rule"
|
||||||
export * from "./rule-attribute-options"
|
export * from "./rule-attribute-options"
|
||||||
export * from "./rule-operator-options"
|
export * from "./rule-operator-options"
|
||||||
|
export * from "./rule-value-options"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface RuleValueOptionsResponse {
|
||||||
|
id: string
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface AdminRuleValueOptionsListResponse {
|
||||||
|
values: RuleValueOptionsResponse[]
|
||||||
|
}
|
||||||
@@ -53,6 +53,11 @@ export interface ApplicationMethodDTO {
|
|||||||
*/
|
*/
|
||||||
value?: number
|
value?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currency code of the application method
|
||||||
|
*/
|
||||||
|
currency_code?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The max quantity allowed in the cart for the associated promotion to be applied.
|
* The max quantity allowed in the cart for the associated promotion to be applied.
|
||||||
*/
|
*/
|
||||||
@@ -116,6 +121,11 @@ export interface CreateApplicationMethodDTO {
|
|||||||
*/
|
*/
|
||||||
value?: number
|
value?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currency of the value to apply.
|
||||||
|
*/
|
||||||
|
currency_code: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The max quantity allowed in the cart for the associated promotion to be applied.
|
* The max quantity allowed in the cart for the associated promotion to be applied.
|
||||||
*/
|
*/
|
||||||
@@ -158,7 +168,7 @@ export interface UpdateApplicationMethodDTO {
|
|||||||
/**
|
/**
|
||||||
* The ID of the application method.
|
* The ID of the application method.
|
||||||
*/
|
*/
|
||||||
id: string
|
id?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the application method indicating how
|
* The type of the application method indicating how
|
||||||
@@ -184,6 +194,11 @@ export interface UpdateApplicationMethodDTO {
|
|||||||
*/
|
*/
|
||||||
value?: number
|
value?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currency code of the promotions application
|
||||||
|
*/
|
||||||
|
currency_code?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The max quantity allowed in the cart for the associated promotion to be applied.
|
* The max quantity allowed in the cart for the associated promotion to be applied.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ export interface CampaignBudgetDTO {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
used?: number
|
used?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currency of the campaign.
|
||||||
|
*/
|
||||||
|
currency_code?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,11 +21,6 @@ export interface CampaignDTO {
|
|||||||
*/
|
*/
|
||||||
description?: string
|
description?: string
|
||||||
|
|
||||||
/**
|
|
||||||
* The currency of the campaign.
|
|
||||||
*/
|
|
||||||
currency?: string
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The campaign identifier of the campaign.
|
* The campaign identifier of the campaign.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export interface CreatePromotionDTO {
|
|||||||
/**
|
/**
|
||||||
* The associated application method.
|
* The associated application method.
|
||||||
*/
|
*/
|
||||||
application_method?: CreateApplicationMethodDTO
|
application_method: CreateApplicationMethodDTO
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The rules of the promotion.
|
* The rules of the promotion.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type AdminGetPromotionRulesRes = {
|
|||||||
attribute_label: string
|
attribute_label: string
|
||||||
field_type?: string
|
field_type?: string
|
||||||
operator: string
|
operator: string
|
||||||
|
hydrate: boolean
|
||||||
operator_label: string
|
operator_label: string
|
||||||
values: { label?: string; value?: string }[]
|
values: { label?: string; value?: string }[]
|
||||||
disguised?: boolean
|
disguised?: boolean
|
||||||
|
|||||||
@@ -12,12 +12,17 @@ export interface CreateCampaignBudgetDTO {
|
|||||||
/**
|
/**
|
||||||
* The limit of the campaign budget.
|
* The limit of the campaign budget.
|
||||||
*/
|
*/
|
||||||
limit?: number
|
limit?: number | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How much is used of the campaign budget.
|
* How much is used of the campaign budget.
|
||||||
*/
|
*/
|
||||||
used?: number
|
used?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currency of the campaign.
|
||||||
|
*/
|
||||||
|
currency_code?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +42,12 @@ export interface UpdateCampaignBudgetDTO {
|
|||||||
/**
|
/**
|
||||||
* The limit of the campaign budget.
|
* The limit of the campaign budget.
|
||||||
*/
|
*/
|
||||||
limit?: number
|
limit?: number | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The limit of the campaign budget.
|
||||||
|
*/
|
||||||
|
currency_code?: string | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How much is used of the campaign budget.
|
* How much is used of the campaign budget.
|
||||||
@@ -59,11 +69,6 @@ export interface CreateCampaignDTO {
|
|||||||
*/
|
*/
|
||||||
description?: string
|
description?: string
|
||||||
|
|
||||||
/**
|
|
||||||
* The currency of the campaign.
|
|
||||||
*/
|
|
||||||
currency?: string
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The campaign identifier of the campaign.
|
* The campaign identifier of the campaign.
|
||||||
*/
|
*/
|
||||||
@@ -104,11 +109,6 @@ export interface UpdateCampaignDTO {
|
|||||||
*/
|
*/
|
||||||
description?: string
|
description?: string
|
||||||
|
|
||||||
/**
|
|
||||||
* The currency of the campaign.
|
|
||||||
*/
|
|
||||||
currency?: string
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The campaign identifier of the campaign.
|
* The campaign identifier of the campaign.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CampaignBudgetType } from "@medusajs/utils"
|
import { CampaignBudgetType, isPresent } from "@medusajs/utils"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { createFindParams, createSelectParams } from "../../utils/validators"
|
import { createFindParams, createSelectParams } from "../../utils/validators"
|
||||||
|
|
||||||
@@ -10,44 +10,70 @@ export type AdminGetCampaignsParamsType = z.infer<
|
|||||||
export const AdminGetCampaignsParams = createFindParams({
|
export const AdminGetCampaignsParams = createFindParams({
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
}).merge(
|
})
|
||||||
z.object({
|
.merge(
|
||||||
q: z.string().optional(),
|
z.object({
|
||||||
campaign_identifier: z.string().optional(),
|
q: z.string().optional(),
|
||||||
currency: z.string().optional(),
|
campaign_identifier: z.string().optional(),
|
||||||
$and: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
|
budget: z
|
||||||
$or: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
|
.object({
|
||||||
|
currency_code: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
$and: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
|
||||||
|
$or: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
export const CreateCampaignBudget = z
|
||||||
|
.object({
|
||||||
|
type: z.nativeEnum(CampaignBudgetType),
|
||||||
|
limit: z.number().optional().nullable(),
|
||||||
|
currency_code: z.string().optional().nullable(),
|
||||||
})
|
})
|
||||||
)
|
.strict()
|
||||||
|
.refine(
|
||||||
|
(data) =>
|
||||||
|
data.type !== CampaignBudgetType.SPEND || isPresent(data.currency_code),
|
||||||
|
{
|
||||||
|
path: ["currency_code"],
|
||||||
|
message: `currency_code is required when budget type is ${CampaignBudgetType.SPEND}`,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) =>
|
||||||
|
data.type !== CampaignBudgetType.USAGE || !isPresent(data.currency_code),
|
||||||
|
{
|
||||||
|
path: ["currency_code"],
|
||||||
|
message: `currency_code should not be present when budget type is ${CampaignBudgetType.USAGE}`,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const CreateCampaignBudget = z.object({
|
export const UpdateCampaignBudget = z
|
||||||
type: z.nativeEnum(CampaignBudgetType),
|
.object({
|
||||||
limit: z.number(),
|
limit: z.number().optional().nullable(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
const UpdateCampaignBudget = z.object({
|
|
||||||
type: z.nativeEnum(CampaignBudgetType).optional(),
|
|
||||||
limit: z.number().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type AdminCreateCampaignType = z.infer<typeof AdminCreateCampaign>
|
export type AdminCreateCampaignType = z.infer<typeof AdminCreateCampaign>
|
||||||
export const AdminCreateCampaign = z.object({
|
export const AdminCreateCampaign = z
|
||||||
name: z.string(),
|
.object({
|
||||||
campaign_identifier: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional(),
|
campaign_identifier: z.string(),
|
||||||
currency: z.string().optional(),
|
description: z.string().optional(),
|
||||||
budget: CreateCampaignBudget.optional(),
|
budget: CreateCampaignBudget.optional(),
|
||||||
starts_at: z.coerce.date().optional(),
|
starts_at: z.coerce.date().optional(),
|
||||||
ends_at: z.coerce.date().optional(),
|
ends_at: z.coerce.date().optional(),
|
||||||
promotions: z.array(z.object({ id: z.string() })).optional(),
|
promotions: z.array(z.object({ id: z.string() })).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
export type AdminUpdateCampaignType = z.infer<typeof AdminUpdateCampaign>
|
export type AdminUpdateCampaignType = z.infer<typeof AdminUpdateCampaign>
|
||||||
export const AdminUpdateCampaign = z.object({
|
export const AdminUpdateCampaign = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
campaign_identifier: z.string().optional(),
|
campaign_identifier: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
currency: z.string().optional(),
|
|
||||||
budget: UpdateCampaignBudget.optional(),
|
budget: UpdateCampaignBudget.optional(),
|
||||||
starts_at: z.coerce.date().optional(),
|
starts_at: z.coerce.date().optional(),
|
||||||
ends_at: z.coerce.date().optional(),
|
ends_at: z.coerce.date().optional(),
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const GET = async (
|
|||||||
attribute: disguisedRule.id,
|
attribute: disguisedRule.id,
|
||||||
attribute_label: disguisedRule.label,
|
attribute_label: disguisedRule.label,
|
||||||
field_type: disguisedRule.field_type,
|
field_type: disguisedRule.field_type,
|
||||||
|
hydrate: disguisedRule.hydrate || false,
|
||||||
operator: RuleOperator.EQ,
|
operator: RuleOperator.EQ,
|
||||||
operator_label: operatorsMap[RuleOperator.EQ].label,
|
operator_label: operatorsMap[RuleOperator.EQ].label,
|
||||||
values,
|
values,
|
||||||
@@ -67,9 +68,11 @@ export const GET = async (
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const promotionRule of promotionRules) {
|
for (const promotionRule of [...promotionRules, ...transformedRules]) {
|
||||||
const currentRuleAttribute = ruleAttributes.find(
|
const currentRuleAttribute = ruleAttributes.find(
|
||||||
(attr) => attr.value === promotionRule.attribute
|
(attr) =>
|
||||||
|
attr.value === promotionRule.attribute ||
|
||||||
|
attr.value === promotionRule.attribute
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!currentRuleAttribute) {
|
if (!currentRuleAttribute) {
|
||||||
@@ -77,6 +80,11 @@ export const GET = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const queryConfig = ruleQueryConfigurations[currentRuleAttribute.id]
|
const queryConfig = ruleQueryConfigurations[currentRuleAttribute.id]
|
||||||
|
|
||||||
|
if (!queryConfig) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const rows = await remoteQuery(
|
const rows = await remoteQuery(
|
||||||
remoteQueryObjectFromString({
|
remoteQueryObjectFromString({
|
||||||
entryPoint: queryConfig.entryPoint,
|
entryPoint: queryConfig.entryPoint,
|
||||||
@@ -101,15 +109,17 @@ export const GET = async (
|
|||||||
label: valueLabelMap.get(value.value) || value.value,
|
label: valueLabelMap.get(value.value) || value.value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
transformedRules.push({
|
if (!currentRuleAttribute.hydrate) {
|
||||||
...promotionRule,
|
transformedRules.push({
|
||||||
attribute_label: currentRuleAttribute.label,
|
...promotionRule,
|
||||||
field_type: currentRuleAttribute.field_type,
|
attribute_label: currentRuleAttribute.label,
|
||||||
operator_label:
|
field_type: currentRuleAttribute.field_type,
|
||||||
operatorsMap[promotionRule.operator]?.label || promotionRule.operator,
|
operator_label:
|
||||||
disguised: false,
|
operatorsMap[promotionRule.operator]?.label || promotionRule.operator,
|
||||||
required: currentRuleAttribute.required || false,
|
disguised: false,
|
||||||
})
|
required: currentRuleAttribute.required || false,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiredRules.length && !transformedRules.length) {
|
if (requiredRules.length && !transformedRules.length) {
|
||||||
@@ -124,6 +134,7 @@ export const GET = async (
|
|||||||
values: [],
|
values: [],
|
||||||
disguised: true,
|
disguised: true,
|
||||||
required: true,
|
required: true,
|
||||||
|
hydrate: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|||||||
+8
-1
@@ -19,6 +19,13 @@ export const GET = async (
|
|||||||
const { rule_type: ruleType, rule_attribute_id: ruleAttributeId } = req.params
|
const { rule_type: ruleType, rule_attribute_id: ruleAttributeId } = req.params
|
||||||
const queryConfig = ruleQueryConfigurations[ruleAttributeId]
|
const queryConfig = ruleQueryConfigurations[ruleAttributeId]
|
||||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
const filterableFields = req.filterableFields
|
||||||
|
|
||||||
|
if (filterableFields.value) {
|
||||||
|
filterableFields[queryConfig.valueAttr] = filterableFields.value
|
||||||
|
|
||||||
|
delete filterableFields.value
|
||||||
|
}
|
||||||
|
|
||||||
validateRuleType(ruleType)
|
validateRuleType(ruleType)
|
||||||
validateRuleAttribute(ruleType, ruleAttributeId)
|
validateRuleAttribute(ruleType, ruleAttributeId)
|
||||||
@@ -27,7 +34,7 @@ export const GET = async (
|
|||||||
remoteQueryObjectFromString({
|
remoteQueryObjectFromString({
|
||||||
entryPoint: queryConfig.entryPoint,
|
entryPoint: queryConfig.entryPoint,
|
||||||
variables: {
|
variables: {
|
||||||
filters: req.filterableFields,
|
filters: filterableFields,
|
||||||
...req.remoteQueryConfig.pagination,
|
...req.remoteQueryConfig.pagination,
|
||||||
},
|
},
|
||||||
fields: [queryConfig.labelAttr, queryConfig.valueAttr],
|
fields: [queryConfig.labelAttr, queryConfig.valueAttr],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum DisguisedRule {
|
export enum DisguisedRule {
|
||||||
APPLY_TO_QUANTITY = "apply_to_quantity",
|
APPLY_TO_QUANTITY = "apply_to_quantity",
|
||||||
BUY_RULES_MIN_QUANTITY = "buy_rules_min_quantity",
|
BUY_RULES_MIN_QUANTITY = "buy_rules_min_quantity",
|
||||||
|
CURRENCY_CODE = "currency_code",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const disguisedRulesMap = {
|
export const disguisedRulesMap = {
|
||||||
@@ -10,38 +11,48 @@ export const disguisedRulesMap = {
|
|||||||
[DisguisedRule.BUY_RULES_MIN_QUANTITY]: {
|
[DisguisedRule.BUY_RULES_MIN_QUANTITY]: {
|
||||||
relation: "application_method",
|
relation: "application_method",
|
||||||
},
|
},
|
||||||
|
[DisguisedRule.CURRENCY_CODE]: {
|
||||||
|
relation: "application_method",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const ruleAttributes = [
|
const ruleAttributes = [
|
||||||
{
|
{
|
||||||
id: "currency",
|
id: DisguisedRule.CURRENCY_CODE,
|
||||||
value: "currency_code",
|
value: DisguisedRule.CURRENCY_CODE,
|
||||||
label: "Currency code",
|
label: "Currency Code",
|
||||||
|
field_type: "select",
|
||||||
required: true,
|
required: true,
|
||||||
|
disguised: true,
|
||||||
|
hydrate: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "customer_group",
|
id: "customer_group",
|
||||||
value: "customer_group.id",
|
value: "customer.groups.id",
|
||||||
label: "Customer Group",
|
label: "Customer Group",
|
||||||
required: false,
|
required: false,
|
||||||
|
field_type: "multiselect",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "region",
|
id: "region",
|
||||||
value: "region.id",
|
value: "region.id",
|
||||||
label: "Region",
|
label: "Region",
|
||||||
required: false,
|
required: false,
|
||||||
|
field_type: "multiselect",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "country",
|
id: "country",
|
||||||
value: "shipping_address.country_code",
|
value: "shipping_address.country_code",
|
||||||
label: "Country",
|
label: "Country",
|
||||||
required: false,
|
required: false,
|
||||||
|
field_type: "multiselect",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "sales_channel",
|
id: "sales_channel",
|
||||||
value: "sales_channel.id",
|
value: "sales_channel_id",
|
||||||
label: "Sales Channel",
|
label: "Sales Channel",
|
||||||
required: false,
|
required: false,
|
||||||
|
field_type: "multiselect",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -51,30 +62,35 @@ const commonAttributes = [
|
|||||||
value: "items.product.id",
|
value: "items.product.id",
|
||||||
label: "Product",
|
label: "Product",
|
||||||
required: false,
|
required: false,
|
||||||
|
field_type: "multiselect",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "product_category",
|
id: "product_category",
|
||||||
value: "items.product.categories.id",
|
value: "items.product.categories.id",
|
||||||
label: "Product Category",
|
label: "Product Category",
|
||||||
required: false,
|
required: false,
|
||||||
|
field_type: "multiselect",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "product_collection",
|
id: "product_collection",
|
||||||
value: "items.product.collection_id",
|
value: "items.product.collection_id",
|
||||||
label: "Product Collection",
|
label: "Product Collection",
|
||||||
required: false,
|
required: false,
|
||||||
|
field_type: "multiselect",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "product_type",
|
id: "product_type",
|
||||||
value: "items.product.type_id",
|
value: "items.product.type_id",
|
||||||
label: "Product Type",
|
label: "Product Type",
|
||||||
required: false,
|
required: false,
|
||||||
|
field_type: "multiselect",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "product_tag",
|
id: "product_tag",
|
||||||
value: "items.product.tags.id",
|
value: "items.product.tags.id",
|
||||||
label: "Product Tag",
|
label: "Product Tag",
|
||||||
required: false,
|
required: false,
|
||||||
|
field_type: "multiselect",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ export const ruleQueryConfigurations = {
|
|||||||
labelAttr: "name",
|
labelAttr: "name",
|
||||||
valueAttr: "id",
|
valueAttr: "id",
|
||||||
},
|
},
|
||||||
currency: {
|
currency_code: {
|
||||||
entryPoint: "currency",
|
entryPoint: "currency",
|
||||||
labelAttr: "code",
|
labelAttr: "name",
|
||||||
valueAttr: "code",
|
valueAttr: "code",
|
||||||
},
|
},
|
||||||
customer_group: {
|
customer_group: {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
ApplicationMethodAllocation,
|
ApplicationMethodAllocation,
|
||||||
ApplicationMethodTargetType,
|
ApplicationMethodTargetType,
|
||||||
ApplicationMethodType,
|
ApplicationMethodType,
|
||||||
CampaignBudgetType,
|
|
||||||
PromotionRuleOperator,
|
PromotionRuleOperator,
|
||||||
PromotionType,
|
PromotionType,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
createOperatorMap,
|
createOperatorMap,
|
||||||
createSelectParams,
|
createSelectParams,
|
||||||
} from "../../utils/validators"
|
} from "../../utils/validators"
|
||||||
|
import { AdminCreateCampaign } from "../campaigns/validators"
|
||||||
|
|
||||||
export type AdminGetPromotionParamsType = z.infer<
|
export type AdminGetPromotionParamsType = z.infer<
|
||||||
typeof AdminGetPromotionParams
|
typeof AdminGetPromotionParams
|
||||||
@@ -30,6 +30,11 @@ export const AdminGetPromotionsParams = createFindParams({
|
|||||||
q: z.string().optional(),
|
q: z.string().optional(),
|
||||||
code: z.union([z.string(), z.array(z.string())]).optional(),
|
code: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
campaign_id: z.union([z.string(), z.array(z.string())]).optional(),
|
campaign_id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
application_method: z
|
||||||
|
.object({
|
||||||
|
currency_code: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
created_at: createOperatorMap().optional(),
|
created_at: createOperatorMap().optional(),
|
||||||
updated_at: createOperatorMap().optional(),
|
updated_at: createOperatorMap().optional(),
|
||||||
deleted_at: createOperatorMap().optional(),
|
deleted_at: createOperatorMap().optional(),
|
||||||
@@ -58,6 +63,7 @@ export const AdminGetPromotionsRuleValueParams = createFindParams({
|
|||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
q: z.string().optional(),
|
q: z.string().optional(),
|
||||||
|
value: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +75,7 @@ export const AdminCreatePromotionRule = z
|
|||||||
operator: z.nativeEnum(PromotionRuleOperator),
|
operator: z.nativeEnum(PromotionRuleOperator),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
attribute: z.string(),
|
attribute: z.string(),
|
||||||
values: z.array(z.string()),
|
values: z.union([z.string(), z.array(z.string())]),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
@@ -82,7 +88,7 @@ export const AdminUpdatePromotionRule = z
|
|||||||
operator: z.nativeEnum(PromotionRuleOperator).optional(),
|
operator: z.nativeEnum(PromotionRuleOperator).optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
attribute: z.string().optional(),
|
attribute: z.string().optional(),
|
||||||
values: z.array(z.string()).optional(),
|
values: z.union([z.string(), z.array(z.string())]),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
@@ -93,12 +99,12 @@ export const AdminCreateApplicationMethod = z
|
|||||||
.object({
|
.object({
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
value: z.number(),
|
value: z.number(),
|
||||||
max_quantity: z.number().optional(),
|
currency_code: z.string(),
|
||||||
|
max_quantity: z.number().optional().nullable(),
|
||||||
type: z.nativeEnum(ApplicationMethodType),
|
type: z.nativeEnum(ApplicationMethodType),
|
||||||
target_type: z.nativeEnum(ApplicationMethodTargetType),
|
target_type: z.nativeEnum(ApplicationMethodTargetType),
|
||||||
allocation: z.nativeEnum(ApplicationMethodAllocation).optional(),
|
allocation: z.nativeEnum(ApplicationMethodAllocation).optional(),
|
||||||
target_rules: z.array(AdminCreatePromotionRule).optional(),
|
target_rules: z.array(AdminCreatePromotionRule).optional(),
|
||||||
|
|
||||||
buy_rules: z.array(AdminCreatePromotionRule).optional(),
|
buy_rules: z.array(AdminCreatePromotionRule).optional(),
|
||||||
apply_to_quantity: z.number().optional(),
|
apply_to_quantity: z.number().optional(),
|
||||||
buy_rules_min_quantity: z.number().optional(),
|
buy_rules_min_quantity: z.number().optional(),
|
||||||
@@ -111,8 +117,9 @@ export type AdminUpdateApplicationMethodType = z.infer<
|
|||||||
export const AdminUpdateApplicationMethod = z
|
export const AdminUpdateApplicationMethod = z
|
||||||
.object({
|
.object({
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
value: z.string().optional(),
|
value: z.number().optional(),
|
||||||
max_quantity: z.number().optional(),
|
max_quantity: z.number().optional().nullable(),
|
||||||
|
currency_code: z.string().optional(),
|
||||||
type: z.nativeEnum(ApplicationMethodType).optional(),
|
type: z.nativeEnum(ApplicationMethodType).optional(),
|
||||||
target_type: z.nativeEnum(ApplicationMethodTargetType).optional(),
|
target_type: z.nativeEnum(ApplicationMethodTargetType).optional(),
|
||||||
allocation: z.nativeEnum(ApplicationMethodAllocation).optional(),
|
allocation: z.nativeEnum(ApplicationMethodAllocation).optional(),
|
||||||
@@ -140,23 +147,6 @@ const promoRefinement = (promo) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ideally we don't allow for creation of campaigns through promotions, it should be the other way around.
|
|
||||||
const CreateCampaignBudget = z.object({
|
|
||||||
type: z.nativeEnum(CampaignBudgetType),
|
|
||||||
limit: z.number(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type AdminCreateCampaignType = z.infer<typeof AdminCreateCampaign>
|
|
||||||
export const AdminCreateCampaign = z.object({
|
|
||||||
name: z.string(),
|
|
||||||
campaign_identifier: z.string(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
currency: z.string().optional(),
|
|
||||||
budget: CreateCampaignBudget.optional(),
|
|
||||||
starts_at: z.coerce.date().optional(),
|
|
||||||
ends_at: z.coerce.date().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type AdminCreatePromotionType = z.infer<typeof AdminCreatePromotion>
|
export type AdminCreatePromotionType = z.infer<typeof AdminCreatePromotion>
|
||||||
export const AdminCreatePromotion = z
|
export const AdminCreatePromotion = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ export const defaultCampaignsData = [
|
|||||||
id: "campaign-id-1",
|
id: "campaign-id-1",
|
||||||
name: "campaign 1",
|
name: "campaign 1",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-1",
|
campaign_identifier: "test-1",
|
||||||
starts_at: new Date("01/01/2023"),
|
starts_at: new Date("01/01/2023"),
|
||||||
ends_at: new Date("01/01/2024"),
|
ends_at: new Date("01/01/2024"),
|
||||||
budget: {
|
budget: {
|
||||||
type: CampaignBudgetType.SPEND,
|
type: CampaignBudgetType.SPEND,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
|
currency_code: "USD",
|
||||||
used: 0,
|
used: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -19,13 +19,13 @@ export const defaultCampaignsData = [
|
|||||||
id: "campaign-id-2",
|
id: "campaign-id-2",
|
||||||
name: "campaign 1",
|
name: "campaign 1",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-2",
|
campaign_identifier: "test-2",
|
||||||
starts_at: new Date("01/01/2023"),
|
starts_at: new Date("01/01/2023"),
|
||||||
ends_at: new Date("01/01/2024"),
|
ends_at: new Date("01/01/2024"),
|
||||||
budget: {
|
budget: {
|
||||||
type: CampaignBudgetType.USAGE,
|
type: CampaignBudgetType.USAGE,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
|
currency_code: "USD",
|
||||||
used: 0,
|
used: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
|
import { CreatePromotionDTO } from "@medusajs/types"
|
||||||
import { PromotionType } from "@medusajs/utils"
|
import { PromotionType } from "@medusajs/utils"
|
||||||
|
|
||||||
export const defaultPromotionsData = [
|
export const defaultPromotionsData: CreatePromotionDTO[] = [
|
||||||
{
|
{
|
||||||
id: "promotion-id-1",
|
id: "promotion-id-1",
|
||||||
code: "PROMOTION_1",
|
code: "PROMOTION_1",
|
||||||
type: PromotionType.STANDARD,
|
type: PromotionType.STANDARD,
|
||||||
|
application_method: {
|
||||||
|
currency_code: "USD",
|
||||||
|
target_type: "items",
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "across",
|
||||||
|
value: 1000,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "promotion-id-2",
|
id: "promotion-id-2",
|
||||||
code: "PROMOTION_2",
|
code: "PROMOTION_2",
|
||||||
type: PromotionType.STANDARD,
|
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 { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
import { Promotion } from "@models"
|
import { Promotion } from "@models"
|
||||||
import { defaultPromotionsData } from "./data"
|
import { defaultPromotionsData } from "./data"
|
||||||
@@ -21,3 +26,47 @@ export async function createPromotions(
|
|||||||
|
|
||||||
return promotions
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
+54
-4
@@ -1,6 +1,7 @@
|
|||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { IPromotionModuleService } from "@medusajs/types"
|
import { IPromotionModuleService } from "@medusajs/types"
|
||||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||||
|
import { CampaignBudgetType } from "../../../../../../core/utils/src/promotion/index"
|
||||||
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||||
import { createPromotions } from "../../../__fixtures__/promotion"
|
import { createPromotions } from "../../../__fixtures__/promotion"
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ moduleIntegrationTestRunner({
|
|||||||
id: "campaign-id-1",
|
id: "campaign-id-1",
|
||||||
name: "campaign 1",
|
name: "campaign 1",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-1",
|
campaign_identifier: "test-1",
|
||||||
starts_at: expect.any(Date),
|
starts_at: expect.any(Date),
|
||||||
ends_at: expect.any(Date),
|
ends_at: expect.any(Date),
|
||||||
@@ -40,7 +41,7 @@ moduleIntegrationTestRunner({
|
|||||||
id: "campaign-id-2",
|
id: "campaign-id-2",
|
||||||
name: "campaign 1",
|
name: "campaign 1",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
currency: "USD",
|
|
||||||
campaign_identifier: "test-2",
|
campaign_identifier: "test-2",
|
||||||
starts_at: expect.any(Date),
|
starts_at: expect.any(Date),
|
||||||
ends_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 () => {
|
it("should create a basic campaign successfully", async () => {
|
||||||
const startsAt = new Date("01/01/2024")
|
const startsAt = new Date("01/01/2024")
|
||||||
const endsAt = new Date("01/01/2025")
|
const endsAt = new Date("01/01/2025")
|
||||||
@@ -174,7 +195,6 @@ moduleIntegrationTestRunner({
|
|||||||
{
|
{
|
||||||
id: "campaign-id-1",
|
id: "campaign-id-1",
|
||||||
description: "test description 1",
|
description: "test description 1",
|
||||||
currency: "EUR",
|
|
||||||
campaign_identifier: "new",
|
campaign_identifier: "new",
|
||||||
starts_at: new Date("01/01/2024"),
|
starts_at: new Date("01/01/2024"),
|
||||||
ends_at: new Date("01/01/2025"),
|
ends_at: new Date("01/01/2025"),
|
||||||
@@ -184,7 +204,6 @@ moduleIntegrationTestRunner({
|
|||||||
expect(updatedCampaign).toEqual(
|
expect(updatedCampaign).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
description: "test description 1",
|
description: "test description 1",
|
||||||
currency: "EUR",
|
|
||||||
campaign_identifier: "new",
|
campaign_identifier: "new",
|
||||||
starts_at: new Date("01/01/2024"),
|
starts_at: new Date("01/01/2024"),
|
||||||
ends_at: new Date("01/01/2025"),
|
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", () => {
|
describe("retrieveCampaign", () => {
|
||||||
|
|||||||
+1524
-1849
File diff suppressed because it is too large.
Load diff
+291
-496
File diff suppressed because it is too large.
Load diff
+5
-16
@@ -2,6 +2,7 @@ import { Modules } from "@medusajs/modules-sdk"
|
|||||||
import { IPromotionModuleService } from "@medusajs/types"
|
import { IPromotionModuleService } from "@medusajs/types"
|
||||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||||
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||||
|
import { createDefaultPromotion } from "../../../__fixtures__/promotion"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -18,11 +19,7 @@ moduleIntegrationTestRunner({
|
|||||||
|
|
||||||
describe("registerUsage", () => {
|
describe("registerUsage", () => {
|
||||||
it("should register usage for type spend", async () => {
|
it("should register usage for type spend", async () => {
|
||||||
const createdPromotion = await service.create({
|
const createdPromotion = await createDefaultPromotion(service, {})
|
||||||
code: "TEST_PROMO_SPEND",
|
|
||||||
type: "standard",
|
|
||||||
campaign_id: "campaign-id-1",
|
|
||||||
})
|
|
||||||
|
|
||||||
await service.registerUsage([
|
await service.registerUsage([
|
||||||
{
|
{
|
||||||
@@ -53,9 +50,7 @@ moduleIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should register usage for type usage", async () => {
|
it("should register usage for type usage", async () => {
|
||||||
const createdPromotion = await service.create({
|
const createdPromotion = await createDefaultPromotion(service, {
|
||||||
code: "TEST_PROMO_USAGE",
|
|
||||||
type: "standard",
|
|
||||||
campaign_id: "campaign-id-2",
|
campaign_id: "campaign-id-2",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -103,9 +98,7 @@ moduleIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should not register usage when limit is exceed for type usage", async () => {
|
it("should not register usage when limit is exceed for type usage", async () => {
|
||||||
const createdPromotion = await service.create({
|
const createdPromotion = await createDefaultPromotion(service, {
|
||||||
code: "TEST_PROMO_USAGE",
|
|
||||||
type: "standard",
|
|
||||||
campaign_id: "campaign-id-2",
|
campaign_id: "campaign-id-2",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -144,11 +137,7 @@ moduleIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should not register usage above limit when exceeded for type spend", async () => {
|
it("should not register usage above limit when exceeded for type spend", async () => {
|
||||||
const createdPromotion = await service.create({
|
const createdPromotion = await createDefaultPromotion(service, {})
|
||||||
code: "TEST_PROMO_SPEND",
|
|
||||||
type: "standard",
|
|
||||||
campaign_id: "campaign-id-1",
|
|
||||||
})
|
|
||||||
|
|
||||||
await service.updateCampaigns({
|
await service.updateCampaigns({
|
||||||
id: "campaign-id-1",
|
id: "campaign-id-1",
|
||||||
|
|||||||
-58
@@ -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",
|
"name": "public",
|
||||||
"tables": [
|
"tables": [
|
||||||
{
|
{
|
||||||
@@ -31,15 +33,6 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"mappedType": "text"
|
"mappedType": "text"
|
||||||
},
|
},
|
||||||
"currency": {
|
|
||||||
"name": "currency",
|
|
||||||
"type": "text",
|
|
||||||
"unsigned": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"primary": false,
|
|
||||||
"nullable": true,
|
|
||||||
"mappedType": "text"
|
|
||||||
},
|
|
||||||
"campaign_identifier": {
|
"campaign_identifier": {
|
||||||
"name": "campaign_identifier",
|
"name": "campaign_identifier",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -107,14 +100,18 @@
|
|||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"keyName": "IDX_campaign_identifier_unique",
|
"keyName": "IDX_campaign_identifier_unique",
|
||||||
"columnNames": ["campaign_identifier"],
|
"columnNames": [
|
||||||
|
"campaign_identifier"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": true
|
"unique": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keyName": "promotion_campaign_pkey",
|
"keyName": "promotion_campaign_pkey",
|
||||||
"columnNames": ["id"],
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
@@ -141,7 +138,10 @@
|
|||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"enumItems": ["spend", "usage"],
|
"enumItems": [
|
||||||
|
"spend",
|
||||||
|
"usage"
|
||||||
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
"campaign_id": {
|
"campaign_id": {
|
||||||
@@ -153,6 +153,15 @@
|
|||||||
"nullable": false,
|
"nullable": false,
|
||||||
"mappedType": "text"
|
"mappedType": "text"
|
||||||
},
|
},
|
||||||
|
"currency_code": {
|
||||||
|
"name": "currency_code",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "limit",
|
"name": "limit",
|
||||||
"type": "numeric",
|
"type": "numeric",
|
||||||
@@ -177,7 +186,8 @@
|
|||||||
"unsigned": false,
|
"unsigned": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": true,
|
"nullable": false,
|
||||||
|
"default": "0",
|
||||||
"mappedType": "decimal"
|
"mappedType": "decimal"
|
||||||
},
|
},
|
||||||
"raw_used": {
|
"raw_used": {
|
||||||
@@ -186,7 +196,7 @@
|
|||||||
"unsigned": false,
|
"unsigned": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": true,
|
"nullable": false,
|
||||||
"mappedType": "json"
|
"mappedType": "json"
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
@@ -226,14 +236,18 @@
|
|||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"columnNames": ["type"],
|
"columnNames": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "IDX_campaign_budget_type",
|
"keyName": "IDX_campaign_budget_type",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false
|
"unique": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columnNames": ["campaign_id"],
|
"columnNames": [
|
||||||
|
"campaign_id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "promotion_campaign_budget_campaign_id_unique",
|
"keyName": "promotion_campaign_budget_campaign_id_unique",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
@@ -241,7 +255,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keyName": "promotion_campaign_budget_pkey",
|
"keyName": "promotion_campaign_budget_pkey",
|
||||||
"columnNames": ["id"],
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
@@ -251,9 +267,13 @@
|
|||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"promotion_campaign_budget_campaign_id_foreign": {
|
"promotion_campaign_budget_campaign_id_foreign": {
|
||||||
"constraintName": "promotion_campaign_budget_campaign_id_foreign",
|
"constraintName": "promotion_campaign_budget_campaign_id_foreign",
|
||||||
"columnNames": ["campaign_id"],
|
"columnNames": [
|
||||||
|
"campaign_id"
|
||||||
|
],
|
||||||
"localTableName": "public.promotion_campaign_budget",
|
"localTableName": "public.promotion_campaign_budget",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion_campaign",
|
"referencedTableName": "public.promotion_campaign",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
}
|
}
|
||||||
@@ -305,7 +325,10 @@
|
|||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"enumItems": ["standard", "buyget"],
|
"enumItems": [
|
||||||
|
"standard",
|
||||||
|
"buyget"
|
||||||
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
@@ -345,14 +368,18 @@
|
|||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"columnNames": ["code"],
|
"columnNames": [
|
||||||
|
"code"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "IDX_promotion_code",
|
"keyName": "IDX_promotion_code",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false
|
"unique": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columnNames": ["type"],
|
"columnNames": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "IDX_promotion_type",
|
"keyName": "IDX_promotion_type",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
@@ -360,14 +387,18 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keyName": "IDX_promotion_code_unique",
|
"keyName": "IDX_promotion_code_unique",
|
||||||
"columnNames": ["code"],
|
"columnNames": [
|
||||||
|
"code"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": true
|
"unique": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keyName": "promotion_pkey",
|
"keyName": "promotion_pkey",
|
||||||
"columnNames": ["id"],
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
@@ -377,11 +408,16 @@
|
|||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"promotion_campaign_id_foreign": {
|
"promotion_campaign_id_foreign": {
|
||||||
"constraintName": "promotion_campaign_id_foreign",
|
"constraintName": "promotion_campaign_id_foreign",
|
||||||
"columnNames": ["campaign_id"],
|
"columnNames": [
|
||||||
|
"campaign_id"
|
||||||
|
],
|
||||||
"localTableName": "public.promotion",
|
"localTableName": "public.promotion",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion_campaign",
|
"referencedTableName": "public.promotion_campaign",
|
||||||
"deleteRule": "set null"
|
"deleteRule": "set null",
|
||||||
|
"updateRule": "cascade"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -402,7 +438,7 @@
|
|||||||
"unsigned": false,
|
"unsigned": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": true,
|
"nullable": false,
|
||||||
"mappedType": "decimal"
|
"mappedType": "decimal"
|
||||||
},
|
},
|
||||||
"raw_value": {
|
"raw_value": {
|
||||||
@@ -411,9 +447,18 @@
|
|||||||
"unsigned": false,
|
"unsigned": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": true,
|
"nullable": false,
|
||||||
"mappedType": "json"
|
"mappedType": "json"
|
||||||
},
|
},
|
||||||
|
"currency_code": {
|
||||||
|
"name": "currency_code",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
"max_quantity": {
|
"max_quantity": {
|
||||||
"name": "max_quantity",
|
"name": "max_quantity",
|
||||||
"type": "numeric",
|
"type": "numeric",
|
||||||
@@ -448,7 +493,10 @@
|
|||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"enumItems": ["fixed", "percentage"],
|
"enumItems": [
|
||||||
|
"fixed",
|
||||||
|
"percentage"
|
||||||
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
"target_type": {
|
"target_type": {
|
||||||
@@ -458,7 +506,11 @@
|
|||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"enumItems": ["order", "shipping_methods", "items"],
|
"enumItems": [
|
||||||
|
"order",
|
||||||
|
"shipping_methods",
|
||||||
|
"items"
|
||||||
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
"allocation": {
|
"allocation": {
|
||||||
@@ -468,7 +520,10 @@
|
|||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"enumItems": ["each", "across"],
|
"enumItems": [
|
||||||
|
"each",
|
||||||
|
"across"
|
||||||
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
"promotion_id": {
|
"promotion_id": {
|
||||||
@@ -517,36 +572,56 @@
|
|||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"columnNames": ["type"],
|
"columnNames": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "IDX_application_method_type",
|
"keyName": "IDX_application_method_type",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false
|
"unique": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columnNames": ["target_type"],
|
"columnNames": [
|
||||||
|
"target_type"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "IDX_application_method_target_type",
|
"keyName": "IDX_application_method_target_type",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false
|
"unique": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columnNames": ["allocation"],
|
"columnNames": [
|
||||||
|
"allocation"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "IDX_application_method_allocation",
|
"keyName": "IDX_application_method_allocation",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false
|
"unique": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columnNames": ["promotion_id"],
|
"columnNames": [
|
||||||
|
"promotion_id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "promotion_application_method_promotion_id_unique",
|
"keyName": "promotion_application_method_promotion_id_unique",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": true
|
"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",
|
"keyName": "promotion_application_method_pkey",
|
||||||
"columnNames": ["id"],
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
@@ -556,9 +631,13 @@
|
|||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"promotion_application_method_promotion_id_foreign": {
|
"promotion_application_method_promotion_id_foreign": {
|
||||||
"constraintName": "promotion_application_method_promotion_id_foreign",
|
"constraintName": "promotion_application_method_promotion_id_foreign",
|
||||||
"columnNames": ["promotion_id"],
|
"columnNames": [
|
||||||
|
"promotion_id"
|
||||||
|
],
|
||||||
"localTableName": "public.promotion_application_method",
|
"localTableName": "public.promotion_application_method",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion",
|
"referencedTableName": "public.promotion",
|
||||||
"deleteRule": "cascade",
|
"deleteRule": "cascade",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
@@ -601,7 +680,15 @@
|
|||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"enumItems": ["gte", "lte", "gt", "lt", "eq", "ne", "in"],
|
"enumItems": [
|
||||||
|
"gte",
|
||||||
|
"lte",
|
||||||
|
"gt",
|
||||||
|
"lt",
|
||||||
|
"eq",
|
||||||
|
"ne",
|
||||||
|
"in"
|
||||||
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
@@ -641,14 +728,18 @@
|
|||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"columnNames": ["attribute"],
|
"columnNames": [
|
||||||
|
"attribute"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "IDX_promotion_rule_attribute",
|
"keyName": "IDX_promotion_rule_attribute",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false
|
"unique": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columnNames": ["operator"],
|
"columnNames": [
|
||||||
|
"operator"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "IDX_promotion_rule_operator",
|
"keyName": "IDX_promotion_rule_operator",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
@@ -656,7 +747,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keyName": "promotion_rule_pkey",
|
"keyName": "promotion_rule_pkey",
|
||||||
"columnNames": ["id"],
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
@@ -691,7 +784,10 @@
|
|||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"keyName": "promotion_promotion_rule_pkey",
|
"keyName": "promotion_promotion_rule_pkey",
|
||||||
"columnNames": ["promotion_id", "promotion_rule_id"],
|
"columnNames": [
|
||||||
|
"promotion_id",
|
||||||
|
"promotion_rule_id"
|
||||||
|
],
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
@@ -701,18 +797,26 @@
|
|||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"promotion_promotion_rule_promotion_id_foreign": {
|
"promotion_promotion_rule_promotion_id_foreign": {
|
||||||
"constraintName": "promotion_promotion_rule_promotion_id_foreign",
|
"constraintName": "promotion_promotion_rule_promotion_id_foreign",
|
||||||
"columnNames": ["promotion_id"],
|
"columnNames": [
|
||||||
|
"promotion_id"
|
||||||
|
],
|
||||||
"localTableName": "public.promotion_promotion_rule",
|
"localTableName": "public.promotion_promotion_rule",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion",
|
"referencedTableName": "public.promotion",
|
||||||
"deleteRule": "cascade",
|
"deleteRule": "cascade",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
},
|
},
|
||||||
"promotion_promotion_rule_promotion_rule_id_foreign": {
|
"promotion_promotion_rule_promotion_rule_id_foreign": {
|
||||||
"constraintName": "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",
|
"localTableName": "public.promotion_promotion_rule",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion_rule",
|
"referencedTableName": "public.promotion_rule",
|
||||||
"deleteRule": "cascade",
|
"deleteRule": "cascade",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
@@ -745,7 +849,10 @@
|
|||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"keyName": "application_method_target_rules_pkey",
|
"keyName": "application_method_target_rules_pkey",
|
||||||
"columnNames": ["application_method_id", "promotion_rule_id"],
|
"columnNames": [
|
||||||
|
"application_method_id",
|
||||||
|
"promotion_rule_id"
|
||||||
|
],
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
@@ -755,18 +862,26 @@
|
|||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"application_method_target_rules_application_method_id_foreign": {
|
"application_method_target_rules_application_method_id_foreign": {
|
||||||
"constraintName": "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",
|
"localTableName": "public.application_method_target_rules",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion_application_method",
|
"referencedTableName": "public.promotion_application_method",
|
||||||
"deleteRule": "cascade",
|
"deleteRule": "cascade",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
},
|
},
|
||||||
"application_method_target_rules_promotion_rule_id_foreign": {
|
"application_method_target_rules_promotion_rule_id_foreign": {
|
||||||
"constraintName": "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",
|
"localTableName": "public.application_method_target_rules",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion_rule",
|
"referencedTableName": "public.promotion_rule",
|
||||||
"deleteRule": "cascade",
|
"deleteRule": "cascade",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
@@ -799,7 +914,10 @@
|
|||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"keyName": "application_method_buy_rules_pkey",
|
"keyName": "application_method_buy_rules_pkey",
|
||||||
"columnNames": ["application_method_id", "promotion_rule_id"],
|
"columnNames": [
|
||||||
|
"application_method_id",
|
||||||
|
"promotion_rule_id"
|
||||||
|
],
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
@@ -809,18 +927,26 @@
|
|||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"application_method_buy_rules_application_method_id_foreign": {
|
"application_method_buy_rules_application_method_id_foreign": {
|
||||||
"constraintName": "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",
|
"localTableName": "public.application_method_buy_rules",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion_application_method",
|
"referencedTableName": "public.promotion_application_method",
|
||||||
"deleteRule": "cascade",
|
"deleteRule": "cascade",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
},
|
},
|
||||||
"application_method_buy_rules_promotion_rule_id_foreign": {
|
"application_method_buy_rules_promotion_rule_id_foreign": {
|
||||||
"constraintName": "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",
|
"localTableName": "public.application_method_buy_rules",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion_rule",
|
"referencedTableName": "public.promotion_rule",
|
||||||
"deleteRule": "cascade",
|
"deleteRule": "cascade",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
@@ -893,7 +1019,9 @@
|
|||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"columnNames": ["promotion_rule_id"],
|
"columnNames": [
|
||||||
|
"promotion_rule_id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"keyName": "IDX_promotion_rule_promotion_rule_value_id",
|
"keyName": "IDX_promotion_rule_promotion_rule_value_id",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
@@ -901,7 +1029,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keyName": "promotion_rule_value_pkey",
|
"keyName": "promotion_rule_value_pkey",
|
||||||
"columnNames": ["id"],
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
@@ -911,9 +1041,13 @@
|
|||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"promotion_rule_value_promotion_rule_id_foreign": {
|
"promotion_rule_value_promotion_rule_id_foreign": {
|
||||||
"constraintName": "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",
|
"localTableName": "public.promotion_rule_value",
|
||||||
"referencedColumnNames": ["id"],
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"referencedTableName": "public.promotion_rule",
|
"referencedTableName": "public.promotion_rule",
|
||||||
"deleteRule": "cascade",
|
"deleteRule": "cascade",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { Migration } from "@mikro-orm/migrations"
|
|||||||
export class Migration20240227120221 extends Migration {
|
export class Migration20240227120221 extends Migration {
|
||||||
async up(): Promise<void> {
|
async up(): Promise<void> {
|
||||||
this.addSql(
|
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(
|
this.addSql(
|
||||||
'alter table if exists "promotion_campaign" add constraint "IDX_campaign_identifier_unique" unique ("campaign_identifier");'
|
'alter table if exists "promotion_campaign" add constraint "IDX_campaign_identifier_unique" unique ("campaign_identifier");'
|
||||||
)
|
)
|
||||||
|
|
||||||
this.addSql(
|
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(
|
this.addSql(
|
||||||
'create index if not exists "IDX_campaign_budget_type" on "promotion_campaign_budget" ("type");'
|
'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(
|
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;'
|
'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,
|
ApplicationMethodTargetTypeValues,
|
||||||
ApplicationMethodTypeValues,
|
ApplicationMethodTypeValues,
|
||||||
BigNumberRawValue,
|
BigNumberRawValue,
|
||||||
DAL,
|
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
BigNumber,
|
BigNumber,
|
||||||
DALUtils,
|
DALUtils,
|
||||||
MikroOrmBigNumberProperty,
|
MikroOrmBigNumberProperty,
|
||||||
PromotionUtils,
|
PromotionUtils,
|
||||||
|
createPsqlIndexStatementHelper,
|
||||||
generateEntityId,
|
generateEntityId,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
@@ -22,34 +22,34 @@ import {
|
|||||||
ManyToMany,
|
ManyToMany,
|
||||||
OnInit,
|
OnInit,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
OptionalProps,
|
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
import Promotion from "./promotion"
|
import Promotion from "./promotion"
|
||||||
import PromotionRule from "./promotion-rule"
|
import PromotionRule from "./promotion-rule"
|
||||||
|
|
||||||
type OptionalFields =
|
const tableName = "promotion_application_method"
|
||||||
| "value"
|
const CurrencyCodeIndex = createPsqlIndexStatementHelper({
|
||||||
| "max_quantity"
|
tableName,
|
||||||
| "apply_to_quantity"
|
columns: "currency_code",
|
||||||
| "buy_rules_min_quantity"
|
where: "deleted_at IS NOT NULL",
|
||||||
| "allocation"
|
})
|
||||||
| DAL.SoftDeletableEntityDateColumns
|
|
||||||
|
|
||||||
@Entity({ tableName: "promotion_application_method" })
|
@Entity({ tableName })
|
||||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||||
export default class ApplicationMethod {
|
export default class ApplicationMethod {
|
||||||
[OptionalProps]?: OptionalFields
|
|
||||||
|
|
||||||
@PrimaryKey({ columnType: "text" })
|
@PrimaryKey({ columnType: "text" })
|
||||||
id!: string
|
id!: string
|
||||||
|
|
||||||
@MikroOrmBigNumberProperty({ nullable: true })
|
@MikroOrmBigNumberProperty()
|
||||||
value: BigNumber | number | null = null
|
value: BigNumber | number | null
|
||||||
|
|
||||||
@Property({ columnType: "jsonb", nullable: true })
|
@Property({ columnType: "jsonb" })
|
||||||
raw_value: BigNumberRawValue | null = null
|
raw_value: BigNumberRawValue | null
|
||||||
|
|
||||||
|
@Property({ columnType: "text", nullable: true })
|
||||||
|
@CurrencyCodeIndex.MikroORMIndex()
|
||||||
|
currency_code: string | null = null
|
||||||
|
|
||||||
@Property({ columnType: "numeric", nullable: true, serializer: Number })
|
@Property({ columnType: "numeric", nullable: true, serializer: Number })
|
||||||
max_quantity?: number | null = null
|
max_quantity?: number | null = null
|
||||||
|
|||||||
@@ -47,17 +47,20 @@ export default class CampaignBudget {
|
|||||||
})
|
})
|
||||||
campaign: Campaign | null = null
|
campaign: Campaign | null = null
|
||||||
|
|
||||||
|
@Property({ columnType: "text", nullable: true })
|
||||||
|
currency_code: string | null = null
|
||||||
|
|
||||||
@MikroOrmBigNumberProperty({ nullable: true })
|
@MikroOrmBigNumberProperty({ nullable: true })
|
||||||
limit: BigNumber | number | null = null
|
limit: BigNumber | number | null = null
|
||||||
|
|
||||||
@Property({ columnType: "jsonb", nullable: true })
|
@Property({ columnType: "jsonb", nullable: true })
|
||||||
raw_limit: BigNumberRawValue | null = null
|
raw_limit: BigNumberRawValue | null = null
|
||||||
|
|
||||||
@MikroOrmBigNumberProperty({ nullable: true })
|
@MikroOrmBigNumberProperty({ default: 0 })
|
||||||
used: BigNumber | number | null = null
|
used: BigNumber | number = 0
|
||||||
|
|
||||||
@Property({ columnType: "jsonb", nullable: true })
|
@Property({ columnType: "jsonb" })
|
||||||
raw_used: BigNumberRawValue | null = null
|
raw_used: BigNumberRawValue
|
||||||
|
|
||||||
@Property({
|
@Property({
|
||||||
onCreate: () => new Date(),
|
onCreate: () => new Date(),
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import Promotion from "./promotion"
|
|||||||
type OptionalRelations = "budget"
|
type OptionalRelations = "budget"
|
||||||
type OptionalFields =
|
type OptionalFields =
|
||||||
| "description"
|
| "description"
|
||||||
| "currency"
|
|
||||||
| "starts_at"
|
| "starts_at"
|
||||||
| "ends_at"
|
| "ends_at"
|
||||||
| DAL.SoftDeletableEntityDateColumns
|
| DAL.SoftDeletableEntityDateColumns
|
||||||
@@ -40,9 +39,6 @@ export default class Campaign {
|
|||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
description: string | null = null
|
description: string | null = null
|
||||||
|
|
||||||
@Property({ columnType: "text", nullable: true })
|
|
||||||
currency: string | null = null
|
|
||||||
|
|
||||||
@Property({ columnType: "text" })
|
@Property({ columnType: "text" })
|
||||||
@Unique({
|
@Unique({
|
||||||
name: "IDX_campaign_identifier_unique",
|
name: "IDX_campaign_identifier_unique",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
CampaignBudgetTypeValues,
|
||||||
Context,
|
Context,
|
||||||
DAL,
|
DAL,
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
arrayDifference,
|
arrayDifference,
|
||||||
deduplicate,
|
deduplicate,
|
||||||
isDefined,
|
isDefined,
|
||||||
|
isPresent,
|
||||||
isString,
|
isString,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
@@ -475,6 +477,11 @@ export default class PromotionModuleService<
|
|||||||
const promotionsData: CreatePromotionDTO[] = []
|
const promotionsData: CreatePromotionDTO[] = []
|
||||||
const applicationMethodsData: CreateApplicationMethodDTO[] = []
|
const applicationMethodsData: CreateApplicationMethodDTO[] = []
|
||||||
const campaignsData: CreateCampaignDTO[] = []
|
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<
|
const promotionCodeApplicationMethodDataMap = new Map<
|
||||||
string,
|
string,
|
||||||
@@ -504,12 +511,10 @@ export default class PromotionModuleService<
|
|||||||
campaign_id: campaignId,
|
campaign_id: campaignId,
|
||||||
...promotionData
|
...promotionData
|
||||||
} of data) {
|
} of data) {
|
||||||
if (applicationMethodData) {
|
promotionCodeApplicationMethodDataMap.set(
|
||||||
promotionCodeApplicationMethodDataMap.set(
|
promotionData.code,
|
||||||
promotionData.code,
|
applicationMethodData
|
||||||
applicationMethodData
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rulesData) {
|
if (rulesData) {
|
||||||
promotionCodeRulesDataMap.set(promotionData.code, 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) {
|
if (campaignData) {
|
||||||
promotionCodeCampaignMap.set(promotionData.code, campaignData)
|
promotionCodeCampaignMap.set(promotionData.code, campaignData)
|
||||||
}
|
}
|
||||||
@@ -536,7 +573,6 @@ export default class PromotionModuleService<
|
|||||||
promotionsData,
|
promotionsData,
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
const promotionsToAdd: PromotionTypes.AddPromotionsToCampaignDTO[] = []
|
|
||||||
|
|
||||||
for (const promotion of createdPromotions) {
|
for (const promotion of createdPromotions) {
|
||||||
const applMethodData = promotionCodeApplicationMethodDataMap.get(
|
const applMethodData = promotionCodeApplicationMethodDataMap.get(
|
||||||
@@ -708,6 +744,10 @@ export default class PromotionModuleService<
|
|||||||
{ id: promotionIds },
|
{ id: promotionIds },
|
||||||
{ relations: ["application_method"] }
|
{ 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>(
|
const existingPromotionsMap = new Map<string, Promotion>(
|
||||||
existingPromotions.map((promotion) => [promotion.id, promotion])
|
existingPromotions.map((promotion) => [promotion.id, promotion])
|
||||||
@@ -721,20 +761,40 @@ export default class PromotionModuleService<
|
|||||||
campaign_id: campaignId,
|
campaign_id: campaignId,
|
||||||
...promotionData
|
...promotionData
|
||||||
} of data) {
|
} 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 })
|
promotionsData.push({ ...promotionData, campaign_id: campaignId })
|
||||||
} else {
|
} else {
|
||||||
promotionsData.push(promotionData)
|
promotionsData.push(promotionData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applicationMethodData) {
|
if (!applicationMethodData || !existingApplicationMethod) {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingPromotion = existingPromotionsMap.get(promotionData.id)
|
|
||||||
const existingApplicationMethod = existingPromotion?.application_method
|
|
||||||
|
|
||||||
if (!existingApplicationMethod) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,9 +973,11 @@ export default class PromotionModuleService<
|
|||||||
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
|
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<PromotionTypes.PromotionRuleDTO[]> {
|
): Promise<PromotionTypes.PromotionRuleDTO[]> {
|
||||||
const promotion = await this.promotionService_.retrieve(promotionId, {
|
const promotion = await this.promotionService_.retrieve(
|
||||||
relations: ["application_method"],
|
promotionId,
|
||||||
})
|
{ relations: ["application_method"] },
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
const applicationMethod = promotion.application_method
|
const applicationMethod = promotion.application_method
|
||||||
|
|
||||||
@@ -1153,6 +1215,8 @@ export default class PromotionModuleService<
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (campaignBudgetData) {
|
if (campaignBudgetData) {
|
||||||
|
this.validateCampaignBudgetData(campaignBudgetData)
|
||||||
|
|
||||||
campaignBudgetsData.push({
|
campaignBudgetsData.push({
|
||||||
...campaignBudgetData,
|
...campaignBudgetData,
|
||||||
campaign: createdCampaign.id,
|
campaign: createdCampaign.id,
|
||||||
@@ -1170,6 +1234,28 @@ export default class PromotionModuleService<
|
|||||||
return createdCampaigns
|
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(
|
async updateCampaigns(
|
||||||
data: PromotionTypes.UpdateCampaignDTO,
|
data: PromotionTypes.UpdateCampaignDTO,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
@@ -1207,7 +1293,8 @@ export default class PromotionModuleService<
|
|||||||
) {
|
) {
|
||||||
const campaignIds = data.map((d) => d.id)
|
const campaignIds = data.map((d) => d.id)
|
||||||
const campaignsData: UpdateCampaignDTO[] = []
|
const campaignsData: UpdateCampaignDTO[] = []
|
||||||
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
const updateBudgetData: UpdateCampaignBudgetDTO[] = []
|
||||||
|
const createBudgetData: CreateCampaignBudgetDTO[] = []
|
||||||
|
|
||||||
const existingCampaigns = await this.listCampaigns(
|
const existingCampaigns = await this.listCampaigns(
|
||||||
{ id: campaignIds },
|
{ id: campaignIds },
|
||||||
@@ -1220,18 +1307,38 @@ export default class PromotionModuleService<
|
|||||||
)
|
)
|
||||||
|
|
||||||
for (const updateCampaignData of data) {
|
for (const updateCampaignData of data) {
|
||||||
const { budget: campaignBudgetData, ...campaignData } = updateCampaignData
|
const { budget: budgetData, ...campaignData } = updateCampaignData
|
||||||
|
const existingCampaign = existingCampaignsMap.get(campaignData.id)!
|
||||||
const existingCampaign = existingCampaignsMap.get(campaignData.id)
|
|
||||||
const existingCampaignBudget = existingCampaign?.budget
|
|
||||||
|
|
||||||
campaignsData.push(campaignData)
|
campaignsData.push(campaignData)
|
||||||
|
|
||||||
if (existingCampaignBudget && campaignBudgetData) {
|
// Type & currency code of the budget is immutable, we don't allow for it to be updated.
|
||||||
campaignBudgetsData.push({
|
// If an existing budget is present, we remove the type and currency from being updated
|
||||||
id: existingCampaignBudget.id,
|
if (
|
||||||
...campaignBudgetData,
|
(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
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
if (campaignBudgetsData.length) {
|
if (updateBudgetData.length) {
|
||||||
await this.campaignBudgetService_.update(
|
await this.campaignBudgetService_.update(updateBudgetData, sharedContext)
|
||||||
campaignBudgetsData,
|
}
|
||||||
sharedContext
|
|
||||||
)
|
if (createBudgetData.length) {
|
||||||
|
await this.campaignBudgetService_.create(createBudgetData, sharedContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedCampaigns
|
return updatedCampaigns
|
||||||
@@ -1274,7 +1382,7 @@ export default class PromotionModuleService<
|
|||||||
const campaign = await this.campaignService_.retrieve(id, {}, sharedContext)
|
const campaign = await this.campaignService_.retrieve(id, {}, sharedContext)
|
||||||
const promotionsToAdd = await this.promotionService_.list(
|
const promotionsToAdd = await this.promotionService_.list(
|
||||||
{ id: promotionIds, campaign_id: null },
|
{ id: promotionIds, campaign_id: null },
|
||||||
{ take: null },
|
{ take: null, relations: ["application_method"] },
|
||||||
sharedContext
|
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(
|
await this.promotionService_.update(
|
||||||
promotionsToAdd.map((promotion) => ({
|
promotionsToAdd.map((promotion) => ({
|
||||||
id: promotion.id,
|
id: promotion.id,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface CreateApplicationMethodDTO {
|
|||||||
target_type: ApplicationMethodTargetTypeValues
|
target_type: ApplicationMethodTargetTypeValues
|
||||||
allocation?: ApplicationMethodAllocationValues
|
allocation?: ApplicationMethodAllocationValues
|
||||||
value?: number
|
value?: number
|
||||||
|
currency_code: string
|
||||||
promotion: Promotion | string | PromotionDTO
|
promotion: Promotion | string | PromotionDTO
|
||||||
max_quantity?: number | null
|
max_quantity?: number | null
|
||||||
buy_rules_min_quantity?: number | null
|
buy_rules_min_quantity?: number | null
|
||||||
@@ -19,11 +20,12 @@ export interface CreateApplicationMethodDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateApplicationMethodDTO {
|
export interface UpdateApplicationMethodDTO {
|
||||||
id: string
|
id?: string
|
||||||
type?: ApplicationMethodTypeValues
|
type?: ApplicationMethodTypeValues
|
||||||
target_type?: ApplicationMethodTargetTypeValues
|
target_type?: ApplicationMethodTargetTypeValues
|
||||||
allocation?: ApplicationMethodAllocationValues
|
allocation?: ApplicationMethodAllocationValues
|
||||||
value?: number
|
value?: number
|
||||||
|
currency_code?: string
|
||||||
promotion?: Promotion | string | PromotionDTO
|
promotion?: Promotion | string | PromotionDTO
|
||||||
max_quantity?: number | null
|
max_quantity?: number | null
|
||||||
buy_rules_min_quantity?: number | null
|
buy_rules_min_quantity?: number | null
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Campaign } from "@models"
|
|||||||
|
|
||||||
export interface CreateCampaignBudgetDTO {
|
export interface CreateCampaignBudgetDTO {
|
||||||
type?: CampaignBudgetTypeValues
|
type?: CampaignBudgetTypeValues
|
||||||
limit?: number
|
limit?: number | null
|
||||||
|
currency_code?: string | null
|
||||||
used?: number
|
used?: number
|
||||||
campaign?: Campaign | string
|
campaign?: Campaign | string
|
||||||
}
|
}
|
||||||
@@ -11,6 +12,7 @@ export interface CreateCampaignBudgetDTO {
|
|||||||
export interface UpdateCampaignBudgetDTO {
|
export interface UpdateCampaignBudgetDTO {
|
||||||
id: string
|
id: string
|
||||||
type?: CampaignBudgetTypeValues
|
type?: CampaignBudgetTypeValues
|
||||||
limit?: number
|
limit?: number | null
|
||||||
|
currency_code?: string | null
|
||||||
used?: number
|
used?: number
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ import { Promotion } from "@models"
|
|||||||
export interface CreateCampaignDTO {
|
export interface CreateCampaignDTO {
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
currency?: string
|
|
||||||
campaign_identifier: string
|
campaign_identifier: string
|
||||||
starts_at?: Date
|
starts_at?: Date
|
||||||
ends_at?: Date
|
ends_at?: Date
|
||||||
@@ -15,7 +14,6 @@ export interface UpdateCampaignDTO {
|
|||||||
id: string
|
id: string
|
||||||
name?: string
|
name?: string
|
||||||
description?: string
|
description?: string
|
||||||
currency?: string
|
|
||||||
campaign_identifier?: string
|
campaign_identifier?: string
|
||||||
starts_at?: Date
|
starts_at?: Date
|
||||||
ends_at?: Date
|
ends_at?: Date
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export interface CreatePromotionDTO {
|
|||||||
code: string
|
code: string
|
||||||
type: PromotionTypeValues
|
type: PromotionTypeValues
|
||||||
is_automatic?: boolean
|
is_automatic?: boolean
|
||||||
campaign_id?: string
|
campaign_id?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdatePromotionDTO {
|
export interface UpdatePromotionDTO {
|
||||||
@@ -12,5 +12,5 @@ export interface UpdatePromotionDTO {
|
|||||||
code?: string
|
code?: string
|
||||||
type?: PromotionTypeValues
|
type?: PromotionTypeValues
|
||||||
is_automatic?: boolean
|
is_automatic?: boolean
|
||||||
campaign_id?: string
|
campaign_id?: string | null
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user