@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.
Files changed (50) hide show
  1. package/.medusa/server/src/api/admin/buy-x-get-y/[id]/route.js +116 -0
  2. package/.medusa/server/src/api/admin/buy-x-get-y/route.js +83 -0
  3. package/.medusa/server/src/api/admin/campaigns/fix-dates/route.js +103 -0
  4. package/.medusa/server/src/api/admin/campaigns/sync/route.js +138 -0
  5. package/.medusa/server/src/api/admin/flash-sales/[id]/route.js +49 -34
  6. package/.medusa/server/src/api/admin/flash-sales/route.js +46 -19
  7. package/.medusa/server/src/api/admin/force-fix/route.js +176 -0
  8. package/.medusa/server/src/api/admin/test-campaign/route.js +132 -0
  9. package/.medusa/server/src/api/store/buy-x-get-y/[id]/route.js +109 -0
  10. package/.medusa/server/src/api/store/buy-x-get-y/products/[productId]/route.js +94 -0
  11. package/.medusa/server/src/api/store/buy-x-get-y/route.js +114 -0
  12. package/.medusa/server/src/api/store/campaigns/[id]/route.js +132 -70
  13. package/.medusa/server/src/api/store/campaigns/route.js +119 -26
  14. package/.medusa/server/src/index.js +15 -0
  15. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251018000000.js +40 -0
  16. package/.medusa/server/src/modules/custom-campaigns/models/buy-x-get-y-config.js +20 -0
  17. package/.medusa/server/src/modules/custom-campaigns/service.js +3 -1
  18. package/.medusa/server/src/modules/custom-campaigns/types/campaign-type.enum.js +2 -1
  19. package/.medusa/server/src/subscribers/cart-updated.js +23 -0
  20. package/.medusa/server/src/subscribers/order-placed.js +9 -2
  21. package/.medusa/server/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.js +150 -0
  22. package/.medusa/server/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.js +127 -0
  23. package/.medusa/server/src/workflows/custom-campaign/updateBuyXGetYCampaignWorkflow.js +114 -0
  24. package/.medusa/server/src/workflows/custom-campaign/updateBuyXGetYUsageWorkflow.js +51 -0
  25. package/package.json +2 -2
  26. package/src/admin/components/BuyXGetYForm.tsx +422 -0
  27. package/src/api/admin/buy-x-get-y/[id]/route.ts +164 -0
  28. package/src/api/admin/buy-x-get-y/route.ts +104 -0
  29. package/src/api/admin/campaigns/fix-dates/route.ts +107 -0
  30. package/src/api/admin/campaigns/sync/route.ts +153 -0
  31. package/src/api/admin/flash-sales/[id]/route.ts +62 -36
  32. package/src/api/admin/flash-sales/route.ts +57 -21
  33. package/src/api/admin/force-fix/route.ts +184 -0
  34. package/src/api/admin/test-campaign/route.ts +141 -0
  35. package/src/api/store/buy-x-get-y/[id]/route.ts +146 -0
  36. package/src/api/store/buy-x-get-y/products/[productId]/route.ts +129 -0
  37. package/src/api/store/buy-x-get-y/route.ts +134 -0
  38. package/src/api/store/campaigns/[id]/route.ts +159 -79
  39. package/src/api/store/campaigns/route.ts +141 -30
  40. package/src/index.ts +10 -0
  41. package/src/modules/custom-campaigns/migrations/Migration20251018000000.ts +42 -0
  42. package/src/modules/custom-campaigns/models/buy-x-get-y-config.ts +19 -0
  43. package/src/modules/custom-campaigns/service.ts +2 -0
  44. package/src/modules/custom-campaigns/types/campaign-type.enum.ts +1 -0
  45. package/src/subscribers/cart-updated.ts +23 -0
  46. package/src/subscribers/order-placed.ts +9 -1
  47. package/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.ts +222 -0
  48. package/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.ts +210 -0
  49. package/src/workflows/custom-campaign/updateBuyXGetYCampaignWorkflow.ts +190 -0
  50. 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
+ };