feat: Add support for providers to validate their options at loading time (#8853)

* feat: Add support for providers to validate their options at loading time

* fix missing removal

* fix integration tests

* add tests
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-08-29 09:08:49 +02:00
1 parent b8572165cb
commit 77b874f272
11 files changed
+281 -205

No files matched your search

@@ -0,0 +1,9 @@
const service = class TestService {
static validateOptions(options: Record<any, any>) {
throw new Error("Wrong options")
}
}
export default {
services: [service],
}
@@ -1,5 +1,5 @@
import { createMedusaContainer } from "@medusajs/utils" import { createMedusaContainer } from "@medusajs/utils"
import { Lifetime, asFunction } from "awilix" import { asFunction, Lifetime } from "awilix"
import { moduleProviderLoader } from "../module-provider-loader" import { moduleProviderLoader } from "../module-provider-loader"
const logger = { const logger = {
@@ -34,6 +34,24 @@ describe("modules loader", () => {
expect(testService.constructor.name).toEqual("TestService") expect(testService.constructor.name).toEqual("TestService")
}) })
it("should fail to register the provider service", async () => {
const moduleProviders = [
{
resolve: "@providers/default-with-fail-validation",
id: "default",
options: {},
},
]
const err = await moduleProviderLoader({
container,
providers: moduleProviders,
}).catch((e) => e)
expect(err).toBeTruthy()
expect(err.message).toBe("Wrong options")
})
it("should register the provider service with custom register fn", async () => { it("should register the provider service with custom register fn", async () => {
const fn = async (klass, container, details) => { const fn = async (klass, container, details) => {
container.register({ container.register({
@@ -64,6 +82,35 @@ describe("modules loader", () => {
expect(testService.constructor.name).toEqual("TestService") expect(testService.constructor.name).toEqual("TestService")
}) })
it("should fail to register the provider service with custom register fn", async () => {
const fn = async (klass, container, details) => {
container.register({
[`testServiceCustomRegistration`]: asFunction(
(cradle) => new klass(cradle, details.options),
{
lifetime: Lifetime.SINGLETON,
}
),
})
}
const moduleProviders = [
{
resolve: "@providers/default-with-fail-validation",
id: "default",
options: {},
},
]
const err = await moduleProviderLoader({
container,
providers: moduleProviders,
registerServiceFn: fn,
}).catch((e) => e)
expect(err).toBeTruthy()
expect(err.message).toBe("Wrong options")
})
it("should log the errors if no service is defined", async () => { it("should log the errors if no service is defined", async () => {
const moduleProviders = [ const moduleProviders = [
{ {
@@ -60,8 +60,11 @@ export async function loadModuleProvider(
) )
} }
const services = await promiseAll( return await promiseAll(
loadedProvider.services.map(async (service) => { loadedProvider.services.map(async (service) => {
// Ask the provider to validate its options
await service.validateOptions?.(provider.options)
const name = lowerCaseFirst(service.name) const name = lowerCaseFirst(service.name)
if (registerServiceFn) { if (registerServiceFn) {
// Used to register the specific type of service in the provider // Used to register the specific type of service in the provider
@@ -83,6 +86,4 @@ export async function loadModuleProvider(
return service return service
}) })
) )
return services
} }
@@ -1,7 +1,7 @@
import { import {
AuthIdentityProviderService,
AuthenticationInput, AuthenticationInput,
AuthenticationResponse, AuthenticationResponse,
AuthIdentityProviderService,
IAuthProvider, IAuthProvider,
} from "@medusajs/types" } from "@medusajs/types"
@@ -74,6 +74,7 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
* @ignore * @ignore
*/ */
private static DISPLAY_NAME: string private static DISPLAY_NAME: string
/** /**
* @ignore * @ignore
*/ */
@@ -91,6 +92,12 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
return (this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME return (this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME
} }
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
/** /**
* @ignore * @ignore
* *
@@ -2,46 +2,46 @@ import { FileTypes, IFileProvider } from "@medusajs/types"
/** /**
* ### constructor * ### constructor
* *
* The constructor allows you to access resources from the module's container using the first parameter, * The constructor allows you to access resources from the module's container using the first parameter,
* and the module's options using the second parameter. * and the module's options using the second parameter.
* *
* If you're creating a client or establishing a connection with a third-party service, do it in the constructor. * If you're creating a client or establishing a connection with a third-party service, do it in the constructor.
* *
* #### Example * #### Example
* *
* ```ts * ```ts
* import { Logger } from "@medusajs/types" * import { Logger } from "@medusajs/types"
* import { AbstractFileProviderService } from "@medusajs/utils" * import { AbstractFileProviderService } from "@medusajs/utils"
* *
* type InjectedDependencies = { * type InjectedDependencies = {
* logger: Logger * logger: Logger
* } * }
* *
* type Options = { * type Options = {
* apiKey: string * apiKey: string
* } * }
* *
* class MyFileProviderService extends AbstractFileProviderService { * class MyFileProviderService extends AbstractFileProviderService {
* protected logger_: Logger * protected logger_: Logger
* protected options_: Options * protected options_: Options
* // assuming you're initializing a client * // assuming you're initializing a client
* protected client * protected client
* *
* constructor ( * constructor (
* { logger }: InjectedDependencies, * { logger }: InjectedDependencies,
* options: Options * options: Options
* ) { * ) {
* super() * super()
* *
* this.logger_ = logger * this.logger_ = logger
* this.options_ = options * this.options_ = options
* *
* // assuming you're initializing a client * // assuming you're initializing a client
* this.client = new Client(options) * this.client = new Client(options)
* } * }
* } * }
* *
* export default MyFileProviderService * export default MyFileProviderService
* ``` * ```
*/ */
@@ -51,6 +51,12 @@ export class AbstractFileProviderService implements IFileProvider {
*/ */
static identifier: string static identifier: string
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
/** /**
* @ignore * @ignore
*/ */
@@ -60,10 +66,10 @@ export class AbstractFileProviderService implements IFileProvider {
/** /**
* This method uploads a file using your provider's custom logic. * This method uploads a file using your provider's custom logic.
* *
* @param {FileTypes.ProviderUploadFileDTO} file - The file to upload * @param {FileTypes.ProviderUploadFileDTO} file - The file to upload
* @returns {Promise<FileTypes.ProviderFileResultDTO>} The uploaded file's details. * @returns {Promise<FileTypes.ProviderFileResultDTO>} The uploaded file's details.
* *
* @example * @example
* class MyFileProviderService extends AbstractFileProviderService { * class MyFileProviderService extends AbstractFileProviderService {
* // ... * // ...
@@ -72,7 +78,7 @@ export class AbstractFileProviderService implements IFileProvider {
* ): Promise<ProviderFileResultDTO> { * ): Promise<ProviderFileResultDTO> {
* // TODO upload file to third-party provider * // TODO upload file to third-party provider
* // or using custom logic * // or using custom logic
* *
* return { * return {
* url: "some-url.com", * url: "some-url.com",
* key: "file-name" * key: "file-name"
@@ -88,10 +94,10 @@ export class AbstractFileProviderService implements IFileProvider {
/** /**
* This method deletes the file from storage. * This method deletes the file from storage.
* *
* @param {FileTypes.ProviderDeleteFileDTO} file - The details of the file to delete. * @param {FileTypes.ProviderDeleteFileDTO} file - The details of the file to delete.
* @returns {Promise<void>} Resolves when the file is deleted. * @returns {Promise<void>} Resolves when the file is deleted.
* *
* @example * @example
* class MyFileProviderService extends AbstractFileProviderService { * class MyFileProviderService extends AbstractFileProviderService {
* // ... * // ...
@@ -108,16 +114,16 @@ export class AbstractFileProviderService implements IFileProvider {
} }
/** /**
* This method is used to retrieve a download URL of the file. For some providers, * This method is used to retrieve a download URL of the file. For some providers,
* such as S3, a presigned URL indicates a temporary URL to get access to a file. * such as S3, a presigned URL indicates a temporary URL to get access to a file.
* *
* If your provider doesn’t perform or offer a similar functionality, you can * If your provider doesn’t perform or offer a similar functionality, you can
* return the URL to download the file. * return the URL to download the file.
* *
* @param {FileTypes.ProviderGetFileDTO} fileData - The details of the file to get its * @param {FileTypes.ProviderGetFileDTO} fileData - The details of the file to get its
* presigned URL. * presigned URL.
* @returns {Promise<string>} The file's presigned URL. * @returns {Promise<string>} The file's presigned URL.
* *
* @example * @example
* class MyFileProviderService extends AbstractFileProviderService { * class MyFileProviderService extends AbstractFileProviderService {
* // ... * // ...
@@ -11,6 +11,12 @@ export class AbstractFulfillmentProviderService
return obj?.constructor?._isFulfillmentService return obj?.constructor?._isFulfillmentService
} }
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
getIdentifier() { getIdentifier() {
return (this.constructor as any).identifier return (this.constructor as any).identifier
} }
@@ -1,68 +1,74 @@
import { NotificationTypes, INotificationProvider } from "@medusajs/types" import { INotificationProvider, NotificationTypes } from "@medusajs/types"
/** /**
* ### constructor * ### constructor
* *
* The constructor allows you to access resources from the module's container using the first parameter, * The constructor allows you to access resources from the module's container using the first parameter,
* and the module's options using the second parameter. * and the module's options using the second parameter.
* *
* If you're creating a client or establishing a connection with a third-party service, do it in the constructor. * If you're creating a client or establishing a connection with a third-party service, do it in the constructor.
* *
* #### Example * #### Example
* *
* ```ts * ```ts
* import { AbstractNotificationProviderService } from "@medusajs/utils" * import { AbstractNotificationProviderService } from "@medusajs/utils"
* import { Logger } from "@medusajs/types" * import { Logger } from "@medusajs/types"
* *
* type InjectedDependencies = { * type InjectedDependencies = {
* logger: Logger * logger: Logger
* } * }
* *
* type Options = { * type Options = {
* apiKey: string * apiKey: string
* } * }
* *
* class MyNotificationProviderService extends AbstractNotificationProviderService { * class MyNotificationProviderService extends AbstractNotificationProviderService {
* protected logger_: Logger * protected logger_: Logger
* protected options_: Options * protected options_: Options
* // assuming you're initializing a client * // assuming you're initializing a client
* protected client * protected client
* *
* constructor ( * constructor (
* { logger }: InjectedDependencies, * { logger }: InjectedDependencies,
* options: Options * options: Options
* ) { * ) {
* super() * super()
* *
* this.logger_ = logger * this.logger_ = logger
* this.options_ = options * this.options_ = options
* *
* // assuming you're initializing a client * // assuming you're initializing a client
* this.client = new Client(options) * this.client = new Client(options)
* } * }
* } * }
* *
* export default MyNotificationProviderService * export default MyNotificationProviderService
* ``` * ```
*/ */
export class AbstractNotificationProviderService export class AbstractNotificationProviderService
implements INotificationProvider implements INotificationProvider
{ {
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
/** /**
* This method is used to send a notification using the third-party provider or your custom logic. * This method is used to send a notification using the third-party provider or your custom logic.
* *
* @param {NotificationTypes.ProviderSendNotificationDTO} notification - The details of the * @param {NotificationTypes.ProviderSendNotificationDTO} notification - The details of the
* notification to send. * notification to send.
* @returns {Promise<NotificationTypes.ProviderSendNotificationResultsDTO>} The result of sending * @returns {Promise<NotificationTypes.ProviderSendNotificationResultsDTO>} The result of sending
* the notification. * the notification.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* ProviderSendNotificationDTO, * ProviderSendNotificationDTO,
* ProviderSendNotificationResultsDTO * ProviderSendNotificationResultsDTO
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* class MyNotificationProviderService extends AbstractNotificationProviderService { * class MyNotificationProviderService extends AbstractNotificationProviderService {
* // ... * // ...
* async send( * async send(
@@ -7,13 +7,18 @@ import {
PaymentSessionStatus, PaymentSessionStatus,
ProviderWebhookPayload, ProviderWebhookPayload,
UpdatePaymentProviderSession, UpdatePaymentProviderSession,
WebhookActionResult WebhookActionResult,
} from "@medusajs/types" } from "@medusajs/types"
export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>> export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
implements IPaymentProvider implements IPaymentProvider
{ {
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
/** /**
* You can use the `constructor` of the provider's service to access resources in your module's container. * You can use the `constructor` of the provider's service to access resources in your module's container.
* *
@@ -27,45 +32,45 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* *
* @example * @example
* ```ts * ```ts
* import { * import {
* AbstractPaymentProvider * AbstractPaymentProvider
* } from "@medusajs/utils" * } from "@medusajs/utils"
* import { Logger } from "@medusajs/types" * import { Logger } from "@medusajs/types"
* *
* type InjectedDependencies = { * type InjectedDependencies = {
* logger: Logger * logger: Logger
* } * }
* *
* type Options = { * type Options = {
* apiKey: string * apiKey: string
* } * }
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
* protected logger_: Logger * protected logger_: Logger
* protected options_: Options * protected options_: Options
* // Assuming you're using a client to integrate * // Assuming you're using a client to integrate
* // with a third-party service * // with a third-party service
* protected client * protected client
* *
* constructor( * constructor(
* { logger }: InjectedDependencies, * { logger }: InjectedDependencies,
* options: Options * options: Options
* ) { * ) {
* // @ts-ignore * // @ts-ignore
* super(...arguments) * super(...arguments)
* *
* this.logger_ = logger * this.logger_ = logger
* this.options_ = options * this.options_ = options
* *
* // Assuming you're initializing a client * // Assuming you're initializing a client
* this.client = new Client(options) * this.client = new Client(options)
* } * }
* *
* // ... * // ...
* } * }
* *
* export default MyPaymentProviderService * export default MyPaymentProviderService
* ``` * ```
*/ */
@@ -88,7 +93,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* Each payment provider has a unique identifier defined in its class. * Each payment provider has a unique identifier defined in its class.
* *
* @example * @example
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
@@ -123,18 +128,18 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* - A webhook event occurred that instructs the payment provider to capture the payment session. Learn more about handing webhook events in [this guide](https://docs.medusajs.com/v2/resources/commerce-modules/payment/webhook-events). * - A webhook event occurred that instructs the payment provider to capture the payment session. Learn more about handing webhook events in [this guide](https://docs.medusajs.com/v2/resources/commerce-modules/payment/webhook-events).
* *
* In this method, use the third-party provider to capture the payment. * In this method, use the third-party provider to capture the payment.
* *
* @param paymentData - The `data` property of the payment. Make sure to store in it * @param paymentData - The `data` property of the payment. Make sure to store in it
* any helpful identification for your third-party integration. * any helpful identification for your third-party integration.
* @returns The new data to store in the payment's `data` property, or an error object. * @returns The new data to store in the payment's `data` property, or an error object.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * PaymentProviderError,
* PaymentProviderSessionResponse, * PaymentProviderSessionResponse,
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
@@ -142,10 +147,10 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* paymentData: Record<string, unknown> * paymentData: Record<string, unknown>
* ): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> { * ): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
* const externalId = paymentData.id * const externalId = paymentData.id
* *
* try { * try {
* const newData = await this.client.capturePayment(externalId) * const newData = await this.client.capturePayment(externalId)
* *
* return { * return {
* ...newData, * ...newData,
* id: externalId * id: externalId
@@ -158,7 +163,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -169,45 +174,45 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* This method authorizes a payment session. When authorized successfully, a payment is created by the Payment * This method authorizes a payment session. When authorized successfully, a payment is created by the Payment
* Module which can be later captured using the {@link capturePayment} method. * Module which can be later captured using the {@link capturePayment} method.
* *
* Refer to [this guide](https://docs.medusajs.com/v2/resources/commerce-modules/payment/payment-flow#3-authorize-payment-session) * Refer to [this guide](https://docs.medusajs.com/v2/resources/commerce-modules/payment/payment-flow#3-authorize-payment-session)
* to learn more about how this fits into the payment flow and how to handle required actions. * to learn more about how this fits into the payment flow and how to handle required actions.
* *
* To automatically capture the payment after authorization, return the status `captured`. * To automatically capture the payment after authorization, return the status `captured`.
* *
* @param paymentSessionData - The `data` property of the payment session. Make sure to store in it * @param paymentSessionData - The `data` property of the payment session. Make sure to store in it
* any helpful identification for your third-party integration. * any helpful identification for your third-party integration.
* @param context - The context in which the payment is being authorized. For example, in checkout, * @param context - The context in which the payment is being authorized. For example, in checkout,
* the context has a `cart_id` property indicating the ID of the associated cart. * the context has a `cart_id` property indicating the ID of the associated cart.
* @returns Either an object of the new data to store in the created payment's `data` property and the * @returns Either an object of the new data to store in the created payment's `data` property and the
* payment's status, or an error object. Make sure to set in `data` anything useful to later retrieve the session. * payment's status, or an error object. Make sure to set in `data` anything useful to later retrieve the session.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * PaymentProviderError,
* PaymentProviderSessionResponse, * PaymentProviderSessionResponse,
* PaymentSessionStatus * PaymentSessionStatus
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
* async authorizePayment( * async authorizePayment(
* paymentSessionData: Record<string, unknown>, * paymentSessionData: Record<string, unknown>,
* context: Record<string, unknown> * context: Record<string, unknown>
* ): Promise< * ): Promise<
* PaymentProviderError | { * PaymentProviderError | {
* status: PaymentSessionStatus * status: PaymentSessionStatus
* data: PaymentProviderSessionResponse["data"] * data: PaymentProviderSessionResponse["data"]
* } * }
* > { * > {
* const externalId = paymentSessionData.id * const externalId = paymentSessionData.id
* *
* try { * try {
* const paymentData = await this.client.authorizePayment(externalId) * const paymentData = await this.client.authorizePayment(externalId)
* *
* return { * return {
* data: { * data: {
* ...paymentData, * ...paymentData,
@@ -223,7 +228,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -246,19 +251,19 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* This method cancels a payment. * This method cancels a payment.
* *
* @param paymentData - The `data` property of the payment. Make sure to store in it * @param paymentData - The `data` property of the payment. Make sure to store in it
* any helpful identification for your third-party integration. * any helpful identification for your third-party integration.
* @returns An error object if an error occurs, or the data received from the integration. * @returns An error object if an error occurs, or the data received from the integration.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * PaymentProviderError,
* PaymentProviderSessionResponse, * PaymentProviderSessionResponse,
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
@@ -266,7 +271,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* paymentData: Record<string, unknown> * paymentData: Record<string, unknown>
* ): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> { * ): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
* const externalId = paymentData.id * const externalId = paymentData.id
* *
* try { * try {
* const paymentData = await this.client.cancelPayment(externalId) * const paymentData = await this.client.cancelPayment(externalId)
* } catch (e) { * } catch (e) {
@@ -277,7 +282,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -288,36 +293,36 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* This method is used when a payment session is created. It can be used to initiate the payment * This method is used when a payment session is created. It can be used to initiate the payment
* in the third-party session, before authorizing or capturing the payment later. * in the third-party session, before authorizing or capturing the payment later.
* *
* @param context - The details of the payment session and its context. * @param context - The details of the payment session and its context.
* @returns An object whose `data` property is set in the created payment session, or an error * @returns An object whose `data` property is set in the created payment session, or an error
* object. Make sure to set in `data` anything useful to later retrieve the session. * object. Make sure to set in `data` anything useful to later retrieve the session.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * PaymentProviderError,
* PaymentProviderSessionResponse, * PaymentProviderSessionResponse,
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
* async initiatePayment( * async initiatePayment(
* context: CreatePaymentProviderSession * context: CreatePaymentProviderSession
* ): Promise<PaymentProviderError | PaymentProviderSessionResponse> { * ): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
* const { * const {
* amount, * amount,
* currency_code, * currency_code,
* context: customerDetails * context: customerDetails
* } = context * } = context
* *
* try { * try {
* const response = await this.client.init( * const response = await this.client.init(
* amount, currency_code, customerDetails * amount, currency_code, customerDetails
* ) * )
* *
* return { * return {
* ...response, * ...response,
* data: { * data: {
@@ -332,7 +337,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -342,21 +347,21 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* This method is used when a payment session is deleted, which can only happen if it isn't authorized, yet. * This method is used when a payment session is deleted, which can only happen if it isn't authorized, yet.
* *
* Use this to delete or cancel the payment in the third-party service. * Use this to delete or cancel the payment in the third-party service.
* *
* @param paymentSessionData - The `data` property of the payment session. Make sure to store in it * @param paymentSessionData - The `data` property of the payment session. Make sure to store in it
* any helpful identification for your third-party integration. * any helpful identification for your third-party integration.
* @returns An error object or the response from the third-party service. * @returns An error object or the response from the third-party service.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * PaymentProviderError,
* PaymentProviderSessionResponse, * PaymentProviderSessionResponse,
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
@@ -366,7 +371,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* PaymentProviderError | PaymentProviderSessionResponse["data"] * PaymentProviderError | PaymentProviderSessionResponse["data"]
* > { * > {
* const externalId = paymentSessionData.id * const externalId = paymentSessionData.id
* *
* try { * try {
* await this.client.cancelPayment(externalId) * await this.client.cancelPayment(externalId)
* } catch (e) { * } catch (e) {
@@ -377,7 +382,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -387,18 +392,18 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* This method gets the status of a payment session based on the status in the third-party integration. * This method gets the status of a payment session based on the status in the third-party integration.
* *
* @param paymentSessionData - The `data` property of the payment session. Make sure to store in it * @param paymentSessionData - The `data` property of the payment session. Make sure to store in it
* any helpful identification for your third-party integration. * any helpful identification for your third-party integration.
* @returns The payment session's status. * @returns The payment session's status.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentSessionStatus * PaymentSessionStatus
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
@@ -406,10 +411,10 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* paymentSessionData: Record<string, unknown> * paymentSessionData: Record<string, unknown>
* ): Promise<PaymentSessionStatus> { * ): Promise<PaymentSessionStatus> {
* const externalId = paymentSessionData.id * const externalId = paymentSessionData.id
* *
* try { * try {
* const status = await this.client.getStatus(externalId) * const status = await this.client.getStatus(externalId)
* *
* switch (status) { * switch (status) {
* case "requires_capture": * case "requires_capture":
* return "authorized" * return "authorized"
@@ -424,7 +429,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* return "error" * return "error"
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -434,37 +439,37 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* This method refunds an amount of a payment previously captured. * This method refunds an amount of a payment previously captured.
* *
* @param paymentData - The `data` property of the payment. Make sure to store in it * @param paymentData - The `data` property of the payment. Make sure to store in it
* any helpful identification for your third-party integration. * any helpful identification for your third-party integration.
* @param refundAmount The amount to refund. * @param refundAmount The amount to refund.
* @returns The new data to store in the payment's `data` property, or an error object. * @returns The new data to store in the payment's `data` property, or an error object.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * PaymentProviderError,
* PaymentProviderSessionResponse, * PaymentProviderSessionResponse,
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
* async refundPayment( * async refundPayment(
* paymentData: Record<string, unknown>, * paymentData: Record<string, unknown>,
* refundAmount: number * refundAmount: number
* ): Promise< * ): Promise<
* PaymentProviderError | PaymentProviderSessionResponse["data"] * PaymentProviderError | PaymentProviderSessionResponse["data"]
* > { * > {
* const externalId = paymentData.id * const externalId = paymentData.id
* *
* try { * try {
* const newData = await this.client.refund( * const newData = await this.client.refund(
* externalId, * externalId,
* refundAmount * refundAmount
* ) * )
* *
* return { * return {
* ...newData, * ...newData,
* id: externalId * id: externalId
@@ -477,7 +482,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -488,19 +493,19 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* Retrieves the payment's data from the third-party service. * Retrieves the payment's data from the third-party service.
* *
* @param paymentSessionData - The `data` property of the payment. Make sure to store in it * @param paymentSessionData - The `data` property of the payment. Make sure to store in it
* any helpful identification for your third-party integration. * any helpful identification for your third-party integration.
* @returns An object to be stored in the payment's `data` property, or an error object. * @returns An object to be stored in the payment's `data` property, or an error object.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * PaymentProviderError,
* PaymentProviderSessionResponse, * PaymentProviderSessionResponse,
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
@@ -510,7 +515,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* PaymentProviderError | PaymentProviderSessionResponse["data"] * PaymentProviderError | PaymentProviderSessionResponse["data"]
* > { * > {
* const externalId = paymentSessionData.id * const externalId = paymentSessionData.id
* *
* try { * try {
* return await this.client.retrieve(externalId) * return await this.client.retrieve(externalId)
* } catch (e) { * } catch (e) {
@@ -521,7 +526,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -531,34 +536,34 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* Update a payment in the third-party service that was previously initiated with the {@link initiatePayment} method. * Update a payment in the third-party service that was previously initiated with the {@link initiatePayment} method.
* *
* @param context - The details of the payment session and its context. * @param context - The details of the payment session and its context.
* @returns An object whose `data` property is set in the updated payment session, or an error * @returns An object whose `data` property is set in the updated payment session, or an error
* object. Make sure to set in `data` anything useful to later retrieve the session. * object. Make sure to set in `data` anything useful to later retrieve the session.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* UpdatePaymentProviderSession, * UpdatePaymentProviderSession,
* PaymentProviderError, * PaymentProviderError,
* PaymentProviderSessionResponse, * PaymentProviderSessionResponse,
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
* async updatePayment( * async updatePayment(
* context: UpdatePaymentProviderSession * context: UpdatePaymentProviderSession
* ): Promise<PaymentProviderError | PaymentProviderSessionResponse> { * ): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
* const { * const {
* amount, * amount,
* currency_code, * currency_code,
* context: customerDetails, * context: customerDetails,
* data * data
* } = context * } = context
* const externalId = data.id * const externalId = data.id
* *
* try { * try {
* const response = await this.client.update( * const response = await this.client.update(
* externalId, * externalId,
@@ -568,7 +573,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* customerDetails * customerDetails
* } * }
* ) * )
* *
* return { * return {
* ...response, * ...response,
* data: { * data: {
@@ -583,7 +588,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -594,24 +599,24 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
/** /**
* This method is executed when a webhook event is received from the third-party payment provider. Use it * This method is executed when a webhook event is received from the third-party payment provider. Use it
* to process the action of the payment provider. * to process the action of the payment provider.
* *
* Learn more in [this documentation](https://docs.medusajs.com/v2/resources/commerce-modules/payment/webhook-events) * Learn more in [this documentation](https://docs.medusajs.com/v2/resources/commerce-modules/payment/webhook-events)
* *
* @param data - The webhook event's data * @param data - The webhook event's data
* @returns The webhook result. If the `action`'s value is `captured`, the payment is captured within Medusa as well. * @returns The webhook result. If the `action`'s value is `captured`, the payment is captured within Medusa as well.
* If the `action`'s value is `authorized`, the associated payment session is authorized within Medusa. * If the `action`'s value is `authorized`, the associated payment session is authorized within Medusa.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* BigNumber * BigNumber
* } from "@medusajs/utils" * } from "@medusajs/utils"
* import { * import {
* ProviderWebhookPayload, * ProviderWebhookPayload,
* WebhookActionResult * WebhookActionResult
* } from "@medusajs/types" * } from "@medusajs/types"
* *
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
@@ -623,7 +628,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* rawData, * rawData,
* headers * headers
* } = payload * } = payload
* *
* try { * try {
* switch(data.event_type) { * switch(data.event_type) {
* case "authorized_amount": * case "authorized_amount":
@@ -657,7 +662,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
* } * }
* } * }
* *
* // ... * // ...
* } * }
*/ */
@@ -1,9 +1,10 @@
import { generateJwtToken, MedusaError } from "@medusajs/utils" import { generateJwtToken, MedusaError } from "@medusajs/utils"
import { GoogleAuthService } from "../../src/services/google" import { GoogleAuthService } from "../../src/services/google"
jest.setTimeout(100000)
import { http, HttpResponse } from "msw" import { http, HttpResponse } from "msw"
import { setupServer } from "msw/node" import { setupServer } from "msw/node"
jest.setTimeout(100000)
const sampleIdPayload = { const sampleIdPayload = {
iss: "https://accounts.google.com", iss: "https://accounts.google.com",
azp: "199301612397-l1lrg08vd6dvu98r43l7ul0ri2rd2b6r.apps.googleusercontent.com", azp: "199301612397-l1lrg08vd6dvu98r43l7ul0ri2rd2b6r.apps.googleusercontent.com",
@@ -97,15 +98,10 @@ describe("Google auth provider", () => {
it("throw an error if required options are not passed", async () => { it("throw an error if required options are not passed", async () => {
let msg = "" let msg = ""
try { try {
new GoogleAuthService( GoogleAuthService.validateOptions({
{ clientID: "test",
logger: console as any, clientSecret: "test",
}, } as any)
{
clientID: "test",
clientSecret: "test",
} as any
)
} catch (e) { } catch (e) {
msg = e.message msg = e.message
} }
@@ -19,12 +19,25 @@ export class GoogleAuthService extends AbstractAuthModuleProvider {
protected config_: LocalServiceConfig protected config_: LocalServiceConfig
protected logger_: Logger protected logger_: Logger
static validateOptions(options: GoogleAuthProviderOptions) {
if (!options.clientID) {
throw new Error("Google clientID is required")
}
if (!options.clientSecret) {
throw new Error("Google clientSecret is required")
}
if (!options.callbackURL) {
throw new Error("Google callbackUrl is required")
}
}
constructor( constructor(
{ logger }: InjectedDependencies, { logger }: InjectedDependencies,
options: GoogleAuthProviderOptions options: GoogleAuthProviderOptions
) { ) {
super({}, { provider: "google", displayName: "Google Authentication" }) super({}, { provider: "google", displayName: "Google Authentication" })
this.validateConfig(options)
this.config_ = options this.config_ = options
this.logger_ = logger this.logger_ = logger
} }
@@ -173,18 +186,4 @@ export class GoogleAuthService extends AbstractAuthModuleProvider {
return { success: true, location: authUrl.toString() } return { success: true, location: authUrl.toString() }
} }
private validateConfig(config: LocalServiceConfig) {
if (!config.clientID) {
throw new Error("Google clientID is required")
}
if (!config.clientSecret) {
throw new Error("Google clientSecret is required")
}
if (!config.callbackURL) {
throw new Error("Google callbackUrl is required")
}
}
} }
@@ -13,12 +13,12 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { import {
AbstractPaymentProvider, AbstractPaymentProvider,
MedusaError,
PaymentActions,
PaymentSessionStatus,
isDefined, isDefined,
isPaymentProviderError, isPaymentProviderError,
isPresent, isPresent,
MedusaError,
PaymentActions,
PaymentSessionStatus,
} from "@medusajs/utils" } from "@medusajs/utils"
import { import {
ErrorCodes, ErrorCodes,
@@ -36,6 +36,12 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
protected stripe_: Stripe protected stripe_: Stripe
protected container_: MedusaContainer protected container_: MedusaContainer
static validateOptions(options: StripeOptions): void {
if (!isDefined(options.apiKey)) {
throw new Error("Required option `apiKey` is missing in Stripe plugin")
}
}
protected constructor(container: MedusaContainer, options: StripeOptions) { protected constructor(container: MedusaContainer, options: StripeOptions) {
// @ts-ignore // @ts-ignore
super(...arguments) super(...arguments)
@@ -43,23 +49,11 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
this.container_ = container this.container_ = container
this.options_ = options this.options_ = options
this.stripe_ = this.init() this.stripe_ = new Stripe(options.apiKey)
}
protected init() {
this.validateOptions(this.config)
return new Stripe(this.config.apiKey)
} }
abstract get paymentIntentOptions(): PaymentIntentOptions abstract get paymentIntentOptions(): PaymentIntentOptions
private validateOptions(options: StripeOptions): void {
if (!isDefined(options.apiKey)) {
throw new Error("Required option `apiKey` is missing in Stripe plugin")
}
}
get options(): StripeOptions { get options(): StripeOptions {
return this.options_ return this.options_
} }
@@ -370,7 +364,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
return this.stripe_.webhooks.constructEvent( return this.stripe_.webhooks.constructEvent(
data.rawData as string | Buffer, data.rawData as string | Buffer,
signature, signature,
this.config.webhookSecret this.options_.webhookSecret
) )
} }
protected buildError( protected buildError(