@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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import { Modules } from "@medusajs/framework/utils";
|
|
3
|
+
import { updateCampaignsWorkflow } from "@medusajs/medusa/core-flows";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST handler to fix campaign dates - makes all campaigns active for testing
|
|
7
|
+
*/
|
|
8
|
+
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
9
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// Get all campaigns
|
|
13
|
+
const allCampaigns = await promotionService.listCampaigns({});
|
|
14
|
+
|
|
15
|
+
console.log(`Found ${allCampaigns.length} campaigns to fix`);
|
|
16
|
+
|
|
17
|
+
const updatedCampaigns = [];
|
|
18
|
+
|
|
19
|
+
// Update each campaign to be active for the whole year
|
|
20
|
+
for (const campaign of allCampaigns) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await updateCampaignsWorkflow.run({
|
|
23
|
+
input: {
|
|
24
|
+
campaignsData: [
|
|
25
|
+
{
|
|
26
|
+
id: campaign.id,
|
|
27
|
+
starts_at: new Date("2024-01-01T00:00:00Z"),
|
|
28
|
+
ends_at: new Date("2025-12-31T23:59:59Z"),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
updatedCampaigns.push({
|
|
35
|
+
id: campaign.id,
|
|
36
|
+
name: campaign.name,
|
|
37
|
+
old_starts_at: campaign.starts_at,
|
|
38
|
+
old_ends_at: campaign.ends_at,
|
|
39
|
+
new_starts_at: "2024-01-01T00:00:00Z",
|
|
40
|
+
new_ends_at: "2025-12-31T23:59:59Z",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(`✅ Fixed dates for campaign: ${campaign.name}`);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(`❌ Failed to update campaign ${campaign.id}:`, error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
res.status(200).json({
|
|
50
|
+
success: true,
|
|
51
|
+
message: `Updated ${updatedCampaigns.length} campaigns`,
|
|
52
|
+
campaigns: updatedCampaigns,
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error("Error fixing campaign dates:", error);
|
|
56
|
+
res.status(500).json({
|
|
57
|
+
error: "Failed to fix campaign dates",
|
|
58
|
+
details: error instanceof Error ? error.message : String(error),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* GET handler to show current campaign dates
|
|
65
|
+
*/
|
|
66
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
67
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
68
|
+
const now = new Date();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const allCampaigns = await promotionService.listCampaigns({});
|
|
72
|
+
|
|
73
|
+
const campaignDates = allCampaigns.map((campaign: any) => {
|
|
74
|
+
const startsAt = new Date(campaign.starts_at);
|
|
75
|
+
const endsAt = new Date(campaign.ends_at);
|
|
76
|
+
const isActive = startsAt <= now && endsAt >= now;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
id: campaign.id,
|
|
80
|
+
name: campaign.name,
|
|
81
|
+
starts_at: campaign.starts_at,
|
|
82
|
+
ends_at: campaign.ends_at,
|
|
83
|
+
isActive,
|
|
84
|
+
reason: !isActive
|
|
85
|
+
? (startsAt > now ? "Not started yet" : "Already ended")
|
|
86
|
+
: "Active",
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
res.status(200).json({
|
|
91
|
+
currentTime: now.toISOString(),
|
|
92
|
+
campaigns: campaignDates,
|
|
93
|
+
summary: {
|
|
94
|
+
total: campaignDates.length,
|
|
95
|
+
active: campaignDates.filter((c: any) => c.isActive).length,
|
|
96
|
+
notStarted: campaignDates.filter((c: any) => c.reason === "Not started yet").length,
|
|
97
|
+
ended: campaignDates.filter((c: any) => c.reason === "Already ended").length,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("Error checking campaign dates:", error);
|
|
102
|
+
res.status(500).json({
|
|
103
|
+
error: "Failed to check campaign dates",
|
|
104
|
+
details: error instanceof Error ? error.message : String(error),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import { Modules } from "@medusajs/framework/utils";
|
|
3
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../../../modules/custom-campaigns";
|
|
4
|
+
import CustomCampaignModuleService from "../../../../modules/custom-campaigns/service";
|
|
5
|
+
import { CampaignTypeEnum } from "../../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* POST handler to sync existing campaigns with custom campaign types
|
|
9
|
+
* This creates the missing custom_campaign_type records for existing campaigns
|
|
10
|
+
*/
|
|
11
|
+
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
12
|
+
const customCampaignModuleService =
|
|
13
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
14
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Get all campaigns from promotion service
|
|
18
|
+
const allCampaigns = await promotionService.listCampaigns({});
|
|
19
|
+
console.log(`Found ${allCampaigns.length} total campaigns`);
|
|
20
|
+
|
|
21
|
+
// Get existing custom campaign types
|
|
22
|
+
const existingTypes = await customCampaignModuleService.listCustomCampaignTypes({});
|
|
23
|
+
const existingCampaignIds = new Set(existingTypes.map((ct) => ct.campaign_id));
|
|
24
|
+
console.log(`Found ${existingTypes.length} existing custom campaign types`);
|
|
25
|
+
|
|
26
|
+
const created = [];
|
|
27
|
+
const skipped = [];
|
|
28
|
+
|
|
29
|
+
// Create custom campaign types for campaigns that don't have them
|
|
30
|
+
for (const campaign of allCampaigns) {
|
|
31
|
+
if (existingCampaignIds.has(campaign.id)) {
|
|
32
|
+
skipped.push({
|
|
33
|
+
id: campaign.id,
|
|
34
|
+
name: campaign.name,
|
|
35
|
+
reason: "Already has custom campaign type",
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Determine type based on campaign name or identifier
|
|
42
|
+
let type = CampaignTypeEnum.FlashSale; // Default to flash sale
|
|
43
|
+
|
|
44
|
+
const nameLower = campaign.name?.toLowerCase() || "";
|
|
45
|
+
const identifierLower = campaign.campaign_identifier?.toLowerCase() || "";
|
|
46
|
+
|
|
47
|
+
if (nameLower.includes("bogo") ||
|
|
48
|
+
nameLower.includes("buy") && nameLower.includes("get") ||
|
|
49
|
+
identifierLower.includes("buy-x-get-y")) {
|
|
50
|
+
type = CampaignTypeEnum.BuyXGetY;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = await customCampaignModuleService.createCustomCampaignTypes({
|
|
54
|
+
campaign_id: campaign.id,
|
|
55
|
+
type: type,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
created.push({
|
|
59
|
+
id: campaign.id,
|
|
60
|
+
name: campaign.name,
|
|
61
|
+
type: type,
|
|
62
|
+
custom_type_id: result.id,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log(`✅ Created custom type for campaign: ${campaign.name} (${type})`);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`❌ Failed to create custom type for campaign ${campaign.id}:`, error);
|
|
68
|
+
skipped.push({
|
|
69
|
+
id: campaign.id,
|
|
70
|
+
name: campaign.name,
|
|
71
|
+
reason: error instanceof Error ? error.message : "Unknown error",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
res.status(200).json({
|
|
77
|
+
success: true,
|
|
78
|
+
message: `Synced ${created.length} campaigns`,
|
|
79
|
+
created,
|
|
80
|
+
skipped,
|
|
81
|
+
summary: {
|
|
82
|
+
totalCampaigns: allCampaigns.length,
|
|
83
|
+
existingTypes: existingTypes.length,
|
|
84
|
+
newlyCreated: created.length,
|
|
85
|
+
skipped: skipped.length,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("Error syncing campaigns:", error);
|
|
90
|
+
res.status(500).json({
|
|
91
|
+
error: "Failed to sync campaigns",
|
|
92
|
+
details: error instanceof Error ? error.message : String(error),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* GET handler to check sync status
|
|
99
|
+
*/
|
|
100
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
101
|
+
const customCampaignModuleService =
|
|
102
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
103
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Get all campaigns and custom types
|
|
107
|
+
const allCampaigns = await promotionService.listCampaigns({});
|
|
108
|
+
const customTypes = await customCampaignModuleService.listCustomCampaignTypes({});
|
|
109
|
+
|
|
110
|
+
// Map custom types by campaign ID
|
|
111
|
+
const typeMap = new Map(
|
|
112
|
+
customTypes.map((ct) => [ct.campaign_id, ct])
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Check which campaigns have custom types
|
|
116
|
+
const campaignStatus = allCampaigns.map((campaign: any) => {
|
|
117
|
+
const customType = typeMap.get(campaign.id);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: campaign.id,
|
|
121
|
+
name: campaign.name,
|
|
122
|
+
campaign_identifier: campaign.campaign_identifier,
|
|
123
|
+
hasCustomType: !!customType,
|
|
124
|
+
customType: customType ? {
|
|
125
|
+
id: customType.id,
|
|
126
|
+
type: customType.type,
|
|
127
|
+
} : null,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const withCustomType = campaignStatus.filter((c: any) => c.hasCustomType);
|
|
132
|
+
const withoutCustomType = campaignStatus.filter((c: any) => !c.hasCustomType);
|
|
133
|
+
|
|
134
|
+
res.status(200).json({
|
|
135
|
+
summary: {
|
|
136
|
+
totalCampaigns: allCampaigns.length,
|
|
137
|
+
withCustomType: withCustomType.length,
|
|
138
|
+
withoutCustomType: withoutCustomType.length,
|
|
139
|
+
},
|
|
140
|
+
withCustomType,
|
|
141
|
+
withoutCustomType,
|
|
142
|
+
recommendation: withoutCustomType.length > 0
|
|
143
|
+
? "Run POST /admin/campaigns/sync to create missing custom types"
|
|
144
|
+
: "All campaigns have custom types",
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("Error checking sync status:", error);
|
|
148
|
+
res.status(500).json({
|
|
149
|
+
error: "Failed to check sync status",
|
|
150
|
+
details: error instanceof Error ? error.message : String(error),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
import { CampaignTypeEnum } from "../../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
8
8
|
import { CustomCampaign, createCustomCampaignSchema } from "../route";
|
|
9
9
|
import { updateCustomFlashSaleWorkflow } from "../../../../workflows/custom-campaign/updateCustomFlashSaleWorkflow";
|
|
10
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../../../modules/custom-campaigns";
|
|
11
|
+
import CustomCampaignModuleService from "../../../../modules/custom-campaigns/service";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* GET handler for fetching a specific flash sale by ID
|
|
@@ -21,29 +23,34 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
21
23
|
);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
const
|
|
26
|
+
const customCampaignModuleService =
|
|
27
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
25
28
|
const productService = container.resolve(Modules.PRODUCT);
|
|
29
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
26
30
|
|
|
27
31
|
try {
|
|
32
|
+
console.log(`Fetching flash sale with ID: ${id}`);
|
|
33
|
+
|
|
28
34
|
// First, find the custom campaign type by campaign ID
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
} = await query.graph({
|
|
32
|
-
entity: "custom_campaign_type",
|
|
33
|
-
fields: [
|
|
34
|
-
"id",
|
|
35
|
-
"campaign.*",
|
|
36
|
-
"campaign.promotions.*",
|
|
37
|
-
"campaign.promotions.application_method.*",
|
|
38
|
-
"campaign.promotions.application_method.target_rules.*",
|
|
39
|
-
],
|
|
40
|
-
filters: {
|
|
35
|
+
const customCampaignTypes =
|
|
36
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
41
37
|
type: CampaignTypeEnum.FlashSale,
|
|
42
38
|
campaign_id: id,
|
|
43
|
-
}
|
|
44
|
-
});
|
|
39
|
+
});
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
console.log(`Found ${customCampaignTypes.length} campaign types for ID ${id}`);
|
|
42
|
+
|
|
43
|
+
if (customCampaignTypes.length === 0) {
|
|
44
|
+
throw new MedusaError(
|
|
45
|
+
MedusaError.Types.NOT_FOUND,
|
|
46
|
+
`Flash sale with ID ${id} not found`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fetch the campaign from promotion service
|
|
51
|
+
const campaign = await promotionService.retrieveCampaign(id, {
|
|
52
|
+
relations: ["promotions", "promotions.application_method", "promotions.application_method.target_rules"],
|
|
53
|
+
});
|
|
47
54
|
|
|
48
55
|
if (!campaign) {
|
|
49
56
|
throw new MedusaError(
|
|
@@ -53,24 +60,28 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
// Fetch promotion usage limits for the campaign
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
filters: {
|
|
63
|
+
console.log(`Fetching promotion usage limits for campaign ${id}`);
|
|
64
|
+
const promotionUsageLimits =
|
|
65
|
+
await customCampaignModuleService.listPromotionUsageLimits({
|
|
60
66
|
campaign_id: id,
|
|
61
|
-
}
|
|
62
|
-
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(`Found ${promotionUsageLimits.length} promotion usage limits`);
|
|
63
70
|
|
|
64
71
|
// Create a map of promotion usage limits by promotion ID
|
|
65
72
|
const promotionLimitsMap = new Map();
|
|
66
73
|
promotionUsageLimits.forEach((limit) => {
|
|
67
74
|
promotionLimitsMap.set(limit.promotion_id, limit);
|
|
75
|
+
console.log(` Limit for promotion ${limit.promotion_id}: ${limit.limit} (used: ${limit.used})`);
|
|
68
76
|
});
|
|
69
77
|
|
|
70
78
|
// Process promotions to extract product information
|
|
71
79
|
const products: CustomCampaign["products"] = [];
|
|
72
80
|
for await (const promotion of campaign.promotions ?? []) {
|
|
81
|
+
console.log(`Processing promotion ${promotion.id}`);
|
|
82
|
+
|
|
73
83
|
if (!promotion.application_method?.target_rules?.length) {
|
|
84
|
+
console.log(` No target rules for promotion ${promotion.id}`);
|
|
74
85
|
continue;
|
|
75
86
|
}
|
|
76
87
|
|
|
@@ -80,21 +91,36 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
80
91
|
);
|
|
81
92
|
const promotionLimit = promotionLimitsMap.get(promotion.id);
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
if (!promotionLimit) {
|
|
95
|
+
console.log(` No usage limit found for promotion ${promotion.id}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!promotionLimit.product_id) {
|
|
100
|
+
console.log(` No product_id in usage limit for promotion ${promotion.id}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
try {
|
|
105
|
+
const product = await productService.retrieveProduct(
|
|
106
|
+
promotionLimit.product_id
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (productRule && promotion.application_method.value !== undefined) {
|
|
110
|
+
products.push({
|
|
111
|
+
product: {
|
|
112
|
+
id: product.id,
|
|
113
|
+
title: product.title,
|
|
114
|
+
},
|
|
115
|
+
discountType: promotion.application_method?.type,
|
|
116
|
+
discountValue: promotion.application_method?.value,
|
|
117
|
+
maxQty: promotion.application_method?.max_quantity,
|
|
118
|
+
limit: promotionLimit?.limit,
|
|
119
|
+
});
|
|
120
|
+
console.log(` Added product ${product.title} to response`);
|
|
121
|
+
}
|
|
122
|
+
} catch (productError) {
|
|
123
|
+
console.error(` Error fetching product ${promotionLimit.product_id}:`, productError);
|
|
98
124
|
}
|
|
99
125
|
}
|
|
100
126
|
|
|
@@ -2,11 +2,14 @@ import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
|
2
2
|
import {
|
|
3
3
|
ContainerRegistrationKeys,
|
|
4
4
|
MedusaError,
|
|
5
|
+
Modules,
|
|
5
6
|
} from "@medusajs/framework/utils";
|
|
6
7
|
import { createFindParams } from "@medusajs/medusa/api/utils/validators";
|
|
7
8
|
import z from "zod";
|
|
8
9
|
import { CampaignTypeEnum } from "../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
9
10
|
import { createCustomCampaignWorkflow } from "../../../workflows/custom-campaign/createCustomCampaignWorkflow";
|
|
11
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../../modules/custom-campaigns";
|
|
12
|
+
import CustomCampaignModuleService from "../../../modules/custom-campaigns/service";
|
|
10
13
|
|
|
11
14
|
export const createCustomCampaignSchema = z.object({
|
|
12
15
|
name: z.string().min(1, "Name is required"),
|
|
@@ -61,27 +64,60 @@ export const POST = async (
|
|
|
61
64
|
};
|
|
62
65
|
|
|
63
66
|
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
metadata: { count, take, skip } = {
|
|
68
|
-
count: 0,
|
|
69
|
-
take: 20,
|
|
70
|
-
skip: 0,
|
|
71
|
-
},
|
|
72
|
-
} = await query.graph({
|
|
73
|
-
entity: "custom_campaign_type",
|
|
74
|
-
...req.queryConfig,
|
|
75
|
-
fields: ["id", "campaign.*", "campaign.promotions.*"],
|
|
76
|
-
filters: { type: CampaignTypeEnum.FlashSale },
|
|
77
|
-
});
|
|
67
|
+
const customCampaignModuleService =
|
|
68
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
69
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
78
70
|
|
|
79
|
-
|
|
71
|
+
try {
|
|
72
|
+
console.log("Fetching flash sale campaigns...");
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
74
|
+
// Get all flash sale campaign types
|
|
75
|
+
const customCampaignTypes =
|
|
76
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
77
|
+
type: CampaignTypeEnum.FlashSale,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(`Found ${customCampaignTypes.length} flash sale campaign types`);
|
|
81
|
+
|
|
82
|
+
if (customCampaignTypes.length === 0) {
|
|
83
|
+
return res.status(200).json({
|
|
84
|
+
campaigns: [],
|
|
85
|
+
count: 0,
|
|
86
|
+
limit: 20,
|
|
87
|
+
offset: 0,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Get campaign IDs
|
|
92
|
+
const campaignIds = customCampaignTypes.map((ct) => ct.campaign_id);
|
|
93
|
+
|
|
94
|
+
// Fetch campaigns from promotion service
|
|
95
|
+
const campaigns = await promotionService.listCampaigns(
|
|
96
|
+
{
|
|
97
|
+
id: campaignIds,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
relations: ["promotions"],
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
console.log(`Found ${campaigns.length} campaigns from promotion service`);
|
|
105
|
+
|
|
106
|
+
res.status(200).json({
|
|
107
|
+
campaigns,
|
|
108
|
+
count: campaigns.length,
|
|
109
|
+
limit: 20,
|
|
110
|
+
offset: 0,
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("Error fetching flash sales:", error);
|
|
114
|
+
res.status(500).json({
|
|
115
|
+
campaigns: [],
|
|
116
|
+
count: 0,
|
|
117
|
+
limit: 20,
|
|
118
|
+
offset: 0,
|
|
119
|
+
error: "Failed to fetch flash sales",
|
|
120
|
+
details: error instanceof Error ? error.message : String(error),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
87
123
|
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import { Modules } from "@medusajs/framework/utils";
|
|
3
|
+
import { updateCampaignsWorkflow } from "@medusajs/medusa/core-flows";
|
|
4
|
+
import { CampaignTypeEnum } from "../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* POST handler to force fix all campaigns in one go
|
|
8
|
+
* This will sync types and fix dates
|
|
9
|
+
*/
|
|
10
|
+
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
11
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
12
|
+
const results: any = {
|
|
13
|
+
timestamp: new Date().toISOString(),
|
|
14
|
+
steps: []
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Step 1: Get all campaigns
|
|
19
|
+
const allCampaigns = await promotionService.listCampaigns({});
|
|
20
|
+
results.steps.push({
|
|
21
|
+
step: "Get campaigns",
|
|
22
|
+
status: "✅",
|
|
23
|
+
count: allCampaigns.length
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (allCampaigns.length === 0) {
|
|
27
|
+
return res.status(200).json({
|
|
28
|
+
...results,
|
|
29
|
+
message: "No campaigns found to fix"
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Step 2: Try to get custom campaign module
|
|
34
|
+
let customModule: any;
|
|
35
|
+
try {
|
|
36
|
+
customModule = container.resolve("customCampaign");
|
|
37
|
+
results.steps.push({
|
|
38
|
+
step: "Resolve custom module",
|
|
39
|
+
status: "✅"
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
results.steps.push({
|
|
43
|
+
step: "Resolve custom module",
|
|
44
|
+
status: "❌",
|
|
45
|
+
message: "Module not available - creating types manually"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Step 3: Fix dates for all campaigns
|
|
50
|
+
const dateFixResults = [];
|
|
51
|
+
for (const campaign of allCampaigns) {
|
|
52
|
+
try {
|
|
53
|
+
await updateCampaignsWorkflow.run({
|
|
54
|
+
input: {
|
|
55
|
+
campaignsData: [{
|
|
56
|
+
id: campaign.id,
|
|
57
|
+
starts_at: new Date("2024-01-01T00:00:00Z"),
|
|
58
|
+
ends_at: new Date("2025-12-31T23:59:59Z")
|
|
59
|
+
}]
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
dateFixResults.push({
|
|
63
|
+
id: campaign.id,
|
|
64
|
+
name: campaign.name,
|
|
65
|
+
status: "✅ Dates fixed"
|
|
66
|
+
});
|
|
67
|
+
} catch (e) {
|
|
68
|
+
dateFixResults.push({
|
|
69
|
+
id: campaign.id,
|
|
70
|
+
name: campaign.name,
|
|
71
|
+
status: "❌ Failed",
|
|
72
|
+
error: e instanceof Error ? e.message : String(e)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
results.steps.push({
|
|
78
|
+
step: "Fix campaign dates",
|
|
79
|
+
status: "✅",
|
|
80
|
+
fixed: dateFixResults.filter(r => r.status.includes("✅")).length,
|
|
81
|
+
failed: dateFixResults.filter(r => r.status.includes("❌")).length,
|
|
82
|
+
details: dateFixResults
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Step 4: Create custom types if module is available
|
|
86
|
+
if (customModule) {
|
|
87
|
+
const existingTypes = await customModule.listCustomCampaignTypes({});
|
|
88
|
+
const existingCampaignIds = new Set(existingTypes.map((t: any) => t.campaign_id));
|
|
89
|
+
|
|
90
|
+
const typeCreationResults = [];
|
|
91
|
+
for (const campaign of allCampaigns) {
|
|
92
|
+
if (existingCampaignIds.has(campaign.id)) {
|
|
93
|
+
typeCreationResults.push({
|
|
94
|
+
id: campaign.id,
|
|
95
|
+
name: campaign.name,
|
|
96
|
+
status: "⏭️ Already has type"
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Determine type based on name
|
|
103
|
+
const type = campaign.name?.toLowerCase().includes("bogo") ||
|
|
104
|
+
campaign.name?.toLowerCase().includes("buy") && campaign.name?.toLowerCase().includes("get")
|
|
105
|
+
? CampaignTypeEnum.BuyXGetY
|
|
106
|
+
: CampaignTypeEnum.FlashSale;
|
|
107
|
+
|
|
108
|
+
await customModule.createCustomCampaignTypes({
|
|
109
|
+
campaign_id: campaign.id,
|
|
110
|
+
type: type
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
typeCreationResults.push({
|
|
114
|
+
id: campaign.id,
|
|
115
|
+
name: campaign.name,
|
|
116
|
+
type: type,
|
|
117
|
+
status: "✅ Type created"
|
|
118
|
+
});
|
|
119
|
+
} catch (e) {
|
|
120
|
+
typeCreationResults.push({
|
|
121
|
+
id: campaign.id,
|
|
122
|
+
name: campaign.name,
|
|
123
|
+
status: "❌ Failed",
|
|
124
|
+
error: e instanceof Error ? e.message : String(e)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
results.steps.push({
|
|
130
|
+
step: "Create custom types",
|
|
131
|
+
status: "✅",
|
|
132
|
+
created: typeCreationResults.filter(r => r.status.includes("✅")).length,
|
|
133
|
+
skipped: typeCreationResults.filter(r => r.status.includes("⏭️")).length,
|
|
134
|
+
failed: typeCreationResults.filter(r => r.status.includes("❌")).length,
|
|
135
|
+
details: typeCreationResults
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Step 5: Direct database update as fallback
|
|
140
|
+
if (!customModule) {
|
|
141
|
+
try {
|
|
142
|
+
const db = container.resolve("__pg_connection__");
|
|
143
|
+
|
|
144
|
+
for (const campaign of allCampaigns) {
|
|
145
|
+
const type = campaign.name?.toLowerCase().includes("bogo") ||
|
|
146
|
+
campaign.name?.toLowerCase().includes("buy") && campaign.name?.toLowerCase().includes("get")
|
|
147
|
+
? "buy-x-get-y"
|
|
148
|
+
: "flash-sale";
|
|
149
|
+
|
|
150
|
+
await db.raw(`
|
|
151
|
+
INSERT INTO custom_campaign_type (id, campaign_id, type, created_at, updated_at)
|
|
152
|
+
VALUES (?, ?, ?, NOW(), NOW())
|
|
153
|
+
ON CONFLICT (campaign_id) WHERE deleted_at IS NULL
|
|
154
|
+
DO UPDATE SET type = EXCLUDED.type, updated_at = NOW()
|
|
155
|
+
`, [`cct_${Date.now()}_${campaign.id}`, campaign.id, type]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
results.steps.push({
|
|
159
|
+
step: "Direct DB insert for custom types",
|
|
160
|
+
status: "✅"
|
|
161
|
+
});
|
|
162
|
+
} catch (e) {
|
|
163
|
+
results.steps.push({
|
|
164
|
+
step: "Direct DB insert for custom types",
|
|
165
|
+
status: "❌",
|
|
166
|
+
error: e instanceof Error ? e.message : String(e)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
results.success = true;
|
|
172
|
+
results.message = "Force fix completed";
|
|
173
|
+
|
|
174
|
+
res.status(200).json(results);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
res.status(500).json({
|
|
177
|
+
...results,
|
|
178
|
+
success: false,
|
|
179
|
+
error: "Failed to force fix campaigns",
|
|
180
|
+
details: error instanceof Error ? error.message : String(error),
|
|
181
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|