@lodashventure/medusa-campaign 1.3.12 → 1.3.13

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 (54) hide show
  1. package/.medusa/server/src/admin/index.js +1238 -16
  2. package/.medusa/server/src/admin/index.mjs +1240 -18
  3. package/.medusa/server/src/api/admin/campaigns/[id]/detail/route.js +67 -0
  4. package/.medusa/server/src/api/admin/campaigns/[id]/image/route.js +80 -0
  5. package/.medusa/server/src/api/admin/campaigns/[id]/thumbnail/route.js +80 -0
  6. package/.medusa/server/src/api/admin/campaigns/sync/route.js +8 -6
  7. package/.medusa/server/src/api/admin/flash-sales/[id]/route.js +22 -4
  8. package/.medusa/server/src/api/middlewares.js +24 -1
  9. package/.medusa/server/src/api/store/buy-x-get-y/[id]/route.js +1 -1
  10. package/.medusa/server/src/api/store/buy-x-get-y/products/[productId]/route.js +1 -1
  11. package/.medusa/server/src/api/store/buy-x-get-y/route.js +3 -1
  12. package/.medusa/server/src/api/store/campaigns/[id]/route.js +40 -12
  13. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +53 -0
  14. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +22 -0
  15. package/.medusa/server/src/modules/custom-campaigns/models/campaign-detail.js +26 -0
  16. package/.medusa/server/src/modules/custom-campaigns/service.js +3 -1
  17. package/.medusa/server/src/subscribers/order-placed.js +2 -2
  18. package/.medusa/server/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.js +16 -4
  19. package/.medusa/server/src/workflows/campaign-detail/update-campaign-detail.js +55 -0
  20. package/.medusa/server/src/workflows/campaign-detail/upload-campaign-images.js +120 -0
  21. package/.medusa/server/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.js +13 -14
  22. package/package.json +7 -5
  23. package/src/admin/components/campaign-detail-form.tsx +407 -0
  24. package/src/admin/components/campaign-image-uploader.tsx +313 -0
  25. package/src/admin/components/markdown-editor.tsx +298 -0
  26. package/src/admin/routes/flash-sales/[id]/page.tsx +51 -14
  27. package/src/admin/widgets/campaign-detail-widget.tsx +299 -0
  28. package/src/admin/widgets/campaign-stats-widget.tsx +238 -0
  29. package/src/api/admin/campaigns/[id]/detail/route.ts +77 -0
  30. package/src/api/admin/campaigns/[id]/image/route.ts +87 -0
  31. package/src/api/admin/campaigns/[id]/thumbnail/route.ts +87 -0
  32. package/src/api/admin/campaigns/sync/route.ts +53 -28
  33. package/src/api/admin/flash-sales/[id]/route.ts +50 -19
  34. package/src/api/middlewares.ts +21 -0
  35. package/src/api/store/buy-x-get-y/[id]/route.ts +10 -10
  36. package/src/api/store/buy-x-get-y/products/[productId]/route.ts +11 -12
  37. package/src/api/store/buy-x-get-y/route.ts +12 -5
  38. package/src/api/store/campaigns/[id]/route.ts +54 -24
  39. package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +53 -0
  40. package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +19 -0
  41. package/src/modules/custom-campaigns/models/campaign-detail.ts +25 -0
  42. package/src/modules/custom-campaigns/service.ts +2 -0
  43. package/src/subscribers/order-placed.ts +0 -2
  44. package/src/types/index.d.ts +46 -0
  45. package/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.ts +41 -18
  46. package/src/workflows/campaign-detail/update-campaign-detail.ts +85 -0
  47. package/src/workflows/campaign-detail/upload-campaign-images.ts +163 -0
  48. package/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.ts +23 -22
  49. package/.medusa/server/src/api/admin/campaigns/fix-dates/route.js +0 -103
  50. package/.medusa/server/src/api/admin/force-fix/route.js +0 -176
  51. package/.medusa/server/src/api/admin/test-campaign/route.js +0 -132
  52. package/src/api/admin/campaigns/fix-dates/route.ts +0 -107
  53. package/src/api/admin/force-fix/route.ts +0 -184
  54. package/src/api/admin/test-campaign/route.ts +0 -141
@@ -11,14 +11,14 @@ import CustomCampaignModuleService from "../../../../modules/custom-campaigns/se
11
11
 
12
12
  export const GET = async (
13
13
  req: MedusaRequest<{ id: string }>,
14
- res: MedusaResponse
14
+ res: MedusaResponse,
15
15
  ) => {
16
16
  const { id } = req.params;
17
17
 
18
18
  if (!id) {
19
19
  throw new MedusaError(
20
20
  MedusaError.Types.INVALID_DATA,
21
- "Campaign ID is required"
21
+ "Campaign ID is required",
22
22
  );
23
23
  }
24
24
 
@@ -39,7 +39,7 @@ export const GET = async (
39
39
  if (customCampaignTypes.length === 0) {
40
40
  throw new MedusaError(
41
41
  MedusaError.Types.NOT_FOUND,
42
- `Campaign with ID ${id} not found`
42
+ `Campaign with ID ${id} not found`,
43
43
  );
44
44
  }
45
45
 
@@ -48,28 +48,38 @@ export const GET = async (
48
48
  // Get campaign from promotion service
49
49
  const promotionService = container.resolve(Modules.PROMOTION);
50
50
  const campaign = await promotionService.retrieveCampaign(id, {
51
- relations: ["promotions", "promotions.application_method", "promotions.application_method.target_rules"],
51
+ relations: [
52
+ "promotions",
53
+ "promotions.application_method",
54
+ "promotions.application_method.target_rules",
55
+ ],
52
56
  });
53
57
 
54
58
  if (!campaign) {
55
59
  throw new MedusaError(
56
60
  MedusaError.Types.NOT_FOUND,
57
- `Campaign with ID ${id} not found`
61
+ `Campaign with ID ${id} not found`,
58
62
  );
59
63
  }
60
64
 
61
65
  // Check if campaign is active
62
- const startsAt = new Date(campaign.starts_at);
63
- const endsAt = new Date(campaign.ends_at);
66
+ const startsAt = new Date(campaign.starts_at!);
67
+ const endsAt = new Date(campaign.ends_at!);
64
68
  const isActive = startsAt <= now && endsAt >= now;
65
69
 
66
70
  if (!isActive) {
67
71
  throw new MedusaError(
68
72
  MedusaError.Types.NOT_ALLOWED,
69
- "This campaign is not currently active"
73
+ "This campaign is not currently active",
70
74
  );
71
75
  }
72
76
 
77
+ // Fetch campaign detail (images, content, etc.)
78
+ const [campaignDetail] =
79
+ await customCampaignModuleService.listCampaignDetails({
80
+ campaign_id: id,
81
+ });
82
+
73
83
  // Handle different campaign types
74
84
  if (campaignType === CampaignTypeEnum.FlashSale) {
75
85
  // Fetch promotion usage limits for Flash Sale
@@ -97,7 +107,7 @@ export const GET = async (
97
107
  {
98
108
  select: ["id", "title", "thumbnail", "handle", "description"],
99
109
  relations: ["images"],
100
- }
110
+ },
101
111
  );
102
112
 
103
113
  if (promotion.application_method.value !== undefined) {
@@ -105,19 +115,11 @@ export const GET = async (
105
115
  product: {
106
116
  id: product.id,
107
117
  title: product.title,
108
- thumbnail: product.thumbnail,
109
- handle: product.handle,
110
- description: product.description,
111
- images: product.images,
112
118
  },
113
- discountType: promotion.application_method?.type,
119
+ discountType: promotion.application_method?.type as "percentage",
114
120
  discountValue: promotion.application_method?.value,
115
- maxQty: promotion.application_method?.max_quantity,
116
- limit: promotionLimit?.limit,
117
- used: promotionLimit?.used,
118
- remaining: promotionLimit?.limit
119
- ? promotionLimit.limit - promotionLimit.used
120
- : null,
121
+ maxQty: promotion.application_method?.max_quantity ?? 0,
122
+ limit: promotionLimit?.limit ?? 0,
121
123
  });
122
124
  }
123
125
  }
@@ -130,6 +132,20 @@ export const GET = async (
130
132
  starts_at: campaign.starts_at,
131
133
  ends_at: campaign.ends_at,
132
134
  products,
135
+ campaign_detail: campaignDetail
136
+ ? {
137
+ image_url: campaignDetail.image_url,
138
+ thumbnail_url: campaignDetail.thumbnail_url,
139
+ detail_content: campaignDetail.detail_content,
140
+ terms_and_conditions: campaignDetail.terms_and_conditions,
141
+ meta_title: campaignDetail.meta_title,
142
+ meta_description: campaignDetail.meta_description,
143
+ meta_keywords: campaignDetail.meta_keywords,
144
+ link_url: campaignDetail.link_url,
145
+ link_text: campaignDetail.link_text,
146
+ display_order: campaignDetail.display_order,
147
+ }
148
+ : null,
133
149
  });
134
150
  } else if (campaignType === CampaignTypeEnum.BuyXGetY) {
135
151
  // Get BOGO configs for this campaign
@@ -140,7 +156,7 @@ export const GET = async (
140
156
 
141
157
  // Filter out configs that have reached their limit
142
158
  const availableConfigs = buyXGetYConfigs.filter(
143
- (config) => !config.limit || config.used < config.limit
159
+ (config) => !config.limit || config.used < config.limit,
144
160
  );
145
161
 
146
162
  // Build rules with product details
@@ -181,7 +197,7 @@ export const GET = async (
181
197
  rewardValue: config.reward_value,
182
198
  remaining: config.limit ? config.limit - config.used : null,
183
199
  };
184
- })
200
+ }),
185
201
  );
186
202
 
187
203
  res.status(200).json({
@@ -192,11 +208,25 @@ export const GET = async (
192
208
  starts_at: campaign.starts_at,
193
209
  ends_at: campaign.ends_at,
194
210
  rules,
211
+ campaign_detail: campaignDetail
212
+ ? {
213
+ image_url: campaignDetail.image_url,
214
+ thumbnail_url: campaignDetail.thumbnail_url,
215
+ detail_content: campaignDetail.detail_content,
216
+ terms_and_conditions: campaignDetail.terms_and_conditions,
217
+ meta_title: campaignDetail.meta_title,
218
+ meta_description: campaignDetail.meta_description,
219
+ meta_keywords: campaignDetail.meta_keywords,
220
+ link_url: campaignDetail.link_url,
221
+ link_text: campaignDetail.link_text,
222
+ display_order: campaignDetail.display_order,
223
+ }
224
+ : null,
195
225
  });
196
226
  } else {
197
227
  throw new MedusaError(
198
228
  MedusaError.Types.INVALID_DATA,
199
- "Unknown campaign type"
229
+ "Unknown campaign type",
200
230
  );
201
231
  }
202
232
  } catch (error) {
@@ -207,7 +237,7 @@ export const GET = async (
207
237
  console.error("Error fetching campaign:", error);
208
238
  throw new MedusaError(
209
239
  MedusaError.Types.UNEXPECTED_STATE,
210
- "An error occurred while fetching the campaign"
240
+ "An error occurred while fetching the campaign",
211
241
  );
212
242
  }
213
243
  };
@@ -0,0 +1,53 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+
3
+ export class Migration20251024000000 extends Migration {
4
+ override async up(): Promise<void> {
5
+ // Create campaign_detail table
6
+ this.addSql(`
7
+ create table if not exists "campaign_detail" (
8
+ "id" text not null,
9
+ "campaign_id" text not null,
10
+ "image_url" text null,
11
+ "image_file_id" text null,
12
+ "thumbnail_url" text null,
13
+ "thumbnail_file_id" text null,
14
+ "detail_content" text null,
15
+ "terms_and_conditions" text null,
16
+ "meta_title" text null,
17
+ "meta_description" text null,
18
+ "link_url" text null,
19
+ "link_text" text null,
20
+ "display_order" integer not null default 0,
21
+ "created_at" timestamptz not null default now(),
22
+ "updated_at" timestamptz not null default now(),
23
+ "deleted_at" timestamptz null,
24
+ constraint "campaign_detail_pkey" primary key ("id")
25
+ );
26
+ `);
27
+
28
+ // Create unique index on campaign_id
29
+ this.addSql(`
30
+ CREATE UNIQUE INDEX IF NOT EXISTS "IDX_campaign_detail_campaign_id"
31
+ ON "campaign_detail" (campaign_id)
32
+ WHERE deleted_at IS NULL;
33
+ `);
34
+
35
+ // Create index for deleted_at
36
+ this.addSql(`
37
+ CREATE INDEX IF NOT EXISTS "IDX_campaign_detail_deleted_at"
38
+ ON "campaign_detail" (deleted_at)
39
+ WHERE deleted_at IS NULL;
40
+ `);
41
+
42
+ // Create index for display_order
43
+ this.addSql(`
44
+ CREATE INDEX IF NOT EXISTS "IDX_campaign_detail_display_order"
45
+ ON "campaign_detail" (display_order);
46
+ `);
47
+ }
48
+
49
+ override async down(): Promise<void> {
50
+ // Drop campaign_detail table
51
+ this.addSql(`drop table if exists "campaign_detail" cascade;`);
52
+ }
53
+ }
@@ -0,0 +1,19 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+
3
+ export class Migration20251025000000 extends Migration {
4
+ override async up(): Promise<void> {
5
+ // Add meta_keywords column to campaign_detail table
6
+ this.addSql(`
7
+ ALTER TABLE "campaign_detail"
8
+ ADD COLUMN IF NOT EXISTS "meta_keywords" text null;
9
+ `);
10
+ }
11
+
12
+ override async down(): Promise<void> {
13
+ // Remove meta_keywords column from campaign_detail table
14
+ this.addSql(`
15
+ ALTER TABLE "campaign_detail"
16
+ DROP COLUMN IF EXISTS "meta_keywords";
17
+ `);
18
+ }
19
+ }
@@ -0,0 +1,25 @@
1
+ import { model } from "@medusajs/framework/utils";
2
+
3
+ const CampaignDetail = model.define("campaign_detail", {
4
+ id: model.id().primaryKey(),
5
+ campaign_id: model.text().unique(),
6
+ // Image fields
7
+ image_url: model.text().nullable(),
8
+ image_file_id: model.text().nullable(),
9
+ thumbnail_url: model.text().nullable(),
10
+ thumbnail_file_id: model.text().nullable(),
11
+ // Rich text content fields
12
+ detail_content: model.text().nullable(), // Markdown content
13
+ terms_and_conditions: model.text().nullable(), // Markdown content
14
+ // SEO and meta fields
15
+ meta_title: model.text().nullable(),
16
+ meta_description: model.text().nullable(),
17
+ meta_keywords: model.text().nullable(),
18
+ // Link field
19
+ link_url: model.text().nullable(),
20
+ link_text: model.text().nullable(),
21
+ // Display order
22
+ display_order: model.number().default(0),
23
+ });
24
+
25
+ export default CampaignDetail;
@@ -2,11 +2,13 @@ import { MedusaService } from "@medusajs/framework/utils";
2
2
  import CustomCampaignType from "./models/custom-campaign-type";
3
3
  import PromotionUsageLimit from "./models/promotion-usage-limit";
4
4
  import BuyXGetYConfig from "./models/buy-x-get-y-config";
5
+ import CampaignDetail from "./models/campaign-detail";
5
6
 
6
7
  class CustomCampaignModuleService extends MedusaService({
7
8
  CustomCampaignType,
8
9
  PromotionUsageLimit,
9
10
  BuyXGetYConfig,
11
+ CampaignDetail,
10
12
  }) {}
11
13
 
12
14
  export default CustomCampaignModuleService;
@@ -1,6 +1,4 @@
1
1
  import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework";
2
- import { updatePromotionUsageWorkflow } from "../workflows/custom-campaign/updatePromotionUsageWorkflow";
3
- import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework";
4
2
  import { updateBuyXGetYUsageWorkflow } from "../workflows/custom-campaign/updateBuyXGetYUsageWorkflow";
5
3
  import { updatePromotionUsageWorkflow } from "../workflows/custom-campaign/updatePromotionUsageWorkflow";
6
4
 
@@ -0,0 +1,46 @@
1
+ import { MedusaRequest as BaseMedusaRequest } from "@medusajs/framework/http";
2
+
3
+ declare global {
4
+ namespace Express {
5
+ namespace Multer {
6
+ interface File {
7
+ fieldname: string;
8
+ originalname: string;
9
+ encoding: string;
10
+ mimetype: string;
11
+ destination: string;
12
+ filename: string;
13
+ path: string;
14
+ buffer: Buffer;
15
+ size: number;
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ export interface MedusaRequestWithFile<TBody = unknown> extends BaseMedusaRequest<TBody> {
22
+ file?: Express.Multer.File;
23
+ }
24
+
25
+ // Type definitions for brand requests
26
+ export interface CreateBrandRequest {
27
+ name: string;
28
+ slug: string;
29
+ description?: string;
30
+ website?: string;
31
+ is_active?: boolean;
32
+ metadata?: Record<string, any>;
33
+ }
34
+
35
+ export interface UpdateBrandRequest {
36
+ name?: string;
37
+ slug?: string;
38
+ description?: string;
39
+ website?: string;
40
+ is_active?: boolean;
41
+ metadata?: Record<string, any>;
42
+ }
43
+
44
+ export interface SetBrandRequest {
45
+ brand_id: string;
46
+ }
@@ -26,19 +26,29 @@ interface RewardItem {
26
26
  };
27
27
  }
28
28
 
29
- const applyBuyXGetYToCartStep = createStep(
29
+ const applyBuyXGetYToCartStep = createStep<
30
+ ApplyBuyXGetYInput,
31
+ { rewardItemsAdded: any[] },
32
+ { cart_id: string; added_items: any[] }
33
+ >(
30
34
  "apply-buy-x-get-y-to-cart-step",
31
35
  async (data: ApplyBuyXGetYInput) => {
32
36
  const customCampaignModuleService =
33
37
  container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
34
38
  const cartService = container.resolve(Modules.CART);
35
39
  const productService = container.resolve(Modules.PRODUCT);
36
- const linkService = container.resolve("linkModuleService");
40
+ const linkService = container.resolve("linkModuleService") as any;
37
41
 
38
42
  // Fetch the cart with items
39
- const cart = await cartService.retrieve(data.cart_id, {
40
- relations: ["items", "items.variant", "items.product"],
41
- });
43
+ const carts = await cartService.listCarts(
44
+ {
45
+ id: [data.cart_id],
46
+ },
47
+ {
48
+ relations: ["items", "items.variant", "items.product"],
49
+ },
50
+ );
51
+ const cart = carts[0];
42
52
 
43
53
  if (!cart || !cart.items || cart.items.length === 0) {
44
54
  return new StepResponse({ rewardItemsAdded: [] });
@@ -63,7 +73,7 @@ const applyBuyXGetYToCartStep = createStep(
63
73
  });
64
74
 
65
75
  const campaignIds = campaignLinks.map(
66
- (link: any) => link[Modules.PROMOTION].campaign_id
76
+ (link: any) => link[Modules.PROMOTION].campaign_id,
67
77
  );
68
78
 
69
79
  const promotionService = container.resolve(Modules.PROMOTION);
@@ -75,7 +85,7 @@ const applyBuyXGetYToCartStep = createStep(
75
85
  const activeCampaigns = campaigns.filter(
76
86
  (campaign: any) =>
77
87
  new Date(campaign.starts_at) <= now &&
78
- new Date(campaign.ends_at) >= now
88
+ new Date(campaign.ends_at) >= now,
79
89
  );
80
90
 
81
91
  if (activeCampaigns.length === 0) {
@@ -90,13 +100,13 @@ const applyBuyXGetYToCartStep = createStep(
90
100
 
91
101
  // Remove ALL existing BOGO reward items to recalculate from scratch
92
102
  const existingBogoItems = cart.items.filter(
93
- (item: any) => item.metadata?.is_bogo_reward === true
103
+ (item: any) => item.metadata?.is_bogo_reward === true,
94
104
  );
95
105
 
96
106
  const removedItemIds: string[] = [];
97
107
  for (const bogoItem of existingBogoItems) {
98
108
  try {
99
- await cartService.removeLineItems(data.cart_id, [bogoItem.id]);
109
+ await cartService.deleteLineItems([bogoItem.id]);
100
110
  removedItemIds.push(bogoItem.id);
101
111
  } catch (error) {
102
112
  console.error(`Failed to remove existing BOGO item:`, error);
@@ -132,14 +142,14 @@ const applyBuyXGetYToCartStep = createStep(
132
142
  // Calculate total quantity of trigger product
133
143
  const totalTriggerQuantity = triggerItems.reduce(
134
144
  (sum, item) => sum + item.quantity,
135
- 0
145
+ 0,
136
146
  );
137
147
 
138
148
  // Check if trigger quantity is met
139
149
  if (totalTriggerQuantity >= config.trigger_quantity) {
140
150
  // Calculate how many times the reward should be given
141
151
  const rewardMultiplier = Math.floor(
142
- totalTriggerQuantity / config.trigger_quantity
152
+ totalTriggerQuantity / config.trigger_quantity,
143
153
  );
144
154
  const totalRewardQuantity = config.reward_quantity * rewardMultiplier;
145
155
 
@@ -148,12 +158,12 @@ const applyBuyXGetYToCartStep = createStep(
148
158
  config.reward_product_id,
149
159
  {
150
160
  relations: ["variants"],
151
- }
161
+ },
152
162
  );
153
163
 
154
164
  if (!rewardProduct.variants || rewardProduct.variants.length === 0) {
155
165
  console.warn(
156
- `No variants found for reward product ${config.reward_product_id}`
166
+ `No variants found for reward product ${config.reward_product_id}`,
157
167
  );
158
168
  continue;
159
169
  }
@@ -178,10 +188,20 @@ const applyBuyXGetYToCartStep = createStep(
178
188
  const addedItems: any[] = [];
179
189
  for (const rewardItem of rewardItemsToAdd) {
180
190
  try {
191
+ const variants = await (productService as any).listVariants({
192
+ id: [rewardItem.variant_id],
193
+ });
194
+ const products = await productService.listProducts({
195
+ id: [rewardItem.product_id],
196
+ });
197
+ const product = products[0];
198
+
181
199
  const addedItem = await cartService.addLineItems(data.cart_id, [
182
200
  {
183
201
  variant_id: rewardItem.variant_id,
184
202
  quantity: rewardItem.quantity,
203
+ title: product?.title || "Reward Item",
204
+ unit_price: 0,
185
205
  metadata: rewardItem.metadata,
186
206
  },
187
207
  ]);
@@ -193,7 +213,7 @@ const applyBuyXGetYToCartStep = createStep(
193
213
 
194
214
  return new StepResponse(
195
215
  { rewardItemsAdded: addedItems },
196
- { cart_id: data.cart_id, added_items: addedItems }
216
+ { cart_id: data.cart_id, added_items: addedItems },
197
217
  );
198
218
  },
199
219
  async (compensationData) => {
@@ -204,12 +224,15 @@ const applyBuyXGetYToCartStep = createStep(
204
224
 
205
225
  for (const item of compensationData.added_items) {
206
226
  try {
207
- await cartService.removeLineItems(compensationData.cart_id, [item.id]);
227
+ await cartService.deleteLineItems([item.id]);
208
228
  } catch (error) {
209
- console.error(`Failed to remove reward item during compensation:`, error);
229
+ console.error(
230
+ `Failed to remove reward item during compensation:`,
231
+ error,
232
+ );
210
233
  }
211
234
  }
212
- }
235
+ },
213
236
  );
214
237
 
215
238
  export const applyBuyXGetYToCartWorkflow = createWorkflow(
@@ -218,5 +241,5 @@ export const applyBuyXGetYToCartWorkflow = createWorkflow(
218
241
  const result = applyBuyXGetYToCartStep(data);
219
242
 
220
243
  return new WorkflowResponse(result);
221
- }
244
+ },
222
245
  );
@@ -0,0 +1,85 @@
1
+ import {
2
+ createStep,
3
+ createWorkflow,
4
+ StepResponse,
5
+ WorkflowResponse,
6
+ } from "@medusajs/framework/workflows-sdk";
7
+ import { CUSTOM_CAMPAIGN_MODULE } from "../../modules/custom-campaigns";
8
+ import CustomCampaignModuleService from "../../modules/custom-campaigns/service";
9
+
10
+ type UpdateCampaignDetailInput = {
11
+ campaign_id: string;
12
+ detail_content?: string;
13
+ terms_and_conditions?: string;
14
+ meta_title?: string;
15
+ meta_description?: string;
16
+ link_url?: string;
17
+ link_text?: string;
18
+ display_order?: number;
19
+ };
20
+
21
+ const updateCampaignDetailStep = createStep(
22
+ "update-campaign-detail",
23
+ async (input: UpdateCampaignDetailInput, { container }) => {
24
+ const logger = container.resolve("logger");
25
+ const customCampaignModuleService: CustomCampaignModuleService =
26
+ container.resolve(CUSTOM_CAMPAIGN_MODULE);
27
+
28
+ const { campaign_id, ...updateData } = input;
29
+
30
+ logger.info(`Updating campaign detail for campaign: ${campaign_id}`);
31
+
32
+ // Get or create campaign detail
33
+ const [existingDetail] =
34
+ await customCampaignModuleService.listCampaignDetails({
35
+ campaign_id,
36
+ });
37
+
38
+ let campaignDetail;
39
+
40
+ if (existingDetail) {
41
+ // Update existing detail
42
+ const updated = await customCampaignModuleService.updateCampaignDetails([
43
+ {
44
+ id: existingDetail.id,
45
+ ...updateData,
46
+ },
47
+ ]);
48
+ campaignDetail = updated[0];
49
+ } else {
50
+ // Create new detail
51
+ campaignDetail = await customCampaignModuleService.createCampaignDetails({
52
+ campaign_id,
53
+ ...updateData,
54
+ });
55
+ }
56
+
57
+ return new StepResponse(campaignDetail, existingDetail);
58
+ },
59
+ async (oldDetail, { container }) => {
60
+ const logger = container.resolve("logger");
61
+ const customCampaignModuleService: CustomCampaignModuleService =
62
+ container.resolve(CUSTOM_CAMPAIGN_MODULE);
63
+
64
+ if (oldDetail) {
65
+ try {
66
+ await customCampaignModuleService.updateCampaignDetails([
67
+ {
68
+ ...oldDetail,
69
+ },
70
+ ]);
71
+ logger.info("Rolled back campaign detail update");
72
+ } catch (error) {
73
+ logger.warn("Failed to rollback campaign detail update");
74
+ }
75
+ }
76
+ },
77
+ );
78
+
79
+ export const updateCampaignDetailWorkflow = createWorkflow(
80
+ "update-campaign-detail",
81
+ (input: UpdateCampaignDetailInput) => {
82
+ const detail = updateCampaignDetailStep(input);
83
+ return new WorkflowResponse(detail);
84
+ },
85
+ );