feat: customer bulk endpoint form managing customer groups (#9761)
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
@@ -354,6 +354,68 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /admin/customers/:id/customer-groups", () => {
|
||||
it("should batch add and remove customer to/from customer groups", async () => {
|
||||
const group1 = (
|
||||
await api.post(
|
||||
"/admin/customer-groups",
|
||||
{
|
||||
name: "VIP 1",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.customer_group
|
||||
|
||||
const group2 = (
|
||||
await api.post(
|
||||
"/admin/customer-groups",
|
||||
{
|
||||
name: "VIP 2",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.customer_group
|
||||
|
||||
const group3 = (
|
||||
await api.post(
|
||||
"/admin/customer-groups",
|
||||
{
|
||||
name: "VIP 3",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.customer_group
|
||||
|
||||
// Add with cg endpoint so we can test remove
|
||||
await api.post(
|
||||
`/admin/customer-groups/${group1.id}/customers`,
|
||||
{
|
||||
add: [customer1.id],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/customers/${customer1.id}/customer-groups?fields=groups.id`,
|
||||
{
|
||||
remove: [group1.id],
|
||||
add: [group2.id, group3.id],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
expect(response.data.customer.groups.length).toEqual(2)
|
||||
expect(response.data.customer.groups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: group2.id }),
|
||||
expect.objectContaining({ id: group3.id }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /admin/customers/:id", () => {
|
||||
it("should fetch a customer", async () => {
|
||||
const response = await api.get(
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { customerGroupsQueryKeys } from "./customer-groups"
|
||||
|
||||
const CUSTOMERS_QUERY_KEY = "customers" as const
|
||||
export const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
|
||||
@@ -115,3 +116,35 @@ export const useDeleteCustomer = (
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useBatchCustomerCustomerGroups = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminCustomerResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminBatchLink
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) =>
|
||||
sdk.admin.customer.batchCustomerGroups(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: customerGroupsQueryKeys.details(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: customerGroupsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: customersQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: customersQueryKeys.details(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -829,6 +829,12 @@
|
||||
"list": {
|
||||
"noRecordsMessage": "Please create a customer group first."
|
||||
}
|
||||
},
|
||||
"removed": {
|
||||
"success": "Customer removed from: {{groups}}.",
|
||||
"list": {
|
||||
"noRecordsMessage": "Please create a customer group first."
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table.tsx"
|
||||
import { sdk } from "../../../../../lib/client/index.ts"
|
||||
import { queryClient } from "../../../../../lib/query-client.ts"
|
||||
import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api"
|
||||
|
||||
type CustomerGroupSectionProps = {
|
||||
customer: HttpTypes.AdminCustomer
|
||||
@@ -57,6 +58,9 @@ export const CustomerGroupSection = ({
|
||||
}
|
||||
)
|
||||
|
||||
const { mutateAsync: batchCustomerCustomerGroups } =
|
||||
useBatchCustomerCustomerGroups(customer.id)
|
||||
|
||||
const filters = useCustomerGroupTableFilters()
|
||||
const columns = useColumns(customer.id)
|
||||
|
||||
@@ -94,20 +98,15 @@ export const CustomerGroupSection = ({
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* TODO: use this for now until add customer groups to customers batch is implemented
|
||||
*/
|
||||
const promises = customerGroupIds.map((id) =>
|
||||
sdk.admin.customerGroup.batchCustomers(id, {
|
||||
remove: [customer.id],
|
||||
await batchCustomerCustomerGroups({ remove: customerGroupIds })
|
||||
|
||||
toast.success(
|
||||
t("customers.groups.removed.success", {
|
||||
groups: customer_groups!
|
||||
.filter((cg) => customerGroupIds.includes(cg.id))
|
||||
.map((cg) => cg?.name),
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: customerGroupsQueryKeys.lists(),
|
||||
})
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
|
||||
@@ -17,16 +17,12 @@ import {
|
||||
} from "../../../../../components/modals"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||
import {
|
||||
customerGroupsQueryKeys,
|
||||
useCustomerGroups,
|
||||
} from "../../../../../hooks/api/customer-groups"
|
||||
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
|
||||
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
|
||||
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
|
||||
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { queryClient } from "../../../../../lib/query-client"
|
||||
import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api"
|
||||
|
||||
type AddCustomerGroupsFormProps = {
|
||||
customerId: string
|
||||
@@ -45,6 +41,9 @@ export const AddCustomerGroupsForm = ({
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const { mutateAsync: batchCustomerCustomerGroups } =
|
||||
useBatchCustomerCustomerGroups(customerId)
|
||||
|
||||
const form = useForm<zod.infer<typeof AddCustomerGroupsSchema>>({
|
||||
defaultValues: {
|
||||
customer_group_ids: [],
|
||||
@@ -117,16 +116,7 @@ export const AddCustomerGroupsForm = ({
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
setIsPending(true)
|
||||
try {
|
||||
/**
|
||||
* TODO: use this for now until add customer groups to customers batch is implemented
|
||||
*/
|
||||
const promises = data.customer_group_ids.map((id) =>
|
||||
sdk.admin.customerGroup.batchCustomers(id, {
|
||||
add: [customerId],
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.all(promises)
|
||||
await batchCustomerCustomerGroups({ add: data.customer_group_ids })
|
||||
|
||||
toast.success(
|
||||
t("customers.groups.add.success", {
|
||||
@@ -137,10 +127,6 @@ export const AddCustomerGroupsForm = ({
|
||||
})
|
||||
)
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: customerGroupsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
handleSuccess(`/customers/${customerId}`)
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./update-customer-groups"
|
||||
export * from "./delete-customer-groups"
|
||||
export * from "./create-customer-groups"
|
||||
export * from "./link-customers-customer-group"
|
||||
export * from "./link-customer-groups-customer"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
ICustomerModuleService,
|
||||
LinkWorkflowInput,
|
||||
} from "@medusajs/framework/types"
|
||||
import { Modules, promiseAll } from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
export const linkCustomerGroupsToCustomerStepId =
|
||||
"link-customers-to-customer-group"
|
||||
/**
|
||||
* This step creates one or more links between a customer and customer groups records.
|
||||
*/
|
||||
export const linkCustomerGroupsToCustomerStep = createStep(
|
||||
linkCustomerGroupsToCustomerStepId,
|
||||
async (data: LinkWorkflowInput, { container }) => {
|
||||
const service = container.resolve<ICustomerModuleService>(Modules.CUSTOMER)
|
||||
|
||||
const toAdd = (data.add ?? []).map((customerGroupId) => {
|
||||
return {
|
||||
customer_group_id: customerGroupId,
|
||||
customer_id: data.id,
|
||||
}
|
||||
})
|
||||
|
||||
const toRemove = (data.remove ?? []).map((customerGroupId) => {
|
||||
return {
|
||||
customer_group_id: customerGroupId,
|
||||
customer_id: data.id,
|
||||
}
|
||||
})
|
||||
|
||||
const promises: Promise<any>[] = []
|
||||
if (toAdd.length) {
|
||||
promises.push(service.addCustomerToGroup(toAdd))
|
||||
}
|
||||
if (toRemove.length) {
|
||||
promises.push(service.removeCustomerFromGroup(toRemove))
|
||||
}
|
||||
await promiseAll(promises)
|
||||
|
||||
return new StepResponse(void 0, { toAdd, toRemove })
|
||||
},
|
||||
async (prevData, { container }) => {
|
||||
if (!prevData) {
|
||||
return
|
||||
}
|
||||
const service = container.resolve<ICustomerModuleService>(Modules.CUSTOMER)
|
||||
|
||||
if (prevData.toAdd.length) {
|
||||
await service.removeCustomerFromGroup(prevData.toAdd)
|
||||
}
|
||||
if (prevData.toRemove.length) {
|
||||
await service.addCustomerToGroup(prevData.toRemove)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -2,3 +2,4 @@ export * from "./update-customer-groups"
|
||||
export * from "./delete-customer-groups"
|
||||
export * from "./create-customer-groups"
|
||||
export * from "./link-customers-customer-group"
|
||||
export * from "./link-customer-groups-customer"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { LinkWorkflowInput } from "@medusajs/framework/types"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/framework/workflows-sdk"
|
||||
import { linkCustomerGroupsToCustomerStep } from "../steps"
|
||||
|
||||
export const linkCustomerGroupsToCustomerWorkflowId =
|
||||
"link-customer-groups-to-customer"
|
||||
/**
|
||||
* This workflow creates one or more links between a customer and customer groups.
|
||||
*/
|
||||
export const linkCustomerGroupsToCustomerWorkflow = createWorkflow(
|
||||
linkCustomerGroupsToCustomerWorkflowId,
|
||||
(input: WorkflowData<LinkWorkflowInput>): WorkflowData<void> => {
|
||||
return linkCustomerGroupsToCustomerStep(input)
|
||||
}
|
||||
)
|
||||
@@ -210,4 +210,38 @@ export class Customer {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method manages customer groups for a customer.
|
||||
* It sends a request to the [Manage Customers](https://docs.medusajs.com/api/admin#customers_postcustomersidcustomergroups)
|
||||
* API route.
|
||||
*
|
||||
* @param id - The customer's ID.
|
||||
* @param body - The groups to add customer to or remove customer from.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The customers details.
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.customer.batchCustomerGroups("cus_123", {
|
||||
* add: ["cusgroup_123"],
|
||||
* remove: ["cusgroup_321"]
|
||||
* })
|
||||
* .then(({ customer }) => {
|
||||
* console.log(customer)
|
||||
* })
|
||||
*/
|
||||
async batchCustomerGroups(
|
||||
id: string,
|
||||
body: HttpTypes.AdminBatchLink,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminCustomerResponse>(
|
||||
`/admin/customers/${id}/customer-groups`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { linkCustomerGroupsToCustomerWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
import { HttpTypes, LinkMethodRequest } from "@medusajs/framework/types"
|
||||
|
||||
import { refetchCustomer } from "../../helpers"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<LinkMethodRequest>,
|
||||
res: MedusaResponse<HttpTypes.AdminCustomerResponse>
|
||||
) => {
|
||||
const { id } = req.params
|
||||
const { add, remove } = req.validatedBody
|
||||
|
||||
const workflow = linkCustomerGroupsToCustomerWorkflow(req.scope)
|
||||
await workflow.run({
|
||||
input: {
|
||||
id,
|
||||
add,
|
||||
remove,
|
||||
},
|
||||
})
|
||||
|
||||
const customer = await refetchCustomer(
|
||||
id,
|
||||
req.scope,
|
||||
req.remoteQueryConfig.fields
|
||||
)
|
||||
|
||||
res.status(200).json({ customer: customer })
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
validateAndTransformBody,
|
||||
validateAndTransformQuery,
|
||||
} from "@medusajs/framework"
|
||||
import { createLinkBody } from "../../utils/validators"
|
||||
|
||||
export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
@@ -101,4 +102,15 @@ export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/customers/:id/customer-groups",
|
||||
middlewares: [
|
||||
validateAndTransformBody(createLinkBody()),
|
||||
validateAndTransformQuery(
|
||||
AdminCustomerParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user