@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,141 @@
|
|
|
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
|
+
import { createBuyXGetYCampaignWorkflow } from "../../../workflows/custom-campaign/createBuyXGetYCampaignWorkflow";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST handler to create a test Buy X Get Y campaign
|
|
10
|
+
* This creates a campaign with test data to verify the system is working
|
|
11
|
+
*/
|
|
12
|
+
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
13
|
+
const productService = container.resolve(Modules.PRODUCT);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Get first two products from the system
|
|
17
|
+
const products = await productService.listProducts({}, { take: 2 });
|
|
18
|
+
|
|
19
|
+
if (products.length < 2) {
|
|
20
|
+
return res.status(400).json({
|
|
21
|
+
error: "Need at least 2 products in the system to create a test campaign",
|
|
22
|
+
productsFound: products.length
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const [triggerProduct, rewardProduct] = products;
|
|
27
|
+
|
|
28
|
+
// Create a test campaign that is active for the whole year
|
|
29
|
+
const testCampaignData = {
|
|
30
|
+
name: `Test BOGO Campaign ${Date.now()}`,
|
|
31
|
+
description: "Test Buy 1 Get 1 Free campaign for debugging",
|
|
32
|
+
type: CampaignTypeEnum.BuyXGetY,
|
|
33
|
+
starts_at: new Date("2024-01-01T00:00:00Z"),
|
|
34
|
+
ends_at: new Date("2025-12-31T23:59:59Z"),
|
|
35
|
+
rules: [
|
|
36
|
+
{
|
|
37
|
+
triggerProduct: {
|
|
38
|
+
id: triggerProduct.id,
|
|
39
|
+
title: triggerProduct.title || "Test Product A"
|
|
40
|
+
},
|
|
41
|
+
triggerQuantity: 1,
|
|
42
|
+
rewardProduct: {
|
|
43
|
+
id: rewardProduct.id,
|
|
44
|
+
title: rewardProduct.title || "Test Product B"
|
|
45
|
+
},
|
|
46
|
+
rewardQuantity: 1,
|
|
47
|
+
rewardType: "free" as const,
|
|
48
|
+
rewardValue: undefined,
|
|
49
|
+
limit: undefined
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
console.log("Creating test campaign with data:", JSON.stringify(testCampaignData, null, 2));
|
|
55
|
+
|
|
56
|
+
const result = await createBuyXGetYCampaignWorkflow.run({
|
|
57
|
+
input: testCampaignData
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
console.log("Test campaign created successfully:", result);
|
|
61
|
+
|
|
62
|
+
res.status(201).json({
|
|
63
|
+
success: true,
|
|
64
|
+
message: "Test campaign created successfully",
|
|
65
|
+
campaign: result.result?.campaign,
|
|
66
|
+
promotions: result.result?.promotions,
|
|
67
|
+
configs: result.result?.buyXGetYConfigs,
|
|
68
|
+
testData: {
|
|
69
|
+
triggerProduct: {
|
|
70
|
+
id: triggerProduct.id,
|
|
71
|
+
title: triggerProduct.title
|
|
72
|
+
},
|
|
73
|
+
rewardProduct: {
|
|
74
|
+
id: rewardProduct.id,
|
|
75
|
+
title: rewardProduct.title
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("Error creating test campaign:", error);
|
|
81
|
+
res.status(500).json({
|
|
82
|
+
error: "Failed to create test campaign",
|
|
83
|
+
details: error instanceof Error ? error.message : String(error),
|
|
84
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* GET handler to verify test campaign exists
|
|
91
|
+
*/
|
|
92
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
93
|
+
const customCampaignModuleService =
|
|
94
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
95
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Get all Buy X Get Y campaigns
|
|
99
|
+
const customCampaignTypes =
|
|
100
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
101
|
+
type: CampaignTypeEnum.BuyXGetY
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const campaignDetails = [];
|
|
105
|
+
|
|
106
|
+
for (const ct of customCampaignTypes) {
|
|
107
|
+
try {
|
|
108
|
+
const campaign = await promotionService.retrieveCampaign(ct.campaign_id);
|
|
109
|
+
const configs = await customCampaignModuleService.listBuyXGetYConfigs({
|
|
110
|
+
campaign_id: ct.campaign_id
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
campaignDetails.push({
|
|
114
|
+
customCampaignType: ct,
|
|
115
|
+
campaign: {
|
|
116
|
+
id: campaign.id,
|
|
117
|
+
name: campaign.name,
|
|
118
|
+
description: campaign.description,
|
|
119
|
+
starts_at: campaign.starts_at,
|
|
120
|
+
ends_at: campaign.ends_at,
|
|
121
|
+
campaign_identifier: campaign.campaign_identifier
|
|
122
|
+
},
|
|
123
|
+
bogoConfigs: configs
|
|
124
|
+
});
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error(`Error fetching campaign ${ct.campaign_id}:`, e);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
res.status(200).json({
|
|
131
|
+
totalBuyXGetYCampaigns: customCampaignTypes.length,
|
|
132
|
+
campaigns: campaignDetails
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("Error verifying test campaigns:", error);
|
|
136
|
+
res.status(500).json({
|
|
137
|
+
error: "Failed to verify test campaigns",
|
|
138
|
+
details: error instanceof Error ? error.message : String(error)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import { MedusaError, Modules } from "@medusajs/framework/utils";
|
|
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";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET handler for fetching a specific Buy X Get Y campaign for storefront
|
|
9
|
+
*/
|
|
10
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
11
|
+
const { id } = req.params;
|
|
12
|
+
|
|
13
|
+
if (!id) {
|
|
14
|
+
throw new MedusaError(
|
|
15
|
+
MedusaError.Types.INVALID_DATA,
|
|
16
|
+
"Campaign ID is required"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const customCampaignModuleService =
|
|
21
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
22
|
+
const productService = container.resolve(Modules.PRODUCT);
|
|
23
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
|
|
28
|
+
// Find the custom campaign type by campaign ID
|
|
29
|
+
const customCampaignTypes =
|
|
30
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
31
|
+
type: CampaignTypeEnum.BuyXGetY,
|
|
32
|
+
campaign_id: id,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (customCampaignTypes.length === 0) {
|
|
36
|
+
throw new MedusaError(
|
|
37
|
+
MedusaError.Types.NOT_FOUND,
|
|
38
|
+
`Buy X Get Y campaign with ID ${id} not found`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get campaign from promotion service
|
|
43
|
+
const campaign = await promotionService.retrieveCampaign(id);
|
|
44
|
+
|
|
45
|
+
if (!campaign) {
|
|
46
|
+
throw new MedusaError(
|
|
47
|
+
MedusaError.Types.NOT_FOUND,
|
|
48
|
+
`Buy X Get Y campaign with ID ${id} not found`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if campaign is active
|
|
53
|
+
const startsAt = new Date(campaign.starts_at);
|
|
54
|
+
const endsAt = new Date(campaign.ends_at);
|
|
55
|
+
const isActive = startsAt <= now && endsAt >= now;
|
|
56
|
+
|
|
57
|
+
if (!isActive) {
|
|
58
|
+
throw new MedusaError(
|
|
59
|
+
MedusaError.Types.NOT_ALLOWED,
|
|
60
|
+
"This campaign is not currently active"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get BOGO configs for this campaign
|
|
65
|
+
const buyXGetYConfigs =
|
|
66
|
+
await customCampaignModuleService.listBuyXGetYConfigs({
|
|
67
|
+
campaign_id: id,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Filter out configs that have reached their limit
|
|
71
|
+
const availableConfigs = buyXGetYConfigs.filter(
|
|
72
|
+
(config) => !config.limit || config.used < config.limit
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (availableConfigs.length === 0) {
|
|
76
|
+
throw new MedusaError(
|
|
77
|
+
MedusaError.Types.NOT_ALLOWED,
|
|
78
|
+
"This campaign has no available promotions"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build rules with product details
|
|
83
|
+
const rules = await Promise.all(
|
|
84
|
+
availableConfigs.map(async (config) => {
|
|
85
|
+
const [triggerProduct, rewardProduct] = await Promise.all([
|
|
86
|
+
productService.retrieveProduct(config.trigger_product_id, {
|
|
87
|
+
select: ["id", "title", "description", "thumbnail", "handle"],
|
|
88
|
+
relations: ["images", "variants"],
|
|
89
|
+
}),
|
|
90
|
+
productService.retrieveProduct(config.reward_product_id, {
|
|
91
|
+
select: ["id", "title", "description", "thumbnail", "handle"],
|
|
92
|
+
relations: ["images", "variants"],
|
|
93
|
+
}),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
id: config.id,
|
|
98
|
+
triggerProduct: {
|
|
99
|
+
id: triggerProduct.id,
|
|
100
|
+
title: triggerProduct.title,
|
|
101
|
+
description: triggerProduct.description,
|
|
102
|
+
thumbnail: triggerProduct.thumbnail,
|
|
103
|
+
handle: triggerProduct.handle,
|
|
104
|
+
images: triggerProduct.images,
|
|
105
|
+
variants: triggerProduct.variants,
|
|
106
|
+
},
|
|
107
|
+
triggerQuantity: config.trigger_quantity,
|
|
108
|
+
rewardProduct: {
|
|
109
|
+
id: rewardProduct.id,
|
|
110
|
+
title: rewardProduct.title,
|
|
111
|
+
description: rewardProduct.description,
|
|
112
|
+
thumbnail: rewardProduct.thumbnail,
|
|
113
|
+
handle: rewardProduct.handle,
|
|
114
|
+
images: rewardProduct.images,
|
|
115
|
+
variants: rewardProduct.variants,
|
|
116
|
+
},
|
|
117
|
+
rewardQuantity: config.reward_quantity,
|
|
118
|
+
rewardType: config.reward_type,
|
|
119
|
+
rewardValue: config.reward_value,
|
|
120
|
+
remaining: config.limit ? config.limit - config.used : null,
|
|
121
|
+
};
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
res.status(200).json({
|
|
126
|
+
campaign: {
|
|
127
|
+
id: campaign.id,
|
|
128
|
+
name: campaign.name,
|
|
129
|
+
description: campaign.description,
|
|
130
|
+
starts_at: campaign.starts_at,
|
|
131
|
+
ends_at: campaign.ends_at,
|
|
132
|
+
rules,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof MedusaError) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.error("Error fetching Buy X Get Y campaign:", error);
|
|
141
|
+
throw new MedusaError(
|
|
142
|
+
MedusaError.Types.UNEXPECTED_STATE,
|
|
143
|
+
"An error occurred while fetching the campaign"
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import { MedusaError, 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
|
+
* GET handler to check if a product has any active BOGO promotions
|
|
9
|
+
* Useful for displaying BOGO badges on product cards and detail pages
|
|
10
|
+
*/
|
|
11
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
12
|
+
const { productId } = req.params;
|
|
13
|
+
|
|
14
|
+
if (!productId) {
|
|
15
|
+
throw new MedusaError(
|
|
16
|
+
MedusaError.Types.INVALID_DATA,
|
|
17
|
+
"Product ID is required"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const customCampaignModuleService =
|
|
22
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
23
|
+
const productService = container.resolve(Modules.PRODUCT);
|
|
24
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const now = new Date();
|
|
28
|
+
|
|
29
|
+
// Verify product exists
|
|
30
|
+
await productService.retrieveProduct(productId, {
|
|
31
|
+
select: ["id"],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Get all BOGO configs where this product is the trigger
|
|
35
|
+
const buyXGetYConfigs =
|
|
36
|
+
await customCampaignModuleService.listBuyXGetYConfigs({
|
|
37
|
+
trigger_product_id: productId,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (buyXGetYConfigs.length === 0) {
|
|
41
|
+
return res.status(200).json({
|
|
42
|
+
has_promotions: false,
|
|
43
|
+
promotions: [],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get campaign details for each config
|
|
48
|
+
const campaignIds = Array.from(
|
|
49
|
+
new Set(buyXGetYConfigs.map((config) => config.campaign_id))
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const campaigns = await promotionService.listCampaigns({
|
|
53
|
+
id: campaignIds,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Filter for active campaigns
|
|
57
|
+
const activeCampaignIds = new Set(
|
|
58
|
+
campaigns
|
|
59
|
+
.filter((campaign: any) => {
|
|
60
|
+
const startsAt = new Date(campaign.starts_at);
|
|
61
|
+
const endsAt = new Date(campaign.ends_at);
|
|
62
|
+
return startsAt <= now && endsAt >= now;
|
|
63
|
+
})
|
|
64
|
+
.map((campaign: any) => campaign.id)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Filter configs by active campaigns and availability
|
|
68
|
+
const activeConfigs = buyXGetYConfigs.filter(
|
|
69
|
+
(config) =>
|
|
70
|
+
activeCampaignIds.has(config.campaign_id) &&
|
|
71
|
+
(!config.limit || config.used < config.limit)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (activeConfigs.length === 0) {
|
|
75
|
+
return res.status(200).json({
|
|
76
|
+
has_promotions: false,
|
|
77
|
+
promotions: [],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Build promotion details
|
|
82
|
+
const promotions = await Promise.all(
|
|
83
|
+
activeConfigs.map(async (config) => {
|
|
84
|
+
const campaign = campaigns.find(
|
|
85
|
+
(c: any) => c.id === config.campaign_id
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const rewardProduct = await productService.retrieveProduct(
|
|
89
|
+
config.reward_product_id,
|
|
90
|
+
{
|
|
91
|
+
select: ["id", "title", "thumbnail", "handle"],
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
campaign_id: campaign.id,
|
|
97
|
+
campaign_name: campaign.name,
|
|
98
|
+
trigger_quantity: config.trigger_quantity,
|
|
99
|
+
reward_product: {
|
|
100
|
+
id: rewardProduct.id,
|
|
101
|
+
title: rewardProduct.title,
|
|
102
|
+
thumbnail: rewardProduct.thumbnail,
|
|
103
|
+
handle: rewardProduct.handle,
|
|
104
|
+
},
|
|
105
|
+
reward_quantity: config.reward_quantity,
|
|
106
|
+
reward_type: config.reward_type,
|
|
107
|
+
reward_value: config.reward_value,
|
|
108
|
+
remaining: config.limit ? config.limit - config.used : null,
|
|
109
|
+
ends_at: campaign.ends_at,
|
|
110
|
+
};
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
res.status(200).json({
|
|
115
|
+
has_promotions: true,
|
|
116
|
+
promotions,
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof MedusaError) {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.error("Error checking product BOGO promotions:", error);
|
|
124
|
+
throw new MedusaError(
|
|
125
|
+
MedusaError.Types.UNEXPECTED_STATE,
|
|
126
|
+
"An error occurred while checking promotions"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import { Modules } from "@medusajs/framework/utils";
|
|
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";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET handler for listing active Buy X Get Y campaigns for storefront
|
|
9
|
+
*/
|
|
10
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
11
|
+
const customCampaignModuleService =
|
|
12
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
13
|
+
const productService = container.resolve(Modules.PRODUCT);
|
|
14
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const now = new Date();
|
|
18
|
+
|
|
19
|
+
// Get all Buy X Get Y campaign types
|
|
20
|
+
const customCampaignTypes =
|
|
21
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
22
|
+
type: CampaignTypeEnum.BuyXGetY,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
console.log(`Found ${customCampaignTypes.length} Buy X Get Y campaign types`);
|
|
26
|
+
|
|
27
|
+
if (customCampaignTypes.length === 0) {
|
|
28
|
+
return res.status(200).json({
|
|
29
|
+
campaigns: [],
|
|
30
|
+
count: 0,
|
|
31
|
+
limit: 20,
|
|
32
|
+
offset: 0,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get campaign IDs
|
|
37
|
+
const campaignIds = customCampaignTypes.map((ct) => ct.campaign_id);
|
|
38
|
+
|
|
39
|
+
// Fetch campaigns from promotion service
|
|
40
|
+
const allCampaigns = await promotionService.listCampaigns({
|
|
41
|
+
id: campaignIds,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Filter for active campaigns only (time-based)
|
|
45
|
+
const activeCampaigns = allCampaigns.filter((campaign: any) => {
|
|
46
|
+
const startsAt = new Date(campaign.starts_at);
|
|
47
|
+
const endsAt = new Date(campaign.ends_at);
|
|
48
|
+
return startsAt <= now && endsAt >= now;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Enrich with BOGO configuration details
|
|
52
|
+
const enrichedCampaigns = await Promise.all(
|
|
53
|
+
activeCampaigns.map(async (campaign: any) => {
|
|
54
|
+
// Get BOGO configs for this campaign
|
|
55
|
+
const buyXGetYConfigs =
|
|
56
|
+
await customCampaignModuleService.listBuyXGetYConfigs({
|
|
57
|
+
campaign_id: campaign.id,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Filter out configs that have reached their limit
|
|
61
|
+
const availableConfigs = buyXGetYConfigs.filter(
|
|
62
|
+
(config) => !config.limit || config.used < config.limit
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (availableConfigs.length === 0) {
|
|
66
|
+
return null; // Skip campaigns with no available rules
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Build rules with product details
|
|
70
|
+
const rules = await Promise.all(
|
|
71
|
+
availableConfigs.map(async (config) => {
|
|
72
|
+
const [triggerProduct, rewardProduct] = await Promise.all([
|
|
73
|
+
productService.retrieveProduct(config.trigger_product_id, {
|
|
74
|
+
select: ["id", "title", "thumbnail", "handle"],
|
|
75
|
+
}),
|
|
76
|
+
productService.retrieveProduct(config.reward_product_id, {
|
|
77
|
+
select: ["id", "title", "thumbnail", "handle"],
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id: config.id,
|
|
83
|
+
triggerProduct: {
|
|
84
|
+
id: triggerProduct.id,
|
|
85
|
+
title: triggerProduct.title,
|
|
86
|
+
thumbnail: triggerProduct.thumbnail,
|
|
87
|
+
handle: triggerProduct.handle,
|
|
88
|
+
},
|
|
89
|
+
triggerQuantity: config.trigger_quantity,
|
|
90
|
+
rewardProduct: {
|
|
91
|
+
id: rewardProduct.id,
|
|
92
|
+
title: rewardProduct.title,
|
|
93
|
+
thumbnail: rewardProduct.thumbnail,
|
|
94
|
+
handle: rewardProduct.handle,
|
|
95
|
+
},
|
|
96
|
+
rewardQuantity: config.reward_quantity,
|
|
97
|
+
rewardType: config.reward_type,
|
|
98
|
+
rewardValue: config.reward_value,
|
|
99
|
+
remaining: config.limit ? config.limit - config.used : null,
|
|
100
|
+
};
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
id: campaign.id,
|
|
106
|
+
name: campaign.name,
|
|
107
|
+
description: campaign.description,
|
|
108
|
+
starts_at: campaign.starts_at,
|
|
109
|
+
ends_at: campaign.ends_at,
|
|
110
|
+
rules,
|
|
111
|
+
};
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Filter out null campaigns (those with no available rules)
|
|
116
|
+
const validCampaigns = enrichedCampaigns.filter((c) => c !== null);
|
|
117
|
+
|
|
118
|
+
res.status(200).json({
|
|
119
|
+
campaigns: validCampaigns,
|
|
120
|
+
count: validCampaigns.length,
|
|
121
|
+
limit: take,
|
|
122
|
+
offset: skip,
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error("Error fetching Buy X Get Y campaigns for storefront:", error);
|
|
126
|
+
res.status(500).json({
|
|
127
|
+
campaigns: [],
|
|
128
|
+
count: 0,
|
|
129
|
+
limit: 20,
|
|
130
|
+
offset: 0,
|
|
131
|
+
error: "Failed to fetch campaigns",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|