From 439b091d55849ea09aa8a6d4ef1acc1734cea0fd Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Wed, 31 Jan 2024 13:55:28 +0100 Subject: [PATCH 1/3] wip --- .../admin/create-customer-addresses.ts | 35 ++++++++++++++ .../src/customer/steps/create-addresses.ts | 21 +++++++-- .../services/customer-module/index.spec.ts | 4 +- .../customer/src/services/customer-module.ts | 46 ++++++++++++++----- packages/utils/src/exceptions/index.ts | 2 + .../src/exceptions/is-duplicate-error.ts | 4 ++ .../utils/src/exceptions/postgres-error.ts | 6 +++ packages/utils/src/index.ts | 1 + 8 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 packages/utils/src/exceptions/index.ts create mode 100644 packages/utils/src/exceptions/is-duplicate-error.ts create mode 100644 packages/utils/src/exceptions/postgres-error.ts diff --git a/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts b/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts index c860f237c53cb..baecbb301b804 100644 --- a/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts +++ b/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts @@ -78,4 +78,39 @@ describe("POST /admin/customers/:id/addresses", () => { expect(customerWithAddresses.addresses?.length).toEqual(1) }) + + it.only("customer's cannot have two default shipping addresses", async () => { + // Create a customer + const customer = await customerModuleService.create({ + first_name: "John", + last_name: "Doe", + addresses: [ + { + first_name: "John", + last_name: "Doe", + address_1: "Test street 1", + is_default_shipping: true, + }, + ], + }) + + 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 2", + is_default_shipping: true, + }, + adminHeaders + ) + .catch((err) => err.response) + + expect(response.status).toEqual(422) + expect(response.data.message).toEqual( + "Customer already has a default shipping address" + ) + }) }) diff --git a/packages/core-flows/src/customer/steps/create-addresses.ts b/packages/core-flows/src/customer/steps/create-addresses.ts index 139aebf0d248a..c0127d9ec3c28 100644 --- a/packages/core-flows/src/customer/steps/create-addresses.ts +++ b/packages/core-flows/src/customer/steps/create-addresses.ts @@ -13,12 +13,23 @@ export const createCustomerAddressesStep = createStep( ModuleRegistrationName.CUSTOMER ) - const addresses = await service.addAddresses(data) + try { + const addresses = await service.addAddresses(data) - return new StepResponse( - addresses, - addresses.map((address) => address.id) - ) + return new StepResponse( + addresses, + addresses.map((address) => address.id) + ) + } catch (error) { + console.log("things went wrong") + console.log(error) + console.log(error.message) + console.log(error.stack) + console.log(error.code) + console.log(error.constraint) + console.log(Object.keys(error)) + throw error + } }, async (ids, { container }) => { if (!ids?.length) { 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 7cde20eed819d..d896f615f4fee 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 @@ -631,7 +631,7 @@ describe("Customer Module Service", () => { ) }) - it("should only be possible to add one default shipping address per customer", async () => { + it.only("should only be possible to add one default shipping address per customer", async () => { const customer = await service.create({ first_name: "John", last_name: "Doe", @@ -662,7 +662,7 @@ describe("Customer Module Service", () => { country_code: "US", is_default_shipping: true, }) - ).rejects.toThrow() + ).rejects.toThrow("A default shipping address already exists") }) it("should only be possible to add one default billing address per customer", async () => { diff --git a/packages/customer/src/services/customer-module.ts b/packages/customer/src/services/customer-module.ts index 542b618c57cc1..5e98201c4812f 100644 --- a/packages/customer/src/services/customer-module.ts +++ b/packages/customer/src/services/customer-module.ts @@ -8,7 +8,6 @@ import { CustomerTypes, SoftDeleteReturn, RestoreReturn, - CustomerUpdatableFields, } from "@medusajs/types" import { @@ -18,9 +17,12 @@ import { mapObjectTo, isString, isObject, + isDuplicateError, } from "@medusajs/utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import * as services from "../services" +import { Address } from "@models" +import { MedusaError } from "@medusajs/utils" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -452,20 +454,40 @@ export default class CustomerModuleService implements ICustomerModuleService { ): Promise< CustomerTypes.CustomerAddressDTO | CustomerTypes.CustomerAddressDTO[] > { - const addresses = await this.addressService_.create( - Array.isArray(data) ? data : [data], - sharedContext - ) + debugger + try { + const addresses = await this.addressService_.create( + Array.isArray(data) ? data : [data], + sharedContext + ) + const serialized = await this.baseRepository_.serialize< + CustomerTypes.CustomerAddressDTO[] + >(addresses, { populate: true }) - const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerAddressDTO[] - >(addresses, { populate: true }) + console.log("all good") + if (Array.isArray(data)) { + return serialized + } - if (Array.isArray(data)) { - return serialized - } + return serialized[0] + } catch (err) { + console.log("hi") + console.log(err) + + console.log(err.message) + console.log(err.code) + console.log(Object.keys(err)) + + if (isDuplicateError(err)) { + // Check if constriant is shipping + throw new MedusaError( + MedusaError.Types.DUPLICATE_ERROR, + "A default shipping address already exists." + ) + } - return serialized[0] + throw err + } } async updateAddress( diff --git a/packages/utils/src/exceptions/index.ts b/packages/utils/src/exceptions/index.ts new file mode 100644 index 0000000000000..0fb0a338463cc --- /dev/null +++ b/packages/utils/src/exceptions/index.ts @@ -0,0 +1,2 @@ +export * from "./postgres-error" +export * from "./is-duplicate-error" diff --git a/packages/utils/src/exceptions/is-duplicate-error.ts b/packages/utils/src/exceptions/is-duplicate-error.ts new file mode 100644 index 0000000000000..5e86ec06fcd8b --- /dev/null +++ b/packages/utils/src/exceptions/is-duplicate-error.ts @@ -0,0 +1,4 @@ +import { PostgresError } from "./postgres-error" +export const isDuplicateError = (err: Error & { code?: string }) => { + return err.code === PostgresError.DUPLICATE_ERROR +} diff --git a/packages/utils/src/exceptions/postgres-error.ts b/packages/utils/src/exceptions/postgres-error.ts new file mode 100644 index 0000000000000..046a9383989db --- /dev/null +++ b/packages/utils/src/exceptions/postgres-error.ts @@ -0,0 +1,6 @@ +export enum PostgresError { + DUPLICATE_ERROR = "23505", + FOREIGN_KEY_ERROR = "23503", + SERIALIZATION_FAILURE = "40001", + NULL_VIOLATION = "23502", +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index c1ca69b9a3706..df223c851e648 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -13,3 +13,4 @@ export * from "./promotion" export * from "./search" export * from "./shipping" export * from "./orchestration" +export * from "./exceptions" From fd63685ce93a6f14c9dde9be2497ef7fbd988cbe Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Thu, 1 Feb 2024 10:00:31 +0100 Subject: [PATCH 2/3] feat: handle default address management --- .../admin/create-customer-addresses.ts | 63 +++++++++++--- .../admin/update-customer-address.spec.ts | 85 +++++++++++++++++++ .../src/customer/steps/create-addresses.ts | 21 ++--- .../core-flows/src/customer/steps/index.ts | 2 + .../maybe-unset-default-billing-addresses.ts | 61 +++++++++++++ .../maybe-unset-default-shipping-addresses.ts | 60 +++++++++++++ .../src/customer/steps/utils/index.ts | 2 + .../steps/utils/unset-address-for-create.ts | 33 +++++++ .../steps/utils/unset-address-for-update.ts | 40 +++++++++ .../customer/workflows/create-addresses.ts | 22 ++++- .../customer/workflows/update-addresses.ts | 22 ++++- .../services/customer-module/index.spec.ts | 4 +- .../customer/src/services/customer-module.ts | 43 ++++++---- 13 files changed, 408 insertions(+), 50 deletions(-) create mode 100644 packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts create mode 100644 packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts create mode 100644 packages/core-flows/src/customer/steps/utils/index.ts create mode 100644 packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts create mode 100644 packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts diff --git a/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts b/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts index baecbb301b804..ed58e4312045c 100644 --- a/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts +++ b/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts @@ -79,8 +79,7 @@ describe("POST /admin/customers/:id/addresses", () => { expect(customerWithAddresses.addresses?.length).toEqual(1) }) - it.only("customer's cannot have two default shipping addresses", async () => { - // Create a customer + it("sets new shipping address as default and unsets the old one", async () => { const customer = await customerModuleService.create({ first_name: "John", last_name: "Doe", @@ -95,22 +94,60 @@ describe("POST /admin/customers/:id/addresses", () => { }) const api = useApi() as any - const response = await api - .post( - `/admin/customers/${customer.id}/addresses`, + const response = await api.post( + `/admin/customers/${customer.id}/addresses`, + { + first_name: "John", + last_name: "Doe", + address_1: "Test street 2", + is_default_shipping: true, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + + const [address] = await customerModuleService.listAddresses({ + customer_id: customer.id, + is_default_shipping: true, + }) + + expect(address.address_1).toEqual("Test street 2") + }) + + it("sets new billing address as default and unsets the old one", async () => { + const customer = await customerModuleService.create({ + first_name: "John", + last_name: "Doe", + addresses: [ { first_name: "John", last_name: "Doe", - address_1: "Test street 2", - is_default_shipping: true, + address_1: "Test street 1", + is_default_billing: true, }, - adminHeaders - ) - .catch((err) => err.response) + ], + }) - expect(response.status).toEqual(422) - expect(response.data.message).toEqual( - "Customer already has a default shipping address" + 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 2", + is_default_billing: true, + }, + adminHeaders ) + + expect(response.status).toEqual(200) + + const [address] = await customerModuleService.listAddresses({ + customer_id: customer.id, + is_default_billing: true, + }) + + expect(address.address_1).toEqual("Test street 2") }) }) 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 index 4ce6fac25d715..9fb91ddaaca7b 100644 --- a/integration-tests/plugins/__tests__/customer/admin/update-customer-address.spec.ts +++ b/integration-tests/plugins/__tests__/customer/admin/update-customer-address.spec.ts @@ -74,4 +74,89 @@ describe("POST /admin/customers/:id/addresses/:address_id", () => { }) ) }) + + it("updates a new address to be default and unsets old one", 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", + is_default_shipping: true, + }, + { + customer_id: customer.id, + first_name: "John", + last_name: "Doe", + address_1: "Test street 2", + }, + ]) + + const api = useApi() as any + const response = await api.post( + `/admin/customers/${customer.id}/addresses/${address.id}`, + { + first_name: "jane", + is_default_shipping: true, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + + const [defaultAddress] = await customerModuleService.listAddresses({ + customer_id: customer.id, + is_default_shipping: true, + }) + + expect(defaultAddress.first_name).toEqual("jane") + expect(defaultAddress.address_1).toEqual("Test street 2") + }) + + // do the same as above but for billing address + it("updates a new address to be default and unsets old one", 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", + is_default_billing: true, + }, + { + customer_id: customer.id, + first_name: "John", + last_name: "Doe", + address_1: "Test street 2", + }, + ]) + + const api = useApi() as any + const response = await api.post( + `/admin/customers/${customer.id}/addresses/${address.id}`, + { + first_name: "jane", + is_default_billing: true, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + + const [defaultAddress] = await customerModuleService.listAddresses({ + customer_id: customer.id, + is_default_billing: true, + }) + + expect(defaultAddress.first_name).toEqual("jane") + expect(defaultAddress.address_1).toEqual("Test street 2") + }) }) diff --git a/packages/core-flows/src/customer/steps/create-addresses.ts b/packages/core-flows/src/customer/steps/create-addresses.ts index c0127d9ec3c28..139aebf0d248a 100644 --- a/packages/core-flows/src/customer/steps/create-addresses.ts +++ b/packages/core-flows/src/customer/steps/create-addresses.ts @@ -13,23 +13,12 @@ export const createCustomerAddressesStep = createStep( ModuleRegistrationName.CUSTOMER ) - try { - const addresses = await service.addAddresses(data) + const addresses = await service.addAddresses(data) - return new StepResponse( - addresses, - addresses.map((address) => address.id) - ) - } catch (error) { - console.log("things went wrong") - console.log(error) - console.log(error.message) - console.log(error.stack) - console.log(error.code) - console.log(error.constraint) - console.log(Object.keys(error)) - throw error - } + return new StepResponse( + addresses, + addresses.map((address) => address.id) + ) }, async (ids, { container }) => { if (!ids?.length) { diff --git a/packages/core-flows/src/customer/steps/index.ts b/packages/core-flows/src/customer/steps/index.ts index cf2fe8e9b0cac..00a01f8281ffc 100644 --- a/packages/core-flows/src/customer/steps/index.ts +++ b/packages/core-flows/src/customer/steps/index.ts @@ -4,3 +4,5 @@ export * from "./delete-customers" export * from "./create-addresses" export * from "./update-addresses" export * from "./delete-addresses" +export * from "./maybe-unset-default-billing-addresses" +export * from "./maybe-unset-default-shipping-addresses" diff --git a/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts b/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts new file mode 100644 index 0000000000000..50a2b36aca19d --- /dev/null +++ b/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts @@ -0,0 +1,61 @@ +import { + ICustomerModuleService, + CreateCustomerAddressDTO, + FilterableCustomerAddressProps, + CustomerAddressDTO, +} from "@medusajs/types" +import { createStep } from "@medusajs/workflows-sdk" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { unsetForUpdate, unsetForCreate } from "./utils" +import { isDefined } from "@medusajs/utils" + +type StepInput = { + create?: CreateCustomerAddressDTO[] + update?: { + selector: FilterableCustomerAddressProps + update: Partial + } +} + +export const maybeUnsetDefaultBillingAddressesStepId = + "maybe-unset-default-billing-customer-addresses" +export const maybeUnsetDefaultBillingAddressesStep = createStep( + maybeUnsetDefaultBillingAddressesStepId, + async (data: StepInput, { container }) => { + const customerModuleService = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + if (isDefined(data.create)) { + return unsetForCreate( + data.create, + customerModuleService, + "is_default_billing" + ) + } + + if (isDefined(data.update)) { + return unsetForUpdate( + data.update, + customerModuleService, + "is_default_billing" + ) + } + + throw new Error("Invalid step input") + }, + async (addressesToSet, { container }) => { + if (!addressesToSet?.length) { + return + } + + const customerModuleService = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + await customerModuleService.updateAddress( + { id: addressesToSet }, + { is_default_billing: true } + ) + } +) diff --git a/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts b/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts new file mode 100644 index 0000000000000..7ffbaf41b0e16 --- /dev/null +++ b/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts @@ -0,0 +1,60 @@ +import { + ICustomerModuleService, + CreateCustomerAddressDTO, + FilterableCustomerAddressProps, + CustomerAddressDTO, +} from "@medusajs/types" +import { createStep } from "@medusajs/workflows-sdk" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { unsetForUpdate, unsetForCreate } from "./utils" +import { isDefined } from "@medusajs/utils" + +type StepInput = { + create?: CreateCustomerAddressDTO[] + update?: { + selector: FilterableCustomerAddressProps + update: Partial + } +} + +export const maybeUnsetDefaultShippingAddressesStepId = + "maybe-unset-default-shipping-customer-addresses" +export const maybeUnsetDefaultShippingAddressesStep = createStep( + maybeUnsetDefaultShippingAddressesStepId, + async (data: StepInput, { container }) => { + const customerModuleService = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + if (isDefined(data.create)) { + return unsetForCreate( + data.create, + customerModuleService, + "is_default_shipping" + ) + } + + if (isDefined(data.update)) { + return unsetForUpdate( + data.update, + customerModuleService, + "is_default_shipping" + ) + } + + throw new Error("Invalid step input") + }, + async (addressesToSet, { container }) => { + if (!addressesToSet?.length) { + return + } + + const customerModuleService = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + await customerModuleService.updateAddress( + { id: addressesToSet }, + { is_default_shipping: true } + ) + } +) diff --git a/packages/core-flows/src/customer/steps/utils/index.ts b/packages/core-flows/src/customer/steps/utils/index.ts new file mode 100644 index 0000000000000..933792bc53256 --- /dev/null +++ b/packages/core-flows/src/customer/steps/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./unset-address-for-create" +export * from "./unset-address-for-update" diff --git a/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts b/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts new file mode 100644 index 0000000000000..25c6e15df2479 --- /dev/null +++ b/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts @@ -0,0 +1,33 @@ +import { + CreateCustomerAddressDTO, + ICustomerModuleService, +} from "@medusajs/types" +import { StepResponse } from "@medusajs/workflows-sdk" + +export const unsetForCreate = async ( + data: CreateCustomerAddressDTO[], + customerService: ICustomerModuleService, + field: "is_default_billing" | "is_default_shipping" +) => { + const customerIds = data.reduce((acc, curr) => { + if (curr[field]) { + acc.push(curr.customer_id) + } + return acc + }, []) + + const customerDefaultAddresses = await customerService.listAddresses({ + customer_id: customerIds, + [field]: true, + }) + + await customerService.updateAddress( + { customer_id: customerIds, [field]: true }, + { [field]: false } + ) + + return new StepResponse( + void 0, + customerDefaultAddresses.map((address) => address.id) + ) +} diff --git a/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts b/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts new file mode 100644 index 0000000000000..aa12bb2122f2b --- /dev/null +++ b/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts @@ -0,0 +1,40 @@ +import { + CustomerAddressDTO, + FilterableCustomerAddressProps, + ICustomerModuleService, +} from "@medusajs/types" +import { StepResponse } from "@medusajs/workflows-sdk" + +export const unsetForUpdate = async ( + data: { + selector: FilterableCustomerAddressProps + update: Partial + }, + customerService: ICustomerModuleService, + field: "is_default_billing" | "is_default_shipping" +) => { + if (!data.update[field]) { + return new StepResponse(void 0) + } + + const affectedCustomers = await customerService.listAddresses(data.selector, { + select: ["id", "customer_id"], + }) + + const customerIds = affectedCustomers.map((address) => address.customer_id) + + const customerDefaultAddresses = await customerService.listAddresses({ + customer_id: customerIds, + [field]: true, + }) + + await customerService.updateAddress( + { customer_id: customerIds, [field]: true }, + { [field]: false } + ) + + return new StepResponse( + void 0, + customerDefaultAddresses.map((address) => address.id) + ) +} diff --git a/packages/core-flows/src/customer/workflows/create-addresses.ts b/packages/core-flows/src/customer/workflows/create-addresses.ts index 12bc2d2ff9db7..ea08335498aa0 100644 --- a/packages/core-flows/src/customer/workflows/create-addresses.ts +++ b/packages/core-flows/src/customer/workflows/create-addresses.ts @@ -1,6 +1,15 @@ import { CreateCustomerAddressDTO, CustomerAddressDTO } from "@medusajs/types" -import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { createCustomerAddressesStep } from "../steps" +import { + WorkflowData, + createWorkflow, + parallelize, + transform, +} from "@medusajs/workflows-sdk" +import { + createCustomerAddressesStep, + maybeUnsetDefaultBillingAddressesStep, + maybeUnsetDefaultShippingAddressesStep, +} from "../steps" type WorkflowInput = { addresses: CreateCustomerAddressDTO[] } @@ -8,6 +17,15 @@ export const createCustomerAddressesWorkflowId = "create-customer-addresses" export const createCustomerAddressesWorkflow = createWorkflow( createCustomerAddressesWorkflowId, (input: WorkflowData): WorkflowData => { + const unsetInput = transform(input, (data) => ({ + create: data.addresses, + })) + + parallelize( + maybeUnsetDefaultShippingAddressesStep(unsetInput), + maybeUnsetDefaultBillingAddressesStep(unsetInput) + ) + return createCustomerAddressesStep(input.addresses) } ) diff --git a/packages/core-flows/src/customer/workflows/update-addresses.ts b/packages/core-flows/src/customer/workflows/update-addresses.ts index fa8aff557c292..3d9015cb011f1 100644 --- a/packages/core-flows/src/customer/workflows/update-addresses.ts +++ b/packages/core-flows/src/customer/workflows/update-addresses.ts @@ -2,8 +2,17 @@ import { FilterableCustomerAddressProps, CustomerAddressDTO, } from "@medusajs/types" -import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { updateCustomerAddressesStep } from "../steps" +import { + WorkflowData, + createWorkflow, + parallelize, + transform, +} from "@medusajs/workflows-sdk" +import { + maybeUnsetDefaultBillingAddressesStep, + maybeUnsetDefaultShippingAddressesStep, + updateCustomerAddressesStep, +} from "../steps" type WorkflowInput = { selector: FilterableCustomerAddressProps @@ -14,6 +23,15 @@ export const updateCustomerAddressesWorkflowId = "update-customer-addresses" export const updateCustomerAddressesWorkflow = createWorkflow( updateCustomerAddressesWorkflowId, (input: WorkflowData): WorkflowData => { + const unsetInput = transform(input, (data) => ({ + update: data, + })) + + parallelize( + maybeUnsetDefaultShippingAddressesStep(unsetInput), + maybeUnsetDefaultBillingAddressesStep(unsetInput) + ) + 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 d896f615f4fee..8f8386e100ec9 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 @@ -631,7 +631,7 @@ describe("Customer Module Service", () => { ) }) - it.only("should only be possible to add one default shipping address per customer", async () => { + it("should only be possible to add one default shipping address per customer", async () => { const customer = await service.create({ first_name: "John", last_name: "Doe", @@ -696,7 +696,7 @@ describe("Customer Module Service", () => { country_code: "US", is_default_billing: true, }) - ).rejects.toThrow() + ).rejects.toThrow("A default billing address already exists") }) }) diff --git a/packages/customer/src/services/customer-module.ts b/packages/customer/src/services/customer-module.ts index 5e98201c4812f..19692da181a43 100644 --- a/packages/customer/src/services/customer-module.ts +++ b/packages/customer/src/services/customer-module.ts @@ -21,8 +21,13 @@ import { } from "@medusajs/utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import * as services from "../services" -import { Address } from "@models" import { MedusaError } from "@medusajs/utils" +import { EntityManager } from "@mikro-orm/core" + +const UNIQUE_CUSTOMER_SHIPPING_ADDRESS = + "IDX_customer_address_unique_customer_shipping" +const UNIQUE_CUSTOMER_BILLING_ADDRESS = + "IDX_customer_address_unique_customer_billing" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -454,36 +459,39 @@ export default class CustomerModuleService implements ICustomerModuleService { ): Promise< CustomerTypes.CustomerAddressDTO | CustomerTypes.CustomerAddressDTO[] > { - debugger try { const addresses = await this.addressService_.create( Array.isArray(data) ? data : [data], sharedContext ) + + await this.flush(sharedContext) + const serialized = await this.baseRepository_.serialize< CustomerTypes.CustomerAddressDTO[] >(addresses, { populate: true }) - console.log("all good") if (Array.isArray(data)) { return serialized } return serialized[0] } catch (err) { - console.log("hi") - console.log(err) - - console.log(err.message) - console.log(err.code) - console.log(Object.keys(err)) - if (isDuplicateError(err)) { - // Check if constriant is shipping - throw new MedusaError( - MedusaError.Types.DUPLICATE_ERROR, - "A default shipping address already exists." - ) + switch (err.constraint) { + case UNIQUE_CUSTOMER_SHIPPING_ADDRESS: + throw new MedusaError( + MedusaError.Types.DUPLICATE_ERROR, + "A default shipping address already exists" + ) + case UNIQUE_CUSTOMER_BILLING_ADDRESS: + throw new MedusaError( + MedusaError.Types.DUPLICATE_ERROR, + "A default billing address already exists" + ) + default: + break + } } throw err @@ -797,4 +805,9 @@ export default class CustomerModuleService implements ICustomerModuleService { ) : void 0 } + + private async flush(context: Context) { + const em = (context.manager ?? context.transactionManager) as EntityManager + await em.flush() + } } From 8372a59f5984a8b30f277cd49435d6b744417dd6 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Thu, 1 Feb 2024 11:19:08 +0100 Subject: [PATCH 3/3] fix: catch unique constraints on update --- .../services/customer-module/index.spec.ts | 54 +++++++++++++ .../customer/src/services/customer-module.ts | 80 +++++++++---------- 2 files changed, 93 insertions(+), 41 deletions(-) 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 8f8386e100ec9..26beb2c4abf66 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 @@ -100,6 +100,37 @@ describe("Customer Module Service", () => { ) }) + it("should fail to create two default shipping", async () => { + const customerData = { + company_name: "Acme Corp", + first_name: "John", + last_name: "Doe", + addresses: [ + { + address_1: "Testvej 1", + address_2: "Testvej 2", + city: "Testby", + country_code: "DK", + province: "Test", + postal_code: "8000", + phone: "123456789", + metadata: { membership: "gold" }, + is_default_shipping: true, + }, + { + address_1: "Test Ave 1", + address_2: "Test Ave 2", + city: "Testville", + country_code: "US", + is_default_shipping: true, + }, + ], + } + await expect(service.create(customerData)).rejects.toThrow( + "A default shipping address already exists" + ) + }) + it("should create multiple customers", async () => { const customersData = [ { @@ -813,6 +844,29 @@ describe("Customer Module Service", () => { ]) ) }) + + it("should fail when updating address to a default shipping address when one already exists", async () => { + const customer = await service.create({ + first_name: "John", + last_name: "Doe", + addresses: [ + { + address_name: "Home", + address_1: "123 Main St", + is_default_shipping: true, + }, + ], + }) + const address = await service.addAddresses({ + customer_id: customer.id, + address_name: "Work", + address_1: "456 Main St", + }) + + await expect( + service.updateAddress(address.id, { is_default_shipping: true }) + ).rejects.toThrow("A default shipping address already exists") + }) }) describe("listAddresses", () => { diff --git a/packages/customer/src/services/customer-module.ts b/packages/customer/src/services/customer-module.ts index 19692da181a43..7a41d481b84f9 100644 --- a/packages/customer/src/services/customer-module.ts +++ b/packages/customer/src/services/customer-module.ts @@ -104,15 +104,10 @@ export default class CustomerModuleService implements ICustomerModuleService { ) { const data = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray] - // 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) => { + const addressDataWithCustomerIds = data + .map(({ addresses }, i) => { if (!addresses) { return [] } @@ -124,7 +119,7 @@ export default class CustomerModuleService implements ICustomerModuleService { }) .flat() - await this.addressService_.create(addressDataWithCustomerIds, sharedContext) + await this.addAddresses(addressDataWithCustomerIds, sharedContext) const serialized = await this.baseRepository_.serialize< CustomerTypes.CustomerDTO[] @@ -459,43 +454,22 @@ export default class CustomerModuleService implements ICustomerModuleService { ): Promise< CustomerTypes.CustomerAddressDTO | CustomerTypes.CustomerAddressDTO[] > { - try { - const addresses = await this.addressService_.create( - Array.isArray(data) ? data : [data], - sharedContext - ) - - await this.flush(sharedContext) - - const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerAddressDTO[] - >(addresses, { populate: true }) + const addresses = await this.addressService_.create( + Array.isArray(data) ? data : [data], + sharedContext + ) - if (Array.isArray(data)) { - return serialized - } + await this.flush(sharedContext).catch(this.handleDbErrors) - return serialized[0] - } catch (err) { - if (isDuplicateError(err)) { - switch (err.constraint) { - case UNIQUE_CUSTOMER_SHIPPING_ADDRESS: - throw new MedusaError( - MedusaError.Types.DUPLICATE_ERROR, - "A default shipping address already exists" - ) - case UNIQUE_CUSTOMER_BILLING_ADDRESS: - throw new MedusaError( - MedusaError.Types.DUPLICATE_ERROR, - "A default billing address already exists" - ) - default: - break - } - } + const serialized = await this.baseRepository_.serialize< + CustomerTypes.CustomerAddressDTO[] + >(addresses, { populate: true }) - throw err + if (Array.isArray(data)) { + return serialized } + + return serialized[0] } async updateAddress( @@ -552,6 +526,9 @@ export default class CustomerModuleService implements ICustomerModuleService { updateData, sharedContext ) + + await this.flush(sharedContext).catch(this.handleDbErrors) + const serialized = await this.baseRepository_.serialize< CustomerTypes.CustomerAddressDTO[] >(addresses, { populate: true }) @@ -810,4 +787,25 @@ export default class CustomerModuleService implements ICustomerModuleService { const em = (context.manager ?? context.transactionManager) as EntityManager await em.flush() } + + private async handleDbErrors(err: any) { + if (isDuplicateError(err)) { + switch (err.constraint) { + case UNIQUE_CUSTOMER_SHIPPING_ADDRESS: + throw new MedusaError( + MedusaError.Types.DUPLICATE_ERROR, + "A default shipping address already exists" + ) + case UNIQUE_CUSTOMER_BILLING_ADDRESS: + throw new MedusaError( + MedusaError.Types.DUPLICATE_ERROR, + "A default billing address already exists" + ) + default: + break + } + } + + throw err + } }