@lodashventure/medusa-campaign 1.4.1 → 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,109 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import {
|
|
3
|
+
MedusaError,
|
|
4
|
+
Modules,
|
|
5
|
+
} from "@medusajs/framework/utils";
|
|
6
|
+
import { createFindParams } from "@medusajs/medusa/api/utils/validators";
|
|
7
|
+
import z from "zod";
|
|
8
|
+
import { CampaignTypeEnum } from "../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
9
|
+
import { createCouponCampaignWorkflow } from "../../../workflows/custom-campaign/createCouponCampaignWorkflow";
|
|
10
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../../modules/custom-campaigns";
|
|
11
|
+
import CustomCampaignModuleService from "../../../modules/custom-campaigns/service";
|
|
12
|
+
|
|
13
|
+
export const createCouponCampaignSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
name: z.string().min(1, "Name is required"),
|
|
16
|
+
description: z.string().min(1, "Description is required"),
|
|
17
|
+
type: z.literal(CampaignTypeEnum.Coupon),
|
|
18
|
+
code: z.string().min(1, "Coupon code is required"),
|
|
19
|
+
starts_at: z.coerce.date(),
|
|
20
|
+
ends_at: z.coerce.date(),
|
|
21
|
+
discount_type: z.enum(["percentage", "fixed"]),
|
|
22
|
+
discount_value: z.number().positive("Discount value must be positive"),
|
|
23
|
+
currency_code: z.string().optional(),
|
|
24
|
+
allocation: z.enum(["each", "total"]).optional(),
|
|
25
|
+
target_type: z.enum(["order", "items"]).optional(),
|
|
26
|
+
})
|
|
27
|
+
.superRefine((data, ctx) => {
|
|
28
|
+
if (data.ends_at <= data.starts_at) {
|
|
29
|
+
ctx.addIssue({
|
|
30
|
+
code: z.ZodIssueCode.custom,
|
|
31
|
+
message: "End date must be after start date",
|
|
32
|
+
path: ["ends_at"],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (data.discount_type === "fixed" && !data.currency_code) {
|
|
37
|
+
ctx.addIssue({
|
|
38
|
+
code: z.ZodIssueCode.custom,
|
|
39
|
+
message: "currency_code is required for fixed discount type",
|
|
40
|
+
path: ["currency_code"],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type CouponCampaignInput = z.infer<typeof createCouponCampaignSchema>;
|
|
46
|
+
|
|
47
|
+
export const GetCouponCampaignsSchema = createFindParams({
|
|
48
|
+
order: "-created_at",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const POST = async (
|
|
52
|
+
req: MedusaRequest<CouponCampaignInput>,
|
|
53
|
+
res: MedusaResponse,
|
|
54
|
+
) => {
|
|
55
|
+
const body = req.body;
|
|
56
|
+
|
|
57
|
+
if (body.type !== CampaignTypeEnum.Coupon) {
|
|
58
|
+
throw new MedusaError(
|
|
59
|
+
MedusaError.Types.INVALID_DATA,
|
|
60
|
+
"Invalid campaign type for coupons",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { result } = await createCouponCampaignWorkflow.run({
|
|
65
|
+
input: {
|
|
66
|
+
...body,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
res.status(200).json(result);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
74
|
+
const customCampaignModuleService =
|
|
75
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
76
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
77
|
+
|
|
78
|
+
const customCampaignTypes =
|
|
79
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
80
|
+
type: CampaignTypeEnum.Coupon,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!customCampaignTypes.length) {
|
|
84
|
+
return res.status(200).json({
|
|
85
|
+
campaigns: [],
|
|
86
|
+
count: 0,
|
|
87
|
+
limit: 20,
|
|
88
|
+
offset: 0,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const campaignIds = customCampaignTypes.map((ct) => ct.campaign_id);
|
|
93
|
+
|
|
94
|
+
const campaigns = await promotionService.listCampaigns(
|
|
95
|
+
{
|
|
96
|
+
id: campaignIds,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
relations: ["promotions", "promotions.application_method"],
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
res.status(200).json({
|
|
104
|
+
campaigns,
|
|
105
|
+
count: campaigns.length,
|
|
106
|
+
limit: campaigns.length,
|
|
107
|
+
offset: 0,
|
|
108
|
+
});
|
|
109
|
+
};
|
package/src/api/middlewares.ts
CHANGED
|
@@ -7,7 +7,12 @@ import {
|
|
|
7
7
|
createCustomCampaignSchema,
|
|
8
8
|
GetFlashSalesSchema,
|
|
9
9
|
} from "./admin/flash-sales/route";
|
|
10
|
+
import {
|
|
11
|
+
createCouponCampaignSchema,
|
|
12
|
+
GetCouponCampaignsSchema,
|
|
13
|
+
} from "./admin/coupons/route";
|
|
10
14
|
import multer from "multer";
|
|
15
|
+
import { collectCouponSchema } from "./store/customers/me/coupons/route";
|
|
11
16
|
|
|
12
17
|
const upload = multer({
|
|
13
18
|
storage: multer.memoryStorage(),
|
|
@@ -34,6 +39,30 @@ export default defineMiddlewares([
|
|
|
34
39
|
}),
|
|
35
40
|
],
|
|
36
41
|
},
|
|
42
|
+
{
|
|
43
|
+
methods: ["POST"],
|
|
44
|
+
matcher: "/admin/coupons",
|
|
45
|
+
middlewares: [
|
|
46
|
+
validateAndTransformBody(createCouponCampaignSchema as any),
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
methods: ["PUT"],
|
|
51
|
+
matcher: "/admin/coupons/:id",
|
|
52
|
+
middlewares: [
|
|
53
|
+
validateAndTransformBody(createCouponCampaignSchema as any),
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
methods: ["GET"],
|
|
58
|
+
matcher: "/admin/coupons",
|
|
59
|
+
middlewares: [
|
|
60
|
+
validateAndTransformQuery(GetCouponCampaignsSchema as any, {
|
|
61
|
+
defaults: ["*", "campaign.*"],
|
|
62
|
+
isList: true,
|
|
63
|
+
}),
|
|
64
|
+
],
|
|
65
|
+
},
|
|
37
66
|
{
|
|
38
67
|
methods: ["POST"],
|
|
39
68
|
matcher: "/admin/campaigns/:id/image",
|
|
@@ -50,4 +79,9 @@ export default defineMiddlewares([
|
|
|
50
79
|
upload.single("file"),
|
|
51
80
|
],
|
|
52
81
|
},
|
|
82
|
+
{
|
|
83
|
+
methods: ["POST"],
|
|
84
|
+
matcher: "/store/customers/me/coupons",
|
|
85
|
+
middlewares: [validateAndTransformBody(collectCouponSchema as any)],
|
|
86
|
+
},
|
|
53
87
|
]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
-
import {
|
|
2
|
+
import { Modules } from "@medusajs/framework/utils";
|
|
3
3
|
import { CampaignTypeEnum } from "../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
4
4
|
import { CUSTOM_CAMPAIGN_MODULE } from "../../../modules/custom-campaigns";
|
|
5
5
|
import CustomCampaignModuleService from "../../../modules/custom-campaigns/service";
|
|
@@ -11,6 +11,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
11
11
|
const customCampaignModuleService =
|
|
12
12
|
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
13
13
|
const promotionService = container.resolve(Modules.PROMOTION);
|
|
14
|
+
const productService = container.resolve(Modules.PRODUCT);
|
|
14
15
|
const now = new Date();
|
|
15
16
|
|
|
16
17
|
try {
|
|
@@ -65,9 +66,24 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
65
66
|
const allCampaigns = await promotionService.listCampaigns({});
|
|
66
67
|
console.log(`Total campaigns in system: ${allCampaigns.length}`);
|
|
67
68
|
|
|
68
|
-
// Then fetch specific campaigns
|
|
69
|
+
// Then fetch specific campaigns with relations
|
|
69
70
|
const campaigns = campaignIds.length > 0
|
|
70
|
-
?
|
|
71
|
+
? await Promise.all(
|
|
72
|
+
campaignIds.map(async (campaignId) => {
|
|
73
|
+
try {
|
|
74
|
+
return await promotionService.retrieveCampaign(campaignId, {
|
|
75
|
+
relations: [
|
|
76
|
+
"promotions",
|
|
77
|
+
"promotions.application_method",
|
|
78
|
+
"promotions.application_method.target_rules",
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(`Error fetching campaign ${campaignId}:`, error);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
).then((results) => results.filter((c) => c !== null))
|
|
71
87
|
: [];
|
|
72
88
|
|
|
73
89
|
console.log(`Found ${campaigns.length} matching campaigns`);
|
|
@@ -83,27 +99,94 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
83
99
|
);
|
|
84
100
|
|
|
85
101
|
// Process ALL campaigns first (without date filter) for debugging
|
|
86
|
-
const allProcessedCampaigns =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
const allProcessedCampaigns = await Promise.all(
|
|
103
|
+
campaigns.map(async (campaign: any) => {
|
|
104
|
+
const startsAt = new Date(campaign.starts_at);
|
|
105
|
+
const endsAt = new Date(campaign.ends_at);
|
|
106
|
+
const isActive = startsAt <= now && endsAt >= now;
|
|
107
|
+
|
|
108
|
+
console.log(` Checking campaign "${campaign.name}":`);
|
|
109
|
+
console.log(` Start: ${startsAt.toISOString()} (${startsAt <= now ? 'started' : 'not started'})`);
|
|
110
|
+
console.log(` End: ${endsAt.toISOString()} (${endsAt >= now ? 'not ended' : 'ended'})`);
|
|
111
|
+
console.log(` Active: ${isActive}`);
|
|
112
|
+
|
|
113
|
+
const campaignType = campaignTypeMap.get(campaign.id);
|
|
114
|
+
|
|
115
|
+
// Fetch products for flash-sale campaigns
|
|
116
|
+
let products: any[] = [];
|
|
117
|
+
if (campaignType === CampaignTypeEnum.FlashSale) {
|
|
118
|
+
try {
|
|
119
|
+
// Fetch promotion usage limits for this campaign
|
|
120
|
+
const promotionUsageLimits =
|
|
121
|
+
await customCampaignModuleService.listPromotionUsageLimits({
|
|
122
|
+
campaign_id: campaign.id,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Create a map of promotion usage limits by promotion ID
|
|
126
|
+
const promotionLimitsMap = new Map();
|
|
127
|
+
promotionUsageLimits.forEach((limit) => {
|
|
128
|
+
promotionLimitsMap.set(limit.promotion_id, limit);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Process promotions to extract product information
|
|
132
|
+
for (const promotion of campaign.promotions ?? []) {
|
|
133
|
+
if (!promotion.application_method?.target_rules?.length) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const promotionLimit = promotionLimitsMap.get(promotion.id);
|
|
138
|
+
if (!promotionLimit?.product_id) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const product = await productService.retrieveProduct(
|
|
144
|
+
promotionLimit.product_id,
|
|
145
|
+
{
|
|
146
|
+
select: ["id", "title", "thumbnail", "handle", "description"],
|
|
147
|
+
relations: ["images"],
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (promotion.application_method.value !== undefined) {
|
|
152
|
+
products.push({
|
|
153
|
+
product: {
|
|
154
|
+
id: product.id,
|
|
155
|
+
title: product.title,
|
|
156
|
+
thumbnail: product.thumbnail,
|
|
157
|
+
handle: product.handle,
|
|
158
|
+
description: product.description,
|
|
159
|
+
images: product.images,
|
|
160
|
+
},
|
|
161
|
+
discountType: promotion.application_method?.type as "percentage",
|
|
162
|
+
discountValue: promotion.application_method?.value,
|
|
163
|
+
maxQty: promotion.application_method?.max_quantity ?? 0,
|
|
164
|
+
limit: promotionLimit?.limit ?? 0,
|
|
165
|
+
used: promotionLimit?.used ?? 0,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`Error fetching product ${promotionLimit.product_id}:`, error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(`Error fetching products for campaign ${campaign.id}:`, error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
id: campaign.id,
|
|
179
|
+
name: campaign.name,
|
|
180
|
+
description: campaign.description,
|
|
181
|
+
campaign_identifier: campaign.campaign_identifier,
|
|
182
|
+
starts_at: campaign.starts_at,
|
|
183
|
+
ends_at: campaign.ends_at,
|
|
184
|
+
campaign_type: campaignType,
|
|
185
|
+
is_active: isActive,
|
|
186
|
+
...(products.length > 0 && { products }),
|
|
187
|
+
};
|
|
188
|
+
})
|
|
189
|
+
);
|
|
107
190
|
|
|
108
191
|
// Filter for active campaigns
|
|
109
192
|
const activeCampaigns = allProcessedCampaigns.filter(c => c.is_active);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse, container } 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
|
+
type UsablePromoModuleService = {
|
|
8
|
+
listUsablePromotions: (selector: Record<string, unknown>) => Promise<
|
|
9
|
+
Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
promotion_id: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
}>
|
|
14
|
+
>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type PromotionCacheService = {
|
|
18
|
+
exists: (key: string) => Promise<boolean>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const USABLE_PROMO_MODULE = "usable_promotion";
|
|
22
|
+
const PROMOTION_CACHE_MODULE = "promotion_cache";
|
|
23
|
+
|
|
24
|
+
const resolveOptional = <T>(token: string): T | null => {
|
|
25
|
+
try {
|
|
26
|
+
return container.resolve<T>(token);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
33
|
+
const customCampaignModuleService =
|
|
34
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
35
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
36
|
+
|
|
37
|
+
const usablePromoService =
|
|
38
|
+
resolveOptional<UsablePromoModuleService>(USABLE_PROMO_MODULE);
|
|
39
|
+
const promotionCacheService = resolveOptional<PromotionCacheService>(
|
|
40
|
+
PROMOTION_CACHE_MODULE,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const now = new Date();
|
|
44
|
+
const customerId = (
|
|
45
|
+
req as unknown as { auth_context?: { actor_id?: string } }
|
|
46
|
+
)?.auth_context?.actor_id;
|
|
47
|
+
|
|
48
|
+
const parsedLimit = req.query?.limit ? Number(req.query.limit) : 20;
|
|
49
|
+
const parsedOffset = req.query?.offset ? Number(req.query.offset) : 0;
|
|
50
|
+
|
|
51
|
+
const limit = Number.isNaN(parsedLimit) ? 20 : parsedLimit;
|
|
52
|
+
const offset = Number.isNaN(parsedOffset) ? 0 : parsedOffset;
|
|
53
|
+
|
|
54
|
+
const couponCampaignTypes =
|
|
55
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
56
|
+
type: CampaignTypeEnum.Coupon,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!couponCampaignTypes.length) {
|
|
60
|
+
return res.status(200).json({
|
|
61
|
+
coupons: [],
|
|
62
|
+
count: 0,
|
|
63
|
+
limit,
|
|
64
|
+
offset,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const campaignIds = couponCampaignTypes.map((ct) => ct.campaign_id);
|
|
69
|
+
|
|
70
|
+
const promotions = await promotionService.listPromotions(
|
|
71
|
+
{
|
|
72
|
+
campaign_id: { $in: campaignIds },
|
|
73
|
+
} as any,
|
|
74
|
+
{
|
|
75
|
+
relations: ["campaign", "application_method"],
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const campaignDetails = await customCampaignModuleService.listCampaignDetails(
|
|
80
|
+
{
|
|
81
|
+
campaign_id: campaignIds,
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const detailMap = new Map(
|
|
86
|
+
campaignDetails.map((detail) => [detail.campaign_id, detail]),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const usablePromotions = usablePromoService
|
|
90
|
+
? await usablePromoService.listUsablePromotions({
|
|
91
|
+
promotion_id: promotions.map((promo) => promo.id),
|
|
92
|
+
})
|
|
93
|
+
: [];
|
|
94
|
+
|
|
95
|
+
const usableMap = new Map(
|
|
96
|
+
usablePromotions.map((usable) => [usable.promotion_id, usable.enabled]),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const filteredPromotions = promotions.filter((promotion) => {
|
|
100
|
+
const campaign = promotion.campaign;
|
|
101
|
+
if (!campaign) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const startsAt = campaign.starts_at ? new Date(campaign.starts_at) : null;
|
|
106
|
+
const endsAt = campaign.ends_at ? new Date(campaign.ends_at) : null;
|
|
107
|
+
|
|
108
|
+
const withinWindow =
|
|
109
|
+
(!startsAt || startsAt <= now) && (!endsAt || endsAt >= now);
|
|
110
|
+
|
|
111
|
+
const isCollectable = usableMap.has(promotion.id)
|
|
112
|
+
? usableMap.get(promotion.id)!
|
|
113
|
+
: true;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
campaign &&
|
|
117
|
+
withinWindow &&
|
|
118
|
+
promotion.status === "active" &&
|
|
119
|
+
!promotion.is_automatic &&
|
|
120
|
+
isCollectable
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const paginated = filteredPromotions.slice(offset, offset + limit);
|
|
125
|
+
|
|
126
|
+
const coupons = await Promise.all(
|
|
127
|
+
paginated.map(async (promotion) => {
|
|
128
|
+
const campaign = promotion.campaign!;
|
|
129
|
+
const detail = detailMap.get(campaign.id);
|
|
130
|
+
let isCollected = false;
|
|
131
|
+
|
|
132
|
+
if (customerId && promotionCacheService) {
|
|
133
|
+
const key = `user:${customerId}:promotion:${promotion.id}`;
|
|
134
|
+
isCollected = await promotionCacheService.exists(key);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
id: promotion.id,
|
|
139
|
+
code: promotion.code,
|
|
140
|
+
name: campaign.name,
|
|
141
|
+
description: campaign.description,
|
|
142
|
+
starts_at: campaign.starts_at,
|
|
143
|
+
ends_at: campaign.ends_at,
|
|
144
|
+
discount_type: promotion.application_method?.type,
|
|
145
|
+
discount_value: promotion.application_method?.value,
|
|
146
|
+
allocation: promotion.application_method?.allocation,
|
|
147
|
+
target_type: promotion.application_method?.target_type,
|
|
148
|
+
currency_code: promotion.application_method?.currency_code,
|
|
149
|
+
campaign_id: campaign.id,
|
|
150
|
+
is_collectable: usableMap.has(promotion.id)
|
|
151
|
+
? usableMap.get(promotion.id)!
|
|
152
|
+
: true,
|
|
153
|
+
is_collected: isCollected,
|
|
154
|
+
detail: detail ?? null,
|
|
155
|
+
};
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
res.status(200).json({
|
|
160
|
+
coupons,
|
|
161
|
+
count: filteredPromotions.length,
|
|
162
|
+
limit,
|
|
163
|
+
offset,
|
|
164
|
+
});
|
|
165
|
+
};
|