@lodashventure/medusa-campaign 1.3.12 → 1.4.0
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/admin/index.js +1238 -16
- package/.medusa/server/src/admin/index.mjs +1240 -18
- package/.medusa/server/src/api/admin/campaigns/[id]/detail/route.js +67 -0
- package/.medusa/server/src/api/admin/campaigns/[id]/image/route.js +80 -0
- package/.medusa/server/src/api/admin/campaigns/[id]/thumbnail/route.js +80 -0
- package/.medusa/server/src/api/admin/campaigns/sync/route.js +8 -6
- package/.medusa/server/src/api/admin/flash-sales/[id]/route.js +22 -4
- package/.medusa/server/src/api/middlewares.js +24 -1
- package/.medusa/server/src/api/store/buy-x-get-y/[id]/route.js +1 -1
- package/.medusa/server/src/api/store/buy-x-get-y/products/[productId]/route.js +1 -1
- package/.medusa/server/src/api/store/buy-x-get-y/route.js +3 -1
- package/.medusa/server/src/api/store/campaigns/[id]/route.js +40 -12
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +53 -0
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +22 -0
- package/.medusa/server/src/modules/custom-campaigns/models/campaign-detail.js +26 -0
- package/.medusa/server/src/modules/custom-campaigns/service.js +3 -1
- package/.medusa/server/src/subscribers/order-placed.js +2 -2
- package/.medusa/server/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.js +16 -4
- package/.medusa/server/src/workflows/campaign-detail/update-campaign-detail.js +55 -0
- package/.medusa/server/src/workflows/campaign-detail/upload-campaign-images.js +120 -0
- package/.medusa/server/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.js +13 -14
- package/package.json +7 -5
- package/src/admin/components/campaign-detail-form.tsx +407 -0
- package/src/admin/components/campaign-image-uploader.tsx +313 -0
- package/src/admin/components/markdown-editor.tsx +298 -0
- package/src/admin/routes/flash-sales/[id]/page.tsx +51 -14
- package/src/admin/widgets/campaign-detail-widget.tsx +299 -0
- package/src/admin/widgets/campaign-stats-widget.tsx +238 -0
- package/src/api/admin/campaigns/[id]/detail/route.ts +77 -0
- package/src/api/admin/campaigns/[id]/image/route.ts +87 -0
- package/src/api/admin/campaigns/[id]/thumbnail/route.ts +87 -0
- package/src/api/admin/campaigns/sync/route.ts +53 -28
- package/src/api/admin/flash-sales/[id]/route.ts +50 -19
- package/src/api/middlewares.ts +21 -0
- package/src/api/store/buy-x-get-y/[id]/route.ts +10 -10
- package/src/api/store/buy-x-get-y/products/[productId]/route.ts +11 -12
- package/src/api/store/buy-x-get-y/route.ts +12 -5
- package/src/api/store/campaigns/[id]/route.ts +54 -24
- package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +53 -0
- package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +19 -0
- package/src/modules/custom-campaigns/models/campaign-detail.ts +25 -0
- package/src/modules/custom-campaigns/service.ts +2 -0
- package/src/subscribers/order-placed.ts +0 -2
- package/src/types/index.d.ts +46 -0
- package/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.ts +41 -18
- package/src/workflows/campaign-detail/update-campaign-detail.ts +85 -0
- package/src/workflows/campaign-detail/upload-campaign-images.ts +163 -0
- package/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.ts +23 -22
- package/.medusa/server/src/api/admin/campaigns/fix-dates/route.js +0 -103
- package/.medusa/server/src/api/admin/force-fix/route.js +0 -176
- package/.medusa/server/src/api/admin/test-campaign/route.js +0 -132
- package/src/api/admin/campaigns/fix-dates/route.ts +0 -107
- package/src/api/admin/force-fix/route.ts +0 -184
- package/src/api/admin/test-campaign/route.ts +0 -141
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
|
|
2
|
+
import { uploadCampaignImagesWorkflow } from "../../../../../workflows/campaign-detail/upload-campaign-images";
|
|
3
|
+
import { deleteFilesWorkflow } from "@medusajs/medusa/core-flows";
|
|
4
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../../../../modules/custom-campaigns";
|
|
5
|
+
import CustomCampaignModuleService from "../../../../../modules/custom-campaigns/service";
|
|
6
|
+
|
|
7
|
+
// POST upload campaign thumbnail
|
|
8
|
+
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
9
|
+
const { id: campaign_id } = req.params;
|
|
10
|
+
const file = (req as any).file;
|
|
11
|
+
|
|
12
|
+
if (!file) {
|
|
13
|
+
return res.status(400).json({
|
|
14
|
+
error: "No file provided",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const { result } = await uploadCampaignImagesWorkflow(req.scope).run({
|
|
20
|
+
input: {
|
|
21
|
+
campaign_id,
|
|
22
|
+
files: [
|
|
23
|
+
{
|
|
24
|
+
filename: Buffer.from(file.originalname, "latin1").toString("utf8"),
|
|
25
|
+
mimeType: file.mimetype,
|
|
26
|
+
content: file.buffer.toString("binary"),
|
|
27
|
+
access: "public" as const,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
image_type: "thumbnail",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
res.status(200).json({ campaign_detail: result });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("Error uploading campaign thumbnail:", error);
|
|
37
|
+
res.status(500).json({
|
|
38
|
+
error: "Failed to upload campaign thumbnail",
|
|
39
|
+
details: error instanceof Error ? error.message : String(error),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// DELETE campaign thumbnail
|
|
45
|
+
export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
46
|
+
const { id: campaign_id } = req.params;
|
|
47
|
+
const customCampaignModuleService =
|
|
48
|
+
req.scope.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const [campaignDetail] =
|
|
52
|
+
await customCampaignModuleService.listCampaignDetails({
|
|
53
|
+
campaign_id,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!campaignDetail || !campaignDetail.thumbnail_file_id) {
|
|
57
|
+
return res.status(404).json({
|
|
58
|
+
error: "Campaign thumbnail not found",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Delete file
|
|
63
|
+
await deleteFilesWorkflow(req.scope).run({
|
|
64
|
+
input: {
|
|
65
|
+
ids: [campaignDetail.thumbnail_file_id],
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Update campaign detail
|
|
70
|
+
const [updatedDetail] =
|
|
71
|
+
await customCampaignModuleService.updateCampaignDetails([
|
|
72
|
+
{
|
|
73
|
+
id: campaignDetail.id,
|
|
74
|
+
thumbnail_url: null,
|
|
75
|
+
thumbnail_file_id: null,
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
res.status(200).json({ campaign_detail: updatedDetail });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("Error deleting campaign thumbnail:", error);
|
|
82
|
+
res.status(500).json({
|
|
83
|
+
error: "Failed to delete campaign thumbnail",
|
|
84
|
+
details: error instanceof Error ? error.message : String(error),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
@@ -19,12 +19,24 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
19
19
|
console.log(`Found ${allCampaigns.length} total campaigns`);
|
|
20
20
|
|
|
21
21
|
// Get existing custom campaign types
|
|
22
|
-
const existingTypes =
|
|
23
|
-
|
|
22
|
+
const existingTypes =
|
|
23
|
+
await customCampaignModuleService.listCustomCampaignTypes({});
|
|
24
|
+
const existingCampaignIds = new Set(
|
|
25
|
+
existingTypes.map((ct) => ct.campaign_id),
|
|
26
|
+
);
|
|
24
27
|
console.log(`Found ${existingTypes.length} existing custom campaign types`);
|
|
25
28
|
|
|
26
|
-
const created
|
|
27
|
-
|
|
29
|
+
const created: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
name: string | undefined;
|
|
32
|
+
type: CampaignTypeEnum;
|
|
33
|
+
custom_type_id: string;
|
|
34
|
+
}> = [];
|
|
35
|
+
const skipped: Array<{
|
|
36
|
+
id: string;
|
|
37
|
+
name: string | undefined;
|
|
38
|
+
reason: string;
|
|
39
|
+
}> = [];
|
|
28
40
|
|
|
29
41
|
// Create custom campaign types for campaigns that don't have them
|
|
30
42
|
for (const campaign of allCampaigns) {
|
|
@@ -42,18 +54,22 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
42
54
|
let type = CampaignTypeEnum.FlashSale; // Default to flash sale
|
|
43
55
|
|
|
44
56
|
const nameLower = campaign.name?.toLowerCase() || "";
|
|
45
|
-
const identifierLower =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
const identifierLower =
|
|
58
|
+
campaign.campaign_identifier?.toLowerCase() || "";
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
nameLower.includes("bogo") ||
|
|
62
|
+
(nameLower.includes("buy") && nameLower.includes("get")) ||
|
|
63
|
+
identifierLower.includes("buy-x-get-y")
|
|
64
|
+
) {
|
|
50
65
|
type = CampaignTypeEnum.BuyXGetY;
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
const result =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
const result =
|
|
69
|
+
await customCampaignModuleService.createCustomCampaignTypes({
|
|
70
|
+
campaign_id: campaign.id,
|
|
71
|
+
type: type,
|
|
72
|
+
});
|
|
57
73
|
|
|
58
74
|
created.push({
|
|
59
75
|
id: campaign.id,
|
|
@@ -62,9 +78,14 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
62
78
|
custom_type_id: result.id,
|
|
63
79
|
});
|
|
64
80
|
|
|
65
|
-
console.log(
|
|
81
|
+
console.log(
|
|
82
|
+
`✅ Created custom type for campaign: ${campaign.name} (${type})`,
|
|
83
|
+
);
|
|
66
84
|
} catch (error) {
|
|
67
|
-
console.error(
|
|
85
|
+
console.error(
|
|
86
|
+
`❌ Failed to create custom type for campaign ${campaign.id}:`,
|
|
87
|
+
error,
|
|
88
|
+
);
|
|
68
89
|
skipped.push({
|
|
69
90
|
id: campaign.id,
|
|
70
91
|
name: campaign.name,
|
|
@@ -105,12 +126,11 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
105
126
|
try {
|
|
106
127
|
// Get all campaigns and custom types
|
|
107
128
|
const allCampaigns = await promotionService.listCampaigns({});
|
|
108
|
-
const customTypes =
|
|
129
|
+
const customTypes =
|
|
130
|
+
await customCampaignModuleService.listCustomCampaignTypes({});
|
|
109
131
|
|
|
110
132
|
// Map custom types by campaign ID
|
|
111
|
-
const typeMap = new Map(
|
|
112
|
-
customTypes.map((ct) => [ct.campaign_id, ct])
|
|
113
|
-
);
|
|
133
|
+
const typeMap = new Map(customTypes.map((ct) => [ct.campaign_id, ct]));
|
|
114
134
|
|
|
115
135
|
// Check which campaigns have custom types
|
|
116
136
|
const campaignStatus = allCampaigns.map((campaign: any) => {
|
|
@@ -121,15 +141,19 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
121
141
|
name: campaign.name,
|
|
122
142
|
campaign_identifier: campaign.campaign_identifier,
|
|
123
143
|
hasCustomType: !!customType,
|
|
124
|
-
customType: customType
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
customType: customType
|
|
145
|
+
? {
|
|
146
|
+
id: customType.id,
|
|
147
|
+
type: customType.type,
|
|
148
|
+
}
|
|
149
|
+
: null,
|
|
128
150
|
};
|
|
129
151
|
});
|
|
130
152
|
|
|
131
153
|
const withCustomType = campaignStatus.filter((c: any) => c.hasCustomType);
|
|
132
|
-
const withoutCustomType = campaignStatus.filter(
|
|
154
|
+
const withoutCustomType = campaignStatus.filter(
|
|
155
|
+
(c: any) => !c.hasCustomType,
|
|
156
|
+
);
|
|
133
157
|
|
|
134
158
|
res.status(200).json({
|
|
135
159
|
summary: {
|
|
@@ -139,9 +163,10 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
139
163
|
},
|
|
140
164
|
withCustomType,
|
|
141
165
|
withoutCustomType,
|
|
142
|
-
recommendation:
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
recommendation:
|
|
167
|
+
withoutCustomType.length > 0
|
|
168
|
+
? "Run POST /admin/campaigns/sync to create missing custom types"
|
|
169
|
+
: "All campaigns have custom types",
|
|
145
170
|
});
|
|
146
171
|
} catch (error) {
|
|
147
172
|
console.error("Error checking sync status:", error);
|
|
@@ -150,4 +175,4 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
150
175
|
details: error instanceof Error ? error.message : String(error),
|
|
151
176
|
});
|
|
152
177
|
}
|
|
153
|
-
};
|
|
178
|
+
};
|
|
@@ -19,7 +19,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
19
19
|
if (!id) {
|
|
20
20
|
throw new MedusaError(
|
|
21
21
|
MedusaError.Types.INVALID_DATA,
|
|
22
|
-
"Campaign ID is required"
|
|
22
|
+
"Campaign ID is required",
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -38,24 +38,30 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
38
38
|
campaign_id: id,
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
console.log(
|
|
41
|
+
console.log(
|
|
42
|
+
`Found ${customCampaignTypes.length} campaign types for ID ${id}`,
|
|
43
|
+
);
|
|
42
44
|
|
|
43
45
|
if (customCampaignTypes.length === 0) {
|
|
44
46
|
throw new MedusaError(
|
|
45
47
|
MedusaError.Types.NOT_FOUND,
|
|
46
|
-
`Flash sale with ID ${id} not found
|
|
48
|
+
`Flash sale with ID ${id} not found`,
|
|
47
49
|
);
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// Fetch the campaign from promotion service
|
|
51
53
|
const campaign = await promotionService.retrieveCampaign(id, {
|
|
52
|
-
relations: [
|
|
54
|
+
relations: [
|
|
55
|
+
"promotions",
|
|
56
|
+
"promotions.application_method",
|
|
57
|
+
"promotions.application_method.target_rules",
|
|
58
|
+
],
|
|
53
59
|
});
|
|
54
60
|
|
|
55
61
|
if (!campaign) {
|
|
56
62
|
throw new MedusaError(
|
|
57
63
|
MedusaError.Types.NOT_FOUND,
|
|
58
|
-
`Flash sale with ID ${id} not found
|
|
64
|
+
`Flash sale with ID ${id} not found`,
|
|
59
65
|
);
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -72,9 +78,23 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
72
78
|
const promotionLimitsMap = new Map();
|
|
73
79
|
promotionUsageLimits.forEach((limit) => {
|
|
74
80
|
promotionLimitsMap.set(limit.promotion_id, limit);
|
|
75
|
-
console.log(
|
|
81
|
+
console.log(
|
|
82
|
+
` Limit for promotion ${limit.promotion_id}: ${limit.limit} (used: ${limit.used})`,
|
|
83
|
+
);
|
|
76
84
|
});
|
|
77
85
|
|
|
86
|
+
// Fetch campaign details
|
|
87
|
+
console.log(`Fetching campaign details for campaign ${id}`);
|
|
88
|
+
const [campaignDetail] =
|
|
89
|
+
await customCampaignModuleService.listCampaignDetails({
|
|
90
|
+
campaign_id: id,
|
|
91
|
+
});
|
|
92
|
+
console.log(
|
|
93
|
+
campaignDetail
|
|
94
|
+
? `Found campaign detail with image: ${campaignDetail.image_url}`
|
|
95
|
+
: "No campaign detail found",
|
|
96
|
+
);
|
|
97
|
+
|
|
78
98
|
// Process promotions to extract product information
|
|
79
99
|
const products: CustomCampaign["products"] = [];
|
|
80
100
|
for await (const promotion of campaign.promotions ?? []) {
|
|
@@ -87,7 +107,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
87
107
|
|
|
88
108
|
const productRule = promotion.application_method.target_rules.find(
|
|
89
109
|
(rule) =>
|
|
90
|
-
rule.attribute === "items.product.id" && rule.operator === "eq"
|
|
110
|
+
rule.attribute === "items.product.id" && rule.operator === "eq",
|
|
91
111
|
);
|
|
92
112
|
const promotionLimit = promotionLimitsMap.get(promotion.id);
|
|
93
113
|
|
|
@@ -97,13 +117,15 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
if (!promotionLimit.product_id) {
|
|
100
|
-
console.log(
|
|
120
|
+
console.log(
|
|
121
|
+
` No product_id in usage limit for promotion ${promotion.id}`,
|
|
122
|
+
);
|
|
101
123
|
continue;
|
|
102
124
|
}
|
|
103
125
|
|
|
104
126
|
try {
|
|
105
127
|
const product = await productService.retrieveProduct(
|
|
106
|
-
promotionLimit.product_id
|
|
128
|
+
promotionLimit.product_id,
|
|
107
129
|
);
|
|
108
130
|
|
|
109
131
|
if (productRule && promotion.application_method.value !== undefined) {
|
|
@@ -112,22 +134,31 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
112
134
|
id: product.id,
|
|
113
135
|
title: product.title,
|
|
114
136
|
},
|
|
115
|
-
discountType: promotion.application_method?.type,
|
|
137
|
+
discountType: promotion.application_method?.type as "percentage",
|
|
116
138
|
discountValue: promotion.application_method?.value,
|
|
117
|
-
maxQty: promotion.application_method?.max_quantity,
|
|
118
|
-
limit: promotionLimit?.limit,
|
|
139
|
+
maxQty: promotion.application_method?.max_quantity ?? 0,
|
|
140
|
+
limit: promotionLimit?.limit ?? 0,
|
|
119
141
|
});
|
|
120
142
|
console.log(` Added product ${product.title} to response`);
|
|
121
143
|
}
|
|
122
144
|
} catch (productError) {
|
|
123
|
-
console.error(
|
|
145
|
+
console.error(
|
|
146
|
+
` Error fetching product ${promotionLimit.product_id}:`,
|
|
147
|
+
productError,
|
|
148
|
+
);
|
|
124
149
|
}
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
res.status(200).json({
|
|
128
153
|
...campaign,
|
|
154
|
+
name: campaign.name!,
|
|
155
|
+
description: campaign.description!,
|
|
156
|
+
starts_at: campaign.starts_at!,
|
|
157
|
+
ends_at: campaign.ends_at!,
|
|
158
|
+
type: CampaignTypeEnum.FlashSale,
|
|
129
159
|
products,
|
|
130
|
-
|
|
160
|
+
campaign_detail: campaignDetail || null,
|
|
161
|
+
} satisfies CustomCampaign & { campaign_detail: any });
|
|
131
162
|
} catch (error) {
|
|
132
163
|
if (error instanceof MedusaError) {
|
|
133
164
|
throw error;
|
|
@@ -136,7 +167,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
136
167
|
console.error("Error fetching flash sale:", error);
|
|
137
168
|
throw new MedusaError(
|
|
138
169
|
MedusaError.Types.UNEXPECTED_STATE,
|
|
139
|
-
"An error occurred while fetching the flash sale"
|
|
170
|
+
"An error occurred while fetching the flash sale",
|
|
140
171
|
);
|
|
141
172
|
}
|
|
142
173
|
};
|
|
@@ -146,7 +177,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
146
177
|
*/
|
|
147
178
|
export const PUT = async (
|
|
148
179
|
req: MedusaRequest<CustomCampaign>,
|
|
149
|
-
res: MedusaResponse
|
|
180
|
+
res: MedusaResponse,
|
|
150
181
|
) => {
|
|
151
182
|
const { id } = req.params;
|
|
152
183
|
const body = req.body;
|
|
@@ -154,7 +185,7 @@ export const PUT = async (
|
|
|
154
185
|
if (!id) {
|
|
155
186
|
throw new MedusaError(
|
|
156
187
|
MedusaError.Types.INVALID_DATA,
|
|
157
|
-
"Campaign ID is required"
|
|
188
|
+
"Campaign ID is required",
|
|
158
189
|
);
|
|
159
190
|
}
|
|
160
191
|
|
|
@@ -163,7 +194,7 @@ export const PUT = async (
|
|
|
163
194
|
if (new Date(body.ends_at) < new Date(body.starts_at)) {
|
|
164
195
|
throw new MedusaError(
|
|
165
196
|
MedusaError.Types.INVALID_DATA,
|
|
166
|
-
"End date must be after start date"
|
|
197
|
+
"End date must be after start date",
|
|
167
198
|
);
|
|
168
199
|
}
|
|
169
200
|
|
|
@@ -184,7 +215,7 @@ export const PUT = async (
|
|
|
184
215
|
console.error("Error updating flash sale:", error);
|
|
185
216
|
throw new MedusaError(
|
|
186
217
|
MedusaError.Types.UNEXPECTED_STATE,
|
|
187
|
-
"An error occurred while updating the flash sale"
|
|
218
|
+
"An error occurred while updating the flash sale",
|
|
188
219
|
);
|
|
189
220
|
}
|
|
190
221
|
};
|
package/src/api/middlewares.ts
CHANGED
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
createCustomCampaignSchema,
|
|
8
8
|
GetFlashSalesSchema,
|
|
9
9
|
} from "./admin/flash-sales/route";
|
|
10
|
+
import multer from "multer";
|
|
11
|
+
|
|
12
|
+
const upload = multer({
|
|
13
|
+
storage: multer.memoryStorage(),
|
|
14
|
+
});
|
|
10
15
|
|
|
11
16
|
export default defineMiddlewares([
|
|
12
17
|
{
|
|
@@ -29,4 +34,20 @@ export default defineMiddlewares([
|
|
|
29
34
|
}),
|
|
30
35
|
],
|
|
31
36
|
},
|
|
37
|
+
{
|
|
38
|
+
methods: ["POST"],
|
|
39
|
+
matcher: "/admin/campaigns/:id/image",
|
|
40
|
+
middlewares: [
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
upload.single("file"),
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
methods: ["POST"],
|
|
47
|
+
matcher: "/admin/campaigns/:id/thumbnail",
|
|
48
|
+
middlewares: [
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
upload.single("file"),
|
|
51
|
+
],
|
|
52
|
+
},
|
|
32
53
|
]);
|
|
@@ -13,7 +13,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
13
13
|
if (!id) {
|
|
14
14
|
throw new MedusaError(
|
|
15
15
|
MedusaError.Types.INVALID_DATA,
|
|
16
|
-
"Campaign ID is required"
|
|
16
|
+
"Campaign ID is required",
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -35,7 +35,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
35
35
|
if (customCampaignTypes.length === 0) {
|
|
36
36
|
throw new MedusaError(
|
|
37
37
|
MedusaError.Types.NOT_FOUND,
|
|
38
|
-
`Buy X Get Y campaign with ID ${id} not found
|
|
38
|
+
`Buy X Get Y campaign with ID ${id} not found`,
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -45,19 +45,19 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
45
45
|
if (!campaign) {
|
|
46
46
|
throw new MedusaError(
|
|
47
47
|
MedusaError.Types.NOT_FOUND,
|
|
48
|
-
`Buy X Get Y campaign with ID ${id} not found
|
|
48
|
+
`Buy X Get Y campaign with ID ${id} not found`,
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Check if campaign is active
|
|
53
|
-
const startsAt = new Date(campaign.starts_at);
|
|
54
|
-
const endsAt = new Date(campaign.ends_at);
|
|
53
|
+
const startsAt = new Date(campaign.starts_at!);
|
|
54
|
+
const endsAt = new Date(campaign.ends_at!);
|
|
55
55
|
const isActive = startsAt <= now && endsAt >= now;
|
|
56
56
|
|
|
57
57
|
if (!isActive) {
|
|
58
58
|
throw new MedusaError(
|
|
59
59
|
MedusaError.Types.NOT_ALLOWED,
|
|
60
|
-
"This campaign is not currently active"
|
|
60
|
+
"This campaign is not currently active",
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -69,13 +69,13 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
69
69
|
|
|
70
70
|
// Filter out configs that have reached their limit
|
|
71
71
|
const availableConfigs = buyXGetYConfigs.filter(
|
|
72
|
-
(config) => !config.limit || config.used < config.limit
|
|
72
|
+
(config) => !config.limit || config.used < config.limit,
|
|
73
73
|
);
|
|
74
74
|
|
|
75
75
|
if (availableConfigs.length === 0) {
|
|
76
76
|
throw new MedusaError(
|
|
77
77
|
MedusaError.Types.NOT_ALLOWED,
|
|
78
|
-
"This campaign has no available promotions"
|
|
78
|
+
"This campaign has no available promotions",
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -119,7 +119,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
119
119
|
rewardValue: config.reward_value,
|
|
120
120
|
remaining: config.limit ? config.limit - config.used : null,
|
|
121
121
|
};
|
|
122
|
-
})
|
|
122
|
+
}),
|
|
123
123
|
);
|
|
124
124
|
|
|
125
125
|
res.status(200).json({
|
|
@@ -140,7 +140,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
140
140
|
console.error("Error fetching Buy X Get Y campaign:", error);
|
|
141
141
|
throw new MedusaError(
|
|
142
142
|
MedusaError.Types.UNEXPECTED_STATE,
|
|
143
|
-
"An error occurred while fetching the campaign"
|
|
143
|
+
"An error occurred while fetching the campaign",
|
|
144
144
|
);
|
|
145
145
|
}
|
|
146
146
|
};
|
|
@@ -2,7 +2,6 @@ import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
|
2
2
|
import { MedusaError, Modules } from "@medusajs/framework/utils";
|
|
3
3
|
import { CUSTOM_CAMPAIGN_MODULE } from "../../../../../modules/custom-campaigns";
|
|
4
4
|
import CustomCampaignModuleService from "../../../../../modules/custom-campaigns/service";
|
|
5
|
-
import { CampaignTypeEnum } from "../../../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* GET handler to check if a product has any active BOGO promotions
|
|
@@ -14,7 +13,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
14
13
|
if (!productId) {
|
|
15
14
|
throw new MedusaError(
|
|
16
15
|
MedusaError.Types.INVALID_DATA,
|
|
17
|
-
"Product ID is required"
|
|
16
|
+
"Product ID is required",
|
|
18
17
|
);
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -46,7 +45,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
46
45
|
|
|
47
46
|
// Get campaign details for each config
|
|
48
47
|
const campaignIds = Array.from(
|
|
49
|
-
new Set(buyXGetYConfigs.map((config) => config.campaign_id))
|
|
48
|
+
new Set(buyXGetYConfigs.map((config) => config.campaign_id)),
|
|
50
49
|
);
|
|
51
50
|
|
|
52
51
|
const campaigns = await promotionService.listCampaigns({
|
|
@@ -61,14 +60,14 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
61
60
|
const endsAt = new Date(campaign.ends_at);
|
|
62
61
|
return startsAt <= now && endsAt >= now;
|
|
63
62
|
})
|
|
64
|
-
.map((campaign: any) => campaign.id)
|
|
63
|
+
.map((campaign: any) => campaign.id),
|
|
65
64
|
);
|
|
66
65
|
|
|
67
66
|
// Filter configs by active campaigns and availability
|
|
68
67
|
const activeConfigs = buyXGetYConfigs.filter(
|
|
69
68
|
(config) =>
|
|
70
69
|
activeCampaignIds.has(config.campaign_id) &&
|
|
71
|
-
(!config.limit || config.used < config.limit)
|
|
70
|
+
(!config.limit || config.used < config.limit),
|
|
72
71
|
);
|
|
73
72
|
|
|
74
73
|
if (activeConfigs.length === 0) {
|
|
@@ -82,19 +81,19 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
82
81
|
const promotions = await Promise.all(
|
|
83
82
|
activeConfigs.map(async (config) => {
|
|
84
83
|
const campaign = campaigns.find(
|
|
85
|
-
(c: any) => c.id === config.campaign_id
|
|
84
|
+
(c: any) => c.id === config.campaign_id,
|
|
86
85
|
);
|
|
87
86
|
|
|
88
87
|
const rewardProduct = await productService.retrieveProduct(
|
|
89
88
|
config.reward_product_id,
|
|
90
89
|
{
|
|
91
90
|
select: ["id", "title", "thumbnail", "handle"],
|
|
92
|
-
}
|
|
91
|
+
},
|
|
93
92
|
);
|
|
94
93
|
|
|
95
94
|
return {
|
|
96
|
-
campaign_id: campaign
|
|
97
|
-
campaign_name: campaign
|
|
95
|
+
campaign_id: campaign!.id,
|
|
96
|
+
campaign_name: campaign!.name,
|
|
98
97
|
trigger_quantity: config.trigger_quantity,
|
|
99
98
|
reward_product: {
|
|
100
99
|
id: rewardProduct.id,
|
|
@@ -106,9 +105,9 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
106
105
|
reward_type: config.reward_type,
|
|
107
106
|
reward_value: config.reward_value,
|
|
108
107
|
remaining: config.limit ? config.limit - config.used : null,
|
|
109
|
-
ends_at: campaign
|
|
108
|
+
ends_at: campaign!.ends_at,
|
|
110
109
|
};
|
|
111
|
-
})
|
|
110
|
+
}),
|
|
112
111
|
);
|
|
113
112
|
|
|
114
113
|
res.status(200).json({
|
|
@@ -123,7 +122,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
123
122
|
console.error("Error checking product BOGO promotions:", error);
|
|
124
123
|
throw new MedusaError(
|
|
125
124
|
MedusaError.Types.UNEXPECTED_STATE,
|
|
126
|
-
"An error occurred while checking promotions"
|
|
125
|
+
"An error occurred while checking promotions",
|
|
127
126
|
);
|
|
128
127
|
}
|
|
129
128
|
};
|
|
@@ -15,6 +15,8 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
17
|
const now = new Date();
|
|
18
|
+
const take = parseInt(req.query.limit as string) || 20;
|
|
19
|
+
const skip = parseInt(req.query.offset as string) || 0;
|
|
18
20
|
|
|
19
21
|
// Get all Buy X Get Y campaign types
|
|
20
22
|
const customCampaignTypes =
|
|
@@ -22,7 +24,9 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
22
24
|
type: CampaignTypeEnum.BuyXGetY,
|
|
23
25
|
});
|
|
24
26
|
|
|
25
|
-
console.log(
|
|
27
|
+
console.log(
|
|
28
|
+
`Found ${customCampaignTypes.length} Buy X Get Y campaign types`,
|
|
29
|
+
);
|
|
26
30
|
|
|
27
31
|
if (customCampaignTypes.length === 0) {
|
|
28
32
|
return res.status(200).json({
|
|
@@ -59,7 +63,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
59
63
|
|
|
60
64
|
// Filter out configs that have reached their limit
|
|
61
65
|
const availableConfigs = buyXGetYConfigs.filter(
|
|
62
|
-
(config) => !config.limit || config.used < config.limit
|
|
66
|
+
(config) => !config.limit || config.used < config.limit,
|
|
63
67
|
);
|
|
64
68
|
|
|
65
69
|
if (availableConfigs.length === 0) {
|
|
@@ -98,7 +102,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
98
102
|
rewardValue: config.reward_value,
|
|
99
103
|
remaining: config.limit ? config.limit - config.used : null,
|
|
100
104
|
};
|
|
101
|
-
})
|
|
105
|
+
}),
|
|
102
106
|
);
|
|
103
107
|
|
|
104
108
|
return {
|
|
@@ -109,7 +113,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
109
113
|
ends_at: campaign.ends_at,
|
|
110
114
|
rules,
|
|
111
115
|
};
|
|
112
|
-
})
|
|
116
|
+
}),
|
|
113
117
|
);
|
|
114
118
|
|
|
115
119
|
// Filter out null campaigns (those with no available rules)
|
|
@@ -122,7 +126,10 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
122
126
|
offset: skip,
|
|
123
127
|
});
|
|
124
128
|
} catch (error) {
|
|
125
|
-
console.error(
|
|
129
|
+
console.error(
|
|
130
|
+
"Error fetching Buy X Get Y campaigns for storefront:",
|
|
131
|
+
error,
|
|
132
|
+
);
|
|
126
133
|
res.status(500).json({
|
|
127
134
|
campaigns: [],
|
|
128
135
|
count: 0,
|