feat: Region PaymentProvider link (#6577)

**What**

- Introduce link between Region and PaymentProvider
- Introduce API endpoint `GET /store/regions/:id/payment-providers` for retrieving providers by region
- Add tests for both
This commit is contained in:
Oli Juhl
2024-03-05 10:40:25 +01:00
committed by GitHub
parent 82db53c99e
commit 7d69e6068e
14 changed files with 371 additions and 43 deletions

View File

@@ -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,
}),
]),
}),
])
)
})
})

View File

@@ -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",
}),
])
})
})

View File

@@ -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"

View File

@@ -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,
},
},
],
}

View File

@@ -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",

View File

@@ -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: [

View File

@@ -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),
})
}

View File

@@ -24,4 +24,9 @@ export const storeRegionRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["GET"],
matcher: "/store/regions/:id/payment-providers",
middlewares: [],
},
]

View File

@@ -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",
},
},
],
}

View File

@@ -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<void> => {
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)
}

View File

@@ -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<PaymentProviderDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<PaymentProviderDTO[]> {
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<PaymentProviderDTO[]>(
providers,
{
populate: true,
}
providersToCreate.push({ id })
}
await this.paymentProviderService_.create(providersToCreate)
)
}
}

View File

@@ -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<PaymentProviderDTO>,
@MedusaContext() sharedContext?: Context
): Promise<PaymentProvider[]> {
return await this.paymentProviderRepository_.find(undefined, sharedContext)
const queryOptions = ModulesSdkUtils.buildQuery<PaymentProvider>(
filters,
config
)
return await this.paymentProviderRepository_.find(
queryOptions,
sharedContext
)
}
retrieveProvider(providerId: string): IPaymentProvider {

View File

@@ -434,3 +434,16 @@ export interface PaymentProviderDTO {
*/
is_enabled: string
}
export interface FilterablePaymentProviderProps
extends BaseFilterable<PaymentProviderDTO> {
/**
* The IDs to filter the payment collection by.
*/
id?: string | string[]
/**
* Filter by enabled status
*/
is_enabled?: boolean
}

View File

@@ -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<PaymentDTO>
listPaymentSessions(
filters?: FilterablePaymentSessionProps,
config?: FindConfig<PaymentSessionDTO>,
sharedContext?: Context
): Promise<PaymentSessionDTO[]>
/* ********** PAYMENT ********** */
/**
@@ -388,19 +396,11 @@ export interface IPaymentModuleService extends IModuleService {
*/
cancelPayment(paymentId: string, sharedContext?: Context): Promise<PaymentDTO>
listPaymentSessions(
filters?: FilterablePaymentSessionProps,
config?: FindConfig<PaymentSessionDTO>,
listPaymentProviders(
filters?: FilterablePaymentProviderProps,
config?: FindConfig<PaymentProviderDTO>,
sharedContext?: Context
): Promise<PaymentSessionDTO[]>
/**
* This method creates providers on load.
*
* @example
* {example-code}
*/
createProvidersOnLoad(): Promise<void>
): Promise<PaymentProviderDTO[]>
/* ********** HOOKS ********** */