@sgftech/medusa-payment-stripe-subscription 0.0.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 (43) hide show
  1. package/.babelrc.js +12 -0
  2. package/.eslintrc.js +112 -0
  3. package/.github/dependabot.yml +21 -0
  4. package/.github/scripts/wait-for-server-live.sh +29 -0
  5. package/.github/workflows/test-cli.yml +149 -0
  6. package/.github/workflows/update-preview-deps-ci.yml +70 -0
  7. package/.github/workflows/update-preview-deps.yml +70 -0
  8. package/.vscode/settings.json +2 -0
  9. package/.yarnrc.yml +1 -0
  10. package/README.md +79 -0
  11. package/data/seed-onboarding.json +141 -0
  12. package/data/seed.json +1006 -0
  13. package/index.js +50 -0
  14. package/medusa-config.js +88 -0
  15. package/package.json +109 -0
  16. package/src/__fixtures__/data.ts +30 -0
  17. package/src/__mocks__/stripe.ts +104 -0
  18. package/src/api/README.md +179 -0
  19. package/src/core/__fixtures__/data.ts +225 -0
  20. package/src/core/__fixtures__/stripe-test.ts +12 -0
  21. package/src/core/__tests__/stripe-base.spec.ts +640 -0
  22. package/src/core/stripe-subscription.ts +523 -0
  23. package/src/index.ts +0 -0
  24. package/src/jobs/README.md +32 -0
  25. package/src/loaders/README.md +19 -0
  26. package/src/migrations/README.md +29 -0
  27. package/src/models/README.md +46 -0
  28. package/src/services/README.md +49 -0
  29. package/src/services/stripe-subscription-provider.ts +83 -0
  30. package/src/subscribers/README.md +44 -0
  31. package/src/subscribers/create-product-stripe.ts +23 -0
  32. package/src/subscribers/delete-product-stripe.ts +24 -0
  33. package/src/subscribers/stripe-event.ts +24 -0
  34. package/src/subscribers/update-product-stripe.ts +23 -0
  35. package/src/types/index.ts +9 -0
  36. package/src/utils/handle-customer-subscription.ts +36 -0
  37. package/src/utils/handle-invoice-subscription.ts +54 -0
  38. package/src/utils/handle-subscription.ts +64 -0
  39. package/src/utils/index.ts +143 -0
  40. package/tsconfig.admin.json +8 -0
  41. package/tsconfig.json +30 -0
  42. package/tsconfig.server.json +8 -0
  43. package/tsconfig.spec.json +5 -0
@@ -0,0 +1,523 @@
1
+ import {
2
+ AbstractPaymentProcessor,
3
+ CartService,
4
+ isPaymentProcessorError,
5
+ Item,
6
+ LineItem,
7
+ LineItemTaxLine,
8
+ PaymentProcessorContext,
9
+ PaymentProcessorError,
10
+ PaymentProcessorSessionResponse,
11
+ PaymentSessionStatus
12
+ } from "@medusajs/medusa";
13
+ import { MedusaError } from "@medusajs/utils";
14
+ import { EOL } from "os";
15
+ import Stripe from "stripe";
16
+ import {
17
+ ErrorCodes,
18
+ ErrorIntentStatus,
19
+ PaymentIntentOptions,
20
+ StripeOptions
21
+ } from "medusa-payment-stripe";
22
+ import { StripeSubscriptionOptions } from "../types";
23
+ import StripeBase from "medusa-payment-stripe/dist/core/stripe-base";
24
+
25
+ abstract class StripeSubscriptionService extends StripeBase {
26
+ static identifier = "stripe-subscription";
27
+
28
+ protected readonly options_: StripeSubscriptionOptions;
29
+ protected stripe_: Stripe;
30
+ cartService: CartService;
31
+
32
+ protected constructor(container: { cartService: CartService }, options) {
33
+ super(container, options);
34
+
35
+ this.options_ = options;
36
+ this.cartService = container.cartService;
37
+
38
+ this.init();
39
+ }
40
+
41
+ protected init(): void {
42
+ this.stripe_ =
43
+ this.stripe_ ||
44
+ new Stripe(this.options_.api_key, {
45
+ apiVersion: "2022-11-15"
46
+ });
47
+ }
48
+
49
+ abstract get paymentIntentOptions(): PaymentIntentOptions;
50
+
51
+ get options(): StripeSubscriptionOptions {
52
+ return this.options_;
53
+ }
54
+
55
+ getStripe(): Stripe {
56
+ return this.stripe_;
57
+ }
58
+
59
+ getPaymentIntentOptions(): PaymentIntentOptions {
60
+ const options: PaymentIntentOptions = {};
61
+
62
+ if (this?.paymentIntentOptions?.capture_method) {
63
+ options.capture_method = this.paymentIntentOptions.capture_method;
64
+ }
65
+
66
+ if (this?.paymentIntentOptions?.setup_future_usage) {
67
+ options.setup_future_usage =
68
+ this.paymentIntentOptions.setup_future_usage;
69
+ }
70
+
71
+ if (this?.paymentIntentOptions?.payment_method_types) {
72
+ options.payment_method_types =
73
+ this.paymentIntentOptions.payment_method_types;
74
+ }
75
+
76
+ return options;
77
+ }
78
+
79
+ async getPaymentStatus(
80
+ paymentSessionData: Record<string, unknown>
81
+ ): Promise<PaymentSessionStatus> {
82
+ const id = paymentSessionData.id as string;
83
+ try {
84
+ const subscription = await this.stripe_.subscriptions.retrieve(id);
85
+ if (
86
+ subscription.status === "active" ||
87
+ subscription.status === "trialing"
88
+ ) {
89
+ return PaymentSessionStatus.AUTHORIZED;
90
+ } else if (subscription.status === "canceled") {
91
+ return PaymentSessionStatus.CANCELED;
92
+ } else if (subscription.status === "incomplete") {
93
+ return PaymentSessionStatus.REQUIRES_MORE;
94
+ } else if (subscription.status === "incomplete_expired") {
95
+ return PaymentSessionStatus.REQUIRES_MORE;
96
+ } else {
97
+ return PaymentSessionStatus.PENDING;
98
+ }
99
+ } catch (e) {
100
+ return super.getPaymentStatus(paymentSessionData);
101
+ }
102
+ }
103
+
104
+ async createStripeTaxRate(
105
+ taxLine: LineItemTaxLine,
106
+ country_code: string
107
+ ): Promise<Stripe.TaxRate> {
108
+ const rate = await this.stripe_.taxRates.create({
109
+ display_name: taxLine.name,
110
+ inclusive: false,
111
+ percentage: taxLine.rate,
112
+ country: country_code
113
+ });
114
+ return rate;
115
+ }
116
+
117
+ async getOrCreateStripeTaxRates(
118
+ i: LineItem,
119
+ country_code: string
120
+ ): Promise<string[]> {
121
+ const promiseStripeTaxLines = i.tax_lines.map(async (taxLine) => {
122
+ let result: Stripe.TaxRate;
123
+ if (taxLine.metadata.stripe_tax_rate_id) {
124
+ try {
125
+ result = await this.stripe_.taxRates.retrieve(
126
+ taxLine.metadata.stripe_tax_rate_id as string
127
+ );
128
+ } catch (e) {
129
+ // don't do anything
130
+ }
131
+ }
132
+
133
+ return (
134
+ result ??
135
+ (await this.createStripeTaxRate(taxLine, country_code))
136
+ );
137
+ });
138
+
139
+ const stripeTaxLines = await Promise.all(promiseStripeTaxLines);
140
+ const stripeTaxLineIds = stripeTaxLines.map((i) => i.id);
141
+ return stripeTaxLineIds;
142
+ }
143
+
144
+ async getStripeSubscriptionItemsFromCart(
145
+ cartId: string
146
+ ): Promise<PaymentProcessorError | Stripe.SubscriptionCreateParams.Item[]> {
147
+ const cart = await this.cartService.retrieve(cartId, {
148
+ relations: [
149
+ "items",
150
+ "items.variant",
151
+ "items.variant.prices",
152
+ "items.variant.product",
153
+ "items.variant.metadata",
154
+ "items.tax_lines"
155
+ ]
156
+ });
157
+ const { region } = await this.cartService.retrieve(cartId, {
158
+ relations: ["region"]
159
+ });
160
+ cart.region = region;
161
+ const subscribableItems = cart.items.filter(
162
+ (i) =>
163
+ (i.variant.metadata.subscription as string).toLowerCase() ==
164
+ "true"
165
+ );
166
+
167
+ if (subscribableItems.length == 0) {
168
+ return this.buildError(
169
+ "No subscribable items found in cart",
170
+ {} as PaymentProcessorError
171
+ );
172
+ }
173
+
174
+ if (subscribableItems.length > 20) {
175
+ return this.buildError(
176
+ "Too subscribable items found in cart",
177
+ {} as PaymentProcessorError
178
+ );
179
+ }
180
+
181
+ const stripeSubscriptionItems = subscribableItems.map(
182
+ async (i): Promise<Stripe.SubscriptionCreateParams.Item> => {
183
+ const product = await this.stripe_.products.retrieve(
184
+ i.variant.product.metadata.stripe_product_id as string
185
+ );
186
+ const price = i.variant.prices.find(
187
+ (p) =>
188
+ p.currency.code.toLowerCase() ==
189
+ cart.region.currency_code.toLowerCase()
190
+ );
191
+ const interval = i.variant.metadata.subscription_interval_period
192
+ ? (parseInt(
193
+ i.variant.metadata
194
+ .subscription_interval_period as string
195
+ ) as number)
196
+ : this.options_.subscription_interval_period ?? 30;
197
+
198
+ const taxRateIds = await this.getOrCreateStripeTaxRates(
199
+ i,
200
+ cart.billing_address.country_code
201
+ );
202
+
203
+ const item: Stripe.SubscriptionCreateParams.Item = {
204
+ quantity: 1,
205
+ price_data: {
206
+ currency: i.cart.region.currency_code.toLowerCase(),
207
+ product: i.variant.product.metadata
208
+ .stripe_product_id as string,
209
+ recurring: {
210
+ interval: i.variant.metadata
211
+ .subscription_interval as
212
+ | "day"
213
+ | "week"
214
+ | "month"
215
+ | "year",
216
+
217
+ interval_count: interval
218
+ },
219
+ tax_behavior: "exclusive",
220
+ unit_amount: price.amount,
221
+ unit_amount_decimal: (price.amount / 100).toFixed(2)
222
+ },
223
+ tax_rates: taxRateIds,
224
+
225
+ metadata: {
226
+ variant_id: i.variant_id,
227
+ region: i.cart.region.id
228
+ }
229
+ };
230
+ return item;
231
+ }
232
+ );
233
+
234
+ const result = await Promise.all(stripeSubscriptionItems);
235
+ return result;
236
+ }
237
+
238
+ async isSubscriptionCart(cartId: string): Promise<boolean> {
239
+ const cart = await this.cartService.retrieve(cartId, {
240
+ relations: ["items", "items.variant"]
241
+ });
242
+
243
+ if (cart.items.some((i) => i.variant.metadata.subscription != "true")) {
244
+ return false;
245
+ } else {
246
+ return true;
247
+ }
248
+ }
249
+
250
+ async initiatePayment(
251
+ context: PaymentProcessorContext
252
+ ): Promise<PaymentProcessorError | PaymentProcessorSessionResponse> {
253
+ const intentRequestData = this.getPaymentIntentOptions();
254
+ const {
255
+ email,
256
+ context: cart_context,
257
+ currency_code,
258
+ amount,
259
+ resource_id,
260
+ customer
261
+ } = context;
262
+
263
+ if (!(await this.isSubscriptionCart(resource_id))) {
264
+ return super.initiatePayment(context);
265
+ }
266
+
267
+ const description = (cart_context.payment_description ??
268
+ this.options_?.payment_description) as string;
269
+
270
+ const intentRequest: Stripe.PaymentIntentCreateParams = {
271
+ description,
272
+ amount: Math.round(amount),
273
+ currency: currency_code,
274
+ metadata: { resource_id },
275
+ capture_method: this.options_.capture ? "automatic" : "manual",
276
+ ...intentRequestData
277
+ };
278
+
279
+ if (this.options_?.automatic_payment_methods) {
280
+ intentRequest.automatic_payment_methods = { enabled: true };
281
+ }
282
+
283
+ if (customer?.metadata?.stripe_id) {
284
+ intentRequest.customer = customer.metadata.stripe_id as string;
285
+ } else {
286
+ let stripeCustomer;
287
+ try {
288
+ stripeCustomer = await this.stripe_.customers.create({
289
+ email
290
+ });
291
+ } catch (e) {
292
+ return this.buildError(
293
+ "An error occurred in initiatePayment when creating a Stripe customer",
294
+ e
295
+ );
296
+ }
297
+
298
+ intentRequest.customer = stripeCustomer.id;
299
+ }
300
+
301
+ let session_data;
302
+ try {
303
+ const sub = await this.stripe_.subscriptions.retrieve(
304
+ session_data.id
305
+ );
306
+ const itemsExpected = await this.getStripeSubscriptionItemsFromCart(
307
+ resource_id
308
+ );
309
+
310
+ if ((itemsExpected as PaymentProcessorError).error) {
311
+ return itemsExpected as PaymentProcessorError;
312
+ }
313
+
314
+ const items =
315
+ itemsExpected as Stripe.SubscriptionCreateParams.Item[];
316
+ const createSubscriptionParams: Stripe.SubscriptionCreateParams = {
317
+ items: items,
318
+ currency: currency_code,
319
+ metadata: { resource_id },
320
+ cancel_at_period_end: this.options.cancel_at_period_end,
321
+
322
+ customer: intentRequest.customer,
323
+ collection_method: "charge_automatically",
324
+ payment_behavior: "error_if_incomplete"
325
+ };
326
+
327
+ session_data = (await this.stripe_.subscriptions.create(
328
+ createSubscriptionParams
329
+ )) as unknown as Record<string, string>;
330
+
331
+ // session_data = (await this.stripe_.paymentIntents.create(
332
+ // intentRequest
333
+ // )) as unknown as Record<string, unknown>
334
+ } catch (e) {
335
+ return this.buildError(
336
+ "An error occurred in InitiatePayment during the creation of the stripe payment intent",
337
+ e
338
+ );
339
+ }
340
+
341
+ return {
342
+ session_data,
343
+ update_requests: customer?.metadata?.stripe_id
344
+ ? undefined
345
+ : {
346
+ customer_metadata: {
347
+ stripe_id: intentRequest.customer
348
+ }
349
+ }
350
+ };
351
+ }
352
+
353
+ async authorizePayment(
354
+ paymentSessionData: Record<string, unknown>,
355
+ context: Record<string, unknown>
356
+ ): Promise<
357
+ | PaymentProcessorError
358
+ | {
359
+ status: PaymentSessionStatus;
360
+ data: PaymentProcessorSessionResponse["session_data"];
361
+ }
362
+ > {
363
+ const status = await this.getPaymentStatus(paymentSessionData);
364
+ return { data: paymentSessionData, status };
365
+ }
366
+
367
+ async cancelPayment(
368
+ paymentSessionData: Record<string, unknown>
369
+ ): Promise<
370
+ PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
371
+ > {
372
+ try {
373
+ const id = paymentSessionData.id as string;
374
+ return (await this.stripe_.subscriptions.cancel(
375
+ id
376
+ )) as unknown as PaymentProcessorSessionResponse["session_data"];
377
+ } catch (error) {
378
+ super.cancelPayment(paymentSessionData);
379
+ // if (error.payment_intent?.status === ErrorIntentStatus.CANCELED) {
380
+ // return error.payment_intent;
381
+ // }
382
+
383
+ // return this.buildError("An error occurred in cancelPayment", error);
384
+ }
385
+ }
386
+
387
+ async capturePayment(
388
+ paymentSessionData: Record<string, unknown>
389
+ ): Promise<
390
+ PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
391
+ > {
392
+ const id = paymentSessionData.id as string;
393
+ try {
394
+ const intent = await this.stripe_.subscriptions.retrieve(id);
395
+ if (intent.status != "active") {
396
+ return this.buildError(
397
+ `Subscription not active. Payment is currently ${intent.status}`,
398
+ {} as PaymentProcessorError
399
+ );
400
+ }
401
+ return intent as unknown as PaymentProcessorSessionResponse["session_data"];
402
+ } catch (error) {
403
+ return super.capturePayment(paymentSessionData);
404
+ }
405
+ }
406
+
407
+ async deletePayment(
408
+ paymentSessionData: Record<string, unknown>
409
+ ): Promise<
410
+ PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
411
+ > {
412
+ return await this.cancelPayment(paymentSessionData);
413
+ }
414
+
415
+ async retrievePayment(
416
+ paymentSessionData: Record<string, unknown>
417
+ ): Promise<
418
+ PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
419
+ > {
420
+ try {
421
+ const id = paymentSessionData.id as string;
422
+ const intent = await this.stripe_.subscriptions.retrieve(id);
423
+ return intent as unknown as PaymentProcessorSessionResponse["session_data"];
424
+ } catch (e) {
425
+ return super.retrievePayment(paymentSessionData);
426
+ }
427
+ }
428
+
429
+ async updatePayment(
430
+ context: PaymentProcessorContext
431
+ ): Promise<PaymentProcessorError | PaymentProcessorSessionResponse | void> {
432
+ const { amount, customer, paymentSessionData, resource_id } = context;
433
+ const stripeId = customer?.metadata?.stripe_id;
434
+ const subscriptionData =
435
+ paymentSessionData as unknown as Stripe.Subscription;
436
+
437
+ if (stripeId !== paymentSessionData.customer) {
438
+ const result = await this.initiatePayment(context);
439
+ if (isPaymentProcessorError(result)) {
440
+ return this.buildError(
441
+ "An error occurred in updatePayment during the initiate of the new payment for the new customer",
442
+ result
443
+ );
444
+ }
445
+
446
+ return result;
447
+ } else {
448
+ if (!(await this.isSubscriptionCart(resource_id))) {
449
+ return super.updatePayment(context);
450
+ }
451
+
452
+ if (amount && paymentSessionData.amount === Math.round(amount)) {
453
+ return;
454
+ }
455
+
456
+ try {
457
+ const id = subscriptionData.id as string;
458
+ const itemsExpected =
459
+ await this.getStripeSubscriptionItemsFromCart(resource_id);
460
+
461
+ if ((itemsExpected as PaymentProcessorError).error) {
462
+ return itemsExpected as PaymentProcessorError;
463
+ }
464
+
465
+ const items =
466
+ itemsExpected as Stripe.SubscriptionCreateParams.Item[];
467
+
468
+ const subscriptionUpdateParams: Stripe.SubscriptionUpdateParams =
469
+ {
470
+ items: items,
471
+ metadata: { resource_id },
472
+ cancel_at_period_end: this.options.cancel_at_period_end,
473
+ collection_method: "charge_automatically",
474
+ payment_behavior: "error_if_incomplete"
475
+ };
476
+
477
+ const sessionData = (await this.stripe_.subscriptions.update(
478
+ id,
479
+ subscriptionUpdateParams
480
+ )) as unknown as PaymentProcessorSessionResponse["session_data"];
481
+
482
+ return { session_data: sessionData };
483
+ } catch (e) {
484
+ return this.buildError("An error occurred in updatePayment", e);
485
+ }
486
+ }
487
+ }
488
+
489
+ async updatePaymentData(
490
+ sessionId: string,
491
+ data: Record<string, unknown>
492
+ ): Promise<Record<string, unknown> | PaymentProcessorError> {
493
+ const subscriptionParams: Stripe.SubscriptionUpdateParams = data;
494
+
495
+ try {
496
+ const result = (await this.stripe_.subscriptions.retrieve(
497
+ sessionId,
498
+ { expand: ["metadata"] }
499
+ )) as unknown as Stripe.Subscription;
500
+ if (!result.metadata.resource_id) {
501
+ return super.updatePaymentData(sessionId, data);
502
+ }
503
+ } catch (e) {
504
+ return this.buildError(
505
+ "Subscription not associated with cart, cannot update",
506
+ e
507
+ );
508
+ }
509
+ try {
510
+ // Prevent from updating the amount from here as it should go through
511
+ // the updatePayment method to perform the correct logic
512
+
513
+ const result = (await this.stripe_.subscriptions.update(sessionId, {
514
+ ...subscriptionParams
515
+ })) as unknown as PaymentProcessorSessionResponse["session_data"];
516
+ return result;
517
+ } catch (e) {
518
+ return this.buildError("An error occurred in updatePaymentData", e);
519
+ }
520
+ }
521
+ }
522
+
523
+ export default StripeSubscriptionService;
package/src/index.ts ADDED
File without changes
@@ -0,0 +1,32 @@
1
+ # Custom scheduled jobs
2
+
3
+ You may define custom scheduled jobs (cron jobs) by creating files in the `/jobs` directory.
4
+
5
+ ```ts
6
+ import {
7
+ ProductService,
8
+ ScheduledJobArgs,
9
+ ScheduledJobConfig,
10
+ } from "@medusajs/medusa";
11
+
12
+ export default async function myCustomJob({ container }: ScheduledJobArgs) {
13
+ const productService: ProductService = container.resolve("productService");
14
+
15
+ const products = await productService.listAndCount();
16
+
17
+ // Do something with the products
18
+ }
19
+
20
+ export const config: ScheduledJobConfig = {
21
+ name: "daily-product-report",
22
+ schedule: "0 0 * * *", // Every day at midnight
23
+ };
24
+ ```
25
+
26
+ A scheduled job is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when the job is scheduled. The `config` is an object which defines the name of the job, the schedule, and an optional data object.
27
+
28
+ The `handler` is a function which takes one parameter, an `object` of type `ScheduledJobArgs` with the following properties:
29
+
30
+ - `container` - a `MedusaContainer` instance which can be used to resolve services.
31
+ - `data` - an `object` containing data passed to the job when it was scheduled. This object is passed in the `config` object.
32
+ - `pluginOptions` - an `object` containing plugin options, if the job is defined in a plugin.
@@ -0,0 +1,19 @@
1
+ # Custom loader
2
+
3
+ The loader allows you have access to the Medusa service container. This allows you to access the database and the services registered on the container.
4
+ you can register custom registrations in the container or run custom code on startup.
5
+
6
+ ```ts
7
+ // src/loaders/my-loader.ts
8
+
9
+ import { AwilixContainer } from 'awilix'
10
+
11
+ /**
12
+ *
13
+ * @param container The container in which the registrations are made
14
+ * @param config The options of the plugin or the entire config object
15
+ */
16
+ export default (container: AwilixContainer, config: Record<string, unknown>): void | Promise<void> => {
17
+ /* Implement your own loader. */
18
+ }
19
+ ```
@@ -0,0 +1,29 @@
1
+ # Custom migrations
2
+
3
+ You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`.
4
+ In that case you also need to provide a migration in order to create the table in the database.
5
+
6
+ ## Example
7
+
8
+ ### 1. Create the migration
9
+
10
+ See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation.
11
+
12
+ ```ts
13
+ // src/migration/my-migration.ts
14
+
15
+ import { MigrationInterface, QueryRunner } from "typeorm"
16
+
17
+ export class MyMigration1617703530229 implements MigrationInterface {
18
+ name = "myMigration1617703530229"
19
+
20
+ public async up(queryRunner: QueryRunner): Promise<void> {
21
+ // write you migration here
22
+ }
23
+
24
+ public async down(queryRunner: QueryRunner): Promise<void> {
25
+ // write you migration here
26
+ }
27
+ }
28
+
29
+ ```
@@ -0,0 +1,46 @@
1
+ # Custom models
2
+
3
+ You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`.
4
+
5
+ ## Example
6
+
7
+ ### 1. Create the Entity
8
+
9
+ ```ts
10
+ // src/models/post.ts
11
+
12
+ import { BeforeInsert, Column, Entity, PrimaryColumn } from "typeorm";
13
+ import { generateEntityId } from "@medusajs/utils";
14
+ import { BaseEntity } from "@medusajs/medusa";
15
+
16
+ @Entity()
17
+ export class Post extends BaseEntity {
18
+ @Column({type: 'varchar'})
19
+ title: string | null;
20
+
21
+ @BeforeInsert()
22
+ private beforeInsert(): void {
23
+ this.id = generateEntityId(this.id, "post")
24
+ }
25
+ }
26
+ ```
27
+
28
+ ### 2. Create the Migration
29
+
30
+ You also need to create a Migration to create the new table in the database. See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation.
31
+
32
+ ### 3. Create a Repository
33
+ Entities data can be easily accessed and modified using [TypeORM Repositories](https://typeorm.io/working-with-repository). To create a repository, create a file in `src/repositories`. For example, here’s a repository `PostRepository` for the `Post` entity:
34
+
35
+ ```ts
36
+ // src/repositories/post.ts
37
+
38
+ import { EntityRepository, Repository } from "typeorm"
39
+
40
+ import { Post } from "../models/post"
41
+
42
+ @EntityRepository(Post)
43
+ export class PostRepository extends Repository<Post> { }
44
+ ```
45
+
46
+ See more about defining and accesing your custom [Entities](https://docs.medusajs.com/advanced/backend/entities/overview) in the documentation.
@@ -0,0 +1,49 @@
1
+ # Custom services
2
+
3
+ You may define custom services that will be registered on the global container by creating files in the `/services` directory that export an instance of `BaseService`.
4
+
5
+ ```ts
6
+ // src/services/my-custom.ts
7
+
8
+ import { Lifetime } from "awilix"
9
+ import { TransactionBaseService } from "@medusajs/medusa";
10
+ import { IEventBusService } from "@medusajs/types";
11
+
12
+ export default class MyCustomService extends TransactionBaseService {
13
+ static LIFE_TIME = Lifetime.SCOPED
14
+ protected readonly eventBusService_: IEventBusService
15
+
16
+ constructor(
17
+ { eventBusService }: { eventBusService: IEventBusService },
18
+ options: Record<string, unknown>
19
+ ) {
20
+ // @ts-ignore
21
+ super(...arguments)
22
+
23
+ this.eventBusService_ = eventBusService
24
+ }
25
+ }
26
+
27
+ ```
28
+
29
+ The first argument to the `constructor` is the global giving you access to easy dependency injection. The container holds all registered services from the core, installed plugins and from other files in the `/services` directory. The registration name is a camelCased version of the file name with the type appended i.e.: `my-custom.js` is registered as `myCustomService`, `custom-thing.js` is registered as `customThingService`.
30
+
31
+ You may use the services you define here in custom endpoints by resolving the services defined.
32
+
33
+ ```js
34
+ import { Router } from "express"
35
+
36
+ export default () => {
37
+ const router = Router()
38
+
39
+ router.get("/hello-product", async (req, res) => {
40
+ const myService = req.scope.resolve("myCustomService")
41
+
42
+ res.json({
43
+ message: await myService.getProductMessage()
44
+ })
45
+ })
46
+
47
+ return router;
48
+ }
49
+ ```