@lodashventure/medusa-campaign 1.4.0 → 1.4.2
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 +939 -504
- package/.medusa/server/src/admin/index.mjs +941 -506
- package/.medusa/server/src/api/admin/buy-x-get-y/[id]/route.js +2 -6
- package/.medusa/server/src/api/admin/coupons/[id]/route.js +76 -0
- package/.medusa/server/src/api/admin/coupons/route.js +88 -0
- package/.medusa/server/src/api/middlewares.js +32 -1
- package/.medusa/server/src/api/store/campaigns/route.js +78 -7
- package/.medusa/server/src/api/store/coupons/public/route.js +110 -0
- package/.medusa/server/src/api/store/customers/me/coupons/route.js +148 -0
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +2 -2
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +2 -2
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251101000000.js +16 -0
- package/.medusa/server/src/modules/custom-campaigns/types/campaign-type.enum.js +2 -1
- package/.medusa/server/src/workflows/custom-campaign/createCouponCampaignWorkflow.js +105 -0
- package/.medusa/server/src/workflows/custom-campaign/updateCouponCampaignWorkflow.js +59 -0
- package/.medusa/server/src/workflows/index.js +6 -2
- package/package.json +15 -30
- package/src/admin/components/BuyXGetYForm.tsx +24 -13
- package/src/admin/components/CouponForm.tsx +352 -0
- package/src/admin/components/CouponPage.tsx +104 -0
- package/src/admin/components/ProductSelector.tsx +22 -11
- package/src/admin/hooks/useCouponById.ts +36 -0
- package/src/admin/hooks/useCoupons.ts +46 -0
- package/src/admin/hooks/useFlashSaleById.ts +36 -10
- package/src/admin/hooks/useFlashSales.ts +36 -10
- package/src/admin/routes/coupons/[id]/page.tsx +147 -0
- package/src/admin/routes/coupons/create/page.tsx +49 -0
- package/src/admin/routes/coupons/page.tsx +15 -0
- package/src/admin/routes/flash-sales/[id]/page.tsx +2 -11
- package/src/admin/routes/flash-sales/create/page.tsx +0 -6
- package/src/admin/widgets/campaign-detail-widget.tsx +33 -26
- package/src/api/admin/buy-x-get-y/[id]/route.ts +11 -15
- package/src/api/admin/coupons/[id]/route.ts +98 -0
- package/src/api/admin/coupons/route.ts +109 -0
- package/src/api/middlewares.ts +34 -0
- package/src/api/store/campaigns/route.ts +107 -24
- package/src/api/store/coupons/public/route.ts +165 -0
- package/src/api/store/customers/me/coupons/route.ts +244 -0
- package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +1 -1
- package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +1 -1
- package/src/modules/custom-campaigns/migrations/Migration20251101000000.ts +21 -0
- package/src/modules/custom-campaigns/types/campaign-type.enum.ts +1 -0
- package/src/workflows/custom-campaign/createCouponCampaignWorkflow.ts +176 -0
- package/src/workflows/custom-campaign/updateCouponCampaignWorkflow.ts +105 -0
- package/src/workflows/index.ts +3 -1
- package/src/admin/widgets/campaign-stats-widget.tsx +0 -238
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthenticatedMedusaRequest,
|
|
3
|
+
MedusaResponse,
|
|
4
|
+
container,
|
|
5
|
+
} from "@medusajs/framework";
|
|
6
|
+
import { MedusaError, Modules } from "@medusajs/framework/utils";
|
|
7
|
+
import z from "zod";
|
|
8
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../../../../modules/custom-campaigns";
|
|
9
|
+
import CustomCampaignModuleService from "../../../../../modules/custom-campaigns/service";
|
|
10
|
+
import { CampaignTypeEnum } from "../../../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
11
|
+
|
|
12
|
+
type PromotionCacheService = {
|
|
13
|
+
scan: <T>(pattern: string) => Promise<T[]>;
|
|
14
|
+
set: (key: string, value: unknown, ttl?: number) => Promise<void>;
|
|
15
|
+
exists: (key: string) => Promise<boolean>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type UsablePromoModuleService = {
|
|
19
|
+
listUsablePromotions: (
|
|
20
|
+
selector: Record<string, unknown>,
|
|
21
|
+
) => Promise<
|
|
22
|
+
Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
promotion_id: string;
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
}>
|
|
27
|
+
>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PROMOTION_CACHE_MODULE = "promotion_cache";
|
|
31
|
+
const USABLE_PROMO_MODULE = "usable_promotion";
|
|
32
|
+
|
|
33
|
+
const resolveOptional = <T>(token: string): T | null => {
|
|
34
|
+
try {
|
|
35
|
+
return container.resolve<T>(token);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const collectCouponSchema = z.object({
|
|
42
|
+
coupon_id: z.string().min(1, "coupon_id is required"),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const GET = async (
|
|
46
|
+
req: AuthenticatedMedusaRequest,
|
|
47
|
+
res: MedusaResponse,
|
|
48
|
+
) => {
|
|
49
|
+
const customerId = req.auth_context.actor_id;
|
|
50
|
+
|
|
51
|
+
if (!customerId) {
|
|
52
|
+
throw new MedusaError(
|
|
53
|
+
MedusaError.Types.NOT_ALLOWED,
|
|
54
|
+
"Customer must be authenticated",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const promoCacheService =
|
|
59
|
+
container.resolve<PromotionCacheService>(PROMOTION_CACHE_MODULE);
|
|
60
|
+
const customCampaignModuleService =
|
|
61
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
62
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
63
|
+
|
|
64
|
+
const cacheKey = `user:${customerId}:promotion:*`;
|
|
65
|
+
const cachedPromotionIds = await promoCacheService.scan<string>(cacheKey);
|
|
66
|
+
|
|
67
|
+
if (!cachedPromotionIds.length) {
|
|
68
|
+
return res.status(200).json({
|
|
69
|
+
coupons: [],
|
|
70
|
+
count: 0,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const promotions = await promotionService.listPromotions(
|
|
75
|
+
{
|
|
76
|
+
id: cachedPromotionIds,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
relations: ["campaign", "application_method"],
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const campaignIds = promotions
|
|
84
|
+
.map((promotion) => promotion.campaign_id)
|
|
85
|
+
.filter((id): id is string => Boolean(id));
|
|
86
|
+
|
|
87
|
+
const campaignTypes =
|
|
88
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
89
|
+
campaign_id: campaignIds,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const couponCampaignIds = new Set(
|
|
93
|
+
campaignTypes
|
|
94
|
+
.filter((type) => type.type === CampaignTypeEnum.Coupon)
|
|
95
|
+
.map((type) => type.campaign_id),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const campaignDetails =
|
|
99
|
+
await customCampaignModuleService.listCampaignDetails({
|
|
100
|
+
campaign_id: Array.from(couponCampaignIds),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const detailMap = new Map(
|
|
104
|
+
campaignDetails.map((detail) => [detail.campaign_id, detail]),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const coupons = promotions
|
|
108
|
+
.filter(
|
|
109
|
+
(promotion) =>
|
|
110
|
+
promotion.campaign_id && couponCampaignIds.has(promotion.campaign_id),
|
|
111
|
+
)
|
|
112
|
+
.map((promotion) => {
|
|
113
|
+
const campaign = promotion.campaign!;
|
|
114
|
+
return {
|
|
115
|
+
id: promotion.id,
|
|
116
|
+
code: promotion.code,
|
|
117
|
+
name: campaign.name,
|
|
118
|
+
description: campaign.description,
|
|
119
|
+
starts_at: campaign.starts_at,
|
|
120
|
+
ends_at: campaign.ends_at,
|
|
121
|
+
discount_type: promotion.application_method?.type,
|
|
122
|
+
discount_value: promotion.application_method?.value,
|
|
123
|
+
allocation: promotion.application_method?.allocation,
|
|
124
|
+
target_type: promotion.application_method?.target_type,
|
|
125
|
+
currency_code: promotion.application_method?.currency_code,
|
|
126
|
+
campaign_id: campaign.id,
|
|
127
|
+
is_collectable: true,
|
|
128
|
+
is_collected: true,
|
|
129
|
+
detail: detailMap.get(campaign.id) ?? null,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
res.status(200).json({
|
|
134
|
+
coupons,
|
|
135
|
+
count: coupons.length,
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const POST = async (
|
|
140
|
+
req: AuthenticatedMedusaRequest,
|
|
141
|
+
res: MedusaResponse,
|
|
142
|
+
) => {
|
|
143
|
+
const body = collectCouponSchema.parse(req.body);
|
|
144
|
+
const customerId = req.auth_context.actor_id;
|
|
145
|
+
|
|
146
|
+
if (!customerId) {
|
|
147
|
+
throw new MedusaError(
|
|
148
|
+
MedusaError.Types.NOT_ALLOWED,
|
|
149
|
+
"Customer must be authenticated",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
154
|
+
const customCampaignModuleService =
|
|
155
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
156
|
+
const promoCacheService =
|
|
157
|
+
container.resolve<PromotionCacheService>(PROMOTION_CACHE_MODULE);
|
|
158
|
+
const usablePromoService = resolveOptional<UsablePromoModuleService>(
|
|
159
|
+
USABLE_PROMO_MODULE,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const promotion = await promotionService.retrievePromotion(
|
|
163
|
+
body.coupon_id,
|
|
164
|
+
{
|
|
165
|
+
relations: ["campaign", "application_method"],
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (!promotion?.campaign_id) {
|
|
170
|
+
throw new MedusaError(
|
|
171
|
+
MedusaError.Types.NOT_FOUND,
|
|
172
|
+
"Coupon promotion not found",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const [campaignType] =
|
|
177
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
178
|
+
campaign_id: promotion.campaign_id,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!campaignType || campaignType.type !== CampaignTypeEnum.Coupon) {
|
|
182
|
+
throw new MedusaError(
|
|
183
|
+
MedusaError.Types.INVALID_DATA,
|
|
184
|
+
"Promotion is not registered as a coupon",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (promotion.is_automatic) {
|
|
189
|
+
throw new MedusaError(
|
|
190
|
+
MedusaError.Types.INVALID_DATA,
|
|
191
|
+
"Automatic promotions cannot be collected",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (promotion.status !== "active") {
|
|
196
|
+
throw new MedusaError(
|
|
197
|
+
MedusaError.Types.INVALID_DATA,
|
|
198
|
+
"Promotion is not active",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (usablePromoService) {
|
|
203
|
+
const [usable] = await usablePromoService.listUsablePromotions({
|
|
204
|
+
promotion_id: promotion.id,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!usable || !usable.enabled) {
|
|
208
|
+
throw new MedusaError(
|
|
209
|
+
MedusaError.Types.INVALID_DATA,
|
|
210
|
+
"This coupon is not available for collection",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const cacheKey = `user:${customerId}:promotion:${promotion.id}`;
|
|
216
|
+
const alreadyCollected = await promoCacheService.exists(cacheKey);
|
|
217
|
+
|
|
218
|
+
if (alreadyCollected) {
|
|
219
|
+
throw new MedusaError(
|
|
220
|
+
MedusaError.Types.INVALID_DATA,
|
|
221
|
+
"Coupon already collected",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await promoCacheService.set(cacheKey, promotion.id);
|
|
226
|
+
|
|
227
|
+
res.status(201).json({
|
|
228
|
+
message: "Coupon collected",
|
|
229
|
+
coupon: {
|
|
230
|
+
id: promotion.id,
|
|
231
|
+
code: promotion.code,
|
|
232
|
+
name: promotion.campaign?.name,
|
|
233
|
+
description: promotion.campaign?.description,
|
|
234
|
+
starts_at: promotion.campaign?.starts_at,
|
|
235
|
+
ends_at: promotion.campaign?.ends_at,
|
|
236
|
+
discount_type: promotion.application_method?.type,
|
|
237
|
+
discount_value: promotion.application_method?.value,
|
|
238
|
+
allocation: promotion.application_method?.allocation,
|
|
239
|
+
target_type: promotion.application_method?.target_type,
|
|
240
|
+
currency_code: promotion.application_method?.currency_code,
|
|
241
|
+
campaign_id: promotion.campaign_id,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
|
|
2
|
+
|
|
3
|
+
export class Migration20251101000000 extends Migration {
|
|
4
|
+
override async up(): Promise<void> {
|
|
5
|
+
this.addSql(
|
|
6
|
+
`alter table if exists "custom_campaign_type" drop constraint if exists "custom_campaign_type_type_check";`,
|
|
7
|
+
);
|
|
8
|
+
this.addSql(
|
|
9
|
+
`alter table if exists "custom_campaign_type" add constraint "custom_campaign_type_type_check" check ("type" in ('flash-sale', 'buy-x-get-y', 'coupon'));`,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override async down(): Promise<void> {
|
|
14
|
+
this.addSql(
|
|
15
|
+
`alter table if exists "custom_campaign_type" drop constraint if exists "custom_campaign_type_type_check";`,
|
|
16
|
+
);
|
|
17
|
+
this.addSql(
|
|
18
|
+
`alter table if exists "custom_campaign_type" add constraint "custom_campaign_type_type_check" check ("type" in ('flash-sale', 'buy-x-get-y'));`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { container } from "@medusajs/framework";
|
|
2
|
+
import {
|
|
3
|
+
CampaignDTO,
|
|
4
|
+
CreateApplicationMethodDTO,
|
|
5
|
+
CreatePromotionDTO,
|
|
6
|
+
PromotionDTO,
|
|
7
|
+
} from "@medusajs/framework/types";
|
|
8
|
+
import {
|
|
9
|
+
ContainerRegistrationKeys,
|
|
10
|
+
MedusaError,
|
|
11
|
+
Modules,
|
|
12
|
+
} from "@medusajs/framework/utils";
|
|
13
|
+
import {
|
|
14
|
+
createStep,
|
|
15
|
+
createWorkflow,
|
|
16
|
+
StepResponse,
|
|
17
|
+
WorkflowResponse,
|
|
18
|
+
} from "@medusajs/framework/workflows-sdk";
|
|
19
|
+
import {
|
|
20
|
+
createCampaignsWorkflow,
|
|
21
|
+
createPromotionsWorkflow,
|
|
22
|
+
deleteCampaignsWorkflow,
|
|
23
|
+
deletePromotionsWorkflow,
|
|
24
|
+
} from "@medusajs/medusa/core-flows";
|
|
25
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../modules/custom-campaigns";
|
|
26
|
+
import CustomCampaignModuleService from "../../modules/custom-campaigns/service";
|
|
27
|
+
import { CampaignTypeEnum } from "../../modules/custom-campaigns/types/campaign-type.enum";
|
|
28
|
+
|
|
29
|
+
export type CreateCouponCampaignInput = {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
code: string;
|
|
33
|
+
type: CampaignTypeEnum.Coupon;
|
|
34
|
+
starts_at: Date;
|
|
35
|
+
ends_at: Date;
|
|
36
|
+
discount_type: "percentage" | "fixed";
|
|
37
|
+
discount_value: number;
|
|
38
|
+
currency_code?: string | null;
|
|
39
|
+
allocation?: "each" | "total";
|
|
40
|
+
target_type?: "order" | "items";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const createCouponCampaignStep = createStep(
|
|
44
|
+
"create-coupon-campaign-step",
|
|
45
|
+
async (input: CreateCouponCampaignInput) => {
|
|
46
|
+
if (new Date(input.ends_at) < new Date(input.starts_at)) {
|
|
47
|
+
throw new MedusaError(
|
|
48
|
+
MedusaError.Types.INVALID_DATA,
|
|
49
|
+
"End date must be after start date",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (input.discount_type === "fixed" && !input.currency_code) {
|
|
54
|
+
throw new MedusaError(
|
|
55
|
+
MedusaError.Types.INVALID_DATA,
|
|
56
|
+
"currency_code is required for fixed discount type",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const link = container.resolve(ContainerRegistrationKeys.LINK);
|
|
61
|
+
const customCampaignModuleService =
|
|
62
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
63
|
+
|
|
64
|
+
const campaignIdentifier = `${input.type}-${Date.now()}`;
|
|
65
|
+
|
|
66
|
+
let campaign: CampaignDTO | undefined;
|
|
67
|
+
let promotion: PromotionDTO | undefined;
|
|
68
|
+
let customCampaignType:
|
|
69
|
+
| Awaited<
|
|
70
|
+
ReturnType<CustomCampaignModuleService["createCustomCampaignTypes"]>
|
|
71
|
+
>[number]
|
|
72
|
+
| undefined;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const {
|
|
76
|
+
result: [createdCampaign],
|
|
77
|
+
} = await createCampaignsWorkflow.run({
|
|
78
|
+
input: {
|
|
79
|
+
campaignsData: [
|
|
80
|
+
{
|
|
81
|
+
name: input.name,
|
|
82
|
+
description: input.description,
|
|
83
|
+
campaign_identifier: campaignIdentifier,
|
|
84
|
+
starts_at: new Date(input.starts_at),
|
|
85
|
+
ends_at: new Date(input.ends_at),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
campaign = createdCampaign;
|
|
92
|
+
|
|
93
|
+
const {
|
|
94
|
+
result: [createdPromotion],
|
|
95
|
+
} = await createPromotionsWorkflow.run({
|
|
96
|
+
input: {
|
|
97
|
+
promotionsData: [
|
|
98
|
+
{
|
|
99
|
+
code: input.code,
|
|
100
|
+
type: "standard",
|
|
101
|
+
status: "active",
|
|
102
|
+
is_automatic: false,
|
|
103
|
+
campaign_id: campaign.id,
|
|
104
|
+
application_method: {
|
|
105
|
+
target_type: input.target_type ?? "order",
|
|
106
|
+
allocation: (input.allocation ?? "across") as any,
|
|
107
|
+
type: input.discount_type,
|
|
108
|
+
value: input.discount_value,
|
|
109
|
+
currency_code:
|
|
110
|
+
input.discount_type === "fixed" && input.currency_code
|
|
111
|
+
? input.currency_code
|
|
112
|
+
: undefined,
|
|
113
|
+
} satisfies CreateApplicationMethodDTO,
|
|
114
|
+
} satisfies CreatePromotionDTO,
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
promotion = createdPromotion;
|
|
120
|
+
|
|
121
|
+
customCampaignType =
|
|
122
|
+
await customCampaignModuleService.createCustomCampaignTypes({
|
|
123
|
+
campaign_id: campaign.id,
|
|
124
|
+
type: input.type,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await link.create([
|
|
128
|
+
{
|
|
129
|
+
[Modules.PROMOTION]: {
|
|
130
|
+
campaign_id: campaign.id,
|
|
131
|
+
},
|
|
132
|
+
[CUSTOM_CAMPAIGN_MODULE]: {
|
|
133
|
+
custom_campaign_type_id: customCampaignType.id,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
return new StepResponse({
|
|
139
|
+
campaign,
|
|
140
|
+
promotion,
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (campaign) {
|
|
144
|
+
await deleteCampaignsWorkflow.run({
|
|
145
|
+
input: {
|
|
146
|
+
ids: campaign.id,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (promotion) {
|
|
152
|
+
await deletePromotionsWorkflow.run({
|
|
153
|
+
input: {
|
|
154
|
+
ids: promotion.id,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (customCampaignType) {
|
|
160
|
+
await customCampaignModuleService.deleteCustomCampaignTypes(
|
|
161
|
+
customCampaignType.id,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
export const createCouponCampaignWorkflow = createWorkflow(
|
|
171
|
+
"create-coupon-campaign",
|
|
172
|
+
(input: CreateCouponCampaignInput) => {
|
|
173
|
+
const couponCampaign = createCouponCampaignStep(input);
|
|
174
|
+
return new WorkflowResponse(couponCampaign);
|
|
175
|
+
},
|
|
176
|
+
);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { container } from "@medusajs/framework";
|
|
2
|
+
import {
|
|
3
|
+
UpdateCampaignDTO,
|
|
4
|
+
UpdatePromotionDTO,
|
|
5
|
+
} from "@medusajs/framework/types";
|
|
6
|
+
import { MedusaError, Modules } from "@medusajs/framework/utils";
|
|
7
|
+
import {
|
|
8
|
+
createStep,
|
|
9
|
+
createWorkflow,
|
|
10
|
+
StepResponse,
|
|
11
|
+
WorkflowResponse,
|
|
12
|
+
} from "@medusajs/framework/workflows-sdk";
|
|
13
|
+
import { updateCampaignsWorkflow } from "@medusajs/medusa/core-flows";
|
|
14
|
+
|
|
15
|
+
export type UpdateCouponCampaignInput = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
code: string;
|
|
20
|
+
starts_at: Date;
|
|
21
|
+
ends_at: Date;
|
|
22
|
+
discount_type: "percentage" | "fixed";
|
|
23
|
+
discount_value: number;
|
|
24
|
+
currency_code?: string | null;
|
|
25
|
+
allocation?: "each" | "total";
|
|
26
|
+
target_type?: "order" | "items";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const updateCouponCampaignStep = createStep(
|
|
30
|
+
"update-coupon-campaign-step",
|
|
31
|
+
async (input: UpdateCouponCampaignInput) => {
|
|
32
|
+
if (new Date(input.ends_at) < new Date(input.starts_at)) {
|
|
33
|
+
throw new MedusaError(
|
|
34
|
+
MedusaError.Types.INVALID_DATA,
|
|
35
|
+
"End date must be after start date",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (input.discount_type === "fixed" && !input.currency_code) {
|
|
40
|
+
throw new MedusaError(
|
|
41
|
+
MedusaError.Types.INVALID_DATA,
|
|
42
|
+
"currency_code is required for fixed discount type",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
47
|
+
|
|
48
|
+
const promotions = await promotionService.listPromotions({
|
|
49
|
+
campaign_id: input.id,
|
|
50
|
+
} as any);
|
|
51
|
+
|
|
52
|
+
if (!promotions.length) {
|
|
53
|
+
throw new MedusaError(
|
|
54
|
+
MedusaError.Types.NOT_FOUND,
|
|
55
|
+
"Coupon promotion not found for campaign",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const promotion = promotions[0];
|
|
60
|
+
|
|
61
|
+
await updateCampaignsWorkflow.run({
|
|
62
|
+
input: {
|
|
63
|
+
campaignsData: [
|
|
64
|
+
{
|
|
65
|
+
id: input.id,
|
|
66
|
+
name: input.name,
|
|
67
|
+
description: input.description,
|
|
68
|
+
starts_at: new Date(input.starts_at),
|
|
69
|
+
ends_at: new Date(input.ends_at),
|
|
70
|
+
} satisfies UpdateCampaignDTO,
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const updateData: UpdatePromotionDTO = {
|
|
76
|
+
id: promotion.id,
|
|
77
|
+
code: input.code,
|
|
78
|
+
application_method: {
|
|
79
|
+
type: input.discount_type,
|
|
80
|
+
target_type: input.target_type ?? "order",
|
|
81
|
+
allocation: (input.allocation ?? "across") as any,
|
|
82
|
+
value: input.discount_value,
|
|
83
|
+
currency_code:
|
|
84
|
+
input.discount_type === "fixed" && input.currency_code
|
|
85
|
+
? input.currency_code
|
|
86
|
+
: undefined,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
await promotionService.updatePromotions([updateData]);
|
|
91
|
+
|
|
92
|
+
return new StepResponse({
|
|
93
|
+
campaign_id: input.id,
|
|
94
|
+
promotion_id: promotion.id,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
export const updateCouponCampaignWorkflow = createWorkflow(
|
|
100
|
+
"update-coupon-campaign",
|
|
101
|
+
(input: UpdateCouponCampaignInput) => {
|
|
102
|
+
const result = updateCouponCampaignStep(input);
|
|
103
|
+
return new WorkflowResponse(result);
|
|
104
|
+
},
|
|
105
|
+
);
|
package/src/workflows/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { createCustomCampaignWorkflow, createCustomCampaignWorkflow as createCustomFlashSaleWorkflow } from "./custom-campaign/createCustomCampaignWorkflow";
|
|
2
2
|
export { updateCustomFlashSaleWorkflow } from "./custom-campaign/updateCustomFlashSaleWorkflow";
|
|
3
3
|
export { updatePromotionUsageWorkflow } from "./custom-campaign/updatePromotionUsageWorkflow";
|
|
4
|
-
export { applyBuyXGetYToCartWorkflow } from "./buy-x-get-y/applyBuyXGetYToCartWorkflow";
|
|
4
|
+
export { applyBuyXGetYToCartWorkflow } from "./buy-x-get-y/applyBuyXGetYToCartWorkflow";
|
|
5
|
+
export { createCouponCampaignWorkflow } from "./custom-campaign/createCouponCampaignWorkflow";
|
|
6
|
+
export { updateCouponCampaignWorkflow } from "./custom-campaign/updateCouponCampaignWorkflow";
|