feat: Admin V2 Customers (#6998)

This commit is contained in:
Oli Juhl
2024-04-07 21:38:50 +02:00
committed by GitHub
parent f132929c7e
commit 4f88743591
53 changed files with 619 additions and 724 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/customer": patch
"@medusajs/medusa": patch
---
feat(medusa, customer): Add list filtering capabilities for customers

View File

@@ -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([
{

View File

@@ -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<string, any>,
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<string, any>,
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 }
}

View File

@@ -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<string, any>,
options?: Omit<
UseQueryOptions<CustomerRes, Error, CustomerRes, QueryKey>,
UseQueryOptions<
AdminCustomerResponse,
Error,
AdminCustomerResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
@@ -34,7 +42,12 @@ export const useCustomer = (
export const useCustomers = (
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<CustomerListRes, Error, CustomerListRes, QueryKey>,
UseQueryOptions<
AdminCustomerListResponse,
Error,
AdminCustomerListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
@@ -48,7 +61,7 @@ export const useCustomers = (
}
export const useCreateCustomer = (
options?: UseMutationOptions<CustomerRes, Error, CreateCustomerReq>
options?: UseMutationOptions<AdminCustomerResponse, Error, CreateCustomerReq>
) => {
return useMutation({
mutationFn: (payload) => client.customers.create(payload),
@@ -62,7 +75,7 @@ export const useCreateCustomer = (
export const useUpdateCustomer = (
id: string,
options?: UseMutationOptions<CustomerRes, Error, UpdateCustomerReq>
options?: UseMutationOptions<AdminCustomerResponse, Error, UpdateCustomerReq>
) => {
return useMutation({
mutationFn: (payload) => client.customers.update(id, payload),

View File

@@ -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: "",

View File

@@ -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,

View File

@@ -0,0 +1,24 @@
import {
AdminCustomerGroupListResponse,
AdminCustomerGroupResponse,
} from "@medusajs/types"
import { getRequest } from "./common"
async function retrieveCustomerGroup(id: string, query?: Record<string, any>) {
return getRequest<AdminCustomerGroupResponse>(
`/admin/customer-groups/${id}`,
query
)
}
async function listCustomerGroups(query?: Record<string, any>) {
return getRequest<AdminCustomerGroupListResponse>(
`/admin/customer-groups`,
query
)
}
export const customerGroups = {
retrieve: retrieveCustomerGroup,
list: listCustomerGroups,
}

View File

@@ -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<string, any>) {
return getRequest<CustomerRes>(`/admin/customers/${id}`, query)
return getRequest<AdminCustomerResponse>(`/admin/customers/${id}`, query)
}
async function listCustomers(query?: Record<string, any>) {
return getRequest<CustomerListRes>(`/admin/customers`, query)
return getRequest<AdminCustomerListResponse>(`/admin/customers`, query)
}
async function createCustomer(payload: CreateCustomerReq) {
return postRequest<CustomerRes>(`/admin/customers`, payload)
return postRequest<AdminCustomerResponse>(`/admin/customers`, payload)
}
async function updateCustomer(id: string, payload: UpdateCustomerReq) {
return postRequest<CustomerRes>(`/admin/customers/${id}`, payload)
return postRequest<AdminCustomerResponse>(`/admin/customers/${id}`, payload)
}
export const customers = {

View File

@@ -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: {

View File

@@ -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"),
},
],
},
],
},
],
},
],

View File

@@ -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 (
<Container className="p-0 divide-y">
<div className="px-6 py-4">
<Heading level="h2">Groups</Heading>
</div>
</Container>
)
}
const columnHelper = createColumnHelper<CustomerGroup>()
const useColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.display({
id: "select",
}),
columnHelper.accessor("name", {
header: t("fields.name"),
cell: ({ getValue }) => getValue(),
}),
],
[t]
)
}

View File

@@ -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

View File

@@ -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<zod.infer<typeof CreateCustomerSchema>>({
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")}
</Text>
</div>
<div className="grid grid-cols-2 gap-4">
<Form.Field
control={form.control}
name="password"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.password")}</Form.Label>
<Form.Control>
<Input
autoComplete="off"
type="password"
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="password_confirmation"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.confirmPassword")}</Form.Label>
<Form.Control>
<Input
autoComplete="off"
type="password"
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
</div>
</RouteFocusModal.Body>

View File

@@ -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 = ({

View File

@@ -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 (
// <Container className="p-0 divide-y">
// <div className="px-6 py-4">
// <Heading level="h2">Groups</Heading>
// </div>
// </Container>
// )
// }
// const columnHelper = createColumnHelper<CustomerGroup>()
// const useColumns = () => {
// const { t } = useTranslation()
// return useMemo(
// () => [
// columnHelper.display({
// id: "select",
// }),
// columnHelper.accessor("name", {
// header: t("fields.name"),
// cell: ({ getValue }) => getValue(),
// }),
// ],
// [t]
// )
// }

View File

@@ -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<typeof customerLoader>
>
const { customer, isLoading, isError, error } = useAdminCustomer(id!, {
const { customer, isLoading, isError, error } = useCustomer(id!, undefined, {
initialData,
})
@@ -30,7 +29,9 @@ export const CustomerDetail = () => {
return (
<div className="flex flex-col gap-y-2">
<CustomerGeneralSection customer={customer} />
<CustomerOrderSection customer={customer} />
{/* <CustomerOrderSection customer={customer} />
// TODO: re-add when order endpoints are added to api-v2
*/}
{/* <CustomerGroupSection customer={customer} /> */}
<JsonViewSection data={customer} />
<Outlet />

View File

@@ -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<Response<AdminCustomersRes>>(query.queryKey) ??
queryClient.getQueryData<Response<AdminCustomerResponse>>(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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<Customer>()
const columnHelper = createColumnHelper<AdminCustomerResponse["customer"]>()
const useColumns = () => {
const columns = useCustomerTableColumns()
@@ -112,5 +110,5 @@ const useColumns = () => {
}),
],
[columns]
) as ColumnDef<Customer>[]
) as ColumnDef<AdminCustomerResponse["customer"]>[]
}

View File

@@ -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"

View File

@@ -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,
})

View File

@@ -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

View File

@@ -0,0 +1 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"

View File

@@ -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,

View File

@@ -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<AdminCustomerResponse>
) => {
const customerModuleService = req.scope.resolve<ICustomerModuleService>(
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<CustomerUpdatableFields>,
res: MedusaResponse
res: MedusaResponse<AdminCustomerResponse>
) => {
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 (

View File

@@ -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
),
],

View File

@@ -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<AdminCustomerListResponse>
) => {
const customerModuleService = req.scope.resolve<ICustomerModuleService>(
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<CreateCustomerDTO>,
res: MedusaResponse
req: AuthenticatedMedusaRequest<AdminCreateCustomerType>,
res: MedusaResponse<AdminCustomerResponse>
) => {
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"] })
}

View File

@@ -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<string>
@IsOptional()
@ValidateNested()
@Type(() => FilterableCustomerGroupPropsValidator)
groups?: FilterableCustomerGroupPropsValidator | string | string[]
@IsOptional()
@IsString({ each: true })
company_name?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
first_name?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsType([String, [String], OperatorMapValidator])
@Transform(({ value }) => (value === "null" ? null : value))
last_name?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
created_by?: string | string[] | null
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
created_at?: OperatorMap<string>
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
updated_at?: OperatorMap<string>
// 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<string>
@IsOptional()
@IsString({ each: true })
created_by?: string | string[] | null
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
created_at?: OperatorMap<string>
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
updated_at?: OperatorMap<string>
}
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<string, unknown>
}
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<string, unknown>
}
export class AdminGetCustomersCustomerAddressesParams extends extendedFindParamsMixin(
{
limit: 100,
offset: 0,
}
) {
@IsOptional()
@IsString({ each: true })
address_name?: string | string[] | OperatorMap<string>
@IsOptional()
@IsBoolean()
is_default_shipping?: boolean
@IsOptional()
@IsBoolean()
is_default_billing?: boolean
@IsOptional()
@IsString({ each: true })
company?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
first_name?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
last_name?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
address_1?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
address_2?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
city?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
country_code?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
province?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
postal_code?: string | string[] | OperatorMap<string> | null
@IsOptional()
@IsString({ each: true })
phone?: string | string[] | OperatorMap<string> | null
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
metadata?: OperatorMap<Record<string, unknown>>
}
}).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<typeof AdminCustomerParams>
export type AdminCustomerGroupParamsType = z.infer<
typeof AdminCustomerGroupParams
>
export type AdminCustomerGroupInCustomerParamsType = z.infer<
typeof AdminCustomerGroupInCustomerParams
>
export type AdminCustomersParamsType = z.infer<typeof AdminCustomersParams>
export type AdminCreateCustomerType = z.infer<typeof AdminCreateCustomer>
export type AdminUpdateCustomerType = z.infer<typeof AdminUpdateCustomer>
export type AdminCreateCustomerAddressType = z.infer<
typeof AdminCreateCustomerAddress
>

View File

@@ -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<AdminGetProductsParamsType>,

View File

@@ -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<string, unknown>
metadata: Record<string, unknown>
/**
* 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
}
/**

View File

@@ -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<string, unknown>
metadata?: Record<string, unknown> | null
}
/**

View File

@@ -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<string, unknown> | 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<string, unknown>
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[]
}

View File

@@ -0,0 +1 @@
export * from "./customer"

View File

@@ -0,0 +1 @@
export * from "./admin"

View File

@@ -1,4 +1,5 @@
export * from "./api-key"
export * from "./customer"
export * from "./fulfillment"
export * from "./pricing"
export * from "./sales-channel"