@lodashventure/medusa-campaign 1.0.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.
Files changed (45) hide show
  1. package/.medusa/server/src/admin/index.js +2203 -273
  2. package/.medusa/server/src/admin/index.mjs +2196 -267
  3. package/.medusa/server/src/workflows/index.js +10 -0
  4. package/package.json +4 -4
  5. package/src/admin/README.md +31 -0
  6. package/src/admin/components/FlashSaleForm.tsx +379 -0
  7. package/src/admin/components/FlashSalePage.tsx +113 -0
  8. package/src/admin/components/ProductSelector.tsx +88 -0
  9. package/src/admin/hooks/useFlashSaleById.ts +21 -0
  10. package/src/admin/hooks/useFlashSales.ts +25 -0
  11. package/src/admin/lib/sdk.ts +10 -0
  12. package/src/admin/routes/flash-sales/[id]/page.tsx +105 -0
  13. package/src/admin/routes/flash-sales/create/page.tsx +51 -0
  14. package/src/admin/routes/flash-sales/page.tsx +15 -0
  15. package/src/admin/tsconfig.json +24 -0
  16. package/src/admin/types/campaign.ts +25 -0
  17. package/src/admin/vite-env.d.ts +1 -0
  18. package/src/api/README.md +133 -0
  19. package/src/api/admin/flash-sales/[id]/route.ts +164 -0
  20. package/src/api/admin/flash-sales/route.ts +87 -0
  21. package/src/api/middlewares.ts +32 -0
  22. package/src/api/store/campaigns/[id]/route.ts +133 -0
  23. package/src/api/store/campaigns/route.ts +36 -0
  24. package/src/jobs/README.md +36 -0
  25. package/src/links/README.md +26 -0
  26. package/src/links/campaign-type.ts +8 -0
  27. package/src/modules/README.md +116 -0
  28. package/src/modules/custom-campaigns/index.ts +8 -0
  29. package/src/modules/custom-campaigns/migrations/.snapshot-medusa-custom-campaign.json +235 -0
  30. package/src/modules/custom-campaigns/migrations/Migration20250524150901.ts +23 -0
  31. package/src/modules/custom-campaigns/migrations/Migration20250526010310.ts +20 -0
  32. package/src/modules/custom-campaigns/migrations/Migration20250529011904.ts +13 -0
  33. package/src/modules/custom-campaigns/models/custom-campaign-type.ts +10 -0
  34. package/src/modules/custom-campaigns/models/promotion-usage-limit.ts +14 -0
  35. package/src/modules/custom-campaigns/service.ts +10 -0
  36. package/src/modules/custom-campaigns/types/campaign-type.enum.ts +3 -0
  37. package/src/providers/README.md +30 -0
  38. package/src/subscribers/README.md +59 -0
  39. package/src/subscribers/order-placed.ts +17 -0
  40. package/src/workflows/README.md +79 -0
  41. package/src/workflows/custom-campaign/createCustomCampaignWorkflow.ts +181 -0
  42. package/src/workflows/custom-campaign/updateCustomFlashSaleWorkflow.ts +185 -0
  43. package/src/workflows/custom-campaign/updatePromotionUsageWorkflow.ts +70 -0
  44. package/src/workflows/hooks/deletePromotionOnCampaignDelete.ts +49 -0
  45. package/src/workflows/index.ts +3 -0
@@ -0,0 +1,14 @@
1
+ import { model } from "@medusajs/framework/utils";
2
+
3
+ const PromotionUsageLimit = model
4
+ .define("promotion_usage_limit", {
5
+ id: model.id().primaryKey(),
6
+ campaign_id: model.text(),
7
+ promotion_id: model.text(),
8
+ product_id: model.text(),
9
+ limit: model.number(),
10
+ used: model.number(),
11
+ })
12
+ .indexes([{ on: ["campaign_id", "promotion_id"], unique: true }]);
13
+
14
+ export default PromotionUsageLimit;
@@ -0,0 +1,10 @@
1
+ import { MedusaService } from "@medusajs/framework/utils";
2
+ import CustomCampaignType from "./models/custom-campaign-type";
3
+ import PromotionUsageLimit from "./models/promotion-usage-limit";
4
+
5
+ class CustomCampaignModuleService extends MedusaService({
6
+ CustomCampaignType,
7
+ PromotionUsageLimit,
8
+ }) {}
9
+
10
+ export default CustomCampaignModuleService;
@@ -0,0 +1,3 @@
1
+ export enum CampaignTypeEnum {
2
+ FlashSale = "flash-sale",
3
+ }
@@ -0,0 +1,30 @@
1
+ ## Module Providers
2
+
3
+ You can create module providers, such as Notification or File Module Providers under a sub-directory of this directory. For example, `src/providers/my-notification`.
4
+
5
+ Then, you register them in the Medusa application as `plugin-name/providers/my-notification`:
6
+
7
+ ```ts
8
+ module.exports = defineConfig({
9
+ // ...
10
+ modules: [
11
+ {
12
+ resolve: "@medusajs/medusa/notification",
13
+ options: {
14
+ providers: [
15
+ {
16
+ resolve: "@myorg/plugin-name/providers/my-notification",
17
+ id: "my-notification",
18
+ options: {
19
+ channels: ["email"],
20
+ // provider options...
21
+ },
22
+ },
23
+ ],
24
+ },
25
+ },
26
+ ],
27
+ })
28
+ ```
29
+
30
+ Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/create).
@@ -0,0 +1,59 @@
1
+ # Custom subscribers
2
+
3
+ Subscribers handle events emitted in the Medusa application.
4
+
5
+ The subscriber is created in a TypeScript or JavaScript file under the `src/subscribers` directory.
6
+
7
+ For example, create the file `src/subscribers/product-created.ts` with the following content:
8
+
9
+ ```ts
10
+ import {
11
+ type SubscriberConfig,
12
+ } from "@medusajs/framework"
13
+
14
+ // subscriber function
15
+ export default async function productCreateHandler() {
16
+ console.log("A product was created")
17
+ }
18
+
19
+ // subscriber config
20
+ export const config: SubscriberConfig = {
21
+ event: "product.created",
22
+ }
23
+ ```
24
+
25
+ A subscriber file must export:
26
+
27
+ - The subscriber function that is an asynchronous function executed whenever the associated event is triggered.
28
+ - A configuration object defining the event this subscriber is listening to.
29
+
30
+ ## Subscriber Parameters
31
+
32
+ A subscriber receives an object having the following properties:
33
+
34
+ - `event`: An object holding the event's details. It has a `data` property, which is the event's data payload.
35
+ - `container`: The Medusa container. Use it to resolve modules' main services and other registered resources.
36
+
37
+ ```ts
38
+ import type {
39
+ SubscriberArgs,
40
+ SubscriberConfig,
41
+ } from "@medusajs/framework"
42
+
43
+ export default async function productCreateHandler({
44
+ event: { data },
45
+ container,
46
+ }: SubscriberArgs<{ id: string }>) {
47
+ const productId = data.id
48
+
49
+ const productModuleService = container.resolve("product")
50
+
51
+ const product = await productModuleService.retrieveProduct(productId)
52
+
53
+ console.log(`The product ${product.title} was created`)
54
+ }
55
+
56
+ export const config: SubscriberConfig = {
57
+ event: "product.created",
58
+ }
59
+ ```
@@ -0,0 +1,17 @@
1
+ import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework";
2
+ import { updatePromotionUsageWorkflow } from "../workflows/custom-campaign/updatePromotionUsageWorkflow";
3
+
4
+ export default async function updatePromotionUsage({
5
+ event: { data },
6
+ }: SubscriberArgs<{ id: string }>) {
7
+ // update promotion usage
8
+ await updatePromotionUsageWorkflow.run({
9
+ input: {
10
+ order_id: data.id,
11
+ },
12
+ });
13
+ }
14
+
15
+ export const config: SubscriberConfig = {
16
+ event: `order.placed`,
17
+ };
@@ -0,0 +1,79 @@
1
+ # Custom Workflows
2
+
3
+ A workflow is a series of queries and actions that complete a task.
4
+
5
+ The workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory.
6
+
7
+ For example:
8
+
9
+ ```ts
10
+ import {
11
+ createStep,
12
+ createWorkflow,
13
+ WorkflowResponse,
14
+ StepResponse,
15
+ } from "@medusajs/framework/workflows-sdk"
16
+
17
+ const step1 = createStep("step-1", async () => {
18
+ return new StepResponse(`Hello from step one!`)
19
+ })
20
+
21
+ type WorkflowInput = {
22
+ name: string
23
+ }
24
+
25
+ const step2 = createStep(
26
+ "step-2",
27
+ async ({ name }: WorkflowInput) => {
28
+ return new StepResponse(`Hello ${name} from step two!`)
29
+ }
30
+ )
31
+
32
+ type WorkflowOutput = {
33
+ message1: string
34
+ message2: string
35
+ }
36
+
37
+ const helloWorldWorkflow = createWorkflow(
38
+ "hello-world",
39
+ (input: WorkflowInput) => {
40
+ const greeting1 = step1()
41
+ const greeting2 = step2(input)
42
+
43
+ return new WorkflowResponse({
44
+ message1: greeting1,
45
+ message2: greeting2
46
+ })
47
+ }
48
+ )
49
+
50
+ export default helloWorldWorkflow
51
+ ```
52
+
53
+ ## Execute Workflow
54
+
55
+ You can execute the workflow from other resources, such as API routes, scheduled jobs, or subscribers.
56
+
57
+ For example, to execute the workflow in an API route:
58
+
59
+ ```ts
60
+ import type {
61
+ MedusaRequest,
62
+ MedusaResponse,
63
+ } from "@medusajs/framework"
64
+ import myWorkflow from "../../../workflows/hello-world"
65
+
66
+ export async function GET(
67
+ req: MedusaRequest,
68
+ res: MedusaResponse
69
+ ) {
70
+ const { result } = await myWorkflow(req.scope)
71
+ .run({
72
+ input: {
73
+ name: req.query.name as string,
74
+ },
75
+ })
76
+
77
+ res.send(result)
78
+ }
79
+ ```
@@ -0,0 +1,181 @@
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 { CustomCampaign } from "../../api/admin/flash-sales/route";
26
+ import { CUSTOM_CAMPAIGN_MODULE } from "../../modules/custom-campaigns";
27
+ import CustomCampaignModuleService from "../../modules/custom-campaigns/service";
28
+
29
+ const createCustomCampaignStep = createStep(
30
+ "create-custom-campaign-step",
31
+ async (data: CustomCampaign) => {
32
+ const link = container.resolve(ContainerRegistrationKeys.LINK);
33
+ const customCampaignModuleService =
34
+ container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
35
+ const campaignIdentifier = `${data.type}-${Date.now()}`;
36
+
37
+ let campaign: CampaignDTO | undefined;
38
+ let promotions: PromotionDTO[] | undefined;
39
+ let customCampaignType:
40
+ | Awaited<
41
+ ReturnType<CustomCampaignModuleService["createCustomCampaignTypes"]>
42
+ >[number]
43
+ | undefined;
44
+ let promotionUsageLimit:
45
+ | Awaited<
46
+ ReturnType<CustomCampaignModuleService["createPromotionUsageLimits"]>
47
+ >
48
+ | undefined;
49
+
50
+ const {
51
+ result: [_campaign],
52
+ } = await createCampaignsWorkflow.run({
53
+ input: {
54
+ campaignsData: [
55
+ {
56
+ name: data.name,
57
+ campaign_identifier: campaignIdentifier,
58
+ description: data.description,
59
+ starts_at: new Date(data.starts_at),
60
+ ends_at: new Date(data.ends_at),
61
+ },
62
+ ],
63
+ },
64
+ });
65
+ campaign = _campaign;
66
+
67
+ try {
68
+ const { result: _promotions } = await createPromotionsWorkflow.run({
69
+ input: {
70
+ promotionsData: data.products.map(
71
+ (product) =>
72
+ ({
73
+ code: `${campaignIdentifier}-${product.product.id}`,
74
+ type: "standard",
75
+ status: "active",
76
+ is_automatic: true,
77
+ campaign_id: campaign.id,
78
+ application_method: {
79
+ target_type: "items",
80
+ allocation: "each",
81
+ type: product.discountType,
82
+ value: product.discountValue,
83
+ max_quantity: product.maxQty,
84
+ target_rules: [
85
+ {
86
+ attribute: "items.product.id",
87
+ operator: "eq",
88
+ values: [product.product.id],
89
+ },
90
+ ],
91
+ } satisfies CreateApplicationMethodDTO,
92
+ } satisfies CreatePromotionDTO)
93
+ ),
94
+ },
95
+ });
96
+ promotions = _promotions;
97
+
98
+ const productPromotionMap = new Map<string, string>();
99
+ promotions.forEach((promotion) => {
100
+ const productId = promotion.code?.split("-").at(-1);
101
+ if (!productId) {
102
+ throw new MedusaError(
103
+ MedusaError.Types.INVALID_DATA,
104
+ "Invalid product id"
105
+ );
106
+ }
107
+ productPromotionMap.set(productId, promotion.id);
108
+ });
109
+
110
+ customCampaignType =
111
+ await customCampaignModuleService.createCustomCampaignTypes({
112
+ campaign_id: campaign.id,
113
+ type: data.type,
114
+ });
115
+
116
+ promotionUsageLimit =
117
+ await customCampaignModuleService.createPromotionUsageLimits(
118
+ data.products.map((product) => ({
119
+ campaign_id: campaign.id,
120
+ promotion_id: productPromotionMap.get(product.product.id),
121
+ product_id: product.product.id,
122
+ limit: product.limit,
123
+ used: 0,
124
+ }))
125
+ );
126
+
127
+ // link campaign type
128
+ await link.create([
129
+ {
130
+ [Modules.PROMOTION]: {
131
+ campaign_id: campaign.id,
132
+ },
133
+ [CUSTOM_CAMPAIGN_MODULE]: {
134
+ custom_campaign_type_id: customCampaignType.id,
135
+ },
136
+ },
137
+ ]);
138
+
139
+ return new StepResponse({
140
+ campaign,
141
+ promotions,
142
+ });
143
+ } catch (error) {
144
+ console.log({ error });
145
+ await deleteCampaignsWorkflow.run({
146
+ input: {
147
+ ids: campaign.id,
148
+ },
149
+ });
150
+
151
+ if (promotions) {
152
+ await deletePromotionsWorkflow.run({
153
+ input: {
154
+ ids: promotions.map((promotion) => promotion.id),
155
+ },
156
+ });
157
+ }
158
+
159
+ if (customCampaignType) {
160
+ await customCampaignModuleService.deleteCustomCampaignTypes(
161
+ customCampaignType.id
162
+ );
163
+ }
164
+
165
+ if (promotionUsageLimit) {
166
+ await customCampaignModuleService.deletePromotionUsageLimits(
167
+ promotionUsageLimit.map((limit) => limit.id)
168
+ );
169
+ }
170
+ }
171
+ }
172
+ );
173
+
174
+ export const createCustomCampaignWorkflow = createWorkflow(
175
+ "create-custom-campaign",
176
+ (data: CustomCampaign) => {
177
+ const customCampaign = createCustomCampaignStep(data);
178
+
179
+ return new WorkflowResponse(customCampaign);
180
+ }
181
+ );
@@ -0,0 +1,185 @@
1
+ import { container } from "@medusajs/framework";
2
+ import {
3
+ CreateApplicationMethodDTO,
4
+ CreatePromotionDTO,
5
+ UpdateCampaignDTO,
6
+ UpdatePromotionDTO,
7
+ } from "@medusajs/framework/types";
8
+ import { MedusaError, Modules } from "@medusajs/framework/utils";
9
+ import {
10
+ createStep,
11
+ createWorkflow,
12
+ StepResponse,
13
+ WorkflowResponse,
14
+ } from "@medusajs/framework/workflows-sdk";
15
+ import {
16
+ createPromotionsWorkflow,
17
+ deletePromotionsWorkflow,
18
+ updateCampaignsWorkflow,
19
+ updatePromotionsWorkflow,
20
+ } from "@medusajs/medusa/core-flows";
21
+ import { CustomCampaign } from "../../api/admin/flash-sales/route";
22
+ import { CUSTOM_CAMPAIGN_MODULE } from "../../modules/custom-campaigns";
23
+ import CustomCampaignModuleService from "../../modules/custom-campaigns/service";
24
+
25
+ interface UpdateFlashSaleInput extends CustomCampaign {
26
+ id: string;
27
+ }
28
+
29
+ const updateCustomFlashSaleStep = createStep(
30
+ "update-custom-flash-sale-step",
31
+ async (data: UpdateFlashSaleInput) => {
32
+ const campaign_id = data.id;
33
+
34
+ const customCampaignModuleService =
35
+ container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
36
+ const promotionService = container.resolve(Modules.PROMOTION);
37
+
38
+ if (!campaign_id) {
39
+ throw new MedusaError(
40
+ MedusaError.Types.INVALID_DATA,
41
+ "Campaign ID is required"
42
+ );
43
+ }
44
+
45
+ // Fetch the existing campaign
46
+ const campaign = await promotionService.retrieveCampaign(campaign_id);
47
+
48
+ // Fetch existing promotion usage limits
49
+ const promotionUsageLimits =
50
+ await customCampaignModuleService.listPromotionUsageLimits({
51
+ campaign_id: campaign_id,
52
+ });
53
+
54
+ // Create maps for existing promotions and usage limits
55
+ const existingPromotionUsageMapByProductId = new Map(
56
+ promotionUsageLimits.map((limit) => [limit.product_id, limit])
57
+ );
58
+ const promotionsToBeCreate = data.products.filter(
59
+ (product) => !existingPromotionUsageMapByProductId.has(product.product.id)
60
+ );
61
+ const productPromotionsToBeUpdate = data.products.filter((product) =>
62
+ existingPromotionUsageMapByProductId.has(product.product.id)
63
+ );
64
+
65
+ const inputProductIdSet = new Set(
66
+ data.products.map((product) => product.product.id)
67
+ );
68
+ const promotionsToBeDelete = promotionUsageLimits.filter(
69
+ (limit) => !inputProductIdSet.has(limit.product_id)
70
+ );
71
+
72
+ console.log("update campaign");
73
+ // 1. Update the campaign details
74
+ await updateCampaignsWorkflow.run({
75
+ input: {
76
+ campaignsData: [
77
+ {
78
+ id: campaign_id,
79
+ name: data.name,
80
+ description: data.description,
81
+ starts_at: new Date(data.starts_at),
82
+ ends_at: new Date(data.ends_at),
83
+ } satisfies UpdateCampaignDTO,
84
+ ],
85
+ },
86
+ });
87
+
88
+ // 2.1 Update promotions to be updated
89
+ await promotionService.updatePromotions(
90
+ productPromotionsToBeUpdate.map((product) => {
91
+ const promotion_id =
92
+ existingPromotionUsageMapByProductId.get(product.product.id)
93
+ ?.promotion_id ?? "";
94
+
95
+ return {
96
+ id: promotion_id,
97
+ application_method: {
98
+ value: product.discountValue,
99
+ max_quantity: product.maxQty,
100
+ },
101
+ } satisfies UpdatePromotionDTO;
102
+ })
103
+ );
104
+
105
+ // 2.2 Update promotion usage limits to be updated
106
+ await customCampaignModuleService.updatePromotionUsageLimits(
107
+ productPromotionsToBeUpdate.map((product) => ({
108
+ id:
109
+ existingPromotionUsageMapByProductId.get(product.product.id)?.id ??
110
+ "",
111
+ limit: product.limit,
112
+ }))
113
+ );
114
+
115
+ // 3. Delete promotions to be deleted
116
+ await deletePromotionsWorkflow.run({
117
+ input: {
118
+ ids: promotionsToBeDelete.map((limit) => limit.promotion_id),
119
+ },
120
+ });
121
+ await customCampaignModuleService.softDeletePromotionUsageLimits(
122
+ promotionsToBeDelete.map((limit) => limit.id)
123
+ );
124
+
125
+ // 4. Create promotions to be created
126
+ const { result: createdPromotions } = await createPromotionsWorkflow.run({
127
+ input: {
128
+ promotionsData: promotionsToBeCreate.map(
129
+ (product) =>
130
+ ({
131
+ code: `${campaign.campaign_identifier}-${product.product.id}`,
132
+ type: "standard",
133
+ status: "active",
134
+ is_automatic: true,
135
+ campaign_id: campaign_id,
136
+ application_method: {
137
+ target_type: "items",
138
+ allocation: "each",
139
+ type: product.discountType,
140
+ value: product.discountValue,
141
+ max_quantity: product.maxQty,
142
+ target_rules: [
143
+ {
144
+ attribute: "items.product.id",
145
+ operator: "eq",
146
+ values: [product.product.id],
147
+ },
148
+ ],
149
+ } satisfies CreateApplicationMethodDTO,
150
+ } satisfies CreatePromotionDTO)
151
+ ),
152
+ },
153
+ });
154
+ const productPromotionMap = new Map<string, string>();
155
+ createdPromotions.forEach((promotion) => {
156
+ const productId = promotion.code?.split("-").at(-1);
157
+ if (!productId) {
158
+ throw new MedusaError(
159
+ MedusaError.Types.INVALID_DATA,
160
+ "Invalid product id"
161
+ );
162
+ }
163
+ productPromotionMap.set(productId, promotion.id);
164
+ });
165
+ await customCampaignModuleService.createPromotionUsageLimits(
166
+ promotionsToBeCreate.map((product) => ({
167
+ campaign_id: campaign.id,
168
+ promotion_id: productPromotionMap.get(product.product.id) ?? "",
169
+ product_id: product.product.id,
170
+ limit: product.limit,
171
+ used: 0,
172
+ }))
173
+ );
174
+ return new StepResponse();
175
+ }
176
+ );
177
+
178
+ export const updateCustomFlashSaleWorkflow = createWorkflow(
179
+ "update-custom-flash-sale",
180
+ (data: UpdateFlashSaleInput) => {
181
+ const updatedFlashSale = updateCustomFlashSaleStep(data);
182
+
183
+ return new WorkflowResponse(updatedFlashSale);
184
+ }
185
+ );
@@ -0,0 +1,70 @@
1
+ import { Modules } from "@medusajs/framework/utils";
2
+ import {
3
+ createStep,
4
+ createWorkflow,
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
+ interface UpdatePromotionUsageInput {
11
+ order_id: string;
12
+ }
13
+
14
+ const updatePromotionUsageStep = createStep(
15
+ "update-promotion-usage-step",
16
+ async ({ order_id }: UpdatePromotionUsageInput, { container }) => {
17
+ const orderService = await container.resolve(Modules.ORDER);
18
+
19
+ const promotionService = await container.resolve(Modules.PROMOTION);
20
+ const customCampaignTypeService =
21
+ container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
22
+
23
+ const order = await orderService.retrieveOrder(order_id, {
24
+ relations: ["items.adjustments"],
25
+ });
26
+
27
+ const promotionSet = new Set(
28
+ order.items
29
+ ?.map((item) =>
30
+ item.adjustments?.map((adjustment) => adjustment.promotion_id)
31
+ )
32
+ .flat()
33
+ );
34
+ const promotionIds = Array.from(promotionSet);
35
+ const promotionUsageLimits =
36
+ await customCampaignTypeService.listPromotionUsageLimits({
37
+ promotion_id: promotionIds,
38
+ });
39
+
40
+ const updatePromotionUsagePayload = promotionUsageLimits.map((limit) => ({
41
+ ...limit,
42
+ used: limit.used + 1,
43
+ }));
44
+
45
+ const limitReachedPromotions = updatePromotionUsagePayload.filter(
46
+ (limit) => limit.used >= limit.limit
47
+ );
48
+
49
+ if (limitReachedPromotions.length > 0) {
50
+ await promotionService.updatePromotions(
51
+ limitReachedPromotions.map((limit) => ({
52
+ id: limit.promotion_id,
53
+ status: "inactive",
54
+ }))
55
+ );
56
+ }
57
+
58
+ await customCampaignTypeService.updatePromotionUsageLimits(
59
+ updatePromotionUsagePayload
60
+ );
61
+ }
62
+ );
63
+
64
+ export const updatePromotionUsageWorkflow = createWorkflow(
65
+ "update-promotion-usage",
66
+ (data: UpdatePromotionUsageInput) => {
67
+ updatePromotionUsageStep(data);
68
+ return new WorkflowResponse({ success: true });
69
+ }
70
+ );