@lodashventure/medusa-campaign 1.1.0 → 1.1.1
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 +1 -67
- package/.medusa/server/src/admin/index.mjs +1 -67
- package/.medusa/server/src/workflows/index.js +10 -0
- package/package.json +4 -4
- package/src/admin/README.md +31 -0
- package/src/admin/components/FlashSaleForm.tsx +379 -0
- package/src/admin/components/FlashSalePage.tsx +113 -0
- package/src/admin/components/ProductSelector.tsx +88 -0
- package/src/admin/hooks/useFlashSaleById.ts +21 -0
- package/src/admin/hooks/useFlashSales.ts +25 -0
- package/src/admin/lib/sdk.ts +10 -0
- package/src/admin/routes/flash-sales/[id]/page.tsx +105 -0
- package/src/admin/routes/flash-sales/create/page.tsx +51 -0
- package/src/admin/routes/flash-sales/page.tsx +15 -0
- package/src/admin/tsconfig.json +24 -0
- package/src/admin/types/campaign.ts +25 -0
- package/src/admin/vite-env.d.ts +1 -0
- package/src/api/README.md +133 -0
- package/src/api/admin/flash-sales/[id]/route.ts +164 -0
- package/src/api/admin/flash-sales/route.ts +87 -0
- package/src/api/middlewares.ts +32 -0
- package/src/api/store/campaigns/[id]/route.ts +133 -0
- package/src/api/store/campaigns/route.ts +36 -0
- package/src/jobs/README.md +36 -0
- package/src/links/README.md +26 -0
- package/src/links/campaign-type.ts +8 -0
- package/src/modules/README.md +116 -0
- package/src/modules/custom-campaigns/index.ts +8 -0
- package/src/modules/custom-campaigns/migrations/.snapshot-medusa-custom-campaign.json +235 -0
- package/src/modules/custom-campaigns/migrations/Migration20250524150901.ts +23 -0
- package/src/modules/custom-campaigns/migrations/Migration20250526010310.ts +20 -0
- package/src/modules/custom-campaigns/migrations/Migration20250529011904.ts +13 -0
- package/src/modules/custom-campaigns/models/custom-campaign-type.ts +10 -0
- package/src/modules/custom-campaigns/models/promotion-usage-limit.ts +14 -0
- package/src/modules/custom-campaigns/service.ts +10 -0
- package/src/modules/custom-campaigns/types/campaign-type.enum.ts +3 -0
- package/src/providers/README.md +30 -0
- package/src/subscribers/README.md +59 -0
- package/src/subscribers/order-placed.ts +17 -0
- package/src/workflows/README.md +79 -0
- package/src/workflows/custom-campaign/createCustomCampaignWorkflow.ts +181 -0
- package/src/workflows/custom-campaign/updateCustomFlashSaleWorkflow.ts +185 -0
- package/src/workflows/custom-campaign/updatePromotionUsageWorkflow.ts +70 -0
- package/src/workflows/hooks/deletePromotionOnCampaignDelete.ts +49 -0
- package/src/workflows/index.ts +3 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import {
|
|
3
|
+
ContainerRegistrationKeys,
|
|
4
|
+
MedusaError,
|
|
5
|
+
Modules,
|
|
6
|
+
} from "@medusajs/framework/utils";
|
|
7
|
+
import { CampaignTypeEnum } from "../../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
8
|
+
import { CustomCampaign, createCustomCampaignSchema } from "../route";
|
|
9
|
+
import { updateCustomFlashSaleWorkflow } from "../../../../workflows/custom-campaign/updateCustomFlashSaleWorkflow";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* GET handler for fetching a specific flash sale by ID
|
|
13
|
+
*/
|
|
14
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
15
|
+
const { id } = req.params;
|
|
16
|
+
|
|
17
|
+
if (!id) {
|
|
18
|
+
throw new MedusaError(
|
|
19
|
+
MedusaError.Types.INVALID_DATA,
|
|
20
|
+
"Campaign ID is required"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY);
|
|
25
|
+
const productService = container.resolve(Modules.PRODUCT);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// First, find the custom campaign type by campaign ID
|
|
29
|
+
const {
|
|
30
|
+
data: [customCampaignTypes],
|
|
31
|
+
} = await query.graph({
|
|
32
|
+
entity: "custom_campaign_type",
|
|
33
|
+
fields: [
|
|
34
|
+
"id",
|
|
35
|
+
"campaign.*",
|
|
36
|
+
"campaign.promotions.*",
|
|
37
|
+
"campaign.promotions.application_method.*",
|
|
38
|
+
"campaign.promotions.application_method.target_rules.*",
|
|
39
|
+
],
|
|
40
|
+
filters: {
|
|
41
|
+
type: CampaignTypeEnum.FlashSale,
|
|
42
|
+
campaign_id: id,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const campaign = customCampaignTypes.campaign;
|
|
47
|
+
|
|
48
|
+
if (!campaign) {
|
|
49
|
+
throw new MedusaError(
|
|
50
|
+
MedusaError.Types.NOT_FOUND,
|
|
51
|
+
`Flash sale with ID ${id} not found`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fetch promotion usage limits for the campaign
|
|
56
|
+
const { data: promotionUsageLimits } = await query.graph({
|
|
57
|
+
entity: "promotion_usage_limit",
|
|
58
|
+
fields: ["id", "promotion_id", "product_id", "limit", "used"],
|
|
59
|
+
filters: {
|
|
60
|
+
campaign_id: id,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Create a map of promotion usage limits by promotion ID
|
|
65
|
+
const promotionLimitsMap = new Map();
|
|
66
|
+
promotionUsageLimits.forEach((limit) => {
|
|
67
|
+
promotionLimitsMap.set(limit.promotion_id, limit);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Process promotions to extract product information
|
|
71
|
+
const products: CustomCampaign["products"] = [];
|
|
72
|
+
for await (const promotion of campaign.promotions ?? []) {
|
|
73
|
+
if (!promotion.application_method?.target_rules?.length) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const productRule = promotion.application_method.target_rules.find(
|
|
78
|
+
(rule) =>
|
|
79
|
+
rule.attribute === "items.product.id" && rule.operator === "eq"
|
|
80
|
+
);
|
|
81
|
+
const promotionLimit = promotionLimitsMap.get(promotion.id);
|
|
82
|
+
|
|
83
|
+
const product = await productService.retrieveProduct(
|
|
84
|
+
promotionLimit?.product_id
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (productRule && promotion.application_method.value) {
|
|
88
|
+
products.push({
|
|
89
|
+
product: {
|
|
90
|
+
id: product.id,
|
|
91
|
+
title: product.title,
|
|
92
|
+
},
|
|
93
|
+
discountType: promotion.application_method?.type,
|
|
94
|
+
discountValue: promotion.application_method?.value,
|
|
95
|
+
maxQty: promotion.application_method?.max_quantity,
|
|
96
|
+
limit: promotionLimitsMap.get(promotion.id)?.limit,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
res.status(200).json({
|
|
102
|
+
...campaign,
|
|
103
|
+
products,
|
|
104
|
+
} satisfies CustomCampaign);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof MedusaError) {
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.error("Error fetching flash sale:", error);
|
|
111
|
+
throw new MedusaError(
|
|
112
|
+
MedusaError.Types.UNEXPECTED_STATE,
|
|
113
|
+
"An error occurred while fetching the flash sale"
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* PUT handler for updating a specific flash sale by ID
|
|
120
|
+
*/
|
|
121
|
+
export const PUT = async (
|
|
122
|
+
req: MedusaRequest<CustomCampaign>,
|
|
123
|
+
res: MedusaResponse
|
|
124
|
+
) => {
|
|
125
|
+
const { id } = req.params;
|
|
126
|
+
const body = req.body;
|
|
127
|
+
|
|
128
|
+
if (!id) {
|
|
129
|
+
throw new MedusaError(
|
|
130
|
+
MedusaError.Types.INVALID_DATA,
|
|
131
|
+
"Campaign ID is required"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Check if start date is before end date
|
|
137
|
+
if (new Date(body.ends_at) < new Date(body.starts_at)) {
|
|
138
|
+
throw new MedusaError(
|
|
139
|
+
MedusaError.Types.INVALID_DATA,
|
|
140
|
+
"End date must be after start date"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Update the flash sale
|
|
145
|
+
const result = await updateCustomFlashSaleWorkflow.run({
|
|
146
|
+
input: {
|
|
147
|
+
...body,
|
|
148
|
+
id,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
res.status(200).json(result.result);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error instanceof MedusaError) {
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.error("Error updating flash sale:", error);
|
|
159
|
+
throw new MedusaError(
|
|
160
|
+
MedusaError.Types.UNEXPECTED_STATE,
|
|
161
|
+
"An error occurred while updating the flash sale"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import {
|
|
3
|
+
ContainerRegistrationKeys,
|
|
4
|
+
MedusaError,
|
|
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 { createCustomCampaignWorkflow } from "../../../workflows/custom-campaign/createCustomCampaignWorkflow";
|
|
10
|
+
|
|
11
|
+
export const createCustomCampaignSchema = z.object({
|
|
12
|
+
name: z.string().min(1, "Name is required"),
|
|
13
|
+
description: z.string().min(1, "Description is required"),
|
|
14
|
+
type: z.nativeEnum(CampaignTypeEnum),
|
|
15
|
+
starts_at: z.coerce.date(),
|
|
16
|
+
ends_at: z.coerce.date(),
|
|
17
|
+
products: z
|
|
18
|
+
.array(
|
|
19
|
+
z.object({
|
|
20
|
+
product: z.object({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
title: z.string(),
|
|
23
|
+
}),
|
|
24
|
+
discountType: z.enum([
|
|
25
|
+
"percentage",
|
|
26
|
+
// need to handle fixed discount and currency
|
|
27
|
+
// "fixed",
|
|
28
|
+
]),
|
|
29
|
+
discountValue: z.number().min(1),
|
|
30
|
+
limit: z.number().min(1),
|
|
31
|
+
maxQty: z.number().min(1),
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
.min(1, "At least one product is required"),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type CustomCampaign = z.infer<typeof createCustomCampaignSchema>;
|
|
38
|
+
|
|
39
|
+
export const GetFlashSalesSchema = createFindParams({
|
|
40
|
+
order: "-created_at",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const POST = async (
|
|
44
|
+
req: MedusaRequest<CustomCampaign>,
|
|
45
|
+
res: MedusaResponse
|
|
46
|
+
) => {
|
|
47
|
+
const body = req.body;
|
|
48
|
+
|
|
49
|
+
if (new Date(body.ends_at) < new Date(body.starts_at)) {
|
|
50
|
+
throw new MedusaError(
|
|
51
|
+
MedusaError.Types.INVALID_DATA,
|
|
52
|
+
"End date must be after start date"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const customCampaign = await createCustomCampaignWorkflow.run({
|
|
57
|
+
input: body,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
res.status(200).json(customCampaign);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
64
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY);
|
|
65
|
+
const {
|
|
66
|
+
data: customCampaigns,
|
|
67
|
+
metadata: { count, take, skip } = {
|
|
68
|
+
count: 0,
|
|
69
|
+
take: 20,
|
|
70
|
+
skip: 0,
|
|
71
|
+
},
|
|
72
|
+
} = await query.graph({
|
|
73
|
+
entity: "custom_campaign_type",
|
|
74
|
+
...req.queryConfig,
|
|
75
|
+
fields: ["id", "campaign.*", "campaign.promotions.*"],
|
|
76
|
+
filters: { type: CampaignTypeEnum.FlashSale },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const campaigns = customCampaigns.map((campaign) => campaign.campaign);
|
|
80
|
+
|
|
81
|
+
res.status(200).json({
|
|
82
|
+
campaigns,
|
|
83
|
+
count,
|
|
84
|
+
limit: take,
|
|
85
|
+
offset: skip,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineMiddlewares,
|
|
3
|
+
validateAndTransformBody,
|
|
4
|
+
validateAndTransformQuery,
|
|
5
|
+
} from "@medusajs/framework";
|
|
6
|
+
import {
|
|
7
|
+
createCustomCampaignSchema,
|
|
8
|
+
GetFlashSalesSchema,
|
|
9
|
+
} from "./admin/flash-sales/route";
|
|
10
|
+
|
|
11
|
+
export default defineMiddlewares([
|
|
12
|
+
{
|
|
13
|
+
methods: ["POST"],
|
|
14
|
+
matcher: "/admin/flash-sales",
|
|
15
|
+
middlewares: [validateAndTransformBody(createCustomCampaignSchema as any)],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
methods: ["PUT"],
|
|
19
|
+
matcher: "/admin/flash-sales/:id",
|
|
20
|
+
middlewares: [validateAndTransformBody(createCustomCampaignSchema as any)],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
methods: ["GET"],
|
|
24
|
+
matcher: "/admin/flash-sales",
|
|
25
|
+
middlewares: [
|
|
26
|
+
validateAndTransformQuery(GetFlashSalesSchema as any, {
|
|
27
|
+
defaults: ["*", "campaign.*"],
|
|
28
|
+
isList: true,
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse, container } from "@medusajs/framework";
|
|
2
|
+
import {
|
|
3
|
+
MedusaError,
|
|
4
|
+
ContainerRegistrationKeys,
|
|
5
|
+
Modules,
|
|
6
|
+
} from "@medusajs/framework/utils";
|
|
7
|
+
import { CampaignTypeEnum } from "../../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
8
|
+
import { CustomCampaign } from "../../../admin/flash-sales/route";
|
|
9
|
+
|
|
10
|
+
export const GET = async (
|
|
11
|
+
req: MedusaRequest<{ id: string }>,
|
|
12
|
+
res: MedusaResponse
|
|
13
|
+
) => {
|
|
14
|
+
const { id } = req.params;
|
|
15
|
+
|
|
16
|
+
if (!id) {
|
|
17
|
+
throw new MedusaError(
|
|
18
|
+
MedusaError.Types.INVALID_DATA,
|
|
19
|
+
"Campaign ID is required"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY);
|
|
24
|
+
const productService = container.resolve(Modules.PRODUCT);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// First, find the custom campaign type by campaign ID
|
|
28
|
+
const {
|
|
29
|
+
data: [customCampaignTypes],
|
|
30
|
+
} = await query.graph({
|
|
31
|
+
entity: "custom_campaign_type",
|
|
32
|
+
fields: [
|
|
33
|
+
"id",
|
|
34
|
+
"campaign.*",
|
|
35
|
+
"campaign.promotions.*",
|
|
36
|
+
"campaign.promotions.application_method.*",
|
|
37
|
+
"campaign.promotions.application_method.target_rules.*",
|
|
38
|
+
],
|
|
39
|
+
filters: {
|
|
40
|
+
type: CampaignTypeEnum.FlashSale,
|
|
41
|
+
campaign_id: id,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const campaign = customCampaignTypes.campaign;
|
|
46
|
+
|
|
47
|
+
if (!campaign) {
|
|
48
|
+
throw new MedusaError(
|
|
49
|
+
MedusaError.Types.NOT_FOUND,
|
|
50
|
+
`Flash sale with ID ${id} not found`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fetch promotion usage limits for the campaign
|
|
55
|
+
const { data: promotionUsageLimits } = await query.graph({
|
|
56
|
+
entity: "promotion_usage_limit",
|
|
57
|
+
fields: ["id", "promotion_id", "product_id", "limit", "used"],
|
|
58
|
+
filters: {
|
|
59
|
+
campaign_id: id,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Create a map of promotion usage limits by promotion ID
|
|
64
|
+
const promotionLimitsMap = new Map();
|
|
65
|
+
promotionUsageLimits.forEach((limit) => {
|
|
66
|
+
promotionLimitsMap.set(limit.promotion_id, limit);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Process promotions to extract product information
|
|
70
|
+
const products: CustomCampaign["products"] = [];
|
|
71
|
+
for await (const promotion of campaign.promotions ?? []) {
|
|
72
|
+
if (!promotion.application_method?.target_rules?.length) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const productRule = promotion.application_method.target_rules.find(
|
|
77
|
+
(rule) =>
|
|
78
|
+
rule.attribute === "items.product.id" && rule.operator === "eq"
|
|
79
|
+
);
|
|
80
|
+
const promotionLimit = promotionLimitsMap.get(promotion.id);
|
|
81
|
+
|
|
82
|
+
const product = await productService.retrieveProduct(
|
|
83
|
+
promotionLimit?.product_id
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (productRule && promotion.application_method.value) {
|
|
87
|
+
products.push({
|
|
88
|
+
product: {
|
|
89
|
+
id: product.id,
|
|
90
|
+
title: product.title,
|
|
91
|
+
},
|
|
92
|
+
discountType: promotion.application_method?.type,
|
|
93
|
+
discountValue: promotion.application_method?.value,
|
|
94
|
+
maxQty: promotion.application_method?.max_quantity,
|
|
95
|
+
limit: promotionLimitsMap.get(promotion.id)?.limit,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const campaignData = {
|
|
101
|
+
id: campaign.id,
|
|
102
|
+
name: campaign.name,
|
|
103
|
+
description: campaign.description,
|
|
104
|
+
type: campaign.type,
|
|
105
|
+
startsAt: campaign.starts_at,
|
|
106
|
+
endsAt: campaign.ends_at,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const productData = products.map((product) => ({
|
|
110
|
+
id: product.product.id,
|
|
111
|
+
title: product.product.title,
|
|
112
|
+
discountType: product.discountType,
|
|
113
|
+
discountValue: product.discountValue,
|
|
114
|
+
maxQty: product.maxQty,
|
|
115
|
+
limit: product.limit,
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
res.status(200).json({
|
|
119
|
+
...campaignData,
|
|
120
|
+
products: productData,
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof MedusaError) {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.error("Error fetching flash sale:", error);
|
|
128
|
+
throw new MedusaError(
|
|
129
|
+
MedusaError.Types.UNEXPECTED_STATE,
|
|
130
|
+
"An error occurred while fetching the flash sale"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import { ContainerRegistrationKeys } from "@medusajs/framework/utils";
|
|
3
|
+
import { CampaignTypeEnum } from "../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
4
|
+
|
|
5
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
6
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY);
|
|
7
|
+
const {
|
|
8
|
+
data: customCampaigns,
|
|
9
|
+
metadata: { count, take, skip } = {
|
|
10
|
+
count: 0,
|
|
11
|
+
take: 20,
|
|
12
|
+
skip: 0,
|
|
13
|
+
},
|
|
14
|
+
} = await query.graph({
|
|
15
|
+
entity: "custom_campaign_type",
|
|
16
|
+
...req.queryConfig,
|
|
17
|
+
fields: [
|
|
18
|
+
"id",
|
|
19
|
+
"campaign.name",
|
|
20
|
+
"campaign.description",
|
|
21
|
+
"campaign.type",
|
|
22
|
+
"campaign.starts_at",
|
|
23
|
+
"campaign.ends_at",
|
|
24
|
+
],
|
|
25
|
+
filters: { type: CampaignTypeEnum.FlashSale },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const campaigns = customCampaigns.map((campaign) => campaign.campaign);
|
|
29
|
+
|
|
30
|
+
res.status(200).json({
|
|
31
|
+
campaigns,
|
|
32
|
+
count,
|
|
33
|
+
limit: take,
|
|
34
|
+
offset: skip,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Custom scheduled jobs
|
|
2
|
+
|
|
3
|
+
A scheduled job is a function executed at a specified interval of time in the background of your Medusa application.
|
|
4
|
+
|
|
5
|
+
A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory.
|
|
6
|
+
|
|
7
|
+
For example, create the file `src/jobs/hello-world.ts` with the following content:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
MedusaContainer
|
|
12
|
+
} from "@medusajs/framework/types";
|
|
13
|
+
|
|
14
|
+
export default async function myCustomJob(container: MedusaContainer) {
|
|
15
|
+
const productService = container.resolve("product")
|
|
16
|
+
|
|
17
|
+
const products = await productService.listAndCountProducts();
|
|
18
|
+
|
|
19
|
+
// Do something with the products
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const config = {
|
|
23
|
+
name: "daily-product-report",
|
|
24
|
+
schedule: "0 0 * * *", // Every day at midnight
|
|
25
|
+
};
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A scheduled job file must export:
|
|
29
|
+
|
|
30
|
+
- The function to be executed whenever it’s time to run the scheduled job.
|
|
31
|
+
- A configuration object defining the job. It has three properties:
|
|
32
|
+
- `name`: a unique name for the job.
|
|
33
|
+
- `schedule`: a [cron expression](https://crontab.guru/).
|
|
34
|
+
- `numberOfExecutions`: an optional integer, specifying how many times the job will execute before being removed
|
|
35
|
+
|
|
36
|
+
The `handler` is a function that accepts one parameter, `container`, which is a `MedusaContainer` instance used to resolve services.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Module Links
|
|
2
|
+
|
|
3
|
+
A module link forms an association between two data models of different modules, while maintaining module isolation.
|
|
4
|
+
|
|
5
|
+
Learn more about links in [this documentation](https://docs.medusajs.com/learn/fundamentals/module-links)
|
|
6
|
+
|
|
7
|
+
For example:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import BlogModule from "../modules/blog"
|
|
11
|
+
import ProductModule from "@medusajs/medusa/product"
|
|
12
|
+
import { defineLink } from "@medusajs/framework/utils"
|
|
13
|
+
|
|
14
|
+
export default defineLink(
|
|
15
|
+
ProductModule.linkable.product,
|
|
16
|
+
BlogModule.linkable.post
|
|
17
|
+
)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This defines a link between the Product Module's `product` data model and the Blog Module (custom module)'s `post` data model.
|
|
21
|
+
|
|
22
|
+
Then, in the Medusa application using this plugin, run the following command to sync the links to the database:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx medusa db:migrate
|
|
26
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineLink } from "@medusajs/framework/utils";
|
|
2
|
+
import PromotionModule from "@medusajs/medusa/promotion";
|
|
3
|
+
import CustomCampaignModule from "../modules/custom-campaigns";
|
|
4
|
+
|
|
5
|
+
export default defineLink(PromotionModule.linkable.campaign, {
|
|
6
|
+
linkable: CustomCampaignModule.linkable.customCampaignType,
|
|
7
|
+
deleteCascade: true,
|
|
8
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Custom Module
|
|
2
|
+
|
|
3
|
+
A module is a package of reusable functionalities. It can be integrated into your Medusa application without affecting the overall system. You can create a module as part of a plugin.
|
|
4
|
+
|
|
5
|
+
Learn more about modules in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules).
|
|
6
|
+
|
|
7
|
+
To create a module:
|
|
8
|
+
|
|
9
|
+
## 1. Create a Data Model
|
|
10
|
+
|
|
11
|
+
A data model represents a table in the database. You create a data model in a TypeScript or JavaScript file under the `models` directory of a module.
|
|
12
|
+
|
|
13
|
+
For example, create the file `src/modules/blog/models/post.ts` with the following content:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { model } from "@medusajs/framework/utils"
|
|
17
|
+
|
|
18
|
+
const Post = model.define("post", {
|
|
19
|
+
id: model.id().primaryKey(),
|
|
20
|
+
title: model.text(),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export default Post
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 2. Create a Service
|
|
27
|
+
|
|
28
|
+
A module must define a service. A service is a TypeScript or JavaScript class holding methods related to a business logic or commerce functionality.
|
|
29
|
+
|
|
30
|
+
For example, create the file `src/modules/blog/service.ts` with the following content:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { MedusaService } from "@medusajs/framework/utils"
|
|
34
|
+
import Post from "./models/post"
|
|
35
|
+
|
|
36
|
+
class BlogModuleService extends MedusaService({
|
|
37
|
+
Post,
|
|
38
|
+
}){
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default BlogModuleService
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 3. Export Module Definition
|
|
45
|
+
|
|
46
|
+
A module must have an `index.ts` file in its root directory that exports its definition. The definition specifies the main service of the module.
|
|
47
|
+
|
|
48
|
+
For example, create the file `src/modules/blog/index.ts` with the following content:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import BlogModuleService from "./service"
|
|
52
|
+
import { Module } from "@medusajs/framework/utils"
|
|
53
|
+
|
|
54
|
+
export const BLOG_MODULE = "blog"
|
|
55
|
+
|
|
56
|
+
export default Module(BLOG_MODULE, {
|
|
57
|
+
service: BlogModuleService,
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 4. Generate Migrations
|
|
62
|
+
|
|
63
|
+
To generate migrations for your module, run the following command in the plugin's directory:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx medusa plugin:db:genreate
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Use Module
|
|
70
|
+
|
|
71
|
+
You can use the module in customizations within the plugin or within the Medusa application using this plugin. When the plugin is added to a Medusa application, all its modules are registered as well.
|
|
72
|
+
|
|
73
|
+
For example, to use the module in an API route:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
|
|
77
|
+
import BlogModuleService from "../../../modules/blog/service"
|
|
78
|
+
import { BLOG_MODULE } from "../../../modules/blog"
|
|
79
|
+
|
|
80
|
+
export async function GET(
|
|
81
|
+
req: MedusaRequest,
|
|
82
|
+
res: MedusaResponse
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const blogModuleService: BlogModuleService = req.scope.resolve(
|
|
85
|
+
BLOG_MODULE
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const posts = await blogModuleService.listPosts()
|
|
89
|
+
|
|
90
|
+
res.json({
|
|
91
|
+
posts
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Module Options
|
|
97
|
+
|
|
98
|
+
When you register the plugin in the Medusa application, it can accept options. These options are passed to the modules within the plugin:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { defineConfig } from "@medusajs/framework/utils"
|
|
102
|
+
|
|
103
|
+
module.exports = defineConfig({
|
|
104
|
+
// ...
|
|
105
|
+
plugins: [
|
|
106
|
+
{
|
|
107
|
+
resolve: "@myorg/plugin-name",
|
|
108
|
+
options: {
|
|
109
|
+
apiKey: process.env.API_KEY,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Learn more about module options in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options).
|