@lodashventure/medusa-campaign 1.1.4 → 1.1.6
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
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
} from "@medusajs/framework/utils";
|
|
7
7
|
import { CampaignTypeEnum } from "../../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
8
8
|
import { CustomCampaign } from "../../../admin/flash-sales/route";
|
|
9
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../../../modules/custom-campaigns";
|
|
10
|
+
import CustomCampaignModuleService from "../../../../modules/custom-campaigns/service";
|
|
9
11
|
|
|
10
12
|
export const GET = async (
|
|
11
13
|
req: MedusaRequest<{ id: string }>,
|
|
@@ -22,112 +24,190 @@ export const GET = async (
|
|
|
22
24
|
|
|
23
25
|
const query = container.resolve(ContainerRegistrationKeys.QUERY);
|
|
24
26
|
const productService = container.resolve(Modules.PRODUCT);
|
|
27
|
+
const customCampaignModuleService =
|
|
28
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
25
29
|
|
|
26
30
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
fields: [
|
|
33
|
-
"id",
|
|
34
|
-
"campaign.*",
|
|
35
|
-
"campaign.promotions.*",
|
|
36
|
-
"campaign.promotions.application_method.*",
|
|
37
|
-
"campaign.promotions.application_method.target_rules.*",
|
|
38
|
-
],
|
|
39
|
-
filters: {
|
|
40
|
-
type: CampaignTypeEnum.FlashSale,
|
|
31
|
+
const now = new Date();
|
|
32
|
+
|
|
33
|
+
// Find the custom campaign type by campaign ID
|
|
34
|
+
const customCampaignTypes =
|
|
35
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
41
36
|
campaign_id: id,
|
|
42
|
-
}
|
|
43
|
-
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (customCampaignTypes.length === 0) {
|
|
40
|
+
throw new MedusaError(
|
|
41
|
+
MedusaError.Types.NOT_FOUND,
|
|
42
|
+
`Campaign with ID ${id} not found`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
44
45
|
|
|
45
|
-
const
|
|
46
|
+
const campaignType = customCampaignTypes[0].type;
|
|
47
|
+
|
|
48
|
+
// Get campaign from promotion service
|
|
49
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
50
|
+
const campaign = await promotionService.retrieveCampaign(id, {
|
|
51
|
+
relations: ["promotions", "promotions.application_method", "promotions.application_method.target_rules"],
|
|
52
|
+
});
|
|
46
53
|
|
|
47
54
|
if (!campaign) {
|
|
48
55
|
throw new MedusaError(
|
|
49
56
|
MedusaError.Types.NOT_FOUND,
|
|
50
|
-
`
|
|
57
|
+
`Campaign with ID ${id} not found`
|
|
51
58
|
);
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
filters: {
|
|
59
|
-
campaign_id: id,
|
|
60
|
-
},
|
|
61
|
-
});
|
|
61
|
+
// Check if campaign is active
|
|
62
|
+
const startsAt = new Date(campaign.starts_at);
|
|
63
|
+
const endsAt = new Date(campaign.ends_at);
|
|
64
|
+
const isActive = startsAt <= now && endsAt >= now;
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
if (!isActive) {
|
|
67
|
+
throw new MedusaError(
|
|
68
|
+
MedusaError.Types.NOT_ALLOWED,
|
|
69
|
+
"This campaign is not currently active"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle different campaign types
|
|
74
|
+
if (campaignType === CampaignTypeEnum.FlashSale) {
|
|
75
|
+
// Fetch promotion usage limits for Flash Sale
|
|
76
|
+
const promotionUsageLimits =
|
|
77
|
+
await customCampaignModuleService.listPromotionUsageLimits({
|
|
78
|
+
campaign_id: id,
|
|
79
|
+
});
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
// Create a map of promotion usage limits by promotion ID
|
|
82
|
+
const promotionLimitsMap = new Map();
|
|
83
|
+
promotionUsageLimits.forEach((limit) => {
|
|
84
|
+
promotionLimitsMap.set(limit.promotion_id, limit);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Process promotions to extract product information
|
|
88
|
+
const products: CustomCampaign["products"] = [];
|
|
89
|
+
for await (const promotion of campaign.promotions ?? []) {
|
|
90
|
+
if (!promotion.application_method?.target_rules?.length) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const promotionLimit = promotionLimitsMap.get(promotion.id);
|
|
95
|
+
const product = await productService.retrieveProduct(
|
|
96
|
+
promotionLimit?.product_id,
|
|
97
|
+
{
|
|
98
|
+
select: ["id", "title", "thumbnail", "handle", "description"],
|
|
99
|
+
relations: ["images"],
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (promotion.application_method.value !== undefined) {
|
|
104
|
+
products.push({
|
|
105
|
+
product: {
|
|
106
|
+
id: product.id,
|
|
107
|
+
title: product.title,
|
|
108
|
+
thumbnail: product.thumbnail,
|
|
109
|
+
handle: product.handle,
|
|
110
|
+
description: product.description,
|
|
111
|
+
images: product.images,
|
|
112
|
+
},
|
|
113
|
+
discountType: promotion.application_method?.type,
|
|
114
|
+
discountValue: promotion.application_method?.value,
|
|
115
|
+
maxQty: promotion.application_method?.max_quantity,
|
|
116
|
+
limit: promotionLimit?.limit,
|
|
117
|
+
used: promotionLimit?.used,
|
|
118
|
+
remaining: promotionLimit?.limit
|
|
119
|
+
? promotionLimit.limit - promotionLimit.used
|
|
120
|
+
: null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
74
123
|
}
|
|
75
124
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
125
|
+
res.status(200).json({
|
|
126
|
+
id: campaign.id,
|
|
127
|
+
name: campaign.name,
|
|
128
|
+
description: campaign.description,
|
|
129
|
+
type: campaignType,
|
|
130
|
+
starts_at: campaign.starts_at,
|
|
131
|
+
ends_at: campaign.ends_at,
|
|
132
|
+
products,
|
|
133
|
+
});
|
|
134
|
+
} else if (campaignType === CampaignTypeEnum.BuyXGetY) {
|
|
135
|
+
// Get BOGO configs for this campaign
|
|
136
|
+
const buyXGetYConfigs =
|
|
137
|
+
await customCampaignModuleService.listBuyXGetYConfigs({
|
|
138
|
+
campaign_id: id,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Filter out configs that have reached their limit
|
|
142
|
+
const availableConfigs = buyXGetYConfigs.filter(
|
|
143
|
+
(config) => !config.limit || config.used < config.limit
|
|
79
144
|
);
|
|
80
|
-
const promotionLimit = promotionLimitsMap.get(promotion.id);
|
|
81
145
|
|
|
82
|
-
|
|
83
|
-
|
|
146
|
+
// Build rules with product details
|
|
147
|
+
const rules = await Promise.all(
|
|
148
|
+
availableConfigs.map(async (config) => {
|
|
149
|
+
const [triggerProduct, rewardProduct] = await Promise.all([
|
|
150
|
+
productService.retrieveProduct(config.trigger_product_id, {
|
|
151
|
+
select: ["id", "title", "description", "thumbnail", "handle"],
|
|
152
|
+
relations: ["images"],
|
|
153
|
+
}),
|
|
154
|
+
productService.retrieveProduct(config.reward_product_id, {
|
|
155
|
+
select: ["id", "title", "description", "thumbnail", "handle"],
|
|
156
|
+
relations: ["images"],
|
|
157
|
+
}),
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
id: config.id,
|
|
162
|
+
triggerProduct: {
|
|
163
|
+
id: triggerProduct.id,
|
|
164
|
+
title: triggerProduct.title,
|
|
165
|
+
description: triggerProduct.description,
|
|
166
|
+
thumbnail: triggerProduct.thumbnail,
|
|
167
|
+
handle: triggerProduct.handle,
|
|
168
|
+
images: triggerProduct.images,
|
|
169
|
+
},
|
|
170
|
+
triggerQuantity: config.trigger_quantity,
|
|
171
|
+
rewardProduct: {
|
|
172
|
+
id: rewardProduct.id,
|
|
173
|
+
title: rewardProduct.title,
|
|
174
|
+
description: rewardProduct.description,
|
|
175
|
+
thumbnail: rewardProduct.thumbnail,
|
|
176
|
+
handle: rewardProduct.handle,
|
|
177
|
+
images: rewardProduct.images,
|
|
178
|
+
},
|
|
179
|
+
rewardQuantity: config.reward_quantity,
|
|
180
|
+
rewardType: config.reward_type,
|
|
181
|
+
rewardValue: config.reward_value,
|
|
182
|
+
remaining: config.limit ? config.limit - config.used : null,
|
|
183
|
+
};
|
|
184
|
+
})
|
|
84
185
|
);
|
|
85
186
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
187
|
+
res.status(200).json({
|
|
188
|
+
id: campaign.id,
|
|
189
|
+
name: campaign.name,
|
|
190
|
+
description: campaign.description,
|
|
191
|
+
type: campaignType,
|
|
192
|
+
starts_at: campaign.starts_at,
|
|
193
|
+
ends_at: campaign.ends_at,
|
|
194
|
+
rules,
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
throw new MedusaError(
|
|
198
|
+
MedusaError.Types.INVALID_DATA,
|
|
199
|
+
"Unknown campaign type"
|
|
200
|
+
);
|
|
98
201
|
}
|
|
99
|
-
|
|
100
|
-
const campaignData = {
|
|
101
|
-
id: campaign.id,
|
|
102
|
-
name: campaign.name,
|
|
103
|
-
description: campaign.description,
|
|
104
|
-
type: campaign.type,
|
|
105
|
-
startsAt: campaign.starts_at,
|
|
106
|
-
endsAt: campaign.ends_at,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const productData = products.map((product) => ({
|
|
110
|
-
id: product.product.id,
|
|
111
|
-
title: product.product.title,
|
|
112
|
-
discountType: product.discountType,
|
|
113
|
-
discountValue: product.discountValue,
|
|
114
|
-
maxQty: product.maxQty,
|
|
115
|
-
limit: product.limit,
|
|
116
|
-
}));
|
|
117
|
-
|
|
118
|
-
res.status(200).json({
|
|
119
|
-
...campaignData,
|
|
120
|
-
products: productData,
|
|
121
|
-
});
|
|
122
202
|
} catch (error) {
|
|
123
203
|
if (error instanceof MedusaError) {
|
|
124
204
|
throw error;
|
|
125
205
|
}
|
|
126
206
|
|
|
127
|
-
console.error("Error fetching
|
|
207
|
+
console.error("Error fetching campaign:", error);
|
|
128
208
|
throw new MedusaError(
|
|
129
209
|
MedusaError.Types.UNEXPECTED_STATE,
|
|
130
|
-
"An error occurred while fetching the
|
|
210
|
+
"An error occurred while fetching the campaign"
|
|
131
211
|
);
|
|
132
212
|
}
|
|
133
213
|
};
|
|
@@ -1,36 +1,147 @@
|
|
|
1
1
|
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
-
import { ContainerRegistrationKeys } from "@medusajs/framework/utils";
|
|
2
|
+
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils";
|
|
3
3
|
import { CampaignTypeEnum } from "../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
4
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../../modules/custom-campaigns";
|
|
5
|
+
import CustomCampaignModuleService from "../../../modules/custom-campaigns/service";
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* GET handler for listing all active campaigns (Flash Sales + Buy X Get Y) for storefront
|
|
9
|
+
*/
|
|
5
10
|
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const customCampaignModuleService =
|
|
12
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
13
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
14
|
+
const now = new Date();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Get the campaign type filter from query params (optional)
|
|
18
|
+
const campaignType = req.query?.type as CampaignTypeEnum | undefined;
|
|
19
|
+
|
|
20
|
+
const filters: any = {};
|
|
21
|
+
if (campaignType && Object.values(CampaignTypeEnum).includes(campaignType)) {
|
|
22
|
+
filters.type = campaignType;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log("=== Store Campaigns Debug ===");
|
|
26
|
+
console.log("Current time:", now.toISOString());
|
|
27
|
+
console.log("Requested type filter:", campaignType);
|
|
28
|
+
|
|
29
|
+
// Get all custom campaign types (including soft deleted check)
|
|
30
|
+
const allCustomCampaignTypes =
|
|
31
|
+
await customCampaignModuleService.listCustomCampaignTypes({});
|
|
32
|
+
|
|
33
|
+
console.log(`Total custom campaign types (unfiltered): ${allCustomCampaignTypes.length}`);
|
|
34
|
+
allCustomCampaignTypes.forEach(ct => {
|
|
35
|
+
console.log(` - Type: ${ct.type}, Campaign ID: ${ct.campaign_id}, Deleted: ${ct.deleted_at}`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Apply filters
|
|
39
|
+
const customCampaignTypes = filters.type
|
|
40
|
+
? allCustomCampaignTypes.filter(ct => ct.type === filters.type && !ct.deleted_at)
|
|
41
|
+
: allCustomCampaignTypes.filter(ct => !ct.deleted_at);
|
|
42
|
+
|
|
43
|
+
console.log(`Filtered custom campaign types: ${customCampaignTypes.length}`);
|
|
44
|
+
|
|
45
|
+
if (customCampaignTypes.length === 0) {
|
|
46
|
+
console.log("No custom campaign types found after filtering");
|
|
47
|
+
return res.status(200).json({
|
|
48
|
+
campaigns: [],
|
|
49
|
+
count: 0,
|
|
50
|
+
limit: 20,
|
|
51
|
+
offset: 0,
|
|
52
|
+
debug: {
|
|
53
|
+
message: "No custom campaign types found",
|
|
54
|
+
filters: filters,
|
|
55
|
+
totalUnfiltered: allCustomCampaignTypes.length
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Get campaign IDs
|
|
61
|
+
const campaignIds = customCampaignTypes.map((ct) => ct.campaign_id);
|
|
62
|
+
console.log("Campaign IDs to fetch:", JSON.stringify(campaignIds));
|
|
63
|
+
|
|
64
|
+
// Fetch ALL campaigns first for debugging
|
|
65
|
+
const allCampaigns = await promotionService.listCampaigns({});
|
|
66
|
+
console.log(`Total campaigns in system: ${allCampaigns.length}`);
|
|
67
|
+
|
|
68
|
+
// Then fetch specific campaigns
|
|
69
|
+
const campaigns = campaignIds.length > 0
|
|
70
|
+
? allCampaigns.filter((c: any) => campaignIds.includes(c.id))
|
|
71
|
+
: [];
|
|
72
|
+
|
|
73
|
+
console.log(`Found ${campaigns.length} matching campaigns`);
|
|
74
|
+
campaigns.forEach((c: any) => {
|
|
75
|
+
console.log(` Campaign: ${c.name} (${c.id})`);
|
|
76
|
+
console.log(` Starts: ${c.starts_at}`);
|
|
77
|
+
console.log(` Ends: ${c.ends_at}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Create a map of campaign_id to campaign_type
|
|
81
|
+
const campaignTypeMap = new Map(
|
|
82
|
+
customCampaignTypes.map((ct) => [ct.campaign_id, ct.type])
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Process ALL campaigns first (without date filter) for debugging
|
|
86
|
+
const allProcessedCampaigns = campaigns.map((campaign: any) => {
|
|
87
|
+
const startsAt = new Date(campaign.starts_at);
|
|
88
|
+
const endsAt = new Date(campaign.ends_at);
|
|
89
|
+
const isActive = startsAt <= now && endsAt >= now;
|
|
90
|
+
|
|
91
|
+
console.log(` Checking campaign "${campaign.name}":`);
|
|
92
|
+
console.log(` Start: ${startsAt.toISOString()} (${startsAt <= now ? 'started' : 'not started'})`);
|
|
93
|
+
console.log(` End: ${endsAt.toISOString()} (${endsAt >= now ? 'not ended' : 'ended'})`);
|
|
94
|
+
console.log(` Active: ${isActive}`);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
id: campaign.id,
|
|
98
|
+
name: campaign.name,
|
|
99
|
+
description: campaign.description,
|
|
100
|
+
campaign_identifier: campaign.campaign_identifier,
|
|
101
|
+
starts_at: campaign.starts_at,
|
|
102
|
+
ends_at: campaign.ends_at,
|
|
103
|
+
campaign_type: campaignTypeMap.get(campaign.id),
|
|
104
|
+
is_active: isActive
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Filter for active campaigns
|
|
109
|
+
const activeCampaigns = allProcessedCampaigns.filter(c => c.is_active);
|
|
110
|
+
|
|
111
|
+
console.log(`Returning ${activeCampaigns.length} active campaigns out of ${allProcessedCampaigns.length} total`);
|
|
112
|
+
|
|
113
|
+
res.status(200).json({
|
|
114
|
+
campaigns: activeCampaigns,
|
|
115
|
+
count: activeCampaigns.length,
|
|
116
|
+
limit: 20,
|
|
117
|
+
offset: 0,
|
|
118
|
+
debug: {
|
|
119
|
+
currentTime: now.toISOString(),
|
|
120
|
+
totalCustomCampaignTypes: allCustomCampaignTypes.length,
|
|
121
|
+
filteredCustomCampaignTypes: customCampaignTypes.length,
|
|
122
|
+
totalCampaigns: allCampaigns.length,
|
|
123
|
+
matchingCampaigns: campaigns.length,
|
|
124
|
+
allProcessedCampaigns: allProcessedCampaigns.length,
|
|
125
|
+
activeCampaigns: activeCampaigns.length,
|
|
126
|
+
inactiveCampaigns: allProcessedCampaigns.filter(c => !c.is_active).map(c => ({
|
|
127
|
+
name: c.name,
|
|
128
|
+
starts_at: c.starts_at,
|
|
129
|
+
ends_at: c.ends_at,
|
|
130
|
+
reason: new Date(c.starts_at) > now ? 'not_started' : 'ended'
|
|
131
|
+
}))
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("Error fetching campaigns for storefront:", error);
|
|
136
|
+
console.error("Stack:", error instanceof Error ? error.stack : 'No stack');
|
|
137
|
+
res.status(500).json({
|
|
138
|
+
campaigns: [],
|
|
10
139
|
count: 0,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"id",
|
|
19
|
-
"campaign.name",
|
|
20
|
-
"campaign.description",
|
|
21
|
-
"campaign.type",
|
|
22
|
-
"campaign.starts_at",
|
|
23
|
-
"campaign.ends_at",
|
|
24
|
-
],
|
|
25
|
-
filters: { type: CampaignTypeEnum.FlashSale },
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const campaigns = customCampaigns.map((campaign) => campaign.campaign);
|
|
29
|
-
|
|
30
|
-
res.status(200).json({
|
|
31
|
-
campaigns,
|
|
32
|
-
count,
|
|
33
|
-
limit: take,
|
|
34
|
-
offset: skip,
|
|
35
|
-
});
|
|
140
|
+
limit: 20,
|
|
141
|
+
offset: 0,
|
|
142
|
+
error: "Failed to fetch campaigns",
|
|
143
|
+
details: error instanceof Error ? error.message : String(error),
|
|
144
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
145
|
+
});
|
|
146
|
+
}
|
|
36
147
|
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import CustomCampaignModule from "./modules/custom-campaigns";
|
|
2
|
+
|
|
3
|
+
// Export the module so MedusaJS can load it
|
|
4
|
+
export default {
|
|
5
|
+
modules: [CustomCampaignModule],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Also export individual modules for direct import
|
|
9
|
+
export { CustomCampaignModule };
|
|
10
|
+
export { CUSTOM_CAMPAIGN_MODULE } from "./modules/custom-campaigns";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20251018000000 extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
// Update custom_campaign_type enum to include 'buy-x-get-y'
|
|
7
|
+
this.addSql(`alter table if exists "custom_campaign_type" drop constraint if exists "custom_campaign_type_type_check";`);
|
|
8
|
+
this.addSql(`alter table if exists "custom_campaign_type" add constraint "custom_campaign_type_type_check" check ("type" in ('flash-sale', 'buy-x-get-y'));`);
|
|
9
|
+
|
|
10
|
+
// Create buy_x_get_y_config table
|
|
11
|
+
this.addSql(`create table if not exists "buy_x_get_y_config" (
|
|
12
|
+
"id" text not null,
|
|
13
|
+
"campaign_id" text not null,
|
|
14
|
+
"promotion_id" text not null,
|
|
15
|
+
"trigger_product_id" text not null,
|
|
16
|
+
"trigger_quantity" integer not null,
|
|
17
|
+
"reward_product_id" text not null,
|
|
18
|
+
"reward_quantity" integer not null,
|
|
19
|
+
"reward_type" text check ("reward_type" in ('free', 'percentage', 'fixed')) not null,
|
|
20
|
+
"reward_value" integer null,
|
|
21
|
+
"limit" integer null,
|
|
22
|
+
"used" integer not null default 0,
|
|
23
|
+
"created_at" timestamptz not null default now(),
|
|
24
|
+
"updated_at" timestamptz not null default now(),
|
|
25
|
+
"deleted_at" timestamptz null,
|
|
26
|
+
constraint "buy_x_get_y_config_pkey" primary key ("id")
|
|
27
|
+
);`);
|
|
28
|
+
|
|
29
|
+
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_buy_x_get_y_config_campaign_promotion" ON "buy_x_get_y_config" (campaign_id, promotion_id) WHERE deleted_at IS NULL;`);
|
|
30
|
+
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_buy_x_get_y_config_deleted_at" ON "buy_x_get_y_config" (deleted_at) WHERE deleted_at IS NULL;`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override async down(): Promise<void> {
|
|
34
|
+
// Drop buy_x_get_y_config table
|
|
35
|
+
this.addSql(`drop table if exists "buy_x_get_y_config" cascade;`);
|
|
36
|
+
|
|
37
|
+
// Revert custom_campaign_type enum
|
|
38
|
+
this.addSql(`alter table if exists "custom_campaign_type" drop constraint if exists "custom_campaign_type_type_check";`);
|
|
39
|
+
this.addSql(`alter table if exists "custom_campaign_type" add constraint "custom_campaign_type_type_check" check ("type" in ('flash-sale'));`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { model } from "@medusajs/framework/utils";
|
|
2
|
+
|
|
3
|
+
const BuyXGetYConfig = model
|
|
4
|
+
.define("buy_x_get_y_config", {
|
|
5
|
+
id: model.id().primaryKey(),
|
|
6
|
+
campaign_id: model.text(),
|
|
7
|
+
promotion_id: model.text(),
|
|
8
|
+
trigger_product_id: model.text(),
|
|
9
|
+
trigger_quantity: model.number(),
|
|
10
|
+
reward_product_id: model.text(),
|
|
11
|
+
reward_quantity: model.number(),
|
|
12
|
+
reward_type: model.enum(["free", "percentage", "fixed"]),
|
|
13
|
+
reward_value: model.number().nullable(),
|
|
14
|
+
limit: model.number().nullable(),
|
|
15
|
+
used: model.number().default(0),
|
|
16
|
+
})
|
|
17
|
+
.indexes([{ on: ["campaign_id", "promotion_id"], unique: true }]);
|
|
18
|
+
|
|
19
|
+
export default BuyXGetYConfig;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { MedusaService } from "@medusajs/framework/utils";
|
|
2
2
|
import CustomCampaignType from "./models/custom-campaign-type";
|
|
3
3
|
import PromotionUsageLimit from "./models/promotion-usage-limit";
|
|
4
|
+
import BuyXGetYConfig from "./models/buy-x-get-y-config";
|
|
4
5
|
|
|
5
6
|
class CustomCampaignModuleService extends MedusaService({
|
|
6
7
|
CustomCampaignType,
|
|
7
8
|
PromotionUsageLimit,
|
|
9
|
+
BuyXGetYConfig,
|
|
8
10
|
}) {}
|
|
9
11
|
|
|
10
12
|
export default CustomCampaignModuleService;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework";
|
|
2
|
+
import { applyBuyXGetYToCartWorkflow } from "../workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow";
|
|
3
|
+
|
|
4
|
+
export default async function handleCartUpdated({
|
|
5
|
+
event: { data },
|
|
6
|
+
}: SubscriberArgs<{ id: string }>) {
|
|
7
|
+
// Apply Buy X Get Y rules to the cart
|
|
8
|
+
await applyBuyXGetYToCartWorkflow.run({
|
|
9
|
+
input: {
|
|
10
|
+
cart_id: data.id,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const config: SubscriberConfig = {
|
|
16
|
+
event: [
|
|
17
|
+
"cart.updated",
|
|
18
|
+
"cart.created",
|
|
19
|
+
"cart-line-item.created",
|
|
20
|
+
"cart-line-item.updated",
|
|
21
|
+
"cart-line-item.deleted",
|
|
22
|
+
],
|
|
23
|
+
};
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework";
|
|
2
2
|
import { updatePromotionUsageWorkflow } from "../workflows/custom-campaign/updatePromotionUsageWorkflow";
|
|
3
|
+
import { updateBuyXGetYUsageWorkflow } from "../workflows/custom-campaign/updateBuyXGetYUsageWorkflow";
|
|
3
4
|
|
|
4
5
|
export default async function updatePromotionUsage({
|
|
5
6
|
event: { data },
|
|
6
7
|
}: SubscriberArgs<{ id: string }>) {
|
|
7
|
-
// update promotion usage
|
|
8
|
+
// update promotion usage for flash sales
|
|
8
9
|
await updatePromotionUsageWorkflow.run({
|
|
9
10
|
input: {
|
|
10
11
|
order_id: data.id,
|
|
11
12
|
},
|
|
12
13
|
});
|
|
14
|
+
|
|
15
|
+
// update promotion usage for Buy X Get Y campaigns
|
|
16
|
+
await updateBuyXGetYUsageWorkflow.run({
|
|
17
|
+
input: {
|
|
18
|
+
order_id: data.id,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
export const config: SubscriberConfig = {
|