@lodashventure/medusa-campaign 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/api/admin/buy-x-get-y/[id]/route.js +116 -0
- package/.medusa/server/src/api/admin/buy-x-get-y/route.js +83 -0
- package/.medusa/server/src/api/admin/campaigns/fix-dates/route.js +103 -0
- package/.medusa/server/src/api/admin/campaigns/sync/route.js +138 -0
- package/.medusa/server/src/api/admin/flash-sales/[id]/route.js +49 -34
- package/.medusa/server/src/api/admin/flash-sales/route.js +46 -19
- package/.medusa/server/src/api/admin/force-fix/route.js +176 -0
- package/.medusa/server/src/api/admin/test-campaign/route.js +132 -0
- package/.medusa/server/src/api/store/buy-x-get-y/[id]/route.js +109 -0
- package/.medusa/server/src/api/store/buy-x-get-y/products/[productId]/route.js +94 -0
- package/.medusa/server/src/api/store/buy-x-get-y/route.js +114 -0
- package/.medusa/server/src/api/store/campaigns/[id]/route.js +132 -70
- package/.medusa/server/src/api/store/campaigns/route.js +119 -26
- package/.medusa/server/src/index.js +15 -0
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251018000000.js +40 -0
- package/.medusa/server/src/modules/custom-campaigns/models/buy-x-get-y-config.js +20 -0
- package/.medusa/server/src/modules/custom-campaigns/service.js +3 -1
- package/.medusa/server/src/modules/custom-campaigns/types/campaign-type.enum.js +2 -1
- package/.medusa/server/src/subscribers/cart-updated.js +23 -0
- package/.medusa/server/src/subscribers/order-placed.js +9 -2
- package/.medusa/server/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.js +150 -0
- package/.medusa/server/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.js +127 -0
- package/.medusa/server/src/workflows/custom-campaign/updateBuyXGetYCampaignWorkflow.js +114 -0
- package/.medusa/server/src/workflows/custom-campaign/updateBuyXGetYUsageWorkflow.js +51 -0
- package/package.json +2 -2
- package/src/admin/components/BuyXGetYForm.tsx +422 -0
- package/src/api/admin/buy-x-get-y/[id]/route.ts +164 -0
- package/src/api/admin/buy-x-get-y/route.ts +104 -0
- package/src/api/admin/campaigns/fix-dates/route.ts +107 -0
- package/src/api/admin/campaigns/sync/route.ts +153 -0
- package/src/api/admin/flash-sales/[id]/route.ts +62 -36
- package/src/api/admin/flash-sales/route.ts +57 -21
- package/src/api/admin/force-fix/route.ts +184 -0
- package/src/api/admin/test-campaign/route.ts +141 -0
- package/src/api/store/buy-x-get-y/[id]/route.ts +146 -0
- package/src/api/store/buy-x-get-y/products/[productId]/route.ts +129 -0
- package/src/api/store/buy-x-get-y/route.ts +134 -0
- package/src/api/store/campaigns/[id]/route.ts +159 -79
- package/src/api/store/campaigns/route.ts +141 -30
- package/src/index.ts +10 -0
- package/src/modules/custom-campaigns/migrations/Migration20251018000000.ts +42 -0
- package/src/modules/custom-campaigns/models/buy-x-get-y-config.ts +19 -0
- package/src/modules/custom-campaigns/service.ts +2 -0
- package/src/modules/custom-campaigns/types/campaign-type.enum.ts +1 -0
- package/src/subscribers/cart-updated.ts +23 -0
- package/src/subscribers/order-placed.ts +9 -1
- package/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.ts +222 -0
- package/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.ts +210 -0
- package/src/workflows/custom-campaign/updateBuyXGetYCampaignWorkflow.ts +190 -0
- package/src/workflows/custom-campaign/updateBuyXGetYUsageWorkflow.ts +86 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { container } from "@medusajs/framework";
|
|
2
|
+
import { Modules } from "@medusajs/framework/utils";
|
|
3
|
+
import {
|
|
4
|
+
createStep,
|
|
5
|
+
createWorkflow,
|
|
6
|
+
StepResponse,
|
|
7
|
+
WorkflowResponse,
|
|
8
|
+
} from "@medusajs/framework/workflows-sdk";
|
|
9
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../modules/custom-campaigns";
|
|
10
|
+
import CustomCampaignModuleService from "../../modules/custom-campaigns/service";
|
|
11
|
+
import { CampaignTypeEnum } from "../../modules/custom-campaigns/types/campaign-type.enum";
|
|
12
|
+
|
|
13
|
+
interface ApplyBuyXGetYInput {
|
|
14
|
+
cart_id: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RewardItem {
|
|
18
|
+
product_id: string;
|
|
19
|
+
variant_id: string;
|
|
20
|
+
quantity: number;
|
|
21
|
+
metadata?: {
|
|
22
|
+
is_bogo_reward?: boolean;
|
|
23
|
+
bogo_config_id?: string;
|
|
24
|
+
reward_type?: string;
|
|
25
|
+
reward_value?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const applyBuyXGetYToCartStep = createStep(
|
|
30
|
+
"apply-buy-x-get-y-to-cart-step",
|
|
31
|
+
async (data: ApplyBuyXGetYInput) => {
|
|
32
|
+
const customCampaignModuleService =
|
|
33
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
34
|
+
const cartService = container.resolve(Modules.CART);
|
|
35
|
+
const productService = container.resolve(Modules.PRODUCT);
|
|
36
|
+
const linkService = container.resolve("linkModuleService");
|
|
37
|
+
|
|
38
|
+
// Fetch the cart with items
|
|
39
|
+
const cart = await cartService.retrieve(data.cart_id, {
|
|
40
|
+
relations: ["items", "items.variant", "items.product"],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!cart || !cart.items || cart.items.length === 0) {
|
|
44
|
+
return new StepResponse({ rewardItemsAdded: [] });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get all active Buy X Get Y campaigns
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const campaignTypes =
|
|
50
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
51
|
+
type: CampaignTypeEnum.BuyXGetY,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (campaignTypes.length === 0) {
|
|
55
|
+
return new StepResponse({ rewardItemsAdded: [] });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get campaigns through link service
|
|
59
|
+
const campaignLinks = await linkService.list({
|
|
60
|
+
[CUSTOM_CAMPAIGN_MODULE]: {
|
|
61
|
+
custom_campaign_type_id: campaignTypes.map((ct) => ct.id),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const campaignIds = campaignLinks.map(
|
|
66
|
+
(link: any) => link[Modules.PROMOTION].campaign_id
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
70
|
+
const campaigns = await promotionService.listCampaigns({
|
|
71
|
+
id: campaignIds,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Filter active campaigns
|
|
75
|
+
const activeCampaigns = campaigns.filter(
|
|
76
|
+
(campaign: any) =>
|
|
77
|
+
new Date(campaign.starts_at) <= now &&
|
|
78
|
+
new Date(campaign.ends_at) >= now
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (activeCampaigns.length === 0) {
|
|
82
|
+
return new StepResponse({ rewardItemsAdded: [] });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get BOGO configurations for active campaigns
|
|
86
|
+
const buyXGetYConfigs =
|
|
87
|
+
await customCampaignModuleService.listBuyXGetYConfigs({
|
|
88
|
+
campaign_id: activeCampaigns.map((c: any) => c.id),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Remove ALL existing BOGO reward items to recalculate from scratch
|
|
92
|
+
const existingBogoItems = cart.items.filter(
|
|
93
|
+
(item: any) => item.metadata?.is_bogo_reward === true
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const removedItemIds: string[] = [];
|
|
97
|
+
for (const bogoItem of existingBogoItems) {
|
|
98
|
+
try {
|
|
99
|
+
await cartService.removeLineItems(data.cart_id, [bogoItem.id]);
|
|
100
|
+
removedItemIds.push(bogoItem.id);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(`Failed to remove existing BOGO item:`, error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Group cart items by product (excluding BOGO items which are now removed)
|
|
107
|
+
const cartItemsByProduct = new Map<string, any[]>();
|
|
108
|
+
cart.items.forEach((item: any) => {
|
|
109
|
+
if (item.metadata?.is_bogo_reward) return; // Skip BOGO items (should be none now)
|
|
110
|
+
const productId = item.product_id;
|
|
111
|
+
if (!cartItemsByProduct.has(productId)) {
|
|
112
|
+
cartItemsByProduct.set(productId, []);
|
|
113
|
+
}
|
|
114
|
+
cartItemsByProduct.get(productId)!.push(item);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const rewardItemsToAdd: RewardItem[] = [];
|
|
118
|
+
|
|
119
|
+
// Check each BOGO configuration
|
|
120
|
+
for (const config of buyXGetYConfigs) {
|
|
121
|
+
// Check if usage limit reached
|
|
122
|
+
if (config.limit && config.used >= config.limit) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Get cart items for the trigger product
|
|
127
|
+
const triggerItems = cartItemsByProduct.get(config.trigger_product_id);
|
|
128
|
+
if (!triggerItems || triggerItems.length === 0) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Calculate total quantity of trigger product
|
|
133
|
+
const totalTriggerQuantity = triggerItems.reduce(
|
|
134
|
+
(sum, item) => sum + item.quantity,
|
|
135
|
+
0
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Check if trigger quantity is met
|
|
139
|
+
if (totalTriggerQuantity >= config.trigger_quantity) {
|
|
140
|
+
// Calculate how many times the reward should be given
|
|
141
|
+
const rewardMultiplier = Math.floor(
|
|
142
|
+
totalTriggerQuantity / config.trigger_quantity
|
|
143
|
+
);
|
|
144
|
+
const totalRewardQuantity = config.reward_quantity * rewardMultiplier;
|
|
145
|
+
|
|
146
|
+
// Get the reward product's default variant
|
|
147
|
+
const rewardProduct = await productService.retrieveProduct(
|
|
148
|
+
config.reward_product_id,
|
|
149
|
+
{
|
|
150
|
+
relations: ["variants"],
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (!rewardProduct.variants || rewardProduct.variants.length === 0) {
|
|
155
|
+
console.warn(
|
|
156
|
+
`No variants found for reward product ${config.reward_product_id}`
|
|
157
|
+
);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const defaultVariant = rewardProduct.variants[0];
|
|
162
|
+
|
|
163
|
+
rewardItemsToAdd.push({
|
|
164
|
+
product_id: config.reward_product_id,
|
|
165
|
+
variant_id: defaultVariant.id,
|
|
166
|
+
quantity: totalRewardQuantity,
|
|
167
|
+
metadata: {
|
|
168
|
+
is_bogo_reward: true,
|
|
169
|
+
bogo_config_id: config.id,
|
|
170
|
+
reward_type: config.reward_type,
|
|
171
|
+
reward_value: config.reward_value ?? undefined,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Add reward items to cart
|
|
178
|
+
const addedItems: any[] = [];
|
|
179
|
+
for (const rewardItem of rewardItemsToAdd) {
|
|
180
|
+
try {
|
|
181
|
+
const addedItem = await cartService.addLineItems(data.cart_id, [
|
|
182
|
+
{
|
|
183
|
+
variant_id: rewardItem.variant_id,
|
|
184
|
+
quantity: rewardItem.quantity,
|
|
185
|
+
metadata: rewardItem.metadata,
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
addedItems.push(addedItem);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`Failed to add reward item:`, error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return new StepResponse(
|
|
195
|
+
{ rewardItemsAdded: addedItems },
|
|
196
|
+
{ cart_id: data.cart_id, added_items: addedItems }
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
async (compensationData) => {
|
|
200
|
+
// Compensation: Remove added reward items if workflow fails
|
|
201
|
+
if (!compensationData || !compensationData.added_items) return;
|
|
202
|
+
|
|
203
|
+
const cartService = container.resolve(Modules.CART);
|
|
204
|
+
|
|
205
|
+
for (const item of compensationData.added_items) {
|
|
206
|
+
try {
|
|
207
|
+
await cartService.removeLineItems(compensationData.cart_id, [item.id]);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(`Failed to remove reward item during compensation:`, error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
export const applyBuyXGetYToCartWorkflow = createWorkflow(
|
|
216
|
+
"apply-buy-x-get-y-to-cart",
|
|
217
|
+
(data: ApplyBuyXGetYInput) => {
|
|
218
|
+
const result = applyBuyXGetYToCartStep(data);
|
|
219
|
+
|
|
220
|
+
return new WorkflowResponse(result);
|
|
221
|
+
}
|
|
222
|
+
);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { container } from "@medusajs/framework";
|
|
2
|
+
import {
|
|
3
|
+
CampaignDTO,
|
|
4
|
+
CreateApplicationMethodDTO,
|
|
5
|
+
CreatePromotionDTO,
|
|
6
|
+
PromotionDTO,
|
|
7
|
+
} from "@medusajs/framework/types";
|
|
8
|
+
import {
|
|
9
|
+
ContainerRegistrationKeys,
|
|
10
|
+
MedusaError,
|
|
11
|
+
Modules,
|
|
12
|
+
} from "@medusajs/framework/utils";
|
|
13
|
+
import {
|
|
14
|
+
createStep,
|
|
15
|
+
createWorkflow,
|
|
16
|
+
StepResponse,
|
|
17
|
+
WorkflowResponse,
|
|
18
|
+
} from "@medusajs/framework/workflows-sdk";
|
|
19
|
+
import {
|
|
20
|
+
createCampaignsWorkflow,
|
|
21
|
+
createPromotionsWorkflow,
|
|
22
|
+
deleteCampaignsWorkflow,
|
|
23
|
+
deletePromotionsWorkflow,
|
|
24
|
+
} from "@medusajs/medusa/core-flows";
|
|
25
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../modules/custom-campaigns";
|
|
26
|
+
import CustomCampaignModuleService from "../../modules/custom-campaigns/service";
|
|
27
|
+
import { CampaignTypeEnum } from "../../modules/custom-campaigns/types/campaign-type.enum";
|
|
28
|
+
|
|
29
|
+
export interface BuyXGetYProduct {
|
|
30
|
+
triggerProduct: {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
};
|
|
34
|
+
triggerQuantity: number;
|
|
35
|
+
rewardProduct: {
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
};
|
|
39
|
+
rewardQuantity: number;
|
|
40
|
+
rewardType: "free" | "percentage" | "fixed";
|
|
41
|
+
rewardValue?: number;
|
|
42
|
+
limit?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface BuyXGetYCampaign {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
type: CampaignTypeEnum.BuyXGetY;
|
|
49
|
+
starts_at: Date;
|
|
50
|
+
ends_at: Date;
|
|
51
|
+
rules: BuyXGetYProduct[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const createBuyXGetYCampaignStep = createStep(
|
|
55
|
+
"create-buy-x-get-y-campaign-step",
|
|
56
|
+
async (data: BuyXGetYCampaign) => {
|
|
57
|
+
const link = container.resolve(ContainerRegistrationKeys.LINK);
|
|
58
|
+
const customCampaignModuleService =
|
|
59
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
60
|
+
const campaignIdentifier = `${data.type}-${Date.now()}`;
|
|
61
|
+
|
|
62
|
+
let campaign: CampaignDTO | undefined;
|
|
63
|
+
let promotions: PromotionDTO[] | undefined;
|
|
64
|
+
let customCampaignType:
|
|
65
|
+
| Awaited<
|
|
66
|
+
ReturnType<CustomCampaignModuleService["createCustomCampaignTypes"]>
|
|
67
|
+
>[number]
|
|
68
|
+
| undefined;
|
|
69
|
+
let buyXGetYConfigs:
|
|
70
|
+
| Awaited<
|
|
71
|
+
ReturnType<CustomCampaignModuleService["createBuyXGetYConfigs"]>
|
|
72
|
+
>
|
|
73
|
+
| undefined;
|
|
74
|
+
|
|
75
|
+
// Create campaign
|
|
76
|
+
const {
|
|
77
|
+
result: [_campaign],
|
|
78
|
+
} = await createCampaignsWorkflow.run({
|
|
79
|
+
input: {
|
|
80
|
+
campaignsData: [
|
|
81
|
+
{
|
|
82
|
+
name: data.name,
|
|
83
|
+
campaign_identifier: campaignIdentifier,
|
|
84
|
+
description: data.description,
|
|
85
|
+
starts_at: new Date(data.starts_at),
|
|
86
|
+
ends_at: new Date(data.ends_at),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
campaign = _campaign;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Create promotions for each BOGO rule
|
|
95
|
+
// Store BOGO configuration in promotion code for easy lookup
|
|
96
|
+
const { result: _promotions } = await createPromotionsWorkflow.run({
|
|
97
|
+
input: {
|
|
98
|
+
promotionsData: data.rules.map(
|
|
99
|
+
(rule) =>
|
|
100
|
+
({
|
|
101
|
+
code: `${campaignIdentifier}-${rule.triggerProduct.id}-${rule.rewardProduct.id}`,
|
|
102
|
+
type: "buyget",
|
|
103
|
+
status: "active",
|
|
104
|
+
is_automatic: true,
|
|
105
|
+
campaign_id: campaign.id,
|
|
106
|
+
application_method: {
|
|
107
|
+
target_type: "items",
|
|
108
|
+
allocation: "each",
|
|
109
|
+
type: rule.rewardType === "percentage" ? "percentage" : "fixed",
|
|
110
|
+
value: rule.rewardType === "free" ? 0 : (rule.rewardValue ?? 0),
|
|
111
|
+
buy_rules_min_quantity: rule.triggerQuantity,
|
|
112
|
+
apply_to_quantity: rule.rewardQuantity,
|
|
113
|
+
target_rules: [
|
|
114
|
+
{
|
|
115
|
+
attribute: "items.product.id",
|
|
116
|
+
operator: "eq",
|
|
117
|
+
values: [rule.triggerProduct.id],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
} satisfies CreateApplicationMethodDTO,
|
|
121
|
+
} satisfies CreatePromotionDTO)
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
promotions = _promotions;
|
|
126
|
+
|
|
127
|
+
// Create custom campaign type
|
|
128
|
+
customCampaignType =
|
|
129
|
+
await customCampaignModuleService.createCustomCampaignTypes({
|
|
130
|
+
campaign_id: campaign.id,
|
|
131
|
+
type: data.type,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Create Buy X Get Y configurations
|
|
135
|
+
buyXGetYConfigs =
|
|
136
|
+
await customCampaignModuleService.createBuyXGetYConfigs(
|
|
137
|
+
data.rules.map((rule, index) => ({
|
|
138
|
+
campaign_id: campaign.id,
|
|
139
|
+
promotion_id: promotions[index].id,
|
|
140
|
+
trigger_product_id: rule.triggerProduct.id,
|
|
141
|
+
trigger_quantity: rule.triggerQuantity,
|
|
142
|
+
reward_product_id: rule.rewardProduct.id,
|
|
143
|
+
reward_quantity: rule.rewardQuantity,
|
|
144
|
+
reward_type: rule.rewardType,
|
|
145
|
+
reward_value: rule.rewardValue ?? null,
|
|
146
|
+
limit: rule.limit ?? null,
|
|
147
|
+
used: 0,
|
|
148
|
+
}))
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Link campaign type
|
|
152
|
+
await link.create([
|
|
153
|
+
{
|
|
154
|
+
[Modules.PROMOTION]: {
|
|
155
|
+
campaign_id: campaign.id,
|
|
156
|
+
},
|
|
157
|
+
[CUSTOM_CAMPAIGN_MODULE]: {
|
|
158
|
+
custom_campaign_type_id: customCampaignType.id,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
return new StepResponse({
|
|
164
|
+
campaign,
|
|
165
|
+
promotions,
|
|
166
|
+
buyXGetYConfigs,
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.log({ error });
|
|
170
|
+
|
|
171
|
+
// Cleanup on error
|
|
172
|
+
await deleteCampaignsWorkflow.run({
|
|
173
|
+
input: {
|
|
174
|
+
ids: campaign.id,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (promotions) {
|
|
179
|
+
await deletePromotionsWorkflow.run({
|
|
180
|
+
input: {
|
|
181
|
+
ids: promotions.map((promotion) => promotion.id),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (customCampaignType) {
|
|
187
|
+
await customCampaignModuleService.deleteCustomCampaignTypes(
|
|
188
|
+
customCampaignType.id
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (buyXGetYConfigs) {
|
|
193
|
+
await customCampaignModuleService.deleteBuyXGetYConfigs(
|
|
194
|
+
buyXGetYConfigs.map((config) => config.id)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
export const createBuyXGetYCampaignWorkflow = createWorkflow(
|
|
204
|
+
"create-buy-x-get-y-campaign",
|
|
205
|
+
(data: BuyXGetYCampaign) => {
|
|
206
|
+
const campaign = createBuyXGetYCampaignStep(data);
|
|
207
|
+
|
|
208
|
+
return new WorkflowResponse(campaign);
|
|
209
|
+
}
|
|
210
|
+
);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { container } from "@medusajs/framework";
|
|
2
|
+
import {
|
|
3
|
+
CreateApplicationMethodDTO,
|
|
4
|
+
CreatePromotionDTO,
|
|
5
|
+
UpdateCampaignDTO,
|
|
6
|
+
UpdatePromotionDTO,
|
|
7
|
+
} from "@medusajs/framework/types";
|
|
8
|
+
import { MedusaError, Modules } from "@medusajs/framework/utils";
|
|
9
|
+
import {
|
|
10
|
+
createStep,
|
|
11
|
+
createWorkflow,
|
|
12
|
+
StepResponse,
|
|
13
|
+
WorkflowResponse,
|
|
14
|
+
} from "@medusajs/framework/workflows-sdk";
|
|
15
|
+
import {
|
|
16
|
+
createPromotionsWorkflow,
|
|
17
|
+
deletePromotionsWorkflow,
|
|
18
|
+
updateCampaignsWorkflow,
|
|
19
|
+
} from "@medusajs/medusa/core-flows";
|
|
20
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../modules/custom-campaigns";
|
|
21
|
+
import CustomCampaignModuleService from "../../modules/custom-campaigns/service";
|
|
22
|
+
import { BuyXGetYCampaign } from "./createBuyXGetYCampaignWorkflow";
|
|
23
|
+
|
|
24
|
+
interface UpdateBuyXGetYInput extends BuyXGetYCampaign {
|
|
25
|
+
id: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const updateBuyXGetYCampaignStep = createStep(
|
|
29
|
+
"update-buy-x-get-y-campaign-step",
|
|
30
|
+
async (data: UpdateBuyXGetYInput) => {
|
|
31
|
+
const campaign_id = data.id;
|
|
32
|
+
|
|
33
|
+
const customCampaignModuleService =
|
|
34
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
35
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
36
|
+
|
|
37
|
+
if (!campaign_id) {
|
|
38
|
+
throw new MedusaError(
|
|
39
|
+
MedusaError.Types.INVALID_DATA,
|
|
40
|
+
"Campaign ID is required"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fetch the existing campaign
|
|
45
|
+
const campaign = await promotionService.retrieveCampaign(campaign_id);
|
|
46
|
+
|
|
47
|
+
// Fetch existing Buy X Get Y configurations
|
|
48
|
+
const existingConfigs =
|
|
49
|
+
await customCampaignModuleService.listBuyXGetYConfigs({
|
|
50
|
+
campaign_id: campaign_id,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Create maps for existing configs
|
|
54
|
+
const existingConfigMapByKey = new Map(
|
|
55
|
+
existingConfigs.map((config) => [
|
|
56
|
+
`${config.trigger_product_id}-${config.reward_product_id}`,
|
|
57
|
+
config,
|
|
58
|
+
])
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const rulesToCreate = data.rules.filter(
|
|
62
|
+
(rule) =>
|
|
63
|
+
!existingConfigMapByKey.has(
|
|
64
|
+
`${rule.triggerProduct.id}-${rule.rewardProduct.id}`
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const rulesToUpdate = data.rules.filter((rule) =>
|
|
69
|
+
existingConfigMapByKey.has(
|
|
70
|
+
`${rule.triggerProduct.id}-${rule.rewardProduct.id}`
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const inputRuleKeySet = new Set(
|
|
75
|
+
data.rules.map(
|
|
76
|
+
(rule) => `${rule.triggerProduct.id}-${rule.rewardProduct.id}`
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const configsToDelete = existingConfigs.filter(
|
|
81
|
+
(config) =>
|
|
82
|
+
!inputRuleKeySet.has(
|
|
83
|
+
`${config.trigger_product_id}-${config.reward_product_id}`
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// 1. Update campaign details
|
|
88
|
+
await updateCampaignsWorkflow.run({
|
|
89
|
+
input: {
|
|
90
|
+
campaignsData: [
|
|
91
|
+
{
|
|
92
|
+
id: campaign_id,
|
|
93
|
+
name: data.name,
|
|
94
|
+
description: data.description,
|
|
95
|
+
starts_at: new Date(data.starts_at),
|
|
96
|
+
ends_at: new Date(data.ends_at),
|
|
97
|
+
} satisfies UpdateCampaignDTO,
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// 2. Update existing rules
|
|
103
|
+
await customCampaignModuleService.updateBuyXGetYConfigs(
|
|
104
|
+
rulesToUpdate.map((rule) => {
|
|
105
|
+
const key = `${rule.triggerProduct.id}-${rule.rewardProduct.id}`;
|
|
106
|
+
const config = existingConfigMapByKey.get(key);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id: config?.id ?? "",
|
|
110
|
+
trigger_quantity: rule.triggerQuantity,
|
|
111
|
+
reward_quantity: rule.rewardQuantity,
|
|
112
|
+
reward_type: rule.rewardType,
|
|
113
|
+
reward_value: rule.rewardValue ?? null,
|
|
114
|
+
limit: rule.limit ?? null,
|
|
115
|
+
};
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// 3. Delete old configurations and their promotions
|
|
120
|
+
if (configsToDelete.length > 0) {
|
|
121
|
+
await deletePromotionsWorkflow.run({
|
|
122
|
+
input: {
|
|
123
|
+
ids: configsToDelete.map((config) => config.promotion_id),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
await customCampaignModuleService.softDeleteBuyXGetYConfigs(
|
|
127
|
+
configsToDelete.map((config) => config.id)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 4. Create new rules
|
|
132
|
+
if (rulesToCreate.length > 0) {
|
|
133
|
+
const { result: createdPromotions } = await createPromotionsWorkflow.run({
|
|
134
|
+
input: {
|
|
135
|
+
promotionsData: rulesToCreate.map(
|
|
136
|
+
(rule) =>
|
|
137
|
+
({
|
|
138
|
+
code: `${campaign.campaign_identifier}-${rule.triggerProduct.id}-${rule.rewardProduct.id}`,
|
|
139
|
+
type: "buyget",
|
|
140
|
+
status: "active",
|
|
141
|
+
is_automatic: true,
|
|
142
|
+
campaign_id: campaign_id,
|
|
143
|
+
application_method: {
|
|
144
|
+
target_type: "items",
|
|
145
|
+
allocation: "each",
|
|
146
|
+
type: rule.rewardType === "percentage" ? "percentage" : "fixed",
|
|
147
|
+
value: rule.rewardType === "free" ? 0 : (rule.rewardValue ?? 0),
|
|
148
|
+
buy_rules_min_quantity: rule.triggerQuantity,
|
|
149
|
+
apply_to_quantity: rule.rewardQuantity,
|
|
150
|
+
target_rules: [
|
|
151
|
+
{
|
|
152
|
+
attribute: "items.product.id",
|
|
153
|
+
operator: "eq",
|
|
154
|
+
values: [rule.triggerProduct.id],
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
} satisfies CreateApplicationMethodDTO,
|
|
158
|
+
} satisfies CreatePromotionDTO)
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await customCampaignModuleService.createBuyXGetYConfigs(
|
|
164
|
+
rulesToCreate.map((rule, index) => ({
|
|
165
|
+
campaign_id: campaign.id,
|
|
166
|
+
promotion_id: createdPromotions[index].id,
|
|
167
|
+
trigger_product_id: rule.triggerProduct.id,
|
|
168
|
+
trigger_quantity: rule.triggerQuantity,
|
|
169
|
+
reward_product_id: rule.rewardProduct.id,
|
|
170
|
+
reward_quantity: rule.rewardQuantity,
|
|
171
|
+
reward_type: rule.rewardType,
|
|
172
|
+
reward_value: rule.rewardValue ?? null,
|
|
173
|
+
limit: rule.limit ?? null,
|
|
174
|
+
used: 0,
|
|
175
|
+
}))
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return new StepResponse();
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
export const updateBuyXGetYCampaignWorkflow = createWorkflow(
|
|
184
|
+
"update-buy-x-get-y-campaign",
|
|
185
|
+
(data: UpdateBuyXGetYInput) => {
|
|
186
|
+
const updatedCampaign = updateBuyXGetYCampaignStep(data);
|
|
187
|
+
|
|
188
|
+
return new WorkflowResponse(updatedCampaign);
|
|
189
|
+
}
|
|
190
|
+
);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Modules } from "@medusajs/framework/utils";
|
|
2
|
+
import {
|
|
3
|
+
createStep,
|
|
4
|
+
createWorkflow,
|
|
5
|
+
WorkflowResponse,
|
|
6
|
+
} from "@medusajs/framework/workflows-sdk";
|
|
7
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../modules/custom-campaigns";
|
|
8
|
+
import CustomCampaignModuleService from "../../modules/custom-campaigns/service";
|
|
9
|
+
|
|
10
|
+
interface UpdateBuyXGetYUsageInput {
|
|
11
|
+
order_id: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const updateBuyXGetYUsageStep = createStep(
|
|
15
|
+
"update-buy-x-get-y-usage-step",
|
|
16
|
+
async ({ order_id }: UpdateBuyXGetYUsageInput, { container }) => {
|
|
17
|
+
const orderService = await container.resolve(Modules.ORDER);
|
|
18
|
+
const promotionService = await container.resolve(Modules.PROMOTION);
|
|
19
|
+
const customCampaignTypeService =
|
|
20
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
21
|
+
|
|
22
|
+
const order = await orderService.retrieveOrder(order_id, {
|
|
23
|
+
relations: ["items"],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Find all BOGO reward items in the order
|
|
27
|
+
const bogoRewardItems = order.items?.filter(
|
|
28
|
+
(item) => item.metadata?.is_bogo_reward === true
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!bogoRewardItems || bogoRewardItems.length === 0) {
|
|
32
|
+
return; // No BOGO items in this order
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Extract BOGO config IDs from reward items
|
|
36
|
+
const bogoConfigIds = Array.from(
|
|
37
|
+
new Set(
|
|
38
|
+
bogoRewardItems
|
|
39
|
+
.map((item) => item.metadata?.bogo_config_id as string)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (bogoConfigIds.length === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get the BOGO configurations
|
|
49
|
+
const buyXGetYConfigs =
|
|
50
|
+
await customCampaignTypeService.listBuyXGetYConfigs({
|
|
51
|
+
id: bogoConfigIds,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Update usage count for each config
|
|
55
|
+
const updatePayload = buyXGetYConfigs.map((config) => ({
|
|
56
|
+
...config,
|
|
57
|
+
used: config.used + 1,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// Find configs that have reached their limit
|
|
61
|
+
const limitReachedConfigs = updatePayload.filter(
|
|
62
|
+
(config) => config.limit && config.used >= config.limit
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Mark promotions as inactive if limit reached
|
|
66
|
+
if (limitReachedConfigs.length > 0) {
|
|
67
|
+
await promotionService.updatePromotions(
|
|
68
|
+
limitReachedConfigs.map((config) => ({
|
|
69
|
+
id: config.promotion_id,
|
|
70
|
+
status: "inactive",
|
|
71
|
+
}))
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Update the usage counts
|
|
76
|
+
await customCampaignTypeService.updateBuyXGetYConfigs(updatePayload);
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export const updateBuyXGetYUsageWorkflow = createWorkflow(
|
|
81
|
+
"update-buy-x-get-y-usage",
|
|
82
|
+
(data: UpdateBuyXGetYUsageInput) => {
|
|
83
|
+
updateBuyXGetYUsageStep(data);
|
|
84
|
+
return new WorkflowResponse({ success: true });
|
|
85
|
+
}
|
|
86
|
+
);
|