diff --git a/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts b/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts new file mode 100644 index 0000000000..c860f237c5 --- /dev/null +++ b/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts @@ -0,0 +1,81 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService } 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" +import adminSeeder from "../../../../helpers/admin-seeder" + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("POST /admin/customers/:id/addresses", () => { + let dbConnection + let appContainer + let shutdownServer + let customerModuleService: ICustomerModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should create a customer address", async () => { + // Create a customer + const customer = await customerModuleService.create({ + first_name: "John", + last_name: "Doe", + }) + + const api = useApi() as any + const response = await api.post( + `/admin/customers/${customer.id}/addresses`, + { + first_name: "John", + last_name: "Doe", + address_1: "Test street 1", + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.address).toEqual( + expect.objectContaining({ + id: expect.any(String), + first_name: "John", + last_name: "Doe", + address_1: "Test street 1", + }) + ) + + const customerWithAddresses = await customerModuleService.retrieve( + customer.id, + { relations: ["addresses"] } + ) + + expect(customerWithAddresses.addresses?.length).toEqual(1) + }) +}) diff --git a/integration-tests/plugins/__tests__/customer/admin/delete-customer-address.spec.ts b/integration-tests/plugins/__tests__/customer/admin/delete-customer-address.spec.ts new file mode 100644 index 0000000000..a604b2b021 --- /dev/null +++ b/integration-tests/plugins/__tests__/customer/admin/delete-customer-address.spec.ts @@ -0,0 +1,73 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService } 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" +import adminSeeder from "../../../../helpers/admin-seeder" + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("DELETE /admin/customers/:id/addresses/:address_id", () => { + let dbConnection + let appContainer + let shutdownServer + let customerModuleService: ICustomerModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should update a customer address", async () => { + const customer = await customerModuleService.create({ + first_name: "John", + last_name: "Doe", + }) + + const address = await customerModuleService.addAddresses({ + customer_id: customer.id, + first_name: "John", + last_name: "Doe", + address_1: "Test street 1", + }) + + const api = useApi() as any + const response = await api.delete( + `/admin/customers/${customer.id}/addresses/${address.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + + const updatedCustomer = await customerModuleService.retrieve(customer.id, { + relations: ["addresses"], + }) + + expect(updatedCustomer.addresses?.length).toEqual(0) + }) +}) diff --git a/integration-tests/plugins/__tests__/customer/admin/list-customer-addresses.ts b/integration-tests/plugins/__tests__/customer/admin/list-customer-addresses.ts new file mode 100644 index 0000000000..b87ac4e592 --- /dev/null +++ b/integration-tests/plugins/__tests__/customer/admin/list-customer-addresses.ts @@ -0,0 +1,111 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService } 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" +import adminSeeder from "../../../../helpers/admin-seeder" + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("GET /admin/customers/:id/addresses", () => { + let dbConnection + let appContainer + let shutdownServer + let customerModuleService: ICustomerModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get all customer addresses and its count", async () => { + const [customer] = await customerModuleService.create([ + { + first_name: "Test", + last_name: "Test", + email: "test@me.com", + addresses: [ + { + first_name: "Test", + last_name: "Test", + address_1: "Test street 1", + }, + { + first_name: "Test", + last_name: "Test", + address_1: "Test street 2", + }, + { + first_name: "Test", + last_name: "Test", + address_1: "Test street 3", + }, + ], + }, + { + first_name: "Test Test", + last_name: "Test Test", + addresses: [ + { + first_name: "Test TEST", + last_name: "Test TEST", + address_1: "NOT street 1", + }, + ], + }, + ]) + + const api = useApi() as any + const response = await api.get( + `/admin/customers/${customer.id}/addresses`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(3) + expect(response.data.addresses).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + customer_id: customer.id, + address_1: "Test street 1", + }), + expect.objectContaining({ + id: expect.any(String), + customer_id: customer.id, + address_1: "Test street 2", + }), + expect.objectContaining({ + id: expect.any(String), + customer_id: customer.id, + address_1: "Test street 3", + }), + ]) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/customer/admin/update-customer-address.spec.ts b/integration-tests/plugins/__tests__/customer/admin/update-customer-address.spec.ts new file mode 100644 index 0000000000..4ce6fac25d --- /dev/null +++ b/integration-tests/plugins/__tests__/customer/admin/update-customer-address.spec.ts @@ -0,0 +1,77 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService } 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" +import adminSeeder from "../../../../helpers/admin-seeder" + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("POST /admin/customers/:id/addresses/:address_id", () => { + let dbConnection + let appContainer + let shutdownServer + let customerModuleService: ICustomerModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should update a customer address", async () => { + const customer = await customerModuleService.create({ + first_name: "John", + last_name: "Doe", + }) + + const address = await customerModuleService.addAddresses({ + customer_id: customer.id, + first_name: "John", + last_name: "Doe", + address_1: "Test street 1", + }) + + const api = useApi() as any + const response = await api.post( + `/admin/customers/${customer.id}/addresses/${address.id}`, + { + first_name: "Jane", + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.address).toEqual( + expect.objectContaining({ + id: expect.any(String), + first_name: "Jane", + last_name: "Doe", + }) + ) + }) +}) diff --git a/packages/core-flows/src/customer/steps/create-addresses.ts b/packages/core-flows/src/customer/steps/create-addresses.ts new file mode 100644 index 0000000000..139aebf0d2 --- /dev/null +++ b/packages/core-flows/src/customer/steps/create-addresses.ts @@ -0,0 +1,34 @@ +import { + ICustomerModuleService, + CreateCustomerAddressDTO, +} from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const createCustomerAddressesStepId = "create-customer-addresses" +export const createCustomerAddressesStep = createStep( + createCustomerAddressesStepId, + async (data: CreateCustomerAddressDTO[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const addresses = await service.addAddresses(data) + + return new StepResponse( + addresses, + addresses.map((address) => address.id) + ) + }, + async (ids, { container }) => { + if (!ids?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + await service.deleteAddress(ids) + } +) diff --git a/packages/core-flows/src/customer/steps/delete-addresses.ts b/packages/core-flows/src/customer/steps/delete-addresses.ts new file mode 100644 index 0000000000..c6ed173299 --- /dev/null +++ b/packages/core-flows/src/customer/steps/delete-addresses.ts @@ -0,0 +1,32 @@ +import { ICustomerModuleService } from "@medusajs/types" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +type DeleteCustomerAddressStepInput = string[] +export const deleteCustomerAddressesStepId = "delete-customer-addresses" +export const deleteCustomerAddressesStep = createStep( + deleteCustomerAddressesStepId, + async (ids: DeleteCustomerAddressStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const existing = await service.listAddresses({ + id: ids, + }) + await service.deleteAddress(ids) + + return new StepResponse(void 0, existing) + }, + async (prevAddresses, { container }) => { + if (!prevAddresses?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + await service.addAddresses(prevAddresses) + } +) diff --git a/packages/core-flows/src/customer/steps/index.ts b/packages/core-flows/src/customer/steps/index.ts index 6ea82b9cd6..cf2fe8e9b0 100644 --- a/packages/core-flows/src/customer/steps/index.ts +++ b/packages/core-flows/src/customer/steps/index.ts @@ -1,3 +1,6 @@ export * from "./create-customers" export * from "./update-customers" export * from "./delete-customers" +export * from "./create-addresses" +export * from "./update-addresses" +export * from "./delete-addresses" diff --git a/packages/core-flows/src/customer/steps/update-addresses.ts b/packages/core-flows/src/customer/steps/update-addresses.ts new file mode 100644 index 0000000000..17d7d68be1 --- /dev/null +++ b/packages/core-flows/src/customer/steps/update-addresses.ts @@ -0,0 +1,54 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + CustomerAddressDTO, + FilterableCustomerAddressProps, + ICustomerModuleService, +} from "@medusajs/types" +import { + getSelectsAndRelationsFromObjectArray, + promiseAll, +} from "@medusajs/utils" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" + +type UpdateCustomerAddresseStepInput = { + selector: FilterableCustomerAddressProps + update: Partial +} + +export const updateCustomerAddresseStepId = "update-customer-addresses" +export const updateCustomerAddressesStep = createStep( + updateCustomerAddresseStepId, + async (data: UpdateCustomerAddresseStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + data.update, + ]) + const prevCustomers = await service.listAddresses(data.selector, { + select: selects, + relations, + }) + + const customerAddresses = await service.updateAddress( + data.selector, + data.update + ) + + return new StepResponse(customerAddresses, prevCustomers) + }, + async (prevCustomerAddresses, { container }) => { + if (!prevCustomerAddresses) { + return + } + + const service = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + await promiseAll( + prevCustomerAddresses.map((c) => service.updateAddress(c.id, { ...c })) + ) + } +) diff --git a/packages/core-flows/src/customer/workflows/create-addresses.ts b/packages/core-flows/src/customer/workflows/create-addresses.ts new file mode 100644 index 0000000000..12bc2d2ff9 --- /dev/null +++ b/packages/core-flows/src/customer/workflows/create-addresses.ts @@ -0,0 +1,13 @@ +import { CreateCustomerAddressDTO, CustomerAddressDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { createCustomerAddressesStep } from "../steps" + +type WorkflowInput = { addresses: CreateCustomerAddressDTO[] } + +export const createCustomerAddressesWorkflowId = "create-customer-addresses" +export const createCustomerAddressesWorkflow = createWorkflow( + createCustomerAddressesWorkflowId, + (input: WorkflowData): WorkflowData => { + return createCustomerAddressesStep(input.addresses) + } +) diff --git a/packages/core-flows/src/customer/workflows/delete-addresses.ts b/packages/core-flows/src/customer/workflows/delete-addresses.ts new file mode 100644 index 0000000000..3a0e2727b4 --- /dev/null +++ b/packages/core-flows/src/customer/workflows/delete-addresses.ts @@ -0,0 +1,12 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { deleteCustomerAddressesStep } from "../steps" + +type WorkflowInput = { ids: string[] } + +export const deleteCustomerAddressesWorkflowId = "delete-customer-addresses" +export const deleteCustomerAddressesWorkflow = createWorkflow( + deleteCustomerAddressesWorkflowId, + (input: WorkflowData): WorkflowData => { + return deleteCustomerAddressesStep(input.ids) + } +) diff --git a/packages/core-flows/src/customer/workflows/index.ts b/packages/core-flows/src/customer/workflows/index.ts index 6ea82b9cd6..cf2fe8e9b0 100644 --- a/packages/core-flows/src/customer/workflows/index.ts +++ b/packages/core-flows/src/customer/workflows/index.ts @@ -1,3 +1,6 @@ export * from "./create-customers" export * from "./update-customers" export * from "./delete-customers" +export * from "./create-addresses" +export * from "./update-addresses" +export * from "./delete-addresses" diff --git a/packages/core-flows/src/customer/workflows/update-addresses.ts b/packages/core-flows/src/customer/workflows/update-addresses.ts new file mode 100644 index 0000000000..fa8aff557c --- /dev/null +++ b/packages/core-flows/src/customer/workflows/update-addresses.ts @@ -0,0 +1,19 @@ +import { + FilterableCustomerAddressProps, + CustomerAddressDTO, +} from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { updateCustomerAddressesStep } from "../steps" + +type WorkflowInput = { + selector: FilterableCustomerAddressProps + update: Partial +} + +export const updateCustomerAddressesWorkflowId = "update-customer-addresses" +export const updateCustomerAddressesWorkflow = createWorkflow( + updateCustomerAddressesWorkflowId, + (input: WorkflowData): WorkflowData => { + return updateCustomerAddressesStep(input) + } +) diff --git a/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts index 09769a8e59..7cde20eed8 100644 --- a/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts +++ b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts @@ -80,26 +80,22 @@ describe("Customer Module Service", () => { } const customer = await service.create(customerData) - expect(customer).toEqual( + const [address] = await service.listAddresses({ + customer_id: customer.id, + }) + + expect(address).toEqual( expect.objectContaining({ id: expect.any(String), - company_name: "Acme Corp", - first_name: "John", - last_name: "Doe", - addresses: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - address_1: "Testvej 1", - address_2: "Testvej 2", - city: "Testby", - country_code: "DK", - province: "Test", - postal_code: "8000", - phone: "123456789", - metadata: expect.objectContaining({ membership: "gold" }), - is_default_shipping: true, - }), - ]), + address_1: "Testvej 1", + address_2: "Testvej 2", + city: "Testby", + country_code: "DK", + province: "Test", + postal_code: "8000", + phone: "123456789", + metadata: expect.objectContaining({ membership: "gold" }), + is_default_shipping: true, }) ) }) diff --git a/packages/customer/src/services/customer-module.ts b/packages/customer/src/services/customer-module.ts index ee50bf7ee8..542b618c57 100644 --- a/packages/customer/src/services/customer-module.ts +++ b/packages/customer/src/services/customer-module.ts @@ -96,10 +96,32 @@ export default class CustomerModuleService implements ICustomerModuleService { @MedusaContext() sharedContext: Context = {} ) { const data = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray] - const customer = await this.customerService_.create(data, sharedContext) + + // keep address data for creation + const addressData = data.map((d) => d.addresses) + + const customers = await this.customerService_.create(data, sharedContext) + + // decorate addresses with customer ids + // filter out addresses without data + const addressDataWithCustomerIds = addressData + .map((addresses, i) => { + if (!addresses) { + return [] + } + + return addresses.map((address) => ({ + ...address, + customer_id: customers[i].id, + })) + }) + .flat() + + await this.addressService_.create(addressDataWithCustomerIds, sharedContext) + const serialized = await this.baseRepository_.serialize< CustomerTypes.CustomerDTO[] - >(customer, { + >(customers, { populate: true, }) return Array.isArray(dataOrArray) ? serialized : serialized[0] @@ -511,6 +533,40 @@ export default class CustomerModuleService implements ICustomerModuleService { return serialized } + async deleteAddress(addressId: string, sharedContext?: Context): Promise + async deleteAddress( + addressIds: string[], + sharedContext?: Context + ): Promise + async deleteAddress( + selector: CustomerTypes.FilterableCustomerAddressProps, + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async deleteAddress( + addressIdOrSelector: + | string + | string[] + | CustomerTypes.FilterableCustomerAddressProps, + @MedusaContext() sharedContext: Context = {} + ) { + let toDelete = Array.isArray(addressIdOrSelector) + ? addressIdOrSelector + : [addressIdOrSelector as string] + + if (isObject(addressIdOrSelector)) { + const ids = await this.addressService_.list( + addressIdOrSelector, + { select: ["id"] }, + sharedContext + ) + toDelete = ids.map(({ id }) => id) + } + + await this.addressService_.delete(toDelete, sharedContext) + } + @InjectManager("baseRepository_") async listAddresses( filters?: CustomerTypes.FilterableCustomerAddressProps, @@ -528,6 +584,27 @@ export default class CustomerModuleService implements ICustomerModuleService { >(addresses, { populate: true }) } + @InjectManager("baseRepository_") + async listAndCountAddresses( + filters?: CustomerTypes.FilterableCustomerAddressProps, + config?: FindConfig, + @MedusaContext() sharedContext: Context = {} + ): Promise<[CustomerTypes.CustomerAddressDTO[], number]> { + const [addresses, count] = await this.addressService_.listAndCount( + filters, + config, + sharedContext + ) + + return [ + await this.baseRepository_.serialize( + addresses, + { populate: true } + ), + count, + ] + } + async removeCustomerFromGroup( groupCustomerPair: CustomerTypes.GroupCustomerPair, sharedContext?: Context diff --git a/packages/medusa/src/api-v2/admin/customers/[id]/addresses/[address_id]/route.ts b/packages/medusa/src/api-v2/admin/customers/[id]/addresses/[address_id]/route.ts new file mode 100644 index 0000000000..6eecdf55c6 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customers/[id]/addresses/[address_id]/route.ts @@ -0,0 +1,60 @@ +import { + updateCustomerAddressesWorkflow, + deleteCustomerAddressesWorkflow, +} from "@medusajs/core-flows" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CustomerAddressDTO, ICustomerModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../../../../types/routing" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const customerModuleService = req.scope.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const [address] = await customerModuleService.listAddresses( + { id: req.params.address_id, customer_id: req.params.id }, + { + select: req.retrieveConfig.select, + relations: req.retrieveConfig.relations, + } + ) + + res.status(200).json({ address }) +} + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const updateAddresses = updateCustomerAddressesWorkflow(req.scope) + const { result, errors } = await updateAddresses.run({ + input: { + selector: { id: req.params.address_id, customer_id: req.params.id }, + update: req.validatedBody as Partial, + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ address: result[0] }) +} + +export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { + const id = req.params.address_id + const deleteAddress = deleteCustomerAddressesWorkflow(req.scope) + + const { errors } = await deleteAddress.run({ + input: { ids: [id] }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ + id, + object: "address", + deleted: true, + }) +} diff --git a/packages/medusa/src/api-v2/admin/customers/[id]/addresses/route.ts b/packages/medusa/src/api-v2/admin/customers/[id]/addresses/route.ts new file mode 100644 index 0000000000..91927dee0a --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customers/[id]/addresses/route.ts @@ -0,0 +1,51 @@ +import { createCustomerAddressesWorkflow } from "@medusajs/core-flows" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + CreateCustomerAddressDTO, + ICustomerModuleService, +} from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const customerId = req.params.id + + const customerModuleService = req.scope.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const [addresses, count] = await customerModuleService.listAndCountAddresses( + { ...req.filterableFields, customer_id: customerId }, + req.listConfig + ) + + const { offset, limit } = req.validatedQuery + + res.json({ + count, + addresses, + offset, + limit, + }) +} + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const customerId = req.params.id + const createAddresses = createCustomerAddressesWorkflow(req.scope) + const addresses = [ + { + ...(req.validatedBody as CreateCustomerAddressDTO), + customer_id: customerId, + }, + ] + + const { result, errors } = await createAddresses.run({ + input: { addresses }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ address: result[0] }) +} diff --git a/packages/medusa/src/api-v2/admin/customers/middlewares.ts b/packages/medusa/src/api-v2/admin/customers/middlewares.ts index 3d72dcc987..3e687eec15 100644 --- a/packages/medusa/src/api-v2/admin/customers/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/customers/middlewares.ts @@ -6,6 +6,9 @@ import { AdminGetCustomersCustomerParams, AdminPostCustomersReq, AdminPostCustomersCustomerReq, + AdminPostCustomersCustomerAddressesReq, + AdminGetCustomersCustomerAddressesParams, + AdminPostCustomersCustomerAddressesAddressReq, } from "./validators" export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [ @@ -39,4 +42,24 @@ export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/admin/customers/:id", middlewares: [transformBody(AdminPostCustomersCustomerReq)], }, + { + method: ["POST"], + matcher: "/admin/customers/:id/addresses", + middlewares: [transformBody(AdminPostCustomersCustomerAddressesReq)], + }, + { + method: ["POST"], + matcher: "/admin/customers/:id/addresses/:address_id", + middlewares: [transformBody(AdminPostCustomersCustomerAddressesAddressReq)], + }, + { + method: ["GET"], + matcher: "/admin/customers/:id/addresses", + middlewares: [ + transformQuery( + AdminGetCustomersCustomerAddressesParams, + QueryConfig.listAddressesTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api-v2/admin/customers/query-config.ts b/packages/medusa/src/api-v2/admin/customers/query-config.ts index 3702bea765..072f73a1e6 100644 --- a/packages/medusa/src/api-v2/admin/customers/query-config.ts +++ b/packages/medusa/src/api-v2/admin/customers/query-config.ts @@ -1,10 +1,5 @@ export const defaultAdminCustomerRelations = [] -export const allowedAdminCustomerRelations = [ - "groups", - "default_shipping_address", - "default_billing_address", - "addresses", -] +export const allowedAdminCustomerRelations = ["groups", "addresses"] export const defaultAdminCustomerFields = [ "id", "company_name", @@ -27,3 +22,35 @@ export const listTransformQueryConfig = { ...retrieveTransformQueryConfig, isList: true, } + +export const defaultAdminCustomerAddressRelations = [] +export const allowedAdminCustomerAddressRelations = ["customer"] +export const defaultAdminCustomerAddressFields = [ + "id", + "company", + "customer_id", + "first_name", + "last_name", + "address_1", + "address_2", + "city", + "province", + "postal_code", + "country_code", + "phone", + "metadata", + "created_at", + "updated_at", +] + +export const retrieveAddressTransformQueryConfig = { + defaultFields: defaultAdminCustomerAddressFields, + defaultRelations: defaultAdminCustomerAddressRelations, + allowedRelations: allowedAdminCustomerAddressRelations, + isList: false, +} + +export const listAddressesTransformQueryConfig = { + ...retrieveAddressTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/customers/validators.ts b/packages/medusa/src/api-v2/admin/customers/validators.ts index 40703d4961..20a0264d32 100644 --- a/packages/medusa/src/api-v2/admin/customers/validators.ts +++ b/packages/medusa/src/api-v2/admin/customers/validators.ts @@ -1,6 +1,7 @@ import { OperatorMap } from "@medusajs/types" import { Transform, Type } from "class-transformer" import { + IsBoolean, IsNotEmpty, IsOptional, IsString, @@ -29,14 +30,6 @@ export class AdminGetCustomersParams extends extendedFindParamsMixin({ @Type(() => FilterableCustomerGroupPropsValidator) groups?: FilterableCustomerGroupPropsValidator | string | string[] - @IsOptional() - @IsString({ each: true }) - default_billing_address_id?: string | string[] | null - - @IsOptional() - @IsString({ each: true }) - default_shipping_address_id?: string | string[] | null - @IsOptional() @IsString({ each: true }) company_name?: string | string[] | OperatorMap | null @@ -154,3 +147,207 @@ export class AdminPostCustomersCustomerReq { @IsOptional() phone?: string } + +export class AdminPostCustomersCustomerAddressesReq { + @IsNotEmpty() + @IsString() + @IsOptional() + address_name?: string + + @IsBoolean() + @IsOptional() + is_default_shipping?: boolean + + @IsBoolean() + @IsOptional() + is_default_billing?: boolean + + @IsNotEmpty() + @IsString() + @IsOptional() + company?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + first_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + last_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + address_1?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + address_2?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + city?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + country_code?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + province?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + postal_code?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + phone?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + metadata?: Record +} + +export class AdminPostCustomersCustomerAddressesAddressReq { + @IsNotEmpty() + @IsString() + @IsOptional() + address_name?: string + + @IsBoolean() + @IsOptional() + is_default_shipping?: boolean + + @IsBoolean() + @IsOptional() + is_default_billing?: boolean + + @IsNotEmpty() + @IsString() + @IsOptional() + company?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + first_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + last_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + address_1?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + address_2?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + city?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + country_code?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + province?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + postal_code?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + phone?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + metadata?: Record +} + +export class AdminGetCustomersCustomerAddressesParams extends extendedFindParamsMixin( + { + limit: 100, + offset: 0, + } +) { + @IsOptional() + @IsString({ each: true }) + address_name?: string | string[] | OperatorMap + + @IsOptional() + @IsBoolean() + is_default_shipping?: boolean + + @IsOptional() + @IsBoolean() + is_default_billing?: boolean + + @IsOptional() + @IsString({ each: true }) + company?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + first_name?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + last_name?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + address_1?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + address_2?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + city?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + country_code?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + province?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + postal_code?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + phone?: string | string[] | OperatorMap | null + + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + metadata?: OperatorMap> +} diff --git a/packages/types/src/customer/service.ts b/packages/types/src/customer/service.ts index f6f3979744..8143bd213a 100644 --- a/packages/types/src/customer/service.ts +++ b/packages/types/src/customer/service.ts @@ -144,12 +144,25 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise + deleteAddress(addressId: string, sharedContext?: Context): Promise + deleteAddress(addressIds: string[], sharedContext?: Context): Promise + deleteAddress( + selector: FilterableCustomerAddressProps, + sharedContext?: Context + ): Promise + listAddresses( filters?: FilterableCustomerAddressProps, config?: FindConfig, sharedContext?: Context ): Promise + listAndCountAddresses( + filters?: FilterableCustomerAddressProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[CustomerAddressDTO[], number]> + listCustomerGroupRelations( filters?: FilterableCustomerGroupCustomerProps, config?: FindConfig,