feat(customer): manage default address selection (#6295)
**What** - Catches unique constraints on customer_id, is_default_billing/is_default_shipping and reformats - Adds an step to create and update of addresses that unsets the previous default shipping/billing address if necessary. - This creates a behavior in the API where you can always set an address to be default and it will automatically unset the previous one for you.
This commit is contained in:
1 parent
a28822e0d4
commit
a2bf6756ac
16 files changed
+506
-15
No files matched your search
@@ -80,4 +80,76 @@ describe("POST /admin/customers/:id/addresses", () => {
|
|||||||
|
|
||||||
expect(customerWithAddresses.addresses?.length).toEqual(1)
|
expect(customerWithAddresses.addresses?.length).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("sets new shipping 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 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
|
||||||
|
)
|
||||||
|
|
||||||
|
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 1",
|
||||||
|
is_default_billing: 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_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")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -76,4 +76,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")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -4,3 +4,5 @@ export * from "./delete-customers"
|
|||||||
export * from "./create-addresses"
|
export * from "./create-addresses"
|
||||||
export * from "./update-addresses"
|
export * from "./update-addresses"
|
||||||
export * from "./delete-addresses"
|
export * from "./delete-addresses"
|
||||||
|
export * from "./maybe-unset-default-billing-addresses"
|
||||||
|
export * from "./maybe-unset-default-shipping-addresses"
|
||||||
@@ -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<CustomerAddressDTO>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const maybeUnsetDefaultBillingAddressesStepId =
|
||||||
|
"maybe-unset-default-billing-customer-addresses"
|
||||||
|
export const maybeUnsetDefaultBillingAddressesStep = createStep(
|
||||||
|
maybeUnsetDefaultBillingAddressesStepId,
|
||||||
|
async (data: StepInput, { container }) => {
|
||||||
|
const customerModuleService = container.resolve<ICustomerModuleService>(
|
||||||
|
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<ICustomerModuleService>(
|
||||||
|
ModuleRegistrationName.CUSTOMER
|
||||||
|
)
|
||||||
|
|
||||||
|
await customerModuleService.updateAddress(
|
||||||
|
{ id: addressesToSet },
|
||||||
|
{ is_default_billing: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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<CustomerAddressDTO>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const maybeUnsetDefaultShippingAddressesStepId =
|
||||||
|
"maybe-unset-default-shipping-customer-addresses"
|
||||||
|
export const maybeUnsetDefaultShippingAddressesStep = createStep(
|
||||||
|
maybeUnsetDefaultShippingAddressesStepId,
|
||||||
|
async (data: StepInput, { container }) => {
|
||||||
|
const customerModuleService = container.resolve<ICustomerModuleService>(
|
||||||
|
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<ICustomerModuleService>(
|
||||||
|
ModuleRegistrationName.CUSTOMER
|
||||||
|
)
|
||||||
|
|
||||||
|
await customerModuleService.updateAddress(
|
||||||
|
{ id: addressesToSet },
|
||||||
|
{ is_default_shipping: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./unset-address-for-create"
|
||||||
|
export * from "./unset-address-for-update"
|
||||||
@@ -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<string[]>((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)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<CustomerAddressDTO>
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
import { CreateCustomerAddressDTO, CustomerAddressDTO } from "@medusajs/types"
|
import { CreateCustomerAddressDTO, CustomerAddressDTO } from "@medusajs/types"
|
||||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
import {
|
||||||
import { createCustomerAddressesStep } from "../steps"
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
parallelize,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import {
|
||||||
|
createCustomerAddressesStep,
|
||||||
|
maybeUnsetDefaultBillingAddressesStep,
|
||||||
|
maybeUnsetDefaultShippingAddressesStep,
|
||||||
|
} from "../steps"
|
||||||
|
|
||||||
type WorkflowInput = { addresses: CreateCustomerAddressDTO[] }
|
type WorkflowInput = { addresses: CreateCustomerAddressDTO[] }
|
||||||
|
|
||||||
@@ -8,6 +17,15 @@ export const createCustomerAddressesWorkflowId = "create-customer-addresses"
|
|||||||
export const createCustomerAddressesWorkflow = createWorkflow(
|
export const createCustomerAddressesWorkflow = createWorkflow(
|
||||||
createCustomerAddressesWorkflowId,
|
createCustomerAddressesWorkflowId,
|
||||||
(input: WorkflowData<WorkflowInput>): WorkflowData<CustomerAddressDTO[]> => {
|
(input: WorkflowData<WorkflowInput>): WorkflowData<CustomerAddressDTO[]> => {
|
||||||
|
const unsetInput = transform(input, (data) => ({
|
||||||
|
create: data.addresses,
|
||||||
|
}))
|
||||||
|
|
||||||
|
parallelize(
|
||||||
|
maybeUnsetDefaultShippingAddressesStep(unsetInput),
|
||||||
|
maybeUnsetDefaultBillingAddressesStep(unsetInput)
|
||||||
|
)
|
||||||
|
|
||||||
return createCustomerAddressesStep(input.addresses)
|
return createCustomerAddressesStep(input.addresses)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -2,8 +2,17 @@ import {
|
|||||||
FilterableCustomerAddressProps,
|
FilterableCustomerAddressProps,
|
||||||
CustomerAddressDTO,
|
CustomerAddressDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
import {
|
||||||
import { updateCustomerAddressesStep } from "../steps"
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
parallelize,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import {
|
||||||
|
maybeUnsetDefaultBillingAddressesStep,
|
||||||
|
maybeUnsetDefaultShippingAddressesStep,
|
||||||
|
updateCustomerAddressesStep,
|
||||||
|
} from "../steps"
|
||||||
|
|
||||||
type WorkflowInput = {
|
type WorkflowInput = {
|
||||||
selector: FilterableCustomerAddressProps
|
selector: FilterableCustomerAddressProps
|
||||||
@@ -14,6 +23,15 @@ export const updateCustomerAddressesWorkflowId = "update-customer-addresses"
|
|||||||
export const updateCustomerAddressesWorkflow = createWorkflow(
|
export const updateCustomerAddressesWorkflow = createWorkflow(
|
||||||
updateCustomerAddressesWorkflowId,
|
updateCustomerAddressesWorkflowId,
|
||||||
(input: WorkflowData<WorkflowInput>): WorkflowData<CustomerAddressDTO[]> => {
|
(input: WorkflowData<WorkflowInput>): WorkflowData<CustomerAddressDTO[]> => {
|
||||||
|
const unsetInput = transform(input, (data) => ({
|
||||||
|
update: data,
|
||||||
|
}))
|
||||||
|
|
||||||
|
parallelize(
|
||||||
|
maybeUnsetDefaultShippingAddressesStep(unsetInput),
|
||||||
|
maybeUnsetDefaultBillingAddressesStep(unsetInput)
|
||||||
|
)
|
||||||
|
|
||||||
return updateCustomerAddressesStep(input)
|
return updateCustomerAddressesStep(input)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
+56
-2
@@ -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 () => {
|
it("should create multiple customers", async () => {
|
||||||
const customersData = [
|
const customersData = [
|
||||||
{
|
{
|
||||||
@@ -662,7 +693,7 @@ describe("Customer Module Service", () => {
|
|||||||
country_code: "US",
|
country_code: "US",
|
||||||
is_default_shipping: true,
|
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 () => {
|
it("should only be possible to add one default billing address per customer", async () => {
|
||||||
@@ -696,7 +727,7 @@ describe("Customer Module Service", () => {
|
|||||||
country_code: "US",
|
country_code: "US",
|
||||||
is_default_billing: true,
|
is_default_billing: true,
|
||||||
})
|
})
|
||||||
).rejects.toThrow()
|
).rejects.toThrow("A default billing address already exists")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -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", () => {
|
describe("listAddresses", () => {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
CustomerTypes,
|
CustomerTypes,
|
||||||
SoftDeleteReturn,
|
SoftDeleteReturn,
|
||||||
RestoreReturn,
|
RestoreReturn,
|
||||||
CustomerUpdatableFields,
|
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,9 +17,17 @@ import {
|
|||||||
mapObjectTo,
|
mapObjectTo,
|
||||||
isString,
|
isString,
|
||||||
isObject,
|
isObject,
|
||||||
|
isDuplicateError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
||||||
import * as services from "../services"
|
import * as services from "../services"
|
||||||
|
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 = {
|
type InjectedDependencies = {
|
||||||
baseRepository: DAL.RepositoryService
|
baseRepository: DAL.RepositoryService
|
||||||
@@ -97,15 +104,10 @@ export default class CustomerModuleService implements ICustomerModuleService {
|
|||||||
) {
|
) {
|
||||||
const data = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray]
|
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)
|
const customers = await this.customerService_.create(data, sharedContext)
|
||||||
|
|
||||||
// decorate addresses with customer ids
|
const addressDataWithCustomerIds = data
|
||||||
// filter out addresses without data
|
.map(({ addresses }, i) => {
|
||||||
const addressDataWithCustomerIds = addressData
|
|
||||||
.map((addresses, i) => {
|
|
||||||
if (!addresses) {
|
if (!addresses) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -117,7 +119,7 @@ export default class CustomerModuleService implements ICustomerModuleService {
|
|||||||
})
|
})
|
||||||
.flat()
|
.flat()
|
||||||
|
|
||||||
await this.addressService_.create(addressDataWithCustomerIds, sharedContext)
|
await this.addAddresses(addressDataWithCustomerIds, sharedContext)
|
||||||
|
|
||||||
const serialized = await this.baseRepository_.serialize<
|
const serialized = await this.baseRepository_.serialize<
|
||||||
CustomerTypes.CustomerDTO[]
|
CustomerTypes.CustomerDTO[]
|
||||||
@@ -457,6 +459,8 @@ export default class CustomerModuleService implements ICustomerModuleService {
|
|||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await this.flush(sharedContext).catch(this.handleDbErrors)
|
||||||
|
|
||||||
const serialized = await this.baseRepository_.serialize<
|
const serialized = await this.baseRepository_.serialize<
|
||||||
CustomerTypes.CustomerAddressDTO[]
|
CustomerTypes.CustomerAddressDTO[]
|
||||||
>(addresses, { populate: true })
|
>(addresses, { populate: true })
|
||||||
@@ -522,6 +526,9 @@ export default class CustomerModuleService implements ICustomerModuleService {
|
|||||||
updateData,
|
updateData,
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await this.flush(sharedContext).catch(this.handleDbErrors)
|
||||||
|
|
||||||
const serialized = await this.baseRepository_.serialize<
|
const serialized = await this.baseRepository_.serialize<
|
||||||
CustomerTypes.CustomerAddressDTO[]
|
CustomerTypes.CustomerAddressDTO[]
|
||||||
>(addresses, { populate: true })
|
>(addresses, { populate: true })
|
||||||
@@ -775,4 +782,30 @@ export default class CustomerModuleService implements ICustomerModuleService {
|
|||||||
)
|
)
|
||||||
: void 0
|
: void 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async flush(context: Context) {
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./postgres-error"
|
||||||
|
export * from "./is-duplicate-error"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PostgresError } from "./postgres-error"
|
||||||
|
export const isDuplicateError = (err: Error & { code?: string }) => {
|
||||||
|
return err.code === PostgresError.DUPLICATE_ERROR
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export enum PostgresError {
|
||||||
|
DUPLICATE_ERROR = "23505",
|
||||||
|
FOREIGN_KEY_ERROR = "23503",
|
||||||
|
SERIALIZATION_FAILURE = "40001",
|
||||||
|
NULL_VIOLATION = "23502",
|
||||||
|
}
|
||||||
@@ -13,3 +13,4 @@ export * from "./promotion"
|
|||||||
export * from "./search"
|
export * from "./search"
|
||||||
export * from "./shipping"
|
export * from "./shipping"
|
||||||
export * from "./orchestration"
|
export * from "./orchestration"
|
||||||
|
export * from "./exceptions"
|
||||||
Reference in new issue
Block a user