From b91d7e032341904da4ee5f795f5b3924f3006ef7 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Thu, 4 Jul 2024 22:43:26 +0200 Subject: [PATCH] feat: Add HTTP endpoints and workflows for price preference management --- .../admin/price-preference.spec.ts | 221 ++++++++++++++++++ .../src/order/workflows/get-orders-list.ts | 6 +- packages/core/core-flows/src/pricing/index.ts | 1 + .../pricing/steps/create-price-preferences.ts | 34 +++ .../pricing/steps/delete-price-preferences.ts | 28 +++ .../core-flows/src/pricing/steps/index.ts | 3 + .../pricing/steps/update-price-preferences.ts | 45 ++++ .../workflows/create-price-preferences.ts | 13 ++ .../workflows/delete-price-preferences.ts | 10 + .../core-flows/src/pricing/workflows/index.ts | 3 + .../workflows/update-price-preferences.ts | 13 ++ .../types/src/http/pricing/admin/entities.ts | 10 + .../types/src/http/pricing/admin/index.ts | 3 + .../types/src/http/pricing/admin/payloads.ts | 11 + .../types/src/http/pricing/admin/queries.ts | 12 + .../types/src/http/pricing/admin/responses.ts | 14 ++ packages/core/types/src/workflow/index.ts | 1 + .../core/types/src/workflow/pricing/index.ts | 18 ++ packages/medusa/src/api/admin/orders/route.ts | 4 +- .../api/admin/price-preferences/[id]/route.ts | 64 +++++ .../admin/price-preferences/middlewares.ts | 55 +++++ .../admin/price-preferences/query-config.ts | 19 ++ .../src/api/admin/price-preferences/route.ts | 45 ++++ .../api/admin/price-preferences/validators.ts | 37 +++ packages/medusa/src/api/middlewares.ts | 2 + packages/modules/pricing/src/joiner-config.ts | 2 +- .../pricing/src/services/pricing-module.ts | 38 ++- 27 files changed, 702 insertions(+), 10 deletions(-) create mode 100644 integration-tests/http/__tests__/price-preference/admin/price-preference.spec.ts create mode 100644 packages/core/core-flows/src/pricing/steps/create-price-preferences.ts create mode 100644 packages/core/core-flows/src/pricing/steps/delete-price-preferences.ts create mode 100644 packages/core/core-flows/src/pricing/steps/update-price-preferences.ts create mode 100644 packages/core/core-flows/src/pricing/workflows/create-price-preferences.ts create mode 100644 packages/core/core-flows/src/pricing/workflows/delete-price-preferences.ts create mode 100644 packages/core/core-flows/src/pricing/workflows/index.ts create mode 100644 packages/core/core-flows/src/pricing/workflows/update-price-preferences.ts create mode 100644 packages/core/types/src/http/pricing/admin/payloads.ts create mode 100644 packages/core/types/src/http/pricing/admin/queries.ts create mode 100644 packages/core/types/src/http/pricing/admin/responses.ts create mode 100644 packages/core/types/src/workflow/pricing/index.ts create mode 100644 packages/medusa/src/api/admin/price-preferences/[id]/route.ts create mode 100644 packages/medusa/src/api/admin/price-preferences/middlewares.ts create mode 100644 packages/medusa/src/api/admin/price-preferences/query-config.ts create mode 100644 packages/medusa/src/api/admin/price-preferences/route.ts create mode 100644 packages/medusa/src/api/admin/price-preferences/validators.ts diff --git a/integration-tests/http/__tests__/price-preference/admin/price-preference.spec.ts b/integration-tests/http/__tests__/price-preference/admin/price-preference.spec.ts new file mode 100644 index 0000000000000..fd5c979e57fbd --- /dev/null +++ b/integration-tests/http/__tests__/price-preference/admin/price-preference.spec.ts @@ -0,0 +1,221 @@ +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { + createAdminUser, + adminHeaders, +} from "../../../../helpers/create-admin-user" + +jest.setTimeout(30000) + +medusaIntegrationTestRunner({ + env: {}, + testSuite: ({ dbConnection, getContainer, api }) => { + let pricePreference1 + let pricePreference2 + + beforeEach(async () => { + const container = getContainer() + await createAdminUser(dbConnection, adminHeaders, container) + + pricePreference1 = ( + await api.post( + "/admin/price-preferences", + { + attribute: "region_id", + value: "region-1", + is_tax_inclusive: true, + }, + adminHeaders + ) + ).data.price_preference + + pricePreference2 = ( + await api.post( + "/admin/price-preferences", + { + attribute: "currency_code", + value: "EUR", + is_tax_inclusive: true, + }, + adminHeaders + ) + ).data.price_preference + }) + + describe("/admin/price-preferences", () => { + describe("POST /admin/price-preferences", () => { + it("creates a price preference", async () => { + const newPricePreference = ( + await api.post( + "/admin/price-preferences", + { + attribute: "region_id", + value: "region-2", + is_tax_inclusive: true, + }, + adminHeaders + ) + ).data.price_preference + + expect(newPricePreference).toEqual( + expect.objectContaining({ + attribute: "region_id", + value: "region-2", + is_tax_inclusive: true, + }) + ) + }) + + it("creates a price preference with false tax inclusivity by default", async () => { + const newPricePreference = ( + await api.post( + "/admin/price-preferences", + { + attribute: "region_id", + value: "region-2", + }, + adminHeaders + ) + ).data.price_preference + + expect(newPricePreference).toEqual( + expect.objectContaining({ + attribute: "region_id", + value: "region-2", + is_tax_inclusive: false, + }) + ) + }) + }) + + describe("GET /admin/price-preferences", () => { + it("returns a list of price preferences", async () => { + const response = ( + await api.get("/admin/price-preferences", adminHeaders) + ).data.price_preferences + + expect(response).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + attribute: "region_id", + value: "region-1", + is_tax_inclusive: true, + }), + expect.objectContaining({ + attribute: "currency_code", + value: "EUR", + is_tax_inclusive: true, + }), + ]) + ) + }) + it("filters price preferences by attribute", async () => { + const response = ( + await api.get( + "/admin/price-preferences?attribute=region_id", + adminHeaders + ) + ).data.price_preferences + + expect(response).toEqual([ + expect.objectContaining({ + attribute: "region_id", + value: "region-1", + is_tax_inclusive: true, + }), + ]) + }) + }) + + describe("GET /admin/price-preferences/:id", () => { + it("returns a price preference by :id", async () => { + const response = ( + await api.get( + `/admin/price-preferences/${pricePreference1.id}`, + adminHeaders + ) + ).data.price_preference + + expect(response).toEqual( + expect.objectContaining({ + attribute: "region_id", + value: "region-1", + is_tax_inclusive: true, + }) + ) + }) + }) + + describe("POST /admin/price-preferences/:id", () => { + it("updates a price preference", async () => { + const response = ( + await api.post( + `/admin/price-preferences/${pricePreference1.id}`, + { + attribute: "region_id", + value: "region-2", + is_tax_inclusive: false, + }, + adminHeaders + ) + ).data.price_preference + + expect(response).toEqual( + expect.objectContaining({ + attribute: "region_id", + value: "region-2", + is_tax_inclusive: false, + }) + ) + }) + it("updates the tax inclusivity in the price preference", async () => { + const response = ( + await api.post( + `/admin/price-preferences/${pricePreference1.id}`, + { + is_tax_inclusive: false, + }, + adminHeaders + ) + ).data.price_preference + + expect(response).toEqual( + expect.objectContaining({ + attribute: "region_id", + value: "region-1", + is_tax_inclusive: false, + }) + ) + }) + }) + + describe("DELETE /admin/price-preferences/:id", () => { + it("Deletes a price preference", async () => { + const deleteResponse = await api.delete( + `/admin/price-preferences/${pricePreference1.id}`, + adminHeaders + ) + + const remainingPricePreferences = ( + await api.get("/admin/price-preferences", adminHeaders) + ).data.price_preferences + + expect(deleteResponse.data).toEqual( + expect.objectContaining({ + id: pricePreference1.id, + object: "price_preference", + deleted: true, + }) + ) + + expect(remainingPricePreferences).toEqual([ + expect.objectContaining({ + attribute: "currency_code", + value: "EUR", + is_tax_inclusive: true, + }), + ]) + }) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/order/workflows/get-orders-list.ts b/packages/core/core-flows/src/order/workflows/get-orders-list.ts index b967d8997d469..c6875c86154b7 100644 --- a/packages/core/core-flows/src/order/workflows/get-orders-list.ts +++ b/packages/core/core-flows/src/order/workflows/get-orders-list.ts @@ -18,9 +18,9 @@ type OrderOutput = metadata: any } -export const getOrdersListlWorkflowId = "get-orders-list" -export const getOrdersListlWorkflow = createWorkflow( - getOrdersListlWorkflowId, +export const getOrdersListWorkflowId = "get-orders-list" +export const getOrdersListWorkflow = createWorkflow( + getOrdersListWorkflowId, ( input: WorkflowData<{ fields: string[] diff --git a/packages/core/core-flows/src/pricing/index.ts b/packages/core/core-flows/src/pricing/index.ts index c1f49c23fa343..68de82c9f92da 100644 --- a/packages/core/core-flows/src/pricing/index.ts +++ b/packages/core/core-flows/src/pricing/index.ts @@ -1 +1,2 @@ export * from "./steps" +export * from "./workflows" diff --git a/packages/core/core-flows/src/pricing/steps/create-price-preferences.ts b/packages/core/core-flows/src/pricing/steps/create-price-preferences.ts new file mode 100644 index 0000000000000..4c3ed46082f68 --- /dev/null +++ b/packages/core/core-flows/src/pricing/steps/create-price-preferences.ts @@ -0,0 +1,34 @@ +import { IPricingModuleService } from "@medusajs/types" +import { PricingWorkflow } from "@medusajs/types/dist/workflow" +import { ModuleRegistrationName } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type StepInput = PricingWorkflow.CreatePricePreferencesWorkflowInput[] + +export const createPricePreferencesStepId = "create-price-preferences" +export const createPricePreferencesStep = createStep( + createPricePreferencesStepId, + async (data: StepInput, { container }) => { + const pricingModule = container.resolve( + ModuleRegistrationName.PRICING + ) + + const pricePreferences = await pricingModule.createPricePreferences(data) + + return new StepResponse( + pricePreferences, + pricePreferences.map((pricePreference) => pricePreference.id) + ) + }, + async (pricePreferences, { container }) => { + if (!pricePreferences?.length) { + return + } + + const pricingModule = container.resolve( + ModuleRegistrationName.PRICING + ) + + await pricingModule.deletePricePreferences(pricePreferences) + } +) diff --git a/packages/core/core-flows/src/pricing/steps/delete-price-preferences.ts b/packages/core/core-flows/src/pricing/steps/delete-price-preferences.ts new file mode 100644 index 0000000000000..b625ea95f25af --- /dev/null +++ b/packages/core/core-flows/src/pricing/steps/delete-price-preferences.ts @@ -0,0 +1,28 @@ +import { IPricingModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const deletePricePreferencesStepId = "delete-price-preferences" +export const deletePricePreferencesStep = createStep( + deletePricePreferencesStepId, + async (ids: string[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.PRICING + ) + + await service.softDeletePricePreferences(ids) + + return new StepResponse(void 0, ids) + }, + async (prevIds, { container }) => { + if (!prevIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.PRICING + ) + + await service.restorePricePreferences(prevIds) + } +) diff --git a/packages/core/core-flows/src/pricing/steps/index.ts b/packages/core/core-flows/src/pricing/steps/index.ts index 04bc6d7d03944..5d4820f2e0da1 100644 --- a/packages/core/core-flows/src/pricing/steps/index.ts +++ b/packages/core/core-flows/src/pricing/steps/index.ts @@ -1,2 +1,5 @@ export * from "./create-price-sets" export * from "./update-price-sets" +export * from "./create-price-preferences" +export * from "./update-price-preferences" +export * from "./delete-price-preferences" diff --git a/packages/core/core-flows/src/pricing/steps/update-price-preferences.ts b/packages/core/core-flows/src/pricing/steps/update-price-preferences.ts new file mode 100644 index 0000000000000..f7427d81eb681 --- /dev/null +++ b/packages/core/core-flows/src/pricing/steps/update-price-preferences.ts @@ -0,0 +1,45 @@ +import { PricingWorkflow, IPricingModuleService } from "@medusajs/types" +import { + ModuleRegistrationName, + getSelectsAndRelationsFromObjectArray, +} from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type StepInput = PricingWorkflow.UpdatePricePreferencesWorkflowInput + +export const updatePricePreferencesStepId = "update-price-preferences" +export const updatePricePreferencesStep = createStep( + updatePricePreferencesStepId, + async (input: StepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.PRICING + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + input.update, + ]) + + const prevData = await service.listPricePreferences(input.selector, { + select: selects, + relations, + }) + + const updatedPricePreferences = await service.updatePricePreferences( + input.selector, + input.update + ) + + return new StepResponse(updatedPricePreferences, prevData) + }, + async (prevData, { container }) => { + if (!prevData?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.PRICING + ) + + await service.upsertPricePreferences(prevData) + } +) diff --git a/packages/core/core-flows/src/pricing/workflows/create-price-preferences.ts b/packages/core/core-flows/src/pricing/workflows/create-price-preferences.ts new file mode 100644 index 0000000000000..b3998e980dd18 --- /dev/null +++ b/packages/core/core-flows/src/pricing/workflows/create-price-preferences.ts @@ -0,0 +1,13 @@ +import { PricingWorkflow } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { createPricePreferencesStep } from "../steps" + +type WorkflowInputData = PricingWorkflow.CreatePricePreferencesWorkflowInput[] + +export const createPricePreferencesWorkflowId = "create-price-preferences" +export const createPricePreferencesWorkflow = createWorkflow( + createPricePreferencesWorkflowId, + (input: WorkflowData) => { + return createPricePreferencesStep(input) + } +) diff --git a/packages/core/core-flows/src/pricing/workflows/delete-price-preferences.ts b/packages/core/core-flows/src/pricing/workflows/delete-price-preferences.ts new file mode 100644 index 0000000000000..fcdb54661a212 --- /dev/null +++ b/packages/core/core-flows/src/pricing/workflows/delete-price-preferences.ts @@ -0,0 +1,10 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { deletePricePreferencesStep } from "../steps" + +export const deletePricePreferencesWorkflowId = "delete-price-preferences" +export const deletePricePreferencesWorkflow = createWorkflow( + deletePricePreferencesWorkflowId, + (input: WorkflowData) => { + return deletePricePreferencesStep(input) + } +) diff --git a/packages/core/core-flows/src/pricing/workflows/index.ts b/packages/core/core-flows/src/pricing/workflows/index.ts new file mode 100644 index 0000000000000..f2c30d229ece9 --- /dev/null +++ b/packages/core/core-flows/src/pricing/workflows/index.ts @@ -0,0 +1,3 @@ +export * from "./create-price-preferences" +export * from "./update-price-preferences" +export * from "./delete-price-preferences" diff --git a/packages/core/core-flows/src/pricing/workflows/update-price-preferences.ts b/packages/core/core-flows/src/pricing/workflows/update-price-preferences.ts new file mode 100644 index 0000000000000..f090954f4e635 --- /dev/null +++ b/packages/core/core-flows/src/pricing/workflows/update-price-preferences.ts @@ -0,0 +1,13 @@ +import { PricingWorkflow } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { updatePricePreferencesStep } from "../steps" + +type WorkflowInputData = PricingWorkflow.UpdatePricePreferencesWorkflowInput + +export const updatePricePreferencesWorkflowId = "update-price-preferences" +export const updatePricePreferencesWorkflow = createWorkflow( + updatePricePreferencesWorkflowId, + (input: WorkflowData) => { + return updatePricePreferencesStep(input) + } +) diff --git a/packages/core/types/src/http/pricing/admin/entities.ts b/packages/core/types/src/http/pricing/admin/entities.ts index 5e5a368dc06e3..85dc5d6ef34cf 100644 --- a/packages/core/types/src/http/pricing/admin/entities.ts +++ b/packages/core/types/src/http/pricing/admin/entities.ts @@ -29,3 +29,13 @@ export interface AdminPrice { updated_at: string deleted_at: string | null } + +export interface AdminPricePreference { + id: string + attribute: string | null + value: string | null + is_tax_inclusive: boolean + created_at: string + updated_at: string + deleted_at: null | string +} diff --git a/packages/core/types/src/http/pricing/admin/index.ts b/packages/core/types/src/http/pricing/admin/index.ts index 8270e0b265e19..1f82a2ead53e3 100644 --- a/packages/core/types/src/http/pricing/admin/index.ts +++ b/packages/core/types/src/http/pricing/admin/index.ts @@ -1 +1,4 @@ export * from "./entities" +export * from "./payloads" +export * from "./queries" +export * from "./responses" diff --git a/packages/core/types/src/http/pricing/admin/payloads.ts b/packages/core/types/src/http/pricing/admin/payloads.ts new file mode 100644 index 0000000000000..f4f14983fa1f9 --- /dev/null +++ b/packages/core/types/src/http/pricing/admin/payloads.ts @@ -0,0 +1,11 @@ +export interface AdminCreatePricePreference { + attribute?: string + value?: string + is_tax_inclusive?: boolean +} + +export interface AdminUpdatePricePreference { + attribute?: string | null + value?: string | null + is_tax_inclusive?: boolean +} diff --git a/packages/core/types/src/http/pricing/admin/queries.ts b/packages/core/types/src/http/pricing/admin/queries.ts new file mode 100644 index 0000000000000..57ad288e2f31e --- /dev/null +++ b/packages/core/types/src/http/pricing/admin/queries.ts @@ -0,0 +1,12 @@ +import { BaseFilterable } from "../../../dal" +import { FindParams, SelectParams } from "../../common" + +export interface AdminPricePreferenceListParams + extends FindParams, + BaseFilterable { + id?: string | string[] + attribute?: string | string[] + value?: string | string[] +} + +export interface AdminPricePreferenceParams extends SelectParams {} diff --git a/packages/core/types/src/http/pricing/admin/responses.ts b/packages/core/types/src/http/pricing/admin/responses.ts new file mode 100644 index 0000000000000..23d1186d00090 --- /dev/null +++ b/packages/core/types/src/http/pricing/admin/responses.ts @@ -0,0 +1,14 @@ +import { DeleteResponse, PaginatedResponse } from "../../common" +import { AdminPricePreference } from "./entities" + +export interface AdminPricePreferenceResponse { + price_preference: AdminPricePreference +} + +export interface AdminPricePreferenceListResponse + extends PaginatedResponse<{ + price_preferences: AdminPricePreference[] + }> {} + +export interface AdminPricePreferenceDeleteResponse + extends DeleteResponse<"price_preference"> {} diff --git a/packages/core/types/src/workflow/index.ts b/packages/core/types/src/workflow/index.ts index 9b592a109351b..3f8a1f35e1ff0 100644 --- a/packages/core/types/src/workflow/index.ts +++ b/packages/core/types/src/workflow/index.ts @@ -10,3 +10,4 @@ export * as RegionWorkflow from "./region" export * as ReservationWorkflow from "./reservation" export * as UserWorkflow from "./user" export * as OrderWorkflow from "./order" +export * as PricingWorkflow from "./pricing" diff --git a/packages/core/types/src/workflow/pricing/index.ts b/packages/core/types/src/workflow/pricing/index.ts new file mode 100644 index 0000000000000..eddbb83f27fca --- /dev/null +++ b/packages/core/types/src/workflow/pricing/index.ts @@ -0,0 +1,18 @@ +import { FilterablePricePreferenceProps } from "../../pricing" + +export interface CreatePricePreferencesWorkflowInput { + attribute?: string + value?: string + is_tax_inclusive?: boolean +} + +interface UpdatePricePreferences { + attribute?: string | null + value?: string | null + is_tax_inclusive?: boolean +} + +export interface UpdatePricePreferencesWorkflowInput { + selector: FilterablePricePreferenceProps + update: UpdatePricePreferences +} diff --git a/packages/medusa/src/api/admin/orders/route.ts b/packages/medusa/src/api/admin/orders/route.ts index 3afcfacebcf32..230d9558e58ba 100644 --- a/packages/medusa/src/api/admin/orders/route.ts +++ b/packages/medusa/src/api/admin/orders/route.ts @@ -1,4 +1,4 @@ -import { getOrdersListlWorkflow } from "@medusajs/core-flows" +import { getOrdersListWorkflow } from "@medusajs/core-flows" import { OrderDTO } from "@medusajs/types" import { AuthenticatedMedusaRequest, @@ -17,7 +17,7 @@ export const GET = async ( ...req.remoteQueryConfig.pagination, } - const workflow = getOrdersListlWorkflow(req.scope) + const workflow = getOrdersListWorkflow(req.scope) const { result } = await workflow.run({ input: { fields: req.remoteQueryConfig.fields, diff --git a/packages/medusa/src/api/admin/price-preferences/[id]/route.ts b/packages/medusa/src/api/admin/price-preferences/[id]/route.ts new file mode 100644 index 0000000000000..9693c68d43c7e --- /dev/null +++ b/packages/medusa/src/api/admin/price-preferences/[id]/route.ts @@ -0,0 +1,64 @@ +import { + deletePricePreferencesWorkflow, + updatePricePreferencesWorkflow, +} from "@medusajs/core-flows" + +import { HttpTypes } from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" +import { refetchEntity } from "../../../utils/refetch-entity" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const price_preference = await refetchEntity( + "price_preference", + req.params.id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ price_preference }) +} + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.params.id + const workflow = updatePricePreferencesWorkflow(req.scope) + + await workflow.run({ + input: { selector: { id: [id] }, update: req.body }, + }) + + const price_preference = await refetchEntity( + "price_preference", + id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ price_preference }) +} + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.params.id + const workflow = deletePricePreferencesWorkflow(req.scope) + + await workflow.run({ + input: [id], + }) + + res.status(200).json({ + id, + object: "price_preference", + deleted: true, + }) +} diff --git a/packages/medusa/src/api/admin/price-preferences/middlewares.ts b/packages/medusa/src/api/admin/price-preferences/middlewares.ts new file mode 100644 index 0000000000000..3f3d5b2112e0b --- /dev/null +++ b/packages/medusa/src/api/admin/price-preferences/middlewares.ts @@ -0,0 +1,55 @@ +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { validateAndTransformBody } from "../../utils/validate-body" +import { validateAndTransformQuery } from "../../utils/validate-query" +import * as QueryConfig from "./query-config" +import { + AdminCreatePricePreference, + AdminGetPricePreferenceParams, + AdminGetPricePreferencesParams, + AdminUpdatePricePreference, +} from "./validators" + +export const adminPricePreferencesRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["GET"], + matcher: "/admin/price-preferences", + middlewares: [ + validateAndTransformQuery( + AdminGetPricePreferencesParams, + QueryConfig.listPricePreferenceQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/price-preferences/:id", + middlewares: [ + validateAndTransformQuery( + AdminGetPricePreferenceParams, + QueryConfig.retrivePricePreferenceQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/price-preferences", + middlewares: [ + validateAndTransformBody(AdminCreatePricePreference), + validateAndTransformQuery( + AdminGetPricePreferenceParams, + QueryConfig.retrivePricePreferenceQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/price-preferences/:id", + middlewares: [ + validateAndTransformBody(AdminUpdatePricePreference), + validateAndTransformQuery( + AdminGetPricePreferenceParams, + QueryConfig.retrivePricePreferenceQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api/admin/price-preferences/query-config.ts b/packages/medusa/src/api/admin/price-preferences/query-config.ts new file mode 100644 index 0000000000000..d3e2f3490e572 --- /dev/null +++ b/packages/medusa/src/api/admin/price-preferences/query-config.ts @@ -0,0 +1,19 @@ +export const adminPricePreferenceRemoteQueryFields = [ + "id", + "attribute", + "value", + "is_tax_inclusive", + "created_at", + "deleted_at", + "updated_at", +] + +export const retrivePricePreferenceQueryConfig = { + defaults: adminPricePreferenceRemoteQueryFields, + isList: false, +} + +export const listPricePreferenceQueryConfig = { + ...retrivePricePreferenceQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api/admin/price-preferences/route.ts b/packages/medusa/src/api/admin/price-preferences/route.ts new file mode 100644 index 0000000000000..2621ee661fdf9 --- /dev/null +++ b/packages/medusa/src/api/admin/price-preferences/route.ts @@ -0,0 +1,45 @@ +import { HttpTypes } from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../types/routing" +import { refetchEntities, refetchEntity } from "../../utils/refetch-entity" +import { createPricePreferencesWorkflow } from "@medusajs/core-flows" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { rows: price_preferences, metadata } = await refetchEntities( + "price_preference", + req.filterableFields, + req.scope, + req.remoteQueryConfig.fields, + req.remoteQueryConfig.pagination + ) + res.json({ + price_preferences: price_preferences, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const workflow = createPricePreferencesWorkflow(req.scope) + const { result } = await workflow.run({ + input: [req.validatedBody], + }) + + const price_preference = await refetchEntity( + "price_preference", + result[0].id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ price_preference }) +} diff --git a/packages/medusa/src/api/admin/price-preferences/validators.ts b/packages/medusa/src/api/admin/price-preferences/validators.ts new file mode 100644 index 0000000000000..166fdcdc84aad --- /dev/null +++ b/packages/medusa/src/api/admin/price-preferences/validators.ts @@ -0,0 +1,37 @@ +import { z } from "zod" +import { createFindParams, createSelectParams } from "../../utils/validators" + +export const AdminGetPricePreferenceParams = createSelectParams() +export const AdminGetPricePreferencesParams = createFindParams({ + offset: 0, + limit: 50, +}).merge( + z.object({ + q: z.string().optional(), + id: z.union([z.string(), z.array(z.string())]).optional(), + attribute: z.union([z.string(), z.array(z.string())]).optional(), + value: z.union([z.string(), z.array(z.string())]).optional(), + $and: z.lazy(() => AdminGetPricePreferencesParams.array()).optional(), + $or: z.lazy(() => AdminGetPricePreferencesParams.array()).optional(), + }) +) + +export const AdminCreatePricePreference = z.object({ + attribute: z.string(), + value: z.string(), + is_tax_inclusive: z.boolean().optional(), +}) + +export type AdminCreatePricePreferencePriceType = z.infer< + typeof AdminCreatePricePreference +> + +export const AdminUpdatePricePreference = z.object({ + attribute: z.string().optional(), + value: z.string().optional(), + is_tax_inclusive: z.boolean().optional(), +}) + +export type AdminUpdatePricePreferenceType = z.infer< + typeof AdminUpdatePricePreference +> diff --git a/packages/medusa/src/api/middlewares.ts b/packages/medusa/src/api/middlewares.ts index 98773b386e980..c4aef9349c62a 100644 --- a/packages/medusa/src/api/middlewares.ts +++ b/packages/medusa/src/api/middlewares.ts @@ -14,6 +14,7 @@ import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares" import { adminOrderRoutesMiddlewares } from "./admin/orders/middlewares" import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares" import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares" +import { adminPricePreferencesRoutesMiddlewares } from "./admin/price-preferences/middlewares" import { adminProductCategoryRoutesMiddlewares } from "./admin/product-categories/middlewares" import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlewares" import { adminProductTagRoutesMiddlewares } from "./admin/product-tags/middlewares" @@ -78,6 +79,7 @@ export const config: MiddlewaresConfig = { ...adminProductRoutesMiddlewares, ...adminPaymentRoutesMiddlewares, ...adminPriceListsRoutesMiddlewares, + ...adminPricePreferencesRoutesMiddlewares, ...adminInventoryRoutesMiddlewares, ...adminCollectionRoutesMiddlewares, ...adminShippingOptionRoutesMiddlewares, diff --git a/packages/modules/pricing/src/joiner-config.ts b/packages/modules/pricing/src/joiner-config.ts index 4df8d395756ca..09a538569c845 100644 --- a/packages/modules/pricing/src/joiner-config.ts +++ b/packages/modules/pricing/src/joiner-config.ts @@ -2,7 +2,7 @@ import { defineJoinerConfig, Modules } from "@medusajs/utils" import { Price, PriceList, PricePreference, PriceSet } from "@models" export const joinerConfig = defineJoinerConfig(Modules.PRICING, { - models: [PriceSet, PriceList, Price], + models: [PriceSet, PriceList, Price, PricePreference], linkableKeys: { price_set_id: PriceSet.name, price_list_id: PriceList.name, diff --git a/packages/modules/pricing/src/services/pricing-module.ts b/packages/modules/pricing/src/services/pricing-module.ts index 6c96c3943495a..89ae2ea8cb5e0 100644 --- a/packages/modules/pricing/src/services/pricing-module.ts +++ b/packages/modules/pricing/src/services/pricing-module.ts @@ -734,7 +734,10 @@ export default class PricingModuleService sharedContext ) - return await this.baseRepository_.serialize(preferences) + const serialized = await this.baseRepository_.serialize< + PricePreferenceDTO[] + >(preferences) + return Array.isArray(data) ? serialized : serialized[0] } async upsertPricePreferences( @@ -800,11 +803,38 @@ export default class PricingModuleService data: PricingTypes.UpdatePricePreferenceDTO, @MedusaContext() sharedContext: Context = {} ): Promise { - const preferences = await this.pricePreferenceService_.update( - data, + let normalizedInput: ServiceTypes.UpdatePricePreferenceInput[] = [] + if (isString(idOrSelector)) { + // Check if the ID exists, it will throw if not. + await this.pricePreferenceService_.retrieve( + idOrSelector, + {}, + sharedContext + ) + normalizedInput = [{ id: idOrSelector, ...data }] + } else { + const pricePreferences = await this.pricePreferenceService_.list( + idOrSelector, + {}, + sharedContext + ) + + normalizedInput = pricePreferences.map((pricePreference) => ({ + id: pricePreference.id, + ...data, + })) + } + + const updateResult = await this.pricePreferenceService_.update( + normalizedInput, sharedContext ) - return await this.baseRepository_.serialize(preferences) + + const pricePreferences = await this.baseRepository_.serialize< + PricePreferenceDTO[] | PricePreferenceDTO + >(updateResult) + + return isString(idOrSelector) ? pricePreferences[0] : pricePreferences } @InjectTransactionManager("baseRepository_")