feat: region payment providers management workflows/api (#6864)

This commit is contained in:
Adrien de Peretti
2024-04-04 20:41:34 +02:00
committed by GitHub
parent 12fcb655cd
commit e944a627f0
48 changed files with 1033 additions and 89 deletions

View File

@@ -0,0 +1,13 @@
---
"@medusajs/medusa": patch
"@medusajs/core-flows": patch
"@medusajs/link-modules": patch
"medusa-test-utils": patch
"@medusajs/modules-sdk": patch
"@medusajs/orchestration": patch
"@medusajs/payment": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat: region payment providers management workflows/api

View File

@@ -1,6 +1,7 @@
import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk"
import { IPaymentModuleService, IRegionModuleService } from "@medusajs/types"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import {ContainerRegistrationKeys} from "@medusajs/utils";
jest.setTimeout(50000)
@@ -20,8 +21,8 @@ medusaIntegrationTestRunner({
appContainer = getContainer()
regionModule = appContainer.resolve(ModuleRegistrationName.REGION)
paymentModule = appContainer.resolve(ModuleRegistrationName.PAYMENT)
remoteQuery = appContainer.resolve("remoteQuery")
remoteLink = appContainer.resolve("remoteLink")
remoteQuery = appContainer.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
remoteLink = appContainer.resolve(ContainerRegistrationKeys.REMOTE_LINK)
})
it("should query region and payment provider link with remote query", async () => {
@@ -73,7 +74,7 @@ medusaIntegrationTestRunner({
])
)
expect(otherLink).toHaveLength(1)
expect(otherLink).toHaveLength(2)
expect(otherLink).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -84,6 +85,10 @@ medusaIntegrationTestRunner({
}),
]),
}),
expect.objectContaining({
id: "pp_system_default_2",
regions: []
}),
])
)
})

View File

@@ -47,10 +47,9 @@ medusaIntegrationTestRunner({
metadata: { foo: "bar" },
})
)
expect(created.data.region.countries.map((c) => c.iso_2)).toEqual([
"us",
"ca",
])
expect(
created.data.region.countries.map((c) => c.iso_2).sort()
).toEqual(["ca", "us"])
const updated = await api.post(
`/admin/regions/${created.data.region.id}`,
@@ -96,6 +95,174 @@ medusaIntegrationTestRunner({
expect(deletedRegion.deleted_at).toBeTruthy()
})
it("should create the region with the available payment providers if the providers exists", async () => {
const paymentProviderId = "pp_system_default"
const created = await api.post(
`/admin/regions?fields=*payment_providers`,
{
name: "Test Region",
currency_code: "usd",
countries: ["us", "ca"],
metadata: { foo: "bar" },
payment_providers: [paymentProviderId],
},
adminHeaders
)
expect(created.status).toEqual(200)
expect(created.data.region).toEqual(
expect.objectContaining({
id: created.data.region.id,
name: "Test Region",
currency_code: "usd",
metadata: { foo: "bar" },
payment_providers: [
expect.objectContaining({
id: paymentProviderId,
}),
],
})
)
})
it("should update the region available payment providers", async () => {
const paymentProviderId = "pp_system_default"
const paymentProvider2Id = "pp_system_default_2"
const created = await api.post(
`/admin/regions`,
{
name: "Test Region",
currency_code: "usd",
countries: ["us", "ca"],
metadata: { foo: "bar" },
},
adminHeaders
)
/**
* Assign a new payment provider
*/
await api.post(
`/admin/regions/${created.data.region.id}`,
{
payment_providers: [paymentProviderId],
},
adminHeaders
)
let regionResponse = await api.get(
`/admin/regions/${created.data.region.id}?fields=*payment_providers`,
adminHeaders
)
expect(regionResponse.status).toEqual(200)
expect(regionResponse.data.region.payment_providers).toHaveLength(1)
expect(regionResponse.data.region).toEqual(
expect.objectContaining({
id: regionResponse.data.region.id,
payment_providers: [
expect.objectContaining({
id: paymentProviderId,
}),
],
})
)
/**
* Replace the region payment providers by a new one set of providers
*/
await api.post(
`/admin/regions/${created.data.region.id}`,
{
payment_providers: [paymentProvider2Id],
},
adminHeaders
)
regionResponse = await api.get(
`/admin/regions/${created.data.region.id}?fields=*payment_providers`,
adminHeaders
)
expect(regionResponse.status).toEqual(200)
expect(regionResponse.data.region.payment_providers).toHaveLength(1)
expect(regionResponse.data.region).toEqual(
expect.objectContaining({
id: regionResponse.data.region.id,
payment_providers: [
expect.objectContaining({
id: paymentProvider2Id,
}),
],
})
)
/**
* Replace the region payment providers with both providers
*/
await api.post(
`/admin/regions/${created.data.region.id}`,
{
payment_providers: [paymentProviderId, paymentProvider2Id],
},
adminHeaders
)
regionResponse = await api.get(
`/admin/regions/${created.data.region.id}?fields=*payment_providers`,
adminHeaders
)
expect(regionResponse.status).toEqual(200)
expect(regionResponse.data.region.payment_providers).toHaveLength(2)
expect(regionResponse.data.region).toEqual(
expect.objectContaining({
id: regionResponse.data.region.id,
payment_providers: [
expect.objectContaining({
id: paymentProvider2Id,
}),
expect.objectContaining({
id: paymentProviderId,
}),
],
})
)
})
it("should throw on update if the given payment providers does not exists", async () => {
const created = await api.post(
`/admin/regions`,
{
name: "Test Region",
currency_code: "usd",
countries: ["us", "ca"],
metadata: { foo: "bar" },
},
adminHeaders
)
const updateErr = await api
.post(
`/admin/regions/${created.data.region.id}`,
{
payment_providers: ["test"],
},
adminHeaders
)
.catch((e) => e)
expect(updateErr.response.status).toEqual(404)
expect(updateErr.response.data.message).toEqual(
"Payment providers with ids test not found or not enabled"
)
})
it("should throw on missing required properties in create", async () => {
const err = await api
.post(`/admin/regions`, {}, adminHeaders)

View File

@@ -0,0 +1,148 @@
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createRegionsWorkflow } from "@medusajs/core-flows"
import { MedusaContainer, RegionDTO } from "@medusajs/types"
import { createAdminUser } from "../../../../helpers/create-admin-user"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
jest.setTimeout(200000)
const env = { MEDUSA_FF_MEDUSA_V2: true }
const adminHeaders = {
headers: { "x-medusa-access-token": "test_token" },
}
medusaIntegrationTestRunner({
env,
testSuite: ({ dbConnection, getContainer, api }) => {
let container: MedusaContainer
let region: RegionDTO
beforeAll(() => {
container = getContainer()
})
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, container)
})
describe("create region workflow", () => {
it("should create a region", async () => {
const paymentProviderId = "pp_system_default"
const data = {
name: "Test Region",
currency_code: "usd",
countries: ["us", "ca"],
metadata: { foo: "bar" },
payment_providers: [paymentProviderId],
}
let {
result: [createRegion],
} = await createRegionsWorkflow(container).run({
input: {
regions: [data],
},
})
const remoteQuery = container.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
;[createRegion] = await remoteQuery(
remoteQueryObjectFromString({
entryPoint: "region",
variables: {
filters: {
id: createRegion.id,
},
},
fields: [
"id",
"name",
"currency_code",
"metadata",
"countries.*",
"payment_providers.*",
],
})
)
expect(createRegion).toEqual(
expect.objectContaining({
id: expect.any(String),
name: data.name,
currency_code: data.currency_code,
countries: expect.arrayContaining(
data.countries.map((iso_2) => expect.objectContaining({ iso_2 }))
),
metadata: data.metadata,
payment_providers: [
expect.objectContaining({
id: paymentProviderId,
}),
],
})
)
})
it("should revert the created region and payment providers", async () => {
const paymentProviderId = "pp_system_default"
const data = {
name: "Test Region",
currency_code: "usd",
countries: ["us", "ca"],
metadata: { foo: "bar" },
payment_providers: [paymentProviderId],
}
const workflow = createRegionsWorkflow(container)
workflow.addAction(
"throw",
{
invoke: async function failStep() {
throw new Error(`Failed to create region`)
},
},
{
noCompensation: true,
}
)
const { errors } = await workflow.run({
input: {
regions: [data],
},
throwOnError: false,
})
expect(errors).toHaveLength(1)
expect(errors[0].error.message).toEqual(`Failed to create region`)
const remoteQuery = container.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
const createRegions = await remoteQuery(
remoteQueryObjectFromString({
entryPoint: "region",
fields: [
"id",
"name",
"currency_code",
"metadata",
"countries.*",
"payment_providers.*",
],
})
)
expect(createRegions).toHaveLength(0)
})
})
},
})

View File

@@ -0,0 +1,112 @@
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { updateRegionsWorkflow } from "@medusajs/core-flows"
import {ContainerLike, MedusaContainer, RegionDTO} from "@medusajs/types"
import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(200000)
const env = { MEDUSA_FF_MEDUSA_V2: true }
const adminHeaders = {
headers: { "x-medusa-access-token": "test_token" },
}
medusaIntegrationTestRunner({
env,
testSuite: ({ dbConnection, getContainer, api }) => {
let container: MedusaContainer
let region: RegionDTO
beforeAll(() => {
container = getContainer()
})
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, container)
region = (
await api.post(
`/admin/regions`,
{
name: "Test Region",
currency_code: "usd",
countries: ["us", "ca"],
metadata: { foo: "bar" },
},
adminHeaders
)
).data.region
})
describe("update region workflow", () => {
it("should update a region", async () => {
const updateData = {
name: "United States update",
}
const {
result: [updatedRegion],
} = await updateRegionsWorkflow(container).run({
input: {
selector: { id: region.id },
update: updateData,
},
})
expect(updatedRegion).toEqual(
expect.objectContaining({
id: region.id,
name: updateData.name,
})
)
})
it("should revert region update and payment providers update when it fails", async () => {
const paymentProviderId = "pp_system_default"
const paymentProvider2Id = "pp_system_default_2"
const updateData = {
name: "United States update",
payment_providers: [paymentProviderId, paymentProvider2Id],
}
const workflow = updateRegionsWorkflow(container)
workflow.addAction(
"throw",
{
invoke: async function failStep() {
throw new Error(`Failed to update region`)
},
},
{
noCompensation: true,
}
)
const { errors } = await workflow.run({
input: {
selector: { id: region.id },
update: updateData,
},
throwOnError: false,
})
expect(errors).toHaveLength(1)
expect(errors[0].error.message).toEqual(`Failed to update region`)
const updatedRegion = (
await api.get(
`/admin/regions/${region.id}?fields=*payment_providers`,
adminHeaders
)
).data.region
expect(updatedRegion).toEqual(
expect.objectContaining({
id: region.id,
name: region.name,
payment_providers: [],
})
)
})
})
},
})

View File

@@ -10,6 +10,17 @@ process.env.LOG_LEVEL = "error"
const enableMedusaV2 = process.env.MEDUSA_FF_MEDUSA_V2 == "true"
const customPaymentProvider = {
resolve: {
services: [require("@medusajs/payment/dist/providers/system").default],
},
options: {
config: {
default_2: {},
},
},
}
module.exports = {
plugins: [],
projectConfig: {
@@ -70,7 +81,13 @@ module.exports = {
[Modules.STORE]: true,
[Modules.TAX]: true,
[Modules.CURRENCY]: true,
[Modules.PAYMENT]: true,
[Modules.PAYMENT]: {
resolve: "@medusajs/payment",
/** @type {import('@medusajs/payment').PaymentModuleOptions}*/
options: {
providers: [customPaymentProvider],
},
},
[Modules.FULFILLMENT]: {
/** @type {import('@medusajs/fulfillment').FulfillmentModuleOptions} */
options: {

View File

@@ -0,0 +1,216 @@
import { ModuleRegistrationName, RemoteLink } from "@medusajs/modules-sdk"
import { IPaymentModuleService, RemoteQueryFunction } from "@medusajs/types"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
import {
arrayDifference,
ContainerRegistrationKeys,
LINKS,
MedusaError,
Modules,
promiseAll,
remoteQueryObjectFromString,
} from "@medusajs/utils"
export interface SetRegionsPaymentProvidersStepInput {
input: {
id: string
payment_providers?: string[]
}[]
}
interface FilteredSetRegionsPaymentProvidersStepInput {
id: string
payment_providers: string[]
}
type LinkItems = {
[Modules.REGION]: { region_id: string }
[Modules.PAYMENT]: { payment_provider_id: string }
}[]
async function validatePaymentProvidersExists(
paymentService: IPaymentModuleService,
paymentProviderIds: string[]
) {
const paymentProviders = await paymentService.listPaymentProviders({
id: { $in: paymentProviderIds },
is_enabled: true,
})
const retrievedPaymentProviderIds = paymentProviders.map((p) => p.id)
const missingProviders = arrayDifference(
paymentProviderIds,
retrievedPaymentProviderIds
)
if (missingProviders.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Payment providers with ids ${missingProviders.join(
", "
)} not found or not enabled`
)
}
}
async function getCurrentRegionPaymentProvidersLinks(
regionIds: string[],
{ remoteQuery }: { remoteQuery: RemoteQueryFunction }
): Promise<
{
[Modules.REGION]: { region_id: string }
[Modules.PAYMENT]: { payment_provider_id: string }
}[]
> {
const query = remoteQueryObjectFromString({
service: LINKS.RegionPaymentProvider,
variables: {
filters: { region_id: regionIds },
take: null,
},
fields: ["region_id", "payment_provider_id"],
})
const regionProviderLinks = (await remoteQuery(query)) as {
region_id: string
payment_provider_id: string
}[]
return regionProviderLinks.map((region) => {
return {
[Modules.REGION]: {
region_id: region.region_id,
},
[Modules.PAYMENT]: {
payment_provider_id: region.payment_provider_id,
},
}
})
}
export const setRegionsPaymentProvidersStepId =
"add-region-payment-providers-step"
export const setRegionsPaymentProvidersStep = createStep(
setRegionsPaymentProvidersStepId,
async (data: SetRegionsPaymentProvidersStepInput, { container }) => {
const dataInputToProcess = data.input.filter((inputData) => {
return inputData.payment_providers?.length
}) as FilteredSetRegionsPaymentProvidersStepInput[]
if (!dataInputToProcess.length) {
return new StepResponse(void 0)
}
const paymentService = container.resolve<IPaymentModuleService>(
ModuleRegistrationName.PAYMENT
)
const remoteLink = container.resolve<RemoteLink>(
ContainerRegistrationKeys.REMOTE_LINK
)
const remoteQuery = container.resolve<RemoteQueryFunction>(
ContainerRegistrationKeys.REMOTE_QUERY
)
const allPaymentProviderIds = dataInputToProcess
.map((inputData) => {
return inputData.payment_providers!
})
.flat()
const uniquePaymentProviderIds = Array.from(
new Set<string>(allPaymentProviderIds)
)
await validatePaymentProvidersExists(
paymentService,
uniquePaymentProviderIds
)
const regionIds = dataInputToProcess.map((inputData) => inputData.id)
const currentExistingLinks = await getCurrentRegionPaymentProvidersLinks(
regionIds,
{ remoteQuery }
)
const linksToRemove: LinkItems = currentExistingLinks
.filter((existingLink) => {
return !dataInputToProcess.some((input) => {
return (
input.id === existingLink[Modules.REGION].region_id &&
input.payment_providers.includes(
existingLink[Modules.PAYMENT].payment_provider_id
)
)
})
})
.map((link) => {
return {
[Modules.REGION]: { region_id: link[Modules.REGION].region_id },
[Modules.PAYMENT]: {
payment_provider_id: link[Modules.PAYMENT].payment_provider_id,
},
}
})
const linksToCreate = dataInputToProcess
.map((inputData) => {
return inputData.payment_providers.map((provider) => {
const alreadyExists = currentExistingLinks.some((link) => {
return (
link[Modules.REGION].region_id === inputData.id &&
link[Modules.PAYMENT].payment_provider_id === provider
)
})
if (alreadyExists) {
return
}
return {
[Modules.REGION]: { region_id: inputData.id },
[Modules.PAYMENT]: { payment_provider_id: provider },
}
})
})
.flat()
.filter((d): d is LinkItems[0] => !!d)
const promises: Promise<unknown[]>[] = []
if (linksToRemove.length) {
promises.push(remoteLink.dismiss(linksToRemove))
}
if (linksToCreate.length) {
promises.push(remoteLink.create(linksToCreate))
}
await promiseAll(promises)
return new StepResponse(void 0, {
linksToCreate: linksToRemove,
linksToRemove: linksToCreate,
})
},
async (rollbackData, { container }) => {
if (!rollbackData) {
return
}
const remoteLink = container.resolve<RemoteLink>(
ContainerRegistrationKeys.REMOTE_LINK
)
const promises: Promise<unknown[]>[] = []
if (rollbackData.linksToRemove.length) {
promises.push(remoteLink.dismiss(rollbackData.linksToRemove))
}
if (rollbackData.linksToCreate.length) {
promises.push(remoteLink.create(rollbackData.linksToCreate))
}
await promiseAll(promises)
}
)

View File

@@ -29,6 +29,10 @@ export const updateRegionsStep = createStep(
relations,
})
if (Object.keys(data.update).length === 0) {
return new StepResponse(prevData, [])
}
const regions = await service.update(data.selector, data.update)
return new StepResponse(regions, prevData)
@@ -48,7 +52,7 @@ export const updateRegionsStep = createStep(
name: r.name,
currency_code: r.currency_code,
metadata: r.metadata,
countries: r.countries.map((c) => c.iso_2),
countries: r.countries?.map((c) => c.iso_2),
}))
)
}

View File

@@ -1,13 +1,57 @@
import { CreateRegionDTO, RegionDTO } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { WorkflowTypes } from "@medusajs/types"
import {
createWorkflow,
transform,
WorkflowData,
} from "@medusajs/workflows-sdk"
import { createRegionsStep } from "../steps"
type WorkflowInput = { regionsData: CreateRegionDTO[] }
import { setRegionsPaymentProvidersStep } from "../steps/set-regions-payment-providers"
export const createRegionsWorkflowId = "create-regions"
export const createRegionsWorkflow = createWorkflow(
createRegionsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<RegionDTO[]> => {
return createRegionsStep(input.regionsData)
(
input: WorkflowData<WorkflowTypes.RegionWorkflow.CreateRegionsWorkflowInput>
): WorkflowData<WorkflowTypes.RegionWorkflow.CreateRegionsWorkflowOutput> => {
const data = transform(input, (data) => {
const regionIndexToPaymentProviders = data.regions.map(
(region, index) => {
return {
region_index: index,
payment_providers: region.payment_providers,
}
}
)
return {
regions: data.regions,
regionIndexToPaymentProviders,
}
})
const regions = createRegionsStep(data.regions)
const normalizedRegionProviderData = transform(
{
regionIndexToPaymentProviders: data.regionIndexToPaymentProviders,
regions,
},
(data) => {
return data.regionIndexToPaymentProviders.map(
({ region_index, payment_providers }) => {
return {
id: data.regions[region_index].id,
payment_providers,
}
}
)
}
)
setRegionsPaymentProvidersStep({
input: normalizedRegionProviderData,
})
return regions
}
)

View File

@@ -1,22 +1,46 @@
import { WorkflowTypes } from "@medusajs/types"
import {
FilterableRegionProps,
RegionDTO,
UpdateRegionDTO,
} from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
createWorkflow,
transform,
WorkflowData,
} from "@medusajs/workflows-sdk"
import { updateRegionsStep } from "../steps"
type UpdateRegionsStepInput = {
selector: FilterableRegionProps
update: UpdateRegionDTO
}
type WorkflowInput = UpdateRegionsStepInput
import { setRegionsPaymentProvidersStep } from "../steps/set-regions-payment-providers"
export const updateRegionsWorkflowId = "update-regions"
export const updateRegionsWorkflow = createWorkflow(
updateRegionsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<RegionDTO[]> => {
return updateRegionsStep(input)
(
input: WorkflowData<WorkflowTypes.RegionWorkflow.UpdateRegionsWorkflowInput>
): WorkflowData<WorkflowTypes.RegionWorkflow.UpdateRegionsWorkflowOutput> => {
const data = transform(input, (data) => {
const { selector, update } = data
const { payment_providers = [], ...rest } = update
return {
selector,
update: rest,
payment_providers,
}
})
const regions = updateRegionsStep(data)
const upsertProvidersNormalizedInput = transform(
{ data, regions },
(data) => {
return data.regions.map((region) => {
return {
id: region.id,
payment_providers: data.data.payment_providers,
}
})
}
)
setRegionsPaymentProvidersStep({
input: upsertProvidersNormalizedInput,
})
return regions
}
)

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const CartPaymentCollection: ModuleJoinerConfig = {
serviceName: LINKS.CartPaymentCollection,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const CartPromotion: ModuleJoinerConfig = {
serviceName: LINKS.CartPromotion,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const FulfillmentSetLocation: ModuleJoinerConfig = {
serviceName: LINKS.FulfillmentSetLocation,

View File

@@ -1,6 +1,5 @@
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const OrderSalesChannel: ModuleJoinerConfig = {
serviceName: LINKS.OrderSalesChannel,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const ProductSalesChannel: ModuleJoinerConfig = {
serviceName: LINKS.ProductSalesChannel,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const ProductShippingProfile: ModuleJoinerConfig = {
serviceName: LINKS.ProductShippingProfile,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const ProductVariantInventoryItem: ModuleJoinerConfig = {
serviceName: LINKS.ProductVariantInventoryItem,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const ProductVariantPriceSet: ModuleJoinerConfig = {
serviceName: LINKS.ProductVariantPriceSet,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const PublishableApiKeySalesChannel: ModuleJoinerConfig = {
serviceName: LINKS.PublishableApiKeySalesChannel,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const RegionPaymentProvider: ModuleJoinerConfig = {
serviceName: LINKS.RegionPaymentProvider,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const SalesChannelLocation: ModuleJoinerConfig = {
serviceName: LINKS.SalesChannelLocation,

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
import { LINKS } from "@medusajs/utils"
export const ShippingOptionPriceSet: ModuleJoinerConfig = {
serviceName: LINKS.ShippingOptionPriceSet,

View File

@@ -2,7 +2,7 @@ import { getDatabaseURL } from "./database"
import { initDb } from "./medusa-test-runner-utils/use-db"
import { startBootstrapApp } from "./medusa-test-runner-utils/bootstrap-app"
import { createDatabase, dropDatabase } from "pg-god"
import { ContainerLike } from "@medusajs/types"
import {ContainerLike, MedusaContainer} from "@medusajs/types"
import { createMedusaContainer } from "@medusajs/utils"
const axios = require("axios").default
@@ -70,7 +70,7 @@ const dbTestUtilFactory = (): any => ({
export interface MedusaSuiteOptions<TService = unknown> {
dbUtils: any
dbConnection: any // Legacy typeorm connection
getContainer: () => ContainerLike
getContainer: () => MedusaContainer
api: any
dbConfig: {
dbName: string
@@ -89,7 +89,7 @@ export function medusaIntegrationTestRunner({
testSuite,
}: {
moduleName?: string
env?: Record<string, string>
env?: Record<string, string | any>
dbName?: string
schema?: string
debug?: boolean

View File

@@ -34,7 +34,7 @@ export function moduleIntegrationTestRunner({
injectedDependencies?: Record<string, any>
resolve?: string
debug?: boolean
testSuite: <TService = unknown>(options: SuiteOptions<TService>) => () => void
testSuite: <TService = unknown>(options: SuiteOptions<TService>) => void
}) {
const moduleSdkImports = require("@medusajs/modules-sdk")

View File

@@ -8,8 +8,10 @@ import {
} from "@medusajs/core-flows"
import { UpdateRegionDTO } from "@medusajs/types"
import { defaultAdminRegionFields } from "../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString
} from "@medusajs/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
@@ -22,7 +24,7 @@ export const GET = async (
const queryObject = remoteQueryObjectFromString({
entryPoint: "region",
variables,
fields: defaultAdminRegionFields,
fields: req.remoteQueryConfig.fields,
})
const [region] = await remoteQuery(queryObject)
@@ -46,7 +48,19 @@ export const POST = async (
throw errors[0].error
}
res.status(200).json({ region: result[0] })
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "region",
variables: {
filters: { id: req.params.id },
},
fields: req.remoteQueryConfig.fields,
})
const regions = await remoteQuery(queryObject)
res.status(200).json({ region: regions[0] })
}
export const DELETE = async (

View File

@@ -40,11 +40,23 @@ export const adminRegionRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["POST"],
matcher: "/admin/regions",
middlewares: [transformBody(AdminPostRegionsReq)],
middlewares: [
transformQuery(
AdminGetRegionsRegionParams,
QueryConfig.retrieveTransformQueryConfig
),
transformBody(AdminPostRegionsReq),
],
},
{
method: ["POST"],
matcher: "/admin/regions/:id",
middlewares: [transformBody(AdminPostRegionsRegionReq)],
middlewares: [
transformQuery(
AdminGetRegionsRegionParams,
QueryConfig.retrieveTransformQueryConfig
),
transformBody(AdminPostRegionsRegionReq),
],
},
]

View File

@@ -1,5 +1,3 @@
export const defaultAdminRegionRelations = ["countries"]
export const allowedAdminRegionRelations = ["countries"]
export const defaultAdminRegionFields = [
"id",
"name",
@@ -16,13 +14,12 @@ export const defaultAdminRegionFields = [
]
export const retrieveTransformQueryConfig = {
defaultFields: defaultAdminRegionFields,
defaultRelations: defaultAdminRegionRelations,
allowedRelations: allowedAdminRegionRelations,
defaults: defaultAdminRegionFields,
isList: false,
}
export const listTransformQueryConfig = {
defaults: defaultAdminRegionFields,
defaultLimit: 20,
isList: true,
}

View File

@@ -5,24 +5,24 @@ import {
import { CreateRegionDTO } from "@medusajs/types"
import { createRegionsWorkflow } from "@medusajs/core-flows"
import { defaultAdminRegionFields } from "./query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "region",
variables: {
filters: req.filterableFields,
order: req.listConfig.order,
skip: req.listConfig.skip,
take: req.listConfig.take,
...req.remoteQueryConfig.pagination,
},
fields: defaultAdminRegionFields,
fields: req.remoteQueryConfig.fields,
})
const { rows: regions, metadata } = await remoteQuery(queryObject)
@@ -46,7 +46,7 @@ export const POST = async (
]
const { result, errors } = await createRegionsWorkflow(req.scope).run({
input: { regionsData: input },
input: { regions: input },
throwOnError: false,
})
@@ -54,5 +54,17 @@ export const POST = async (
throw errors[0].error
}
res.status(200).json({ region: result[0] })
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "region",
variables: {
filters: { id: result[0].id },
},
fields: req.remoteQueryConfig.fields,
})
const regions = await remoteQuery(queryObject)
res.status(200).json({ region: regions[0] })
}

View File

@@ -7,7 +7,7 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
import { extendedFindParamsMixin, FindParams } from "../../../types/common"
import { OperatorMapValidator } from "../../../types/validators/operator-map"
export class AdminGetRegionsRegionParams extends FindParams {}
@@ -90,6 +90,11 @@ export class AdminPostRegionsReq {
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
@IsOptional()
@IsArray()
@IsString({ each: true })
payment_providers?: string[]
}
export class AdminPostRegionsRegionReq {
@@ -108,4 +113,9 @@ export class AdminPostRegionsRegionReq {
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
@IsOptional()
@IsArray()
@IsString({ each: true })
payment_providers?: string[]
}

View File

@@ -11,6 +11,7 @@ export enum LinkModuleUtils {
REMOTE_LINK = "remoteLink",
}
// TODO: Remove this enum and use the one from @medusajs/utils
export enum Modules {
AUTH = "auth",
CACHE = "cacheService",

View File

@@ -70,7 +70,11 @@ export function toRemoteJoinerQuery(
}
if (isEntryPoint) {
remoteJoinerQuery.alias = key
if (value.isServiceAccess) {
remoteJoinerQuery.service = key
} else {
remoteJoinerQuery.alias = key
}
} else {
remoteJoinerQuery.expands!.push(expandObj)
}

View File

@@ -8,3 +8,4 @@ export default moduleDefinition
export { revertMigration, runMigrations }
export * from "./initialize"
export * from "./types"

View File

@@ -1,5 +1,27 @@
import { Logger } from "@medusajs/types"
import {
Logger,
ModuleProviderExports,
ModuleServiceInitializeOptions,
} from "@medusajs/types"
export type InitializeModuleInjectableDependencies = {
logger?: Logger
}
export type PaymentModuleOptions = Partial<ModuleServiceInitializeOptions> & {
/**
* Providers to be registered
*/
providers?: {
/**
* The module provider to be registered
*/
resolve: string | ModuleProviderExports
options: {
/**
* key value pair of the provider name and the configuration to be passed to the provider constructor
*/
config: Record<string, unknown>
}
}[]
}

View File

@@ -465,7 +465,7 @@ export interface FilterablePaymentProviderProps
/**
* The IDs to filter the payment collection by.
*/
id?: string | string[]
id?: string | string[] | OperatorMap<string | string[]>
/**
* Filter by enabled status

View File

@@ -76,7 +76,7 @@ export interface FilterableRegionProps
/**
* The IDs to filter the regions by.
*/
id?: string[] | string
id?: string[] | string | OperatorMap<string | string[]>
/**
* Filter regions by their name.
*/

View File

@@ -4,4 +4,5 @@ export * as ProductWorkflow from "./product"
export * as InventoryWorkflow from "./inventory"
export * as PriceListWorkflow from "./price-list"
export * as UserWorkflow from "./user"
export * as RegionWorkflow from "./region"
export * as InviteWorkflow from "./invite"

View File

@@ -0,0 +1,9 @@
import { CreateRegionDTO, RegionDTO } from "../../region"
export interface CreateRegionsWorkflowInput {
regions: (CreateRegionDTO & {
payment_providers?: string[]
})[]
}
export type CreateRegionsWorkflowOutput = RegionDTO[]

View File

@@ -0,0 +1,2 @@
export * from "./update-regions"
export * from "./create-regions"

View File

@@ -0,0 +1,10 @@
import { FilterableRegionProps, RegionDTO, UpdateRegionDTO } from "../../region"
export interface UpdateRegionsWorkflowInput {
selector: FilterableRegionProps
update: UpdateRegionDTO & {
payment_providers?: string[]
}
}
export type UpdateRegionsWorkflowOutput = RegionDTO[]

View File

@@ -13,4 +13,5 @@ export * as SearchUtils from "./search"
export * as ShippingProfileUtils from "./shipping"
export * as UserUtils from "./user"
export * as InventoryUtils from "./inventory"
export * as LinkUtils from "./link"
export * as ApiKeyUtils from "./api-key"

View File

@@ -48,6 +48,57 @@ describe("remoteQueryObjectFromString", function () {
"url",
"metadata",
],
isServiceAccess: false,
tags: {
fields: ["id", "created_at", "updated_at", "deleted_at", "value"],
},
options: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"title",
"product_id",
"metadata",
],
values: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"value",
"option_id",
"variant_id",
"metadata",
],
},
},
},
})
})
it("should return a remote query object using service entry point", function () {
const output = remoteQueryObjectFromString({
service: "product",
variables: {},
fields,
})
expect(output).toEqual({
product: {
__args: {},
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"url",
"metadata",
],
isServiceAccess: true,
tags: {
fields: ["id", "created_at", "updated_at", "deleted_at", "value"],
},

View File

@@ -1,8 +1,6 @@
/**
* Convert a string fields array to a remote query object
* @param entryPoint
* @param variables
* @param fields
* @param config - The configuration object
*
* @example
* const fields = [
@@ -83,28 +81,41 @@
* // },
* // }
*/
export function remoteQueryObjectFromString({
entryPoint,
variables,
fields,
}: {
entryPoint: string
variables?: any
fields: string[]
}): object {
export function remoteQueryObjectFromString(
config:
| {
entryPoint: string
variables?: any
fields: string[]
}
| {
service: string
variables?: any
fields: string[]
}
): object {
const { entryPoint, service, variables, fields } = {
...config,
entryPoint: "entryPoint" in config ? config.entryPoint : undefined,
service: "service" in config ? config.service : undefined,
}
const entryKey = (entryPoint ?? service) as string
const remoteJoinerConfig: object = {
[entryPoint]: {
[entryKey]: {
fields: [],
isServiceAccess: !!service, // specifies if the entry point is a service
},
}
if (variables) {
remoteJoinerConfig[entryPoint]["__args"] = variables
remoteJoinerConfig[entryKey]["__args"] = variables
}
for (const field of fields) {
if (!field.includes(".")) {
remoteJoinerConfig[entryPoint]["fields"].push(field)
remoteJoinerConfig[entryKey]["fields"].push(field)
continue
}
@@ -114,7 +125,7 @@ export function remoteQueryObjectFromString({
const deepConfigRef = fieldSegments.reduce((acc, curr) => {
acc[curr] ??= {}
return acc[curr]
}, remoteJoinerConfig[entryPoint])
}, remoteJoinerConfig[entryKey])
deepConfigRef["fields"] ??= []
deepConfigRef["fields"].push(fieldProperty)

View File

@@ -22,5 +22,6 @@ export * from "./totals"
export * from "./totals/big-number"
export * from "./user"
export * from "./api-key"
export * from "./link"
export const MedusaModuleType = Symbol.for("MedusaModule")

View File

@@ -0,0 +1,9 @@
import { lowerCaseFirst, toPascalCase } from "../common"
export const composeLinkName = (...args) => {
return lowerCaseFirst(toPascalCase(composeTableName(...args.concat("link"))))
}
export const composeTableName = (...args) => {
return args.map((name) => name.replace(/(_id|Service)$/gi, "")).join("_")
}

View File

@@ -0,0 +1,2 @@
export * from "./links"
export * from "./compose-link-name"

View File

@@ -1,5 +1,5 @@
import { Modules } from "@medusajs/modules-sdk"
import { composeLinkName } from "./utils"
import { Modules } from "../modules-sdk"
import { composeLinkName } from "./compose-link-name"
export const LINKS = {
ProductVariantInventoryItem: composeLinkName(

View File

@@ -0,0 +1,24 @@
export enum Modules {
AUTH = "auth",
CACHE = "cacheService",
CART = "cart",
CUSTOMER = "customer",
EVENT_BUS = "eventBus",
INVENTORY = "inventoryService",
LINK = "linkModules",
PAYMENT = "payment",
PRICING = "pricingService",
PRODUCT = "productService",
PROMOTION = "promotion",
SALES_CHANNEL = "salesChannel",
TAX = "tax",
FULFILLMENT = "fulfillment",
STOCK_LOCATION = "stockLocationService",
USER = "user",
WORKFLOW_ENGINE = "workflows",
REGION = "region",
ORDER = "order",
API_KEY = "apiKey",
STORE = "store",
CURRENCY = "currency",
}

View File

@@ -8,3 +8,4 @@ export * from "./create-pg-connection"
export * from "./migration-scripts"
export * from "./internal-module-service-factory"
export * from "./abstract-module-service-factory"
export * from "./definition"