diff --git a/.changeset/warm-mayflies-deny.md b/.changeset/warm-mayflies-deny.md new file mode 100644 index 0000000000..ebe6f937e8 --- /dev/null +++ b/.changeset/warm-mayflies-deny.md @@ -0,0 +1,6 @@ +--- +"@medusajs/customer": patch +"@medusajs/medusa": patch +--- + +feat(medusa, customer): Add list filtering capabilities for customers diff --git a/integration-tests/modules/__tests__/customer/admin/list-customers.spec.ts b/integration-tests/modules/__tests__/customer/admin/list-customers.spec.ts index c7c1279f90..e083750051 100644 --- a/integration-tests/modules/__tests__/customer/admin/list-customers.spec.ts +++ b/integration-tests/modules/__tests__/customer/admin/list-customers.spec.ts @@ -1,17 +1,19 @@ +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" + import { ICustomerModuleService } from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { createAdminUser } from "../../../../helpers/create-admin-user" -import { medusaIntegrationTestRunner } from "medusa-test-utils" + +const { medusaIntegrationTestRunner } = require("medusa-test-utils") jest.setTimeout(50000) -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - medusaIntegrationTestRunner({ - env, + env: { + MEDUSA_FF_MEDUSA_V2: true, + }, testSuite: ({ dbConnection, getContainer, api }) => { describe("GET /admin/customers", () => { let appContainer @@ -52,6 +54,45 @@ medusaIntegrationTestRunner({ ]) }) + it("should get all customers in specific customer group and its count", async () => { + const vipGroup = await customerModuleService.createCustomerGroup({ + name: "VIP", + }) + + const [john] = await customerModuleService.create([ + { + first_name: "John", + last_name: "Doe", + email: "john.doe@example.com", + }, + { + first_name: "Jane", + last_name: "Smith", + email: "jane.smith@example.com", + }, + ]) + + await customerModuleService.addCustomerToGroup({ + customer_id: john.id, + customer_group_id: vipGroup.id, + }) + + const response = await api.get( + `/admin/customers?limit=20&offset=0&groups%5B0%5D=${vipGroup.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.customers).toEqual([ + expect.objectContaining({ + first_name: "John", + last_name: "Doe", + email: "john.doe@example.com", + }), + ]) + }) + it("should filter customers by last name", async () => { await customerModuleService.create([ { diff --git a/packages/admin-next/dashboard/src/hooks/api/customer-groups.tsx b/packages/admin-next/dashboard/src/hooks/api/customer-groups.tsx new file mode 100644 index 0000000000..fcc1270865 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/api/customer-groups.tsx @@ -0,0 +1,53 @@ +import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query" +import { client } from "../../lib/client" +import { queryKeysFactory } from "../../lib/query-key-factory" +import { + AdminCustomerGroupResponse, + AdminCustomerGroupListResponse, +} from "@medusajs/types" + +const CUSTOMER_GROUPS_QUERY_KEY = "customer_groups" as const +const customerGroupsQueryKeys = queryKeysFactory(CUSTOMER_GROUPS_QUERY_KEY) + +export const useCustomerGroup = ( + id: string, + query?: Record, + options?: Omit< + UseQueryOptions< + AdminCustomerGroupResponse, + Error, + AdminCustomerGroupResponse, + QueryKey + >, + "queryFn" | "queryKey" + > +) => { + const { data, ...rest } = useQuery({ + queryKey: customerGroupsQueryKeys.detail(id), + queryFn: async () => client.customerGroups.retrieve(id, query), + ...options, + }) + + return { ...data, ...rest } +} + +export const useCustomerGroups = ( + query?: Record, + options?: Omit< + UseQueryOptions< + AdminCustomerGroupListResponse, + Error, + AdminCustomerGroupListResponse, + QueryKey + >, + "queryFn" | "queryKey" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => client.customerGroups.list(query), + queryKey: customerGroupsQueryKeys.list(query), + ...options, + }) + + return { ...data, ...rest } +} diff --git a/packages/admin-next/dashboard/src/hooks/api/customers.tsx b/packages/admin-next/dashboard/src/hooks/api/customers.tsx index ca7c2750c6..5afc93be5a 100644 --- a/packages/admin-next/dashboard/src/hooks/api/customers.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/customers.tsx @@ -9,7 +9,10 @@ import { client } from "../../lib/client" import { queryClient } from "../../lib/medusa" import { queryKeysFactory } from "../../lib/query-key-factory" import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads" -import { CustomerListRes, CustomerRes } from "../../types/api-responses" +import { + AdminCustomerResponse, + AdminCustomerListResponse, +} from "@medusajs/types" const CUSTOMERS_QUERY_KEY = "customers" as const const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY) @@ -18,7 +21,12 @@ export const useCustomer = ( id: string, query?: Record, options?: Omit< - UseQueryOptions, + UseQueryOptions< + AdminCustomerResponse, + Error, + AdminCustomerResponse, + QueryKey + >, "queryFn" | "queryKey" > ) => { @@ -34,7 +42,12 @@ export const useCustomer = ( export const useCustomers = ( query?: Record, options?: Omit< - UseQueryOptions, + UseQueryOptions< + AdminCustomerListResponse, + Error, + AdminCustomerListResponse, + QueryKey + >, "queryFn" | "queryKey" > ) => { @@ -48,7 +61,7 @@ export const useCustomers = ( } export const useCreateCustomer = ( - options?: UseMutationOptions + options?: UseMutationOptions ) => { return useMutation({ mutationFn: (payload) => client.customers.create(payload), @@ -62,7 +75,7 @@ export const useCreateCustomer = ( export const useUpdateCustomer = ( id: string, - options?: UseMutationOptions + options?: UseMutationOptions ) => { return useMutation({ mutationFn: (payload) => client.customers.update(id, payload), diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-customer-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-customer-table-filters.tsx index 0e4c8c0958..ca21dc2139 100644 --- a/packages/admin-next/dashboard/src/hooks/table/filters/use-customer-table-filters.tsx +++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-customer-table-filters.tsx @@ -1,6 +1,6 @@ -import { useAdminCustomerGroups } from "medusa-react" import { useTranslation } from "react-i18next" import { Filter } from "../../../components/table/data-table" +import { useCustomerGroups } from "../../api/customer-groups" const excludeableFields = ["groups"] as const @@ -11,7 +11,7 @@ export const useCustomerTableFilters = ( const isGroupsExcluded = exclude?.includes("groups") - const { customer_groups } = useAdminCustomerGroups( + const { customer_groups } = useCustomerGroups( { limit: 1000, expand: "", diff --git a/packages/admin-next/dashboard/src/lib/client/client.ts b/packages/admin-next/dashboard/src/lib/client/client.ts index c10ad98d07..12e5cd94c1 100644 --- a/packages/admin-next/dashboard/src/lib/client/client.ts +++ b/packages/admin-next/dashboard/src/lib/client/client.ts @@ -4,6 +4,7 @@ import { campaigns } from "./campaigns" import { categories } from "./categories" import { collections } from "./collections" import { currencies } from "./currencies" +import { customerGroups } from "./customer-groups" import { customers } from "./customers" import { invites } from "./invites" import { payments } from "./payments" @@ -25,6 +26,7 @@ export const client = { campaigns: campaigns, categories: categories, customers: customers, + customerGroups: customerGroups, currencies: currencies, collections: collections, promotions: promotions, diff --git a/packages/admin-next/dashboard/src/lib/client/customer-groups.ts b/packages/admin-next/dashboard/src/lib/client/customer-groups.ts new file mode 100644 index 0000000000..a73429121d --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/client/customer-groups.ts @@ -0,0 +1,24 @@ +import { + AdminCustomerGroupListResponse, + AdminCustomerGroupResponse, +} from "@medusajs/types" +import { getRequest } from "./common" + +async function retrieveCustomerGroup(id: string, query?: Record) { + return getRequest( + `/admin/customer-groups/${id}`, + query + ) +} + +async function listCustomerGroups(query?: Record) { + return getRequest( + `/admin/customer-groups`, + query + ) +} + +export const customerGroups = { + retrieve: retrieveCustomerGroup, + list: listCustomerGroups, +} diff --git a/packages/admin-next/dashboard/src/lib/client/customers.ts b/packages/admin-next/dashboard/src/lib/client/customers.ts index 027fb0f94d..555ad1b9dc 100644 --- a/packages/admin-next/dashboard/src/lib/client/customers.ts +++ b/packages/admin-next/dashboard/src/lib/client/customers.ts @@ -1,21 +1,24 @@ +import { + AdminCustomerListResponse, + AdminCustomerResponse, +} from "@medusajs/types" import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads" -import { CustomerListRes, CustomerRes } from "../../types/api-responses" import { getRequest, postRequest } from "./common" async function retrieveCustomer(id: string, query?: Record) { - return getRequest(`/admin/customers/${id}`, query) + return getRequest(`/admin/customers/${id}`, query) } async function listCustomers(query?: Record) { - return getRequest(`/admin/customers`, query) + return getRequest(`/admin/customers`, query) } async function createCustomer(payload: CreateCustomerReq) { - return postRequest(`/admin/customers`, payload) + return postRequest(`/admin/customers`, payload) } async function updateCustomer(id: string, payload: UpdateCustomerReq) { - return postRequest(`/admin/customers/${id}`, payload) + return postRequest(`/admin/customers/${id}`, payload) } export const customers = { diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx index 2d3106d977..b68e0e02b8 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx @@ -235,85 +235,6 @@ export const v1Routes: RouteObject[] = [ }, ], }, - { - path: "/customers", - handle: { - crumb: () => "Customers", - }, - children: [ - { - path: "", - lazy: () => import("../../routes/customers/customer-list"), - children: [ - { - path: "create", - lazy: () => - import("../../routes/customers/customer-create"), - }, - ], - }, - { - path: ":id", - lazy: () => import("../../routes/customers/customer-detail"), - handle: { - crumb: (data: AdminCustomersRes) => data.customer.email, - }, - children: [ - { - path: "edit", - lazy: () => import("../../routes/customers/customer-edit"), - }, - ], - }, - ], - }, - { - path: "/customer-groups", - handle: { - crumb: () => "Customer Groups", - }, - children: [ - { - path: "", - lazy: () => - import("../../routes/customer-groups/customer-group-list"), - children: [ - { - path: "create", - lazy: () => - import( - "../../routes/customer-groups/customer-group-create" - ), - }, - ], - }, - { - path: ":id", - lazy: () => - import("../../routes/customer-groups/customer-group-detail"), - handle: { - crumb: (data: AdminCustomerGroupsRes) => - data.customer_group.name, - }, - children: [ - { - path: "add-customers", - lazy: () => - import( - "../../routes/customer-groups/customer-group-add-customers" - ), - }, - { - path: "edit", - lazy: () => - import( - "../../routes/customer-groups/customer-group-edit" - ), - }, - ], - }, - ], - }, { path: "/gift-cards", handle: { diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx index 44c873b3e6..ea216cde9f 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx @@ -1,8 +1,12 @@ import { SalesChannelDTO, UserDTO } from "@medusajs/types" import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom" - import { Spinner } from "@medusajs/icons" -import { AdminCollectionsRes, AdminProductsRes } from "@medusajs/medusa" +import { + AdminCollectionsRes, + AdminProductsRes, + AdminPromotionRes, + AdminRegionsRes, +} from "@medusajs/medusa" import { ErrorBoundary } from "../../components/error/error-boundary" import { MainLayout } from "../../components/layout-v2/main-layout" import { SettingsLayout } from "../../components/layout/settings-layout" @@ -10,6 +14,7 @@ import { useMe } from "../../hooks/api/users" import { AdminApiKeyResponse } from "@medusajs/types" import { SearchProvider } from "../search-provider" import { SidebarProvider } from "../sidebar-provider" +import { AdminCustomersRes } from "@medusajs/client-types" export const ProtectedRoute = () => { const { user, isLoading } = useMe() @@ -232,6 +237,39 @@ export const v2Routes: RouteObject[] = [ }, ], }, + { + path: "/customers", + handle: { + crumb: () => "Customers", + }, + children: [ + { + path: "", + lazy: () => import("../../v2-routes/customers/customer-list"), + children: [ + { + path: "create", + lazy: () => + import("../../v2-routes/customers/customer-create"), + }, + ], + }, + { + path: ":id", + lazy: () => import("../../v2-routes/customers/customer-detail"), + handle: { + crumb: (data: AdminCustomersRes) => data.customer.email, + }, + children: [ + { + path: "edit", + lazy: () => + import("../../v2-routes/customers/customer-edit"), + }, + ], + }, + ], + }, ], }, ], diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx deleted file mode 100644 index db845763c0..0000000000 --- a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Customer, CustomerGroup } from "@medusajs/medusa" -import { Container, Heading } from "@medusajs/ui" -import { createColumnHelper } from "@tanstack/react-table" -import { useAdminCustomerGroups } from "medusa-react" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" - -// TODO: Continue working on this when there is a natural way to get customer groups related to a customer. -type CustomerGroupSectionProps = { - customer: Customer -} - -export const CustomerGroupSection = ({ - customer, -}: CustomerGroupSectionProps) => { - const { customer_groups, isLoading, isError, error } = useAdminCustomerGroups( - { - id: customer.groups.map((g) => g.id).join(","), - } - ) - - if (isError) { - throw error - } - - return ( - -
- Groups -
-
- ) -} - -const columnHelper = createColumnHelper() - -const useColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - columnHelper.display({ - id: "select", - }), - columnHelper.accessor("name", { - header: t("fields.name"), - cell: ({ getValue }) => getValue(), - }), - ], - [t] - ) -} diff --git a/packages/admin-next/dashboard/src/types/api-responses.ts b/packages/admin-next/dashboard/src/types/api-responses.ts index 56a91e2bbe..74548c9fa5 100644 --- a/packages/admin-next/dashboard/src/types/api-responses.ts +++ b/packages/admin-next/dashboard/src/types/api-responses.ts @@ -5,7 +5,6 @@ import { CampaignDTO, CurrencyDTO, - CustomerDTO, InviteDTO, PaymentProviderDTO, ProductCategoryDTO, @@ -39,10 +38,6 @@ type DeleteRes = { // Auth export type EmailPassRes = { token: string } -// Customers -export type CustomerRes = { customer: CustomerDTO } -export type CustomerListRes = { customers: CustomerDTO[] } & ListRes - // Promotions export type PromotionRes = { promotion: PromotionDTO } export type PromotionListRes = { promotions: PromotionDTO[] } & ListRes diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx similarity index 67% rename from packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx index d20f054a6d..45f4b1001a 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx @@ -1,53 +1,37 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { Button, Heading, Input, Text } from "@medusajs/ui" -import { useAdminCreateCustomer } from "medusa-react" -import { useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" import * as zod from "zod" - -import { Form } from "../../../../../components/common/form" +import { Button, Heading, Input, Text } from "@medusajs/ui" import { RouteFocusModal, useRouteModal, } from "../../../../../components/route-modal" +import { Form } from "../../../../../components/common/form" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { zodResolver } from "@hookform/resolvers/zod" +import { useCreateCustomer } from "../../../../../hooks/api/customers" -const CreateCustomerSchema = zod - .object({ - email: zod.string().email(), - first_name: zod.string().min(1), - last_name: zod.string().min(1), - phone: zod.string().min(1).optional(), - password: zod.string().min(8), - password_confirmation: zod.string().min(8), - }) - .superRefine(({ password, password_confirmation }, ctx) => { - if (password !== password_confirmation) { - return ctx.addIssue({ - code: zod.ZodIssueCode.custom, - message: "Passwords do not match", - path: ["password_confirmation"], - }) - } - }) +const CreateCustomerSchema = zod.object({ + email: zod.string().email(), + first_name: zod.string().min(1), + last_name: zod.string().min(1), + phone: zod.string().min(1).optional(), +}) export const CreateCustomerForm = () => { const { t } = useTranslation() const { handleSuccess } = useRouteModal() + const { mutateAsync, isLoading } = useCreateCustomer() + const form = useForm>({ defaultValues: { email: "", first_name: "", last_name: "", - phone: "", - password: "", - password_confirmation: "", }, resolver: zodResolver(CreateCustomerSchema), }) - const { mutateAsync, isLoading } = useAdminCreateCustomer() - const handleSubmit = form.handleSubmit(async (data) => { await mutateAsync( { @@ -55,7 +39,6 @@ export const CreateCustomerForm = () => { first_name: data.first_name, last_name: data.last_name, phone: data.phone, - password: data.password, }, { onSuccess: ({ customer }) => { @@ -164,46 +147,6 @@ export const CreateCustomerForm = () => { {t("customers.passwordHint")} -
- { - return ( - - {t("fields.password")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.confirmPassword")} - - - - - - ) - }} - /> -
diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-create/components/create-customer-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-create/components/create-customer-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/customer-create.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-create/customer-create.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-create/customer-create.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-create/customer-create.tsx diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-create/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-create/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-create/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx similarity index 94% rename from packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx index c5333c75cd..e01b816275 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx @@ -1,11 +1,11 @@ import { PencilSquare } from "@medusajs/icons" -import { Customer } from "@medusajs/medusa" import { Container, Heading, StatusBadge, Text } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" +import { AdminCustomerResponse } from "@medusajs/types" type CustomerGeneralSectionProps = { - customer: Customer + customer: AdminCustomerResponse["customer"] } export const CustomerGeneralSection = ({ diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-general-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-general-section/index.ts diff --git a/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx new file mode 100644 index 0000000000..57d27e77a5 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx @@ -0,0 +1,54 @@ +// TODO: Should be added with Customer Groups UI + +// import { Customer, CustomerGroup } from "@medusajs/medusa" +// import { Container, Heading } from "@medusajs/ui" +// import { createColumnHelper } from "@tanstack/react-table" +// import { useAdminCustomerGroups } from "medusa-react" +// import { useMemo } from "react" +// import { useTranslation } from "react-i18next" + +// // TODO: Continue working on this when there is a natural way to get customer groups related to a customer. +// type CustomerGroupSectionProps = { +// customer: Customer +// } + +// export const CustomerGroupSection = ({ +// customer, +// }: CustomerGroupSectionProps) => { +// const { customer_groups, isLoading, isError, error } = useAdminCustomerGroups( +// { +// id: customer.groups.map((g) => g.id).join(","), +// } +// ) + +// if (isError) { +// throw error +// } + +// return ( +// +//
+// Groups +//
+//
+// ) +// } + +// const columnHelper = createColumnHelper() + +// const useColumns = () => { +// const { t } = useTranslation() + +// return useMemo( +// () => [ +// columnHelper.display({ +// id: "select", +// }), +// columnHelper.accessor("name", { +// header: t("fields.name"), +// cell: ({ getValue }) => getValue(), +// }), +// ], +// [t] +// ) +// } diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-group-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-group-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-order-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/components/customer-order-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/customer-detail.tsx similarity index 76% rename from packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/customer-detail.tsx index a2e9fd338c..c8b3f655e4 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/customer-detail.tsx @@ -1,9 +1,8 @@ -import { useAdminCustomer } from "medusa-react" import { Outlet, json, useLoaderData, useParams } from "react-router-dom" -import { JsonViewSection } from "../../../components/common/json-view-section" import { CustomerGeneralSection } from "./components/customer-general-section" -import { CustomerOrderSection } from "./components/customer-order-section" +import { JsonViewSection } from "../../../components/common/json-view-section" import { customerLoader } from "./loader" +import { useCustomer } from "../../../hooks/api/customers" export const CustomerDetail = () => { const { id } = useParams() @@ -11,7 +10,7 @@ export const CustomerDetail = () => { const initialData = useLoaderData() as Awaited< ReturnType > - const { customer, isLoading, isError, error } = useAdminCustomer(id!, { + const { customer, isLoading, isError, error } = useCustomer(id!, undefined, { initialData, }) @@ -30,7 +29,9 @@ export const CustomerDetail = () => { return (
- + {/* + // TODO: re-add when order endpoints are added to api-v2 + */} {/* */} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-detail/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/loader.ts similarity index 80% rename from packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/loader.ts index 25d7f0c284..61febdc0ce 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts +++ b/packages/admin-next/dashboard/src/v2-routes/customers/customer-detail/loader.ts @@ -1,8 +1,7 @@ -import { AdminCustomersRes } from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" +import { AdminCustomerResponse } from "@medusajs/types" import { adminProductKeys } from "medusa-react" import { LoaderFunctionArgs } from "react-router-dom" - import { medusa, queryClient } from "../../../lib/medusa" const customerDetailQuery = (id: string) => ({ @@ -15,7 +14,7 @@ export const customerLoader = async ({ params }: LoaderFunctionArgs) => { const query = customerDetailQuery(id!) return ( - queryClient.getQueryData>(query.queryKey) ?? + queryClient.getQueryData>(query.queryKey) ?? (await queryClient.fetchQuery(query)) ) } diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx similarity index 94% rename from packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx index 690644fd18..3507eb8b9c 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx @@ -1,19 +1,18 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { Customer } from "@medusajs/medusa" -import { Button, Input } from "@medusajs/ui" -import { useAdminUpdateCustomer } from "medusa-react" -import { useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" import * as zod from "zod" - -import { Form } from "../../../../../components/common/form" +import { Button, Input } from "@medusajs/ui" import { RouteDrawer, useRouteModal, } from "../../../../../components/route-modal" +import { Form } from "../../../../../components/common/form" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { zodResolver } from "@hookform/resolvers/zod" +import { AdminCustomerResponse } from "@medusajs/types" +import { useUpdateCustomer } from "../../../../../hooks/api/customers" type EditCustomerFormProps = { - customer: Customer + customer: AdminCustomerResponse["customer"] } const EditCustomerSchema = zod.object({ @@ -37,7 +36,7 @@ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => { resolver: zodResolver(EditCustomerSchema), }) - const { mutateAsync, isLoading } = useAdminUpdateCustomer(customer.id) + const { mutateAsync, isLoading } = useUpdateCustomer(customer.id) const handleSubmit = form.handleSubmit(async (data) => { await mutateAsync( diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/components/edit-customer-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/components/edit-customer-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/customer-edit.tsx similarity index 83% rename from packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/customer-edit.tsx index 2fcf619a39..bbf51810a2 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/customer-edit.tsx @@ -1,15 +1,15 @@ import { Heading } from "@medusajs/ui" -import { useAdminCustomer } from "medusa-react" import { useTranslation } from "react-i18next" import { useParams } from "react-router-dom" import { RouteDrawer } from "../../../components/route-modal" import { EditCustomerForm } from "./components/edit-customer-form" +import { useCustomer } from "../../../hooks/api/customers" export const CustomerEdit = () => { const { t } = useTranslation() const { id } = useParams() - const { customer, isLoading, isError, error } = useAdminCustomer(id!) + const { customer, isLoading, isError, error } = useCustomer(id!) if (isError) { throw error diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-edit/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-edit/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx similarity index 85% rename from packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx index 0a08e4534d..cd49b58952 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx @@ -1,18 +1,17 @@ import { PencilSquare } from "@medusajs/icons" -import { Customer } from "@medusajs/medusa" import { Button, Container, Heading } from "@medusajs/ui" import { ColumnDef, createColumnHelper } from "@tanstack/react-table" -import { useAdminCustomers } from "medusa-react" +import { AdminCustomerResponse } from "@medusajs/types" import { useMemo } from "react" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" - import { ActionMenu } from "../../../../../components/common/action-menu" import { DataTable } from "../../../../../components/table/data-table" import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns" import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters" import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query" import { useDataTable } from "../../../../../hooks/use-data-table" +import { useCustomers } from "../../../../../hooks/api/customers" const PAGE_SIZE = 20 @@ -20,14 +19,9 @@ export const CustomerListTable = () => { const { t } = useTranslation() const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE }) - const { customers, count, isLoading, isError, error } = useAdminCustomers( - { - ...searchParams, - }, - { - keepPreviousData: true, - } - ) + const { customers, count, isLoading, isError, error } = useCustomers({ + ...searchParams, + }) const filters = useCustomerTableFilters() const columns = useColumns() @@ -78,7 +72,11 @@ export const CustomerListTable = () => { ) } -const CustomerActions = ({ customer }: { customer: Customer }) => { +const CustomerActions = ({ + customer, +}: { + customer: AdminCustomerResponse["customer"] +}) => { const { t } = useTranslation() return ( @@ -98,7 +96,7 @@ const CustomerActions = ({ customer }: { customer: Customer }) => { ) } -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper() const useColumns = () => { const columns = useCustomerTableColumns() @@ -112,5 +110,5 @@ const useColumns = () => { }), ], [columns] - ) as ColumnDef[] + ) as ColumnDef[] } diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-list/components/customer-list-table/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-list/components/customer-list-table/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/customer-list.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-list/customer-list.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-list/customer-list.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-list/customer-list.tsx diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-list/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-list/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-list/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/transfer-customer-order-ownership-form.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/transfer-customer-order-ownership-form.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/transfer-customer-order-ownership-form.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/transfer-customer-order-ownership-form.tsx diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-transfer-ownership/customer-transfer-ownership.tsx b/packages/admin-next/dashboard/src/v2-routes/customers/customer-transfer-ownership/customer-transfer-ownership.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-transfer-ownership/customer-transfer-ownership.tsx rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-transfer-ownership/customer-transfer-ownership.tsx diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-transfer-ownership/index.ts b/packages/admin-next/dashboard/src/v2-routes/customers/customer-transfer-ownership/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/customers/customer-transfer-ownership/index.ts rename to packages/admin-next/dashboard/src/v2-routes/customers/customer-transfer-ownership/index.ts diff --git a/packages/core-flows/src/customer/steps/delete-addresses.ts b/packages/core-flows/src/customer/steps/delete-addresses.ts index 46bfcab654..8f3654b978 100644 --- a/packages/core-flows/src/customer/steps/delete-addresses.ts +++ b/packages/core-flows/src/customer/steps/delete-addresses.ts @@ -1,6 +1,6 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" 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" diff --git a/packages/customer/src/loaders/container.ts b/packages/customer/src/loaders/container.ts index 9a0c5553b4..80023e35a0 100644 --- a/packages/customer/src/loaders/container.ts +++ b/packages/customer/src/loaders/container.ts @@ -1,9 +1,11 @@ -import { MikroOrmBaseRepository, ModulesSdkUtils } from "@medusajs/utils" import * as ModuleModels from "@models" +import * as CustomerRepositories from "@repositories" import * as ModuleServices from "@services" +import { ModulesSdkUtils } from "@medusajs/utils" + export default ModulesSdkUtils.moduleContainerLoaderFactory({ moduleModels: ModuleModels, moduleServices: ModuleServices, - moduleRepositories: { BaseRepository: MikroOrmBaseRepository }, + moduleRepositories: CustomerRepositories, }) diff --git a/packages/customer/src/models/customer.ts b/packages/customer/src/models/customer.ts index e9489c62de..06e64b77fd 100644 --- a/packages/customer/src/models/customer.ts +++ b/packages/customer/src/models/customer.ts @@ -1,5 +1,5 @@ import { DAL } from "@medusajs/types" -import { DALUtils, generateEntityId } from "@medusajs/utils" +import { DALUtils, Searchable, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Cascade, @@ -7,15 +7,15 @@ import { Entity, Filter, ManyToMany, - OneToMany, OnInit, + OneToMany, OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" +import Address from "./address" import CustomerGroup from "./customer-group" import CustomerGroupCustomer from "./customer-group-customer" -import Address from "./address" type OptionalCustomerProps = | "groups" @@ -30,18 +30,23 @@ export default class Customer { @PrimaryKey({ columnType: "text" }) id: string + @Searchable() @Property({ columnType: "text", nullable: true }) company_name: string | null = null + @Searchable() @Property({ columnType: "text", nullable: true }) first_name: string | null = null + @Searchable() @Property({ columnType: "text", nullable: true }) last_name: string | null = null + @Searchable() @Property({ columnType: "text", nullable: true }) email: string | null = null + @Searchable() @Property({ columnType: "text", nullable: true }) phone: string | null = null diff --git a/packages/customer/src/repositories/index.ts b/packages/customer/src/repositories/index.ts new file mode 100644 index 0000000000..147c9cc259 --- /dev/null +++ b/packages/customer/src/repositories/index.ts @@ -0,0 +1 @@ +export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" 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 index 43aeffb596..634ffb3219 100644 --- a/packages/medusa/src/api-v2/admin/customers/[id]/addresses/route.ts +++ b/packages/medusa/src/api-v2/admin/customers/[id]/addresses/route.ts @@ -1,14 +1,13 @@ -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "../../../../../types/routing" +import { createCustomerAddressesWorkflow } from "@medusajs/core-flows" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { CreateCustomerAddressDTO, ICustomerModuleService, } from "@medusajs/types" - -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { createCustomerAddressesWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../types/routing" export const GET = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/medusa/src/api-v2/admin/customers/[id]/route.ts b/packages/medusa/src/api-v2/admin/customers/[id]/route.ts index b2255116fe..d23787cd27 100644 --- a/packages/medusa/src/api-v2/admin/customers/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/customers/[id]/route.ts @@ -1,21 +1,21 @@ -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "../../../../types/routing" -import { - CustomerUpdatableFields, - ICustomerModuleService, -} from "@medusajs/types" import { deleteCustomersWorkflow, updateCustomersWorkflow, } from "@medusajs/core-flows" - import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + AdminCustomerResponse, + CustomerUpdatableFields, + ICustomerModuleService, +} from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" export const GET = async ( req: AuthenticatedMedusaRequest, - res: MedusaResponse + res: MedusaResponse ) => { const customerModuleService = req.scope.resolve( ModuleRegistrationName.CUSTOMER @@ -26,12 +26,12 @@ export const GET = async ( relations: req.retrieveConfig.relations, }) - res.status(200).json({ customer }) + res.status(200).json({ customer: customer as AdminCustomerResponse["customer"] }) } export const POST = async ( req: AuthenticatedMedusaRequest, - res: MedusaResponse + res: MedusaResponse ) => { const updateCustomers = updateCustomersWorkflow(req.scope) const { result, errors } = await updateCustomers.run({ @@ -46,7 +46,7 @@ export const POST = async ( throw errors[0].error } - res.status(200).json({ customer: result[0] }) + res.status(200).json({ customer: result[0] as AdminCustomerResponse["customer"] }) } export const DELETE = async ( diff --git a/packages/medusa/src/api-v2/admin/customers/middlewares.ts b/packages/medusa/src/api-v2/admin/customers/middlewares.ts index 0c8da32caf..4001202fe7 100644 --- a/packages/medusa/src/api-v2/admin/customers/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/customers/middlewares.ts @@ -1,18 +1,19 @@ import * as QueryConfig from "./query-config" import { - AdminGetCustomersCustomerAddressesParams, - AdminGetCustomersCustomerParams, - AdminGetCustomersParams, - AdminPostCustomersCustomerAddressesAddressReq, - AdminPostCustomersCustomerAddressesReq, - AdminPostCustomersCustomerReq, - AdminPostCustomersReq, + AdminCreateCustomer, + AdminCreateCustomerAddress, + AdminCustomerAdressesParams, + AdminCustomerParams, + AdminCustomersParams, + AdminUpdateCustomer, + AdminUpdateCustomerAddress, } from "./validators" -import { transformBody, transformQuery } from "../../../api/middlewares" import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { authenticate } from "../../../utils/authenticate-middleware" +import { validateAndTransformBody } from "../../utils/validate-body" +import { validateAndTransformQuery } from "../../utils/validate-query" export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [ { @@ -24,8 +25,8 @@ export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [ method: ["GET"], matcher: "/admin/customers", middlewares: [ - transformQuery( - AdminGetCustomersParams, + validateAndTransformQuery( + AdminCustomersParams, QueryConfig.listTransformQueryConfig ), ], @@ -33,39 +34,51 @@ export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [ { method: ["POST"], matcher: "/admin/customers", - middlewares: [transformBody(AdminPostCustomersReq)], + middlewares: [ + validateAndTransformBody(AdminCreateCustomer), + validateAndTransformQuery( + AdminCustomerParams, + QueryConfig.listTransformQueryConfig + ), + ], }, { method: ["GET"], matcher: "/admin/customers/:id", middlewares: [ - transformQuery( - AdminGetCustomersCustomerParams, - QueryConfig.retrieveTransformQueryConfig + validateAndTransformQuery( + AdminCustomerParams, + QueryConfig.listTransformQueryConfig ), ], }, { method: ["POST"], matcher: "/admin/customers/:id", - middlewares: [transformBody(AdminPostCustomersCustomerReq)], + middlewares: [ + validateAndTransformBody(AdminUpdateCustomer), + validateAndTransformQuery( + AdminCustomerParams, + QueryConfig.listTransformQueryConfig + ), + ], }, { method: ["POST"], matcher: "/admin/customers/:id/addresses", - middlewares: [transformBody(AdminPostCustomersCustomerAddressesReq)], + middlewares: [validateAndTransformBody(AdminCreateCustomerAddress)], }, { method: ["POST"], matcher: "/admin/customers/:id/addresses/:address_id", - middlewares: [transformBody(AdminPostCustomersCustomerAddressesAddressReq)], + middlewares: [validateAndTransformBody(AdminUpdateCustomerAddress)], }, { method: ["GET"], matcher: "/admin/customers/:id/addresses", middlewares: [ - transformQuery( - AdminGetCustomersCustomerAddressesParams, + validateAndTransformQuery( + AdminCustomerAdressesParams, QueryConfig.listAddressesTransformQueryConfig ), ], diff --git a/packages/medusa/src/api-v2/admin/customers/route.ts b/packages/medusa/src/api-v2/admin/customers/route.ts index a8655b082c..fd522eafbc 100644 --- a/packages/medusa/src/api-v2/admin/customers/route.ts +++ b/packages/medusa/src/api-v2/admin/customers/route.ts @@ -1,56 +1,45 @@ +import { createCustomersWorkflow } from "@medusajs/core-flows" +import { + AdminCustomerListResponse, + AdminCustomerResponse, +} from "@medusajs/types" +import { remoteQueryObjectFromString } from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../types/routing" -import { CreateCustomerDTO, ICustomerModuleService } from "@medusajs/types" - -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { createCustomersWorkflow } from "@medusajs/core-flows" +import { AdminCreateCustomerType } from "./validators" export const GET = async ( req: AuthenticatedMedusaRequest, - res: MedusaResponse + res: MedusaResponse ) => { - const customerModuleService = req.scope.resolve( - ModuleRegistrationName.CUSTOMER - ) + const remoteQuery = req.scope.resolve("remoteQuery") - const [customers, count] = await customerModuleService.listAndCount( - req.filterableFields, - req.listConfig - ) + const variables = { + filters: req.filterableFields, + ...req.remoteQueryConfig.pagination, + } - const { offset, limit } = req.validatedQuery + const query = remoteQueryObjectFromString({ + entryPoint: "customers", + variables, + fields: req.remoteQueryConfig.fields, + }) - // TODO: Replace with remote query - //const remoteQuery = req.scope.resolve("remoteQuery") - - //const variables = { - // filters: req.filterableFields, - // order: req.listConfig.order, - // skip: req.listConfig.skip, - // take: req.listConfig.take, - //} - - //const query = remoteQueryObjectFromString({ - // entryPoint: "customer", - // variables, - // fields: [...req.listConfig.select!, ...req.listConfig.relations!], - //}) - - //const results = await remoteQuery(query) + const { rows: customers, metadata } = await remoteQuery(query) res.json({ - count, customers, - offset, - limit, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, }) } export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse + req: AuthenticatedMedusaRequest, + res: MedusaResponse ) => { const createCustomers = createCustomersWorkflow(req.scope) @@ -70,5 +59,5 @@ export const POST = async ( throw errors[0].error } - res.status(200).json({ customer: result[0] }) + res.status(200).json({ customer: result[0] as AdminCustomerResponse["customer"] }) } diff --git a/packages/medusa/src/api-v2/admin/customers/validators.ts b/packages/medusa/src/api-v2/admin/customers/validators.ts index 20a0264d32..bff38273ce 100644 --- a/packages/medusa/src/api-v2/admin/customers/validators.ts +++ b/packages/medusa/src/api-v2/admin/customers/validators.ts @@ -1,353 +1,107 @@ -import { OperatorMap } from "@medusajs/types" -import { Transform, Type } from "class-transformer" +import { z } from "zod" import { - IsBoolean, - IsNotEmpty, - IsOptional, - IsString, - ValidateNested, -} from "class-validator" -import { FindParams, extendedFindParamsMixin } from "../../../types/common" -import { OperatorMapValidator } from "../../../types/validators/operator-map" -import { IsType } from "../../../utils" + createFindParams, + createOperatorMap, + createSelectParams, +} from "../../utils/validators" -export class AdminGetCustomersCustomerParams extends FindParams {} +export const AdminCustomerParams = createSelectParams() +export const AdminCustomerGroupParams = createSelectParams() -export class AdminGetCustomersParams extends extendedFindParamsMixin({ - limit: 100, +export const AdminCustomerGroupInCustomerParams = z.object({ + id: z.union([z.string(), z.array(z.string())]).optional(), + name: z.union([z.string(), z.array(z.string())]).optional(), + created_at: createOperatorMap().optional(), + updated_at: createOperatorMap().optional(), + deleted_at: createOperatorMap().optional(), +}) + +export const AdminCustomersParams = createFindParams({ + limit: 50, offset: 0, -}) { - @IsOptional() - @IsString({ each: true }) - id?: string | string[] - - @IsOptional() - @IsType([String, [String], OperatorMapValidator]) - email?: string | string[] | OperatorMap - - @IsOptional() - @ValidateNested() - @Type(() => FilterableCustomerGroupPropsValidator) - groups?: FilterableCustomerGroupPropsValidator | string | string[] - - @IsOptional() - @IsString({ each: true }) - company_name?: string | string[] | OperatorMap | null - - @IsOptional() - @IsString({ each: true }) - first_name?: string | string[] | OperatorMap | null - - @IsOptional() - @IsType([String, [String], OperatorMapValidator]) - @Transform(({ value }) => (value === "null" ? null : value)) - last_name?: string | string[] | OperatorMap | null - - @IsOptional() - @IsString({ each: true }) - created_by?: string | string[] | null - - @IsOptional() - @ValidateNested() - @Type(() => OperatorMapValidator) - created_at?: OperatorMap - - @IsOptional() - @ValidateNested() - @Type(() => OperatorMapValidator) - updated_at?: OperatorMap - - // Additional filters from BaseFilterable - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => AdminGetCustomersParams) - $and?: AdminGetCustomersParams[] - - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => AdminGetCustomersParams) - $or?: AdminGetCustomersParams[] -} - -class FilterableCustomerGroupPropsValidator { - @IsOptional() - @IsString({ each: true }) - id?: string | string[] - - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => OperatorMapValidator) - name?: string | OperatorMap - - @IsOptional() - @IsString({ each: true }) - created_by?: string | string[] | null - - @IsOptional() - @ValidateNested() - @Type(() => OperatorMapValidator) - created_at?: OperatorMap - - @IsOptional() - @ValidateNested() - @Type(() => OperatorMapValidator) - updated_at?: OperatorMap -} - -export class AdminPostCustomersReq { - @IsNotEmpty() - @IsString() - @IsOptional() - company_name?: string - - @IsNotEmpty() - @IsString() - @IsOptional() - first_name?: string - - @IsNotEmpty() - @IsString() - @IsOptional() - last_name?: string - - @IsNotEmpty() - @IsString() - @IsOptional() - email?: string - - @IsNotEmpty() - @IsString() - @IsOptional() - phone?: string -} - -export class AdminPostCustomersCustomerReq { - @IsNotEmpty() - @IsString() - @IsOptional() - company_name?: string - - @IsNotEmpty() - @IsString() - @IsOptional() - first_name?: string - - @IsNotEmpty() - @IsString() - @IsOptional() - last_name?: string - - @IsNotEmpty() - @IsString() - @IsOptional() - email?: string - - @IsNotEmpty() - @IsString() - @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> -} +}).merge( + z.object({ + q: z.string().optional(), + id: z.union([z.string(), z.array(z.string())]).optional(), + email: z.union([z.string(), z.array(z.string())]).optional(), + groups: z.union([ + AdminCustomerGroupInCustomerParams, + z.string(), + z.array(z.string()), + ]).optional(), + company_name: z.union([z.string(), z.array(z.string())]).optional(), + first_name: z.union([z.string(), z.array(z.string())]).optional(), + last_name: z.union([z.string(), z.array(z.string())]).optional(), + created_by: z.union([z.string(), z.array(z.string())]).optional(), + created_at: createOperatorMap().optional().optional(), + updated_at: createOperatorMap().optional().optional(), + deleted_at: createOperatorMap().optional().optional(), + $and: z.lazy(() => AdminCustomersParams.array()).optional(), + $or: z.lazy(() => AdminCustomersParams.array()).optional(), + }) +) + +export const AdminCreateCustomer = z.object({ + company_name: z.string().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + email: z.string().optional(), + phone: z.string().optional(), +}) + +export const AdminUpdateCustomer = AdminCreateCustomer + +export const AdminCreateCustomerAddress = z.object({ + address_name: z.string().optional(), + is_default_shipping: z.boolean().optional(), + is_default_billing: z.boolean().optional(), + company: z.string().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + address_1: z.string().optional(), + address_2: z.string().optional(), + city: z.string().optional(), + country_code: z.string().optional(), + province: z.string().optional(), + postal_code: z.string().optional(), + phone: z.string().optional(), + metadata: z.record(z.unknown()).optional(), +}) + +export const AdminUpdateCustomerAddress = AdminCreateCustomerAddress + +export const AdminCustomerAdressesParams = createFindParams({ + offset: 0, + limit: 50, +}).merge( + z.object({ + address_name: z.union([z.string(), z.array(z.string())]).optional(), + is_default_shipping: z.boolean().optional(), + is_default_billing: z.boolean().optional(), + company: z.union([z.string(), z.array(z.string())]).optional(), + first_name: z.union([z.string(), z.array(z.string())]).optional(), + last_name: z.union([z.string(), z.array(z.string())]).optional(), + address_1: z.union([z.string(), z.array(z.string())]).optional(), + address_2: z.union([z.string(), z.array(z.string())]).optional(), + city: z.union([z.string(), z.array(z.string())]).optional(), + country_code: z.union([z.string(), z.array(z.string())]).optional(), + province: z.union([z.string(), z.array(z.string())]).optional(), + postal_code: z.union([z.string(), z.array(z.string())]).optional(), + phone: z.union([z.string(), z.array(z.string())]).optional(), + metadata: z.record(z.unknown()).optional(), + }) +) + +export type AdminCustomerParamsType = z.infer +export type AdminCustomerGroupParamsType = z.infer< + typeof AdminCustomerGroupParams +> +export type AdminCustomerGroupInCustomerParamsType = z.infer< + typeof AdminCustomerGroupInCustomerParams +> +export type AdminCustomersParamsType = z.infer +export type AdminCreateCustomerType = z.infer +export type AdminUpdateCustomerType = z.infer +export type AdminCreateCustomerAddressType = z.infer< + typeof AdminCreateCustomerAddress +> diff --git a/packages/medusa/src/api-v2/admin/products/route.ts b/packages/medusa/src/api-v2/admin/products/route.ts index f06f7af6e2..f598ff43be 100644 --- a/packages/medusa/src/api-v2/admin/products/route.ts +++ b/packages/medusa/src/api-v2/admin/products/route.ts @@ -1,8 +1,8 @@ import { createProductsWorkflow } from "@medusajs/core-flows" +import { CreateProductDTO } from "@medusajs/types" import { ContainerRegistrationKeys, - ProductStatus, - remoteQueryObjectFromString, + remoteQueryObjectFromString } from "@medusajs/utils" import { AuthenticatedMedusaRequest, @@ -17,7 +17,6 @@ import { AdminCreateProductType, AdminGetProductsParamsType, } from "./validators" -import { CreateProductDTO } from "@medusajs/types" export const GET = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/types/src/customer/common.ts b/packages/types/src/customer/common.ts index 202b6a37ab..d220dd75e7 100644 --- a/packages/types/src/customer/common.ts +++ b/packages/types/src/customer/common.ts @@ -449,75 +449,84 @@ export interface CustomerDTO { */ email: string + /** + * A flag indicating if customer has an account or not. + */ + has_account: boolean + /** * The associated default billing address's ID. */ - default_billing_address_id?: string | null + default_billing_address_id: string | null /** * The associated default shipping address's ID. */ - default_shipping_address_id?: string | null + default_shipping_address_id: string | null /** * The company name of the customer. */ - company_name?: string | null + company_name: string | null /** * The first name of the customer. */ - first_name?: string | null + first_name: string | null /** * The last name of the customer. */ - last_name?: string | null + last_name: string | null /** * The addresses of the customer. */ - addresses?: CustomerAddressDTO[] + addresses: CustomerAddressDTO[] /** * The phone of the customer. */ - phone?: string | null + phone: string | null /** * The groups of the customer. */ - groups?: { + groups: { /** * The ID of the group. */ id: string + /** + * The name of the group. + */ + name: string }[] /** * Holds custom data in key-value pairs. */ - metadata?: Record + metadata: Record /** * Who created the customer. */ - created_by?: string | null + created_by: string | null /** * The deletion date of the customer. */ - deleted_at?: Date | string | null + deleted_at: Date | string | null /** * The creation date of the customer. */ - created_at?: Date | string + created_at: Date | string /** * The update date of the customer. */ - updated_at?: Date | string + updated_at: Date | string } /** diff --git a/packages/types/src/customer/mutations.ts b/packages/types/src/customer/mutations.ts index 65ba9ce23e..a5fb70d6ed 100644 --- a/packages/types/src/customer/mutations.ts +++ b/packages/types/src/customer/mutations.ts @@ -90,7 +90,7 @@ export interface UpdateCustomerAddressDTO { /** * The address's name. */ - address_name?: string + address_name?: string | null /** * Whether the address is the default for shipping. @@ -105,62 +105,62 @@ export interface UpdateCustomerAddressDTO { /** * The associated customer's ID. */ - customer_id?: string + customer_id?: string | null /** * The company. */ - company?: string + company?: string | null /** * The first name. */ - first_name?: string + first_name?: string | null /** * The last name. */ - last_name?: string + last_name?: string | null /** * The address 1. */ - address_1?: string + address_1?: string | null /** * The address 2. */ - address_2?: string + address_2?: string | null /** * The city. */ - city?: string + city?: string | null /** * The country code. */ - country_code?: string + country_code?: string | null /** * The province. */ - province?: string + province?: string | null /** * The postal code. */ - postal_code?: string + postal_code?: string | null /** * The phone. */ - phone?: string + phone?: string | null /** * Holds custom data in key-value pairs. */ - metadata?: Record + metadata?: Record | null } /** diff --git a/packages/types/src/http/customer/admin/customer.ts b/packages/types/src/http/customer/admin/customer.ts new file mode 100644 index 0000000000..aa7dfb6719 --- /dev/null +++ b/packages/types/src/http/customer/admin/customer.ts @@ -0,0 +1,83 @@ +import { PaginatedResponse } from "../../../common" + +/** + * @experimental + */ +export interface CustomerGroupResponse { + id: string + name: string | null +} + +/** + * @experimental + */ +export interface CustomerAddressResponse { + id: string + address_name: string | null + is_default_shipping: boolean + is_default_billing: boolean + customer_id: string + company: string | null + first_name: string | null + last_name: string | null + address_1: string | null + address_2: string | null + city: string | null + country_code: string | null + province: string | null + postal_code: string | null + phone: string | null + metadata: Record | null + created_at: string + updated_at: string +} + +/** + * @experimental + */ +interface CustomerResponse { + id: string + email: string + default_billing_address_id: string | null + default_shipping_address_id: string | null + company_name: string | null + first_name: string | null + last_name: string | null + has_account: boolean + addresses: CustomerAddressResponse[] + phone?: string | null + groups?: CustomerGroupResponse[] + metadata?: Record + created_by?: string | null + deleted_at?: Date | string | null + created_at?: Date | string + updated_at?: Date | string +} + +/** + * @experimental + */ +export interface AdminCustomerResponse { + customer: CustomerResponse +} + +/** + * @experimental + */ +export interface AdminCustomerListResponse extends PaginatedResponse { + customers: CustomerResponse[] +} + +/** + * @experimental + */ +export interface AdminCustomerGroupResponse { + customer_group: CustomerGroupResponse +} + +/** + * @experimental + */ +export interface AdminCustomerGroupListResponse extends PaginatedResponse { + customer_groups: CustomerGroupResponse[] +} diff --git a/packages/types/src/http/customer/admin/index.ts b/packages/types/src/http/customer/admin/index.ts new file mode 100644 index 0000000000..a96a5c7747 --- /dev/null +++ b/packages/types/src/http/customer/admin/index.ts @@ -0,0 +1 @@ +export * from "./customer" diff --git a/packages/types/src/http/customer/index.ts b/packages/types/src/http/customer/index.ts new file mode 100644 index 0000000000..26b8eb9dad --- /dev/null +++ b/packages/types/src/http/customer/index.ts @@ -0,0 +1 @@ +export * from "./admin" diff --git a/packages/types/src/http/index.ts b/packages/types/src/http/index.ts index b012a3d07c..a1ae4fdc9c 100644 --- a/packages/types/src/http/index.ts +++ b/packages/types/src/http/index.ts @@ -1,4 +1,5 @@ export * from "./api-key" +export * from "./customer" export * from "./fulfillment" export * from "./pricing" export * from "./sales-channel"