diff --git a/integration-tests/modules/__tests__/link-modules/region-payment-provider.spec.ts b/integration-tests/modules/__tests__/link-modules/region-payment-provider.spec.ts new file mode 100644 index 0000000000..7c6b716a33 --- /dev/null +++ b/integration-tests/modules/__tests__/link-modules/region-payment-provider.spec.ts @@ -0,0 +1,106 @@ +import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk" +import { IPaymentModuleService, IRegionModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../environment-helpers/bootstrap-app" +import { getContainer } from "../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../environment-helpers/use-db" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } + +describe("Region and Payment Providers", () => { + let dbConnection + let appContainer + let shutdownServer + let regionModule: IRegionModuleService + let paymentModule: IPaymentModuleService + let remoteQuery + let remoteLink + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + regionModule = appContainer.resolve(ModuleRegistrationName.REGION) + paymentModule = appContainer.resolve(ModuleRegistrationName.PAYMENT) + remoteQuery = appContainer.resolve("remoteQuery") + remoteLink = appContainer.resolve("remoteLink") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should query region and payment provider link with remote query", async () => { + const region = await regionModule.create({ + name: "North America", + currency_code: "usd", + }) + + await remoteLink.create([ + { + [Modules.REGION]: { + region_id: region.id, + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_system_default", + }, + }, + ]) + + const links = await remoteQuery({ + region: { + fields: ["id"], + payment_providers: { + fields: ["id"], + }, + }, + }) + + const otherLink = await remoteQuery({ + payment_providers: { + fields: ["id"], + regions: { + fields: ["id"], + }, + }, + }) + + expect(links).toHaveLength(1) + expect(links).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: region.id, + payment_providers: expect.arrayContaining([ + expect.objectContaining({ + id: "pp_system_default", + }), + ]), + }), + ]) + ) + + expect(otherLink).toHaveLength(1) + expect(otherLink).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "pp_system_default", + regions: expect.arrayContaining([ + expect.objectContaining({ + id: region.id, + }), + ]), + }), + ]) + ) + }) +}) diff --git a/integration-tests/modules/__tests__/payment/payments.spec.ts b/integration-tests/modules/__tests__/payment/payments.spec.ts new file mode 100644 index 0000000000..54aa7de813 --- /dev/null +++ b/integration-tests/modules/__tests__/payment/payments.spec.ts @@ -0,0 +1,74 @@ +import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk" +import { IRegionModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../environment-helpers/use-api" +import { getContainer } from "../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../environment-helpers/use-db" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } + +describe("Payments", () => { + let dbConnection + let appContainer + let shutdownServer + let regionService: IRegionModuleService + let remoteLink + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + regionService = appContainer.resolve(ModuleRegistrationName.REGION) + remoteLink = appContainer.resolve("remoteLink") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should list payment providers", async () => { + const region = await regionService.create({ + name: "Test Region", + currency_code: "usd", + }) + + const api = useApi() as any + let response = await api.get( + `/store/regions/${region.id}/payment-providers` + ) + + expect(response.status).toEqual(200) + expect(response.data.payment_providers).toEqual([]) + + await remoteLink.create([ + { + [Modules.REGION]: { + region_id: region.id, + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_system_default", + }, + }, + ]) + + response = await api.get(`/store/regions/${region.id}/payment-providers`) + + expect(response.status).toEqual(200) + expect(response.data.payment_providers).toEqual([ + expect.objectContaining({ + id: "pp_system_default", + }), + ]) + }) +}) diff --git a/packages/link-modules/src/definitions/index.ts b/packages/link-modules/src/definitions/index.ts index be6247d361..af88028497 100644 --- a/packages/link-modules/src/definitions/index.ts +++ b/packages/link-modules/src/definitions/index.ts @@ -10,3 +10,5 @@ export * from "./product-shipping-profile" export * from "./product-variant-inventory-item" export * from "./product-variant-price-set" export * from "./publishable-api-key-sales-channel" +export * from "./region-payment-provider" + diff --git a/packages/link-modules/src/definitions/region-payment-provider.ts b/packages/link-modules/src/definitions/region-payment-provider.ts new file mode 100644 index 0000000000..0366ed26c6 --- /dev/null +++ b/packages/link-modules/src/definitions/region-payment-provider.ts @@ -0,0 +1,64 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ModuleJoinerConfig } from "@medusajs/types" +import { LINKS } from "../links" + +export const RegionPaymentProvider: ModuleJoinerConfig = { + serviceName: LINKS.RegionPaymentProvider, + isLink: true, + databaseConfig: { + tableName: "region_payment_provider", + idPrefix: "regpp", + }, + alias: [ + { + name: ["region_payment_provider", "region_payment_providers"], + args: { + entity: "LinkRegionPaymentProvider", + }, + }, + ], + primaryKeys: ["id", "region_id", "payment_provider_id"], + relationships: [ + { + serviceName: Modules.REGION, + primaryKey: "id", + foreignKey: "region_id", + alias: "region", + }, + { + serviceName: Modules.PAYMENT, + primaryKey: "id", + foreignKey: "payment_provider_id", + alias: "payment_provider", + args: { methodSuffix: "PaymentProviders" }, + }, + ], + extends: [ + { + serviceName: Modules.REGION, + fieldAlias: { + payment_providers: "payment_provider_link.payment_provider", + }, + relationship: { + serviceName: LINKS.RegionPaymentProvider, + primaryKey: "region_id", + foreignKey: "id", + alias: "payment_provider_link", + isList: true, + }, + }, + { + serviceName: Modules.PAYMENT, + fieldAlias: { + regions: "region_link.region", + }, + relationship: { + serviceName: LINKS.RegionPaymentProvider, + primaryKey: "payment_provider_id", + foreignKey: "id", + alias: "region_link", + isList: true, + }, + }, + ], +} diff --git a/packages/link-modules/src/links.ts b/packages/link-modules/src/links.ts index 00716128be..04a57d9f2f 100644 --- a/packages/link-modules/src/links.ts +++ b/packages/link-modules/src/links.ts @@ -20,6 +20,12 @@ export const LINKS = { Modules.PAYMENT, "payment_collection_id" ), + RegionPaymentProvider: composeLinkName( + Modules.REGION, + "region_id", + Modules.PAYMENT, + "payment_provider_id" + ), CartPromotion: composeLinkName( Modules.CART, "cart_id", diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 5edab9eb98..37827f08d0 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -1,23 +1,23 @@ import { MiddlewaresConfig } from "../loaders/helpers/routing/types" import { adminApiKeyRoutesMiddlewares } from "./admin/api-keys/middlewares" import { adminCampaignRoutesMiddlewares } from "./admin/campaigns/middlewares" +import { adminCurrencyRoutesMiddlewares } from "./admin/currencies/middlewares" import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares" import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares" -import { adminTaxRegionRoutesMiddlewares } from "./admin/tax-regions/middlewares" import { adminStoreRoutesMiddlewares } from "./admin/stores/middlewares" import { adminTaxRateRoutesMiddlewares } from "./admin/tax-rates/middlewares" +import { adminTaxRegionRoutesMiddlewares } from "./admin/tax-regions/middlewares" import { adminUserRoutesMiddlewares } from "./admin/users/middlewares" import { adminWorkflowsExecutionsMiddlewares } from "./admin/workflows-executions/middlewares" import { authRoutesMiddlewares } from "./auth/middlewares" +import { hooksRoutesMiddlewares } from "./hooks/middlewares" import { storeCartRoutesMiddlewares } from "./store/carts/middlewares" +import { storeCurrencyRoutesMiddlewares } from "./store/currencies/middlewares" import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares" import { storeRegionRoutesMiddlewares } from "./store/regions/middlewares" -import { hooksRoutesMiddlewares } from "./hooks/middlewares" -import { adminCurrencyRoutesMiddlewares } from "./admin/currencies/middlewares" -import { storeCurrencyRoutesMiddlewares } from "./store/currencies/middlewares" export const config: MiddlewaresConfig = { routes: [ diff --git a/packages/medusa/src/api-v2/store/regions/[id]/payment-providers/route.ts b/packages/medusa/src/api-v2/store/regions/[id]/payment-providers/route.ts new file mode 100644 index 0000000000..5ea72ee0be --- /dev/null +++ b/packages/medusa/src/api-v2/store/regions/[id]/payment-providers/route.ts @@ -0,0 +1,20 @@ +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const remoteQuery = req.scope.resolve("remoteQuery") + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "regions", + fields: ["payment_providers.id", "payment_providers.is_enabled"], + variables: { + id: req.params.id, + }, + }) + + const [region] = await remoteQuery(queryObject) + + res.status(200).json({ + payment_providers: region.payment_providers.filter((pp) => pp.is_enabled), + }) +} diff --git a/packages/medusa/src/api-v2/store/regions/middlewares.ts b/packages/medusa/src/api-v2/store/regions/middlewares.ts index 2dcf89281f..901ffc5c7e 100644 --- a/packages/medusa/src/api-v2/store/regions/middlewares.ts +++ b/packages/medusa/src/api-v2/store/regions/middlewares.ts @@ -24,4 +24,9 @@ export const storeRegionRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["GET"], + matcher: "/store/regions/:id/payment-providers", + middlewares: [], + }, ] diff --git a/packages/payment/src/joiner-config.ts b/packages/payment/src/joiner-config.ts index b77f408531..54b56ed8d5 100644 --- a/packages/payment/src/joiner-config.ts +++ b/packages/payment/src/joiner-config.ts @@ -1,11 +1,12 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { MapToConfig } from "@medusajs/utils" -import { Payment, PaymentCollection } from "@models" +import { Payment, PaymentCollection, PaymentProvider } from "@models" export const LinkableKeys = { payment_id: Payment.name, payment_collection_id: PaymentCollection.name, + payment_provider_id: PaymentProvider.name, } const entityLinkableKeysMap: MapToConfig = {} @@ -36,5 +37,12 @@ export const joinerConfig: ModuleJoinerConfig = { entity: PaymentCollection.name, }, }, + { + name: ["payment_provider", "payment_providers"], + args: { + entity: PaymentProvider.name, + methodSuffix: "PaymentProviders", + }, + }, ], } diff --git a/packages/payment/src/loaders/defaults.ts b/packages/payment/src/loaders/defaults.ts index e59529e65a..b9855f5776 100644 --- a/packages/payment/src/loaders/defaults.ts +++ b/packages/payment/src/loaders/defaults.ts @@ -1,10 +1,26 @@ -import { IPaymentModuleService, LoaderOptions } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + CreatePaymentProviderDTO, + LoaderOptions +} from "@medusajs/types" export default async ({ container }: LoaderOptions): Promise => { - const paymentModuleService: IPaymentModuleService = container.resolve( - ModuleRegistrationName.PAYMENT - ) + const providersToLoad = container.resolve("payment_providers") + const paymentProviderService = container.resolve("paymentProviderService") - await paymentModuleService.createProvidersOnLoad() + const providers = await paymentProviderService.list({ + id: providersToLoad, + }) + + const loadedProvidersMap = new Map(providers.map((p) => [p.id, p])) + + const providersToCreate: CreatePaymentProviderDTO[] = [] + for (const id of providersToLoad) { + if (loadedProvidersMap.has(id)) { + continue + } + + providersToCreate.push({ id }) + } + + await paymentProviderService.create(providersToCreate) } diff --git a/packages/payment/src/services/payment-module.ts b/packages/payment/src/services/payment-module.ts index c620c7d722..f4be90da14 100644 --- a/packages/payment/src/services/payment-module.ts +++ b/packages/payment/src/services/payment-module.ts @@ -3,16 +3,18 @@ import { Context, CreateCaptureDTO, CreatePaymentCollectionDTO, - CreatePaymentProviderDTO, CreatePaymentSessionDTO, CreateRefundDTO, DAL, + FilterablePaymentProviderProps, + FindConfig, InternalModuleDeclaration, IPaymentModuleService, ModuleJoinerConfig, ModulesSdkTypes, PaymentCollectionDTO, PaymentDTO, + PaymentProviderDTO, PaymentSessionDTO, PaymentSessionStatus, ProviderWebhookPayload, @@ -590,25 +592,23 @@ export default class PaymentModuleService< } } - async createProvidersOnLoad() { - const providersToLoad = this.__container__["payment_providers"] + @InjectManager("baseRepository_") + async listPaymentProviders( + filters: FilterablePaymentProviderProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext?: Context + ): Promise { + const providers = await this.paymentProviderService_.list( + filters, + config, + sharedContext + ) - const providers = await this.paymentProviderService_.list({ - // @ts-ignore TODO - id: providersToLoad, - }) - - const loadedProvidersMap = new Map(providers.map((p) => [p.id, p])) - - const providersToCreate: CreatePaymentProviderDTO[] = [] - for (const id of providersToLoad) { - if (loadedProvidersMap.has(id)) { - continue + return await this.baseRepository_.serialize( + providers, + { + populate: true, } - - providersToCreate.push({ id }) - } - - await this.paymentProviderService_.create(providersToCreate) + ) } } diff --git a/packages/payment/src/services/payment-provider.ts b/packages/payment/src/services/payment-provider.ts index e9c51baca3..cf1f8f8c9b 100644 --- a/packages/payment/src/services/payment-provider.ts +++ b/packages/payment/src/services/payment-provider.ts @@ -3,10 +3,13 @@ import { CreatePaymentProviderDTO, CreatePaymentProviderSession, DAL, + FilterablePaymentProviderProps, + FindConfig, InternalModuleDeclaration, IPaymentProvider, PaymentProviderAuthorizeResponse, PaymentProviderDataInput, + PaymentProviderDTO, PaymentProviderError, PaymentProviderSessionResponse, PaymentSessionStatus, @@ -19,6 +22,7 @@ import { InjectTransactionManager, isPaymentProviderError, MedusaContext, + ModulesSdkUtils, } from "@medusajs/utils" import { PaymentProvider } from "@models" import { MedusaError } from "medusa-core-utils" @@ -52,9 +56,19 @@ export default class PaymentProviderService { @InjectManager("paymentProviderRepository_") async list( + filters: FilterablePaymentProviderProps, + config: FindConfig, @MedusaContext() sharedContext?: Context ): Promise { - return await this.paymentProviderRepository_.find(undefined, sharedContext) + const queryOptions = ModulesSdkUtils.buildQuery( + filters, + config + ) + + return await this.paymentProviderRepository_.find( + queryOptions, + sharedContext + ) } retrieveProvider(providerId: string): IPaymentProvider { diff --git a/packages/types/src/payment/common.ts b/packages/types/src/payment/common.ts index b174fee5b1..cbfea66265 100644 --- a/packages/types/src/payment/common.ts +++ b/packages/types/src/payment/common.ts @@ -434,3 +434,16 @@ export interface PaymentProviderDTO { */ is_enabled: string } + +export interface FilterablePaymentProviderProps + extends BaseFilterable { + /** + * The IDs to filter the payment collection by. + */ + id?: string | string[] + + /** + * Filter by enabled status + */ + is_enabled?: boolean +} diff --git a/packages/types/src/payment/service.ts b/packages/types/src/payment/service.ts index 2cb734631d..2bef19b5dd 100644 --- a/packages/types/src/payment/service.ts +++ b/packages/types/src/payment/service.ts @@ -4,9 +4,11 @@ import { Context } from "../shared-context" import { FilterablePaymentCollectionProps, FilterablePaymentProps, + FilterablePaymentProviderProps, FilterablePaymentSessionProps, PaymentCollectionDTO, PaymentDTO, + PaymentProviderDTO, PaymentSessionDTO, } from "./common" import { @@ -311,6 +313,12 @@ export interface IPaymentModuleService extends IModuleService { sharedContext?: Context ): Promise + listPaymentSessions( + filters?: FilterablePaymentSessionProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + /* ********** PAYMENT ********** */ /** @@ -388,19 +396,11 @@ export interface IPaymentModuleService extends IModuleService { */ cancelPayment(paymentId: string, sharedContext?: Context): Promise - listPaymentSessions( - filters?: FilterablePaymentSessionProps, - config?: FindConfig, + listPaymentProviders( + filters?: FilterablePaymentProviderProps, + config?: FindConfig, sharedContext?: Context - ): Promise - - /** - * This method creates providers on load. - * - * @example - * {example-code} - */ - createProvidersOnLoad(): Promise + ): Promise /* ********** HOOKS ********** */