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", () => {
|
describe("GET /admin/customers/:id", () => {
|
||||||
it("should fetch a customer", async () => {
|
it("should fetch a customer", async () => {
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { sdk } from "../../lib/client"
|
import { sdk } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
import { customerGroupsQueryKeys } from "./customer-groups"
|
||||||
|
|
||||||
const CUSTOMERS_QUERY_KEY = "customers" as const
|
const CUSTOMERS_QUERY_KEY = "customers" as const
|
||||||
export const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
|
export const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
|
||||||
@@ -115,3 +116,35 @@ export const useDeleteCustomer = (
|
|||||||
...options,
|
...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": {
|
"list": {
|
||||||
"noRecordsMessage": "Please create a customer group first."
|
"noRecordsMessage": "Please create a customer group first."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"removed": {
|
||||||
|
"success": "Customer removed from: {{groups}}.",
|
||||||
|
"list": {
|
||||||
|
"noRecordsMessage": "Please create a customer group first."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
|
|||||||
+11
-12
@@ -28,6 +28,7 @@ import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use
|
|||||||
import { useDataTable } from "../../../../../hooks/use-data-table.tsx"
|
import { useDataTable } from "../../../../../hooks/use-data-table.tsx"
|
||||||
import { sdk } from "../../../../../lib/client/index.ts"
|
import { sdk } from "../../../../../lib/client/index.ts"
|
||||||
import { queryClient } from "../../../../../lib/query-client.ts"
|
import { queryClient } from "../../../../../lib/query-client.ts"
|
||||||
|
import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api"
|
||||||
|
|
||||||
type CustomerGroupSectionProps = {
|
type CustomerGroupSectionProps = {
|
||||||
customer: HttpTypes.AdminCustomer
|
customer: HttpTypes.AdminCustomer
|
||||||
@@ -57,6 +58,9 @@ export const CustomerGroupSection = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { mutateAsync: batchCustomerCustomerGroups } =
|
||||||
|
useBatchCustomerCustomerGroups(customer.id)
|
||||||
|
|
||||||
const filters = useCustomerGroupTableFilters()
|
const filters = useCustomerGroupTableFilters()
|
||||||
const columns = useColumns(customer.id)
|
const columns = useColumns(customer.id)
|
||||||
|
|
||||||
@@ -94,20 +98,15 @@ export const CustomerGroupSection = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/**
|
await batchCustomerCustomerGroups({ remove: customerGroupIds })
|
||||||
* TODO: use this for now until add customer groups to customers batch is implemented
|
|
||||||
*/
|
toast.success(
|
||||||
const promises = customerGroupIds.map((id) =>
|
t("customers.groups.removed.success", {
|
||||||
sdk.admin.customerGroup.batchCustomers(id, {
|
groups: customer_groups!
|
||||||
remove: [customer.id],
|
.filter((cg) => customerGroupIds.includes(cg.id))
|
||||||
|
.map((cg) => cg?.name),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: customerGroupsQueryKeys.lists(),
|
|
||||||
})
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-20
@@ -17,16 +17,12 @@ import {
|
|||||||
} from "../../../../../components/modals"
|
} from "../../../../../components/modals"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||||
import {
|
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
|
||||||
customerGroupsQueryKeys,
|
|
||||||
useCustomerGroups,
|
|
||||||
} from "../../../../../hooks/api/customer-groups"
|
|
||||||
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
|
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
|
||||||
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
|
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
|
||||||
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
|
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { sdk } from "../../../../../lib/client"
|
import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api"
|
||||||
import { queryClient } from "../../../../../lib/query-client"
|
|
||||||
|
|
||||||
type AddCustomerGroupsFormProps = {
|
type AddCustomerGroupsFormProps = {
|
||||||
customerId: string
|
customerId: string
|
||||||
@@ -45,6 +41,9 @@ export const AddCustomerGroupsForm = ({
|
|||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const [isPending, setIsPending] = useState(false)
|
const [isPending, setIsPending] = useState(false)
|
||||||
|
|
||||||
|
const { mutateAsync: batchCustomerCustomerGroups } =
|
||||||
|
useBatchCustomerCustomerGroups(customerId)
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof AddCustomerGroupsSchema>>({
|
const form = useForm<zod.infer<typeof AddCustomerGroupsSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
customer_group_ids: [],
|
customer_group_ids: [],
|
||||||
@@ -117,16 +116,7 @@ export const AddCustomerGroupsForm = ({
|
|||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
setIsPending(true)
|
setIsPending(true)
|
||||||
try {
|
try {
|
||||||
/**
|
await batchCustomerCustomerGroups({ add: data.customer_group_ids })
|
||||||
* 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)
|
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
t("customers.groups.add.success", {
|
t("customers.groups.add.success", {
|
||||||
@@ -137,10 +127,6 @@ export const AddCustomerGroupsForm = ({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: customerGroupsQueryKeys.lists(),
|
|
||||||
})
|
|
||||||
|
|
||||||
handleSuccess(`/customers/${customerId}`)
|
handleSuccess(`/customers/${customerId}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from "./update-customer-groups"
|
|||||||
export * from "./delete-customer-groups"
|
export * from "./delete-customer-groups"
|
||||||
export * from "./create-customer-groups"
|
export * from "./create-customer-groups"
|
||||||
export * from "./link-customers-customer-group"
|
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 "./delete-customer-groups"
|
||||||
export * from "./create-customer-groups"
|
export * from "./create-customer-groups"
|
||||||
export * from "./link-customers-customer-group"
|
export * from "./link-customers-customer-group"
|
||||||
|
export * from "./link-customer-groups-customer"
|
||||||
|
|||||||
+15
@@ -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,
|
validateAndTransformBody,
|
||||||
validateAndTransformQuery,
|
validateAndTransformQuery,
|
||||||
} from "@medusajs/framework"
|
} from "@medusajs/framework"
|
||||||
|
import { createLinkBody } from "../../utils/validators"
|
||||||
|
|
||||||
export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [
|
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