@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,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).
@@ -0,0 +1,8 @@
1
+ import { Module } from "@medusajs/framework/utils";
2
+ import CustomCampaignModuleService from "./service";
3
+
4
+ export const CUSTOM_CAMPAIGN_MODULE = "customCampaign";
5
+
6
+ export default Module(CUSTOM_CAMPAIGN_MODULE, {
7
+ service: CustomCampaignModuleService,
8
+ });
@@ -0,0 +1,235 @@
1
+ {
2
+ "namespaces": [
3
+ "public"
4
+ ],
5
+ "name": "public",
6
+ "tables": [
7
+ {
8
+ "columns": {
9
+ "id": {
10
+ "name": "id",
11
+ "type": "text",
12
+ "unsigned": false,
13
+ "autoincrement": false,
14
+ "primary": false,
15
+ "nullable": false,
16
+ "mappedType": "text"
17
+ },
18
+ "campaign_id": {
19
+ "name": "campaign_id",
20
+ "type": "text",
21
+ "unsigned": false,
22
+ "autoincrement": false,
23
+ "primary": false,
24
+ "nullable": false,
25
+ "mappedType": "text"
26
+ },
27
+ "type": {
28
+ "name": "type",
29
+ "type": "text",
30
+ "unsigned": false,
31
+ "autoincrement": false,
32
+ "primary": false,
33
+ "nullable": false,
34
+ "enumItems": [
35
+ "flash-sale"
36
+ ],
37
+ "mappedType": "enum"
38
+ },
39
+ "created_at": {
40
+ "name": "created_at",
41
+ "type": "timestamptz",
42
+ "unsigned": false,
43
+ "autoincrement": false,
44
+ "primary": false,
45
+ "nullable": false,
46
+ "length": 6,
47
+ "default": "now()",
48
+ "mappedType": "datetime"
49
+ },
50
+ "updated_at": {
51
+ "name": "updated_at",
52
+ "type": "timestamptz",
53
+ "unsigned": false,
54
+ "autoincrement": false,
55
+ "primary": false,
56
+ "nullable": false,
57
+ "length": 6,
58
+ "default": "now()",
59
+ "mappedType": "datetime"
60
+ },
61
+ "deleted_at": {
62
+ "name": "deleted_at",
63
+ "type": "timestamptz",
64
+ "unsigned": false,
65
+ "autoincrement": false,
66
+ "primary": false,
67
+ "nullable": true,
68
+ "length": 6,
69
+ "mappedType": "datetime"
70
+ }
71
+ },
72
+ "name": "custom_campaign_type",
73
+ "schema": "public",
74
+ "indexes": [
75
+ {
76
+ "keyName": "IDX_custom_campaign_type_campaign_id_unique",
77
+ "columnNames": [],
78
+ "composite": false,
79
+ "constraint": false,
80
+ "primary": false,
81
+ "unique": false,
82
+ "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_custom_campaign_type_campaign_id_unique\" ON \"custom_campaign_type\" (campaign_id) WHERE deleted_at IS NULL"
83
+ },
84
+ {
85
+ "keyName": "IDX_custom_campaign_type_deleted_at",
86
+ "columnNames": [],
87
+ "composite": false,
88
+ "constraint": false,
89
+ "primary": false,
90
+ "unique": false,
91
+ "expression": "CREATE INDEX IF NOT EXISTS \"IDX_custom_campaign_type_deleted_at\" ON \"custom_campaign_type\" (deleted_at) WHERE deleted_at IS NULL"
92
+ },
93
+ {
94
+ "keyName": "custom_campaign_type_pkey",
95
+ "columnNames": [
96
+ "id"
97
+ ],
98
+ "composite": false,
99
+ "constraint": true,
100
+ "primary": true,
101
+ "unique": true
102
+ }
103
+ ],
104
+ "checks": [],
105
+ "foreignKeys": {},
106
+ "nativeEnums": {}
107
+ },
108
+ {
109
+ "columns": {
110
+ "id": {
111
+ "name": "id",
112
+ "type": "text",
113
+ "unsigned": false,
114
+ "autoincrement": false,
115
+ "primary": false,
116
+ "nullable": false,
117
+ "mappedType": "text"
118
+ },
119
+ "campaign_id": {
120
+ "name": "campaign_id",
121
+ "type": "text",
122
+ "unsigned": false,
123
+ "autoincrement": false,
124
+ "primary": false,
125
+ "nullable": false,
126
+ "mappedType": "text"
127
+ },
128
+ "promotion_id": {
129
+ "name": "promotion_id",
130
+ "type": "text",
131
+ "unsigned": false,
132
+ "autoincrement": false,
133
+ "primary": false,
134
+ "nullable": false,
135
+ "mappedType": "text"
136
+ },
137
+ "product_id": {
138
+ "name": "product_id",
139
+ "type": "text",
140
+ "unsigned": false,
141
+ "autoincrement": false,
142
+ "primary": false,
143
+ "nullable": false,
144
+ "mappedType": "text"
145
+ },
146
+ "limit": {
147
+ "name": "limit",
148
+ "type": "integer",
149
+ "unsigned": false,
150
+ "autoincrement": false,
151
+ "primary": false,
152
+ "nullable": false,
153
+ "mappedType": "integer"
154
+ },
155
+ "used": {
156
+ "name": "used",
157
+ "type": "integer",
158
+ "unsigned": false,
159
+ "autoincrement": false,
160
+ "primary": false,
161
+ "nullable": false,
162
+ "mappedType": "integer"
163
+ },
164
+ "created_at": {
165
+ "name": "created_at",
166
+ "type": "timestamptz",
167
+ "unsigned": false,
168
+ "autoincrement": false,
169
+ "primary": false,
170
+ "nullable": false,
171
+ "length": 6,
172
+ "default": "now()",
173
+ "mappedType": "datetime"
174
+ },
175
+ "updated_at": {
176
+ "name": "updated_at",
177
+ "type": "timestamptz",
178
+ "unsigned": false,
179
+ "autoincrement": false,
180
+ "primary": false,
181
+ "nullable": false,
182
+ "length": 6,
183
+ "default": "now()",
184
+ "mappedType": "datetime"
185
+ },
186
+ "deleted_at": {
187
+ "name": "deleted_at",
188
+ "type": "timestamptz",
189
+ "unsigned": false,
190
+ "autoincrement": false,
191
+ "primary": false,
192
+ "nullable": true,
193
+ "length": 6,
194
+ "mappedType": "datetime"
195
+ }
196
+ },
197
+ "name": "promotion_usage_limit",
198
+ "schema": "public",
199
+ "indexes": [
200
+ {
201
+ "keyName": "IDX_promotion_usage_limit_deleted_at",
202
+ "columnNames": [],
203
+ "composite": false,
204
+ "constraint": false,
205
+ "primary": false,
206
+ "unique": false,
207
+ "expression": "CREATE INDEX IF NOT EXISTS \"IDX_promotion_usage_limit_deleted_at\" ON \"promotion_usage_limit\" (deleted_at) WHERE deleted_at IS NULL"
208
+ },
209
+ {
210
+ "keyName": "IDX_promotion_usage_limit_campaign_id_promotion_id_unique",
211
+ "columnNames": [],
212
+ "composite": false,
213
+ "constraint": false,
214
+ "primary": false,
215
+ "unique": false,
216
+ "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_promotion_usage_limit_campaign_id_promotion_id_unique\" ON \"promotion_usage_limit\" (campaign_id, promotion_id) WHERE deleted_at IS NULL"
217
+ },
218
+ {
219
+ "keyName": "promotion_usage_limit_pkey",
220
+ "columnNames": [
221
+ "id"
222
+ ],
223
+ "composite": false,
224
+ "constraint": true,
225
+ "primary": true,
226
+ "unique": true
227
+ }
228
+ ],
229
+ "checks": [],
230
+ "foreignKeys": {},
231
+ "nativeEnums": {}
232
+ }
233
+ ],
234
+ "nativeEnums": {}
235
+ }
@@ -0,0 +1,23 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20250524150901 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table if exists "promotion_usage_limit" drop constraint if exists "promotion_usage_limit_promotion_id_unique";`);
7
+ this.addSql(`alter table if exists "custom_campaign_type" drop constraint if exists "custom_campaign_type_campaign_id_unique";`);
8
+ this.addSql(`create table if not exists "custom_campaign_type" ("id" text not null, "campaign_id" text not null, "type" text check ("type" in ('flash-sale')) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "custom_campaign_type_pkey" primary key ("id"));`);
9
+ this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_custom_campaign_type_campaign_id_unique" ON "custom_campaign_type" (campaign_id) WHERE deleted_at IS NULL;`);
10
+ this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_custom_campaign_type_deleted_at" ON "custom_campaign_type" (deleted_at) WHERE deleted_at IS NULL;`);
11
+
12
+ this.addSql(`create table if not exists "promotion_usage_limit" ("id" text not null, "promotion_id" text not null, "limit" integer not null, "used" integer not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_usage_limit_pkey" primary key ("id"));`);
13
+ this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_promotion_usage_limit_promotion_id_unique" ON "promotion_usage_limit" (promotion_id) WHERE deleted_at IS NULL;`);
14
+ this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_promotion_usage_limit_deleted_at" ON "promotion_usage_limit" (deleted_at) WHERE deleted_at IS NULL;`);
15
+ }
16
+
17
+ override async down(): Promise<void> {
18
+ this.addSql(`drop table if exists "custom_campaign_type" cascade;`);
19
+
20
+ this.addSql(`drop table if exists "promotion_usage_limit" cascade;`);
21
+ }
22
+
23
+ }
@@ -0,0 +1,20 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20250526010310 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table if exists "promotion_usage_limit" drop constraint if exists "promotion_usage_limit_campaign_id_promotion_id_unique";`);
7
+ this.addSql(`drop index if exists "IDX_promotion_usage_limit_promotion_id_unique";`);
8
+
9
+ this.addSql(`alter table if exists "promotion_usage_limit" add column if not exists "campaign_id" text not null;`);
10
+ this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_promotion_usage_limit_campaign_id_promotion_id_unique" ON "promotion_usage_limit" (campaign_id, promotion_id) WHERE deleted_at IS NULL;`);
11
+ }
12
+
13
+ override async down(): Promise<void> {
14
+ this.addSql(`drop index if exists "IDX_promotion_usage_limit_campaign_id_promotion_id_unique";`);
15
+ this.addSql(`alter table if exists "promotion_usage_limit" drop column if exists "campaign_id";`);
16
+
17
+ this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_promotion_usage_limit_promotion_id_unique" ON "promotion_usage_limit" (promotion_id) WHERE deleted_at IS NULL;`);
18
+ }
19
+
20
+ }
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20250529011904 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table if exists "promotion_usage_limit" add column if not exists "product_id" text not null;`);
7
+ }
8
+
9
+ override async down(): Promise<void> {
10
+ this.addSql(`alter table if exists "promotion_usage_limit" drop column if exists "product_id";`);
11
+ }
12
+
13
+ }
@@ -0,0 +1,10 @@
1
+ import { model } from "@medusajs/framework/utils";
2
+ import { CampaignTypeEnum } from "../types/campaign-type.enum";
3
+
4
+ const CustomCampaignType = model.define("custom_campaign_type", {
5
+ id: model.id().primaryKey(),
6
+ campaign_id: model.text().unique(),
7
+ type: model.enum(CampaignTypeEnum),
8
+ });
9
+
10
+ export default CustomCampaignType;