@lodashventure/medusa-campaign 1.3.12 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +1238 -16
- package/.medusa/server/src/admin/index.mjs +1240 -18
- package/.medusa/server/src/api/admin/campaigns/[id]/detail/route.js +67 -0
- package/.medusa/server/src/api/admin/campaigns/[id]/image/route.js +80 -0
- package/.medusa/server/src/api/admin/campaigns/[id]/thumbnail/route.js +80 -0
- package/.medusa/server/src/api/admin/campaigns/sync/route.js +8 -6
- package/.medusa/server/src/api/admin/flash-sales/[id]/route.js +22 -4
- package/.medusa/server/src/api/middlewares.js +24 -1
- package/.medusa/server/src/api/store/buy-x-get-y/[id]/route.js +1 -1
- package/.medusa/server/src/api/store/buy-x-get-y/products/[productId]/route.js +1 -1
- package/.medusa/server/src/api/store/buy-x-get-y/route.js +3 -1
- package/.medusa/server/src/api/store/campaigns/[id]/route.js +40 -12
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +53 -0
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +22 -0
- package/.medusa/server/src/modules/custom-campaigns/models/campaign-detail.js +26 -0
- package/.medusa/server/src/modules/custom-campaigns/service.js +3 -1
- package/.medusa/server/src/subscribers/order-placed.js +2 -2
- package/.medusa/server/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.js +16 -4
- package/.medusa/server/src/workflows/campaign-detail/update-campaign-detail.js +55 -0
- package/.medusa/server/src/workflows/campaign-detail/upload-campaign-images.js +120 -0
- package/.medusa/server/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.js +13 -14
- package/package.json +7 -5
- package/src/admin/components/campaign-detail-form.tsx +407 -0
- package/src/admin/components/campaign-image-uploader.tsx +313 -0
- package/src/admin/components/markdown-editor.tsx +298 -0
- package/src/admin/routes/flash-sales/[id]/page.tsx +51 -14
- package/src/admin/widgets/campaign-detail-widget.tsx +299 -0
- package/src/admin/widgets/campaign-stats-widget.tsx +238 -0
- package/src/api/admin/campaigns/[id]/detail/route.ts +77 -0
- package/src/api/admin/campaigns/[id]/image/route.ts +87 -0
- package/src/api/admin/campaigns/[id]/thumbnail/route.ts +87 -0
- package/src/api/admin/campaigns/sync/route.ts +53 -28
- package/src/api/admin/flash-sales/[id]/route.ts +50 -19
- package/src/api/middlewares.ts +21 -0
- package/src/api/store/buy-x-get-y/[id]/route.ts +10 -10
- package/src/api/store/buy-x-get-y/products/[productId]/route.ts +11 -12
- package/src/api/store/buy-x-get-y/route.ts +12 -5
- package/src/api/store/campaigns/[id]/route.ts +54 -24
- package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +53 -0
- package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +19 -0
- package/src/modules/custom-campaigns/models/campaign-detail.ts +25 -0
- package/src/modules/custom-campaigns/service.ts +2 -0
- package/src/subscribers/order-placed.ts +0 -2
- package/src/types/index.d.ts +46 -0
- package/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.ts +41 -18
- package/src/workflows/campaign-detail/update-campaign-detail.ts +85 -0
- package/src/workflows/campaign-detail/upload-campaign-images.ts +163 -0
- package/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.ts +23 -22
- package/.medusa/server/src/api/admin/campaigns/fix-dates/route.js +0 -103
- package/.medusa/server/src/api/admin/force-fix/route.js +0 -176
- package/.medusa/server/src/api/admin/test-campaign/route.js +0 -132
- package/src/api/admin/campaigns/fix-dates/route.ts +0 -107
- package/src/api/admin/force-fix/route.ts +0 -184
- package/src/api/admin/test-campaign/route.ts +0 -141
|
@@ -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: [
|
|
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
|
|
40
|
-
|
|
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.
|
|
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.
|
|
227
|
+
await cartService.deleteLineItems([item.id]);
|
|
208
228
|
} catch (error) {
|
|
209
|
-
console.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
|
+
);
|