feat(customer): add customer group management apis (#6233)

**What**
```
POST /admin/customer-groups
POST /admin/customer-groups/:id
GET /admin/customer-groups/:id
DELETE /admin/customer-groups/:id
```

- Workflows
This commit is contained in:
Sebastian Rindom
2024-01-30 20:37:53 +01:00
committed by GitHub
parent 8c7a031090
commit ca0e0631af
20 changed files with 540 additions and 20 deletions

View File

@@ -0,0 +1,65 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICustomerModuleService } from "@medusajs/types"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import adminSeeder from "../../../../helpers/admin-seeder"
const env = { MEDUSA_FF_MEDUSA_V2: true }
const adminHeaders = {
headers: { "x-medusa-access-token": "test_token" },
}
describe("POST /admin/customer-groups", () => {
let dbConnection
let appContainer
let shutdownServer
let customerModuleService: ICustomerModuleService
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd, env } as any)
shutdownServer = await startBootstrapApp({ cwd, env })
appContainer = getContainer()
customerModuleService = appContainer.resolve(
ModuleRegistrationName.CUSTOMER
)
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
await shutdownServer()
})
beforeEach(async () => {
await adminSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should create a customer group", async () => {
const api = useApi() as any
const response = await api.post(
`/admin/customer-groups`,
{
name: "VIP",
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.customer_group).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "VIP",
created_by: "admin_user",
})
)
})
})

View File

@@ -0,0 +1,65 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICustomerModuleService } from "@medusajs/types"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import adminSeeder from "../../../../helpers/admin-seeder"
const env = { MEDUSA_FF_MEDUSA_V2: true }
const adminHeaders = {
headers: { "x-medusa-access-token": "test_token" },
}
describe("DELETE /admin/customer-groups/:id", () => {
let dbConnection
let appContainer
let shutdownServer
let customerModuleService: ICustomerModuleService
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd, env } as any)
shutdownServer = await startBootstrapApp({ cwd, env })
appContainer = getContainer()
customerModuleService = appContainer.resolve(
ModuleRegistrationName.CUSTOMER
)
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
await shutdownServer()
})
beforeEach(async () => {
await adminSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should delete a group", async () => {
const group = await customerModuleService.createCustomerGroup({
name: "VIP",
})
const api = useApi() as any
const response = await api.delete(
`/admin/customer-groups/${group.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
const deletedCustomer = await customerModuleService.retrieveCustomerGroup(
group.id,
{ withDeleted: true }
)
expect(deletedCustomer.deleted_at).toBeTruthy()
})
})

View File

@@ -53,7 +53,7 @@ describe("GET /admin/customer-groups", () => {
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.groups).toEqual([
expect(response.data.customer_groups).toEqual([
expect.objectContaining({
id: expect.any(String),
name: "Test",

View File

@@ -0,0 +1,65 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICustomerModuleService } from "@medusajs/types"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import adminSeeder from "../../../../helpers/admin-seeder"
const env = { MEDUSA_FF_MEDUSA_V2: true }
const adminHeaders = {
headers: { "x-medusa-access-token": "test_token" },
}
describe("GET /admin/customer-groups/:id", () => {
let dbConnection
let appContainer
let shutdownServer
let customerModuleService: ICustomerModuleService
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd, env } as any)
shutdownServer = await startBootstrapApp({ cwd, env })
appContainer = getContainer()
customerModuleService = appContainer.resolve(
ModuleRegistrationName.CUSTOMER
)
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
await shutdownServer()
})
beforeEach(async () => {
await adminSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should retrieve customer group", async () => {
const group = await customerModuleService.createCustomerGroup({
name: "Test",
})
const api = useApi() as any
const response = await api.get(
`/admin/customer-groups/${group.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.customer_group).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "Test",
})
)
})
})

View File

@@ -0,0 +1,68 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICustomerModuleService } from "@medusajs/types"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import adminSeeder from "../../../../helpers/admin-seeder"
const env = { MEDUSA_FF_MEDUSA_V2: true }
const adminHeaders = {
headers: { "x-medusa-access-token": "test_token" },
}
describe("POST /admin/customer-groups/:id", () => {
let dbConnection
let appContainer
let shutdownServer
let customerModuleService: ICustomerModuleService
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd, env } as any)
shutdownServer = await startBootstrapApp({ cwd, env })
appContainer = getContainer()
customerModuleService = appContainer.resolve(
ModuleRegistrationName.CUSTOMER
)
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
await shutdownServer()
})
beforeEach(async () => {
await adminSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should update a customer group", async () => {
const customer = await customerModuleService.createCustomerGroup({
name: "VIP",
})
const api = useApi() as any
const response = await api.post(
`/admin/customer-groups/${customer.id}`,
{
name: "regular",
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.customer_group).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "regular",
})
)
})
})

View File

@@ -0,0 +1,2 @@
export * from "./workflows"
export * from "./steps"

View File

@@ -0,0 +1,33 @@
import { CreateCustomerGroupDTO, ICustomerModuleService } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
export const createCustomerGroupsStepId = "create-customer-groups"
export const createCustomerGroupsStep = createStep(
createCustomerGroupsStepId,
async (data: CreateCustomerGroupDTO[], { container }) => {
const service = container.resolve<ICustomerModuleService>(
ModuleRegistrationName.CUSTOMER
)
const createdCustomerGroups = await service.createCustomerGroup(data)
return new StepResponse(
createdCustomerGroups,
createdCustomerGroups.map(
(createdCustomerGroups) => createdCustomerGroups.id
)
)
},
async (createdCustomerGroupIds, { container }) => {
if (!createdCustomerGroupIds?.length) {
return
}
const service = container.resolve<ICustomerModuleService>(
ModuleRegistrationName.CUSTOMER
)
await service.delete(createdCustomerGroupIds)
}
)

View File

@@ -0,0 +1,30 @@
import { ICustomerModuleService } from "@medusajs/types"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
type DeleteCustomerGroupStepInput = string[]
export const deleteCustomerGroupStepId = "delete-customer-groups"
export const deleteCustomerGroupStep = createStep(
deleteCustomerGroupStepId,
async (ids: DeleteCustomerGroupStepInput, { container }) => {
const service = container.resolve<ICustomerModuleService>(
ModuleRegistrationName.CUSTOMER
)
await service.softDeleteCustomerGroup(ids)
return new StepResponse(void 0, ids)
},
async (prevCustomerGroups, { container }) => {
if (!prevCustomerGroups) {
return
}
const service = container.resolve<ICustomerModuleService>(
ModuleRegistrationName.CUSTOMER
)
await service.restoreCustomerGroup(prevCustomerGroups)
}
)

View File

@@ -0,0 +1,3 @@
export * from "./update-customer-groups"
export * from "./delete-customer-groups"
export * from "./create-customer-groups"

View File

@@ -0,0 +1,58 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
FilterableCustomerGroupProps,
ICustomerModuleService,
CustomerGroupUpdatableFields,
} from "@medusajs/types"
import {
getSelectsAndRelationsFromObjectArray,
promiseAll,
} from "@medusajs/utils"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
type UpdateCustomerGroupStepInput = {
selector: FilterableCustomerGroupProps
update: CustomerGroupUpdatableFields
}
export const updateCustomerGroupStepId = "update-customer-groups"
export const updateCustomerGroupsStep = createStep(
updateCustomerGroupStepId,
async (data: UpdateCustomerGroupStepInput, { container }) => {
const service = container.resolve<ICustomerModuleService>(
ModuleRegistrationName.CUSTOMER
)
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])
const prevCustomerGroups = await service.listCustomerGroups(data.selector, {
select: selects,
relations,
})
const customers = await service.updateCustomerGroup(
data.selector,
data.update
)
return new StepResponse(customers, prevCustomerGroups)
},
async (prevCustomerGroups, { container }) => {
if (!prevCustomerGroups) {
return
}
const service = container.resolve<ICustomerModuleService>(
ModuleRegistrationName.CUSTOMER
)
await promiseAll(
prevCustomerGroups.map((c) =>
service.updateCustomerGroup(c.id, {
name: c.name,
})
)
)
}
)

View File

@@ -0,0 +1,13 @@
import { CustomerGroupDTO, CreateCustomerGroupDTO } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { createCustomerGroupsStep } from "../steps"
type WorkflowInput = { customersData: CreateCustomerGroupDTO[] }
export const createCustomerGroupsWorkflowId = "create-customer-groups"
export const createCustomerGroupsWorkflow = createWorkflow(
createCustomerGroupsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<CustomerGroupDTO[]> => {
return createCustomerGroupsStep(input.customersData)
}
)

View File

@@ -0,0 +1,12 @@
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { deleteCustomerGroupStep } from "../steps"
type WorkflowInput = { ids: string[] }
export const deleteCustomerGroupsWorkflowId = "delete-customer-groups"
export const deleteCustomerGroupsWorkflow = createWorkflow(
deleteCustomerGroupsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
return deleteCustomerGroupStep(input.ids)
}
)

View File

@@ -0,0 +1,3 @@
export * from "./update-customer-groups"
export * from "./delete-customer-groups"
export * from "./create-customer-groups"

View File

@@ -0,0 +1,20 @@
import {
CustomerGroupDTO,
FilterableCustomerGroupProps,
CustomerGroupUpdatableFields,
} from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { updateCustomerGroupsStep } from "../steps"
type WorkflowInput = {
selector: FilterableCustomerGroupProps
update: CustomerGroupUpdatableFields
}
export const updateCustomerGroupsWorkflowId = "update-customer-groups"
export const updateCustomerGroupsWorkflow = createWorkflow(
updateCustomerGroupsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<CustomerGroupDTO[]> => {
return updateCustomerGroupsStep(input)
}
)

View File

@@ -3,3 +3,4 @@ export * from "./definitions"
export * as Handlers from "./handlers"
export * from "./promotion"
export * from "./customer"
export * from "./customer-group"

View File

@@ -19,11 +19,7 @@ import {
isString,
isObject,
} from "@medusajs/utils"
import {
entityNameToLinkableKeysMap,
LinkableKeys,
joinerConfig,
} from "../joiner-config"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import * as services from "../services"
type InjectedDependencies = {
@@ -111,24 +107,24 @@ export default class CustomerModuleService implements ICustomerModuleService {
update(
customerId: string,
data: CustomerUpdatableFields,
data: CustomerTypes.CustomerUpdatableFields,
sharedContext?: Context
): Promise<CustomerTypes.CustomerDTO>
update(
customerIds: string[],
data: CustomerUpdatableFields,
data: CustomerTypes.CustomerUpdatableFields,
sharedContext?: Context
): Promise<CustomerTypes.CustomerDTO[]>
update(
selector: CustomerTypes.FilterableCustomerProps,
data: CustomerUpdatableFields,
data: CustomerTypes.CustomerUpdatableFields,
sharedContext?: Context
): Promise<CustomerTypes.CustomerDTO[]>
@InjectTransactionManager("baseRepository_")
async update(
idsOrSelector: string | string[] | CustomerTypes.FilterableCustomerProps,
data: CustomerUpdatableFields,
data: CustomerTypes.CustomerUpdatableFields,
@MedusaContext() sharedContext: Context = {}
) {
let updateData: CustomerTypes.UpdateCustomerDTO[] = []
@@ -291,17 +287,17 @@ export default class CustomerModuleService implements ICustomerModuleService {
async updateCustomerGroup(
groupId: string,
data: Partial<CustomerTypes.CreateCustomerGroupDTO>,
data: CustomerTypes.CustomerGroupUpdatableFields,
sharedContext?: Context
): Promise<CustomerTypes.CustomerGroupDTO>
async updateCustomerGroup(
groupIds: string[],
data: Partial<CustomerTypes.CreateCustomerGroupDTO>,
data: CustomerTypes.CustomerGroupUpdatableFields,
sharedContext?: Context
): Promise<CustomerTypes.CustomerGroupDTO[]>
async updateCustomerGroup(
selector: CustomerTypes.FilterableCustomerGroupProps,
data: Partial<CustomerTypes.CreateCustomerGroupDTO>,
data: CustomerTypes.CustomerGroupUpdatableFields,
sharedContext?: Context
): Promise<CustomerTypes.CustomerGroupDTO[]>
@@ -311,7 +307,7 @@ export default class CustomerModuleService implements ICustomerModuleService {
| string
| string[]
| CustomerTypes.FilterableCustomerGroupProps,
data: Partial<CustomerTypes.CreateCustomerGroupDTO>,
data: CustomerTypes.CustomerGroupUpdatableFields,
@MedusaContext() sharedContext: Context = {}
) {
let updateData: CustomerTypes.UpdateCustomerGroupDTO[] = []

View File

@@ -0,0 +1,63 @@
import {
updateCustomerGroupsWorkflow,
deleteCustomerGroupsWorkflow,
} from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
CustomerGroupUpdatableFields,
ICustomerModuleService,
} from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const customerModuleService = req.scope.resolve<ICustomerModuleService>(
ModuleRegistrationName.CUSTOMER
)
const group = await customerModuleService.retrieveCustomerGroup(
req.params.id,
{
select: req.retrieveConfig.select,
relations: req.retrieveConfig.relations,
}
)
res.status(200).json({ customer_group: group })
}
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
const updateGroups = updateCustomerGroupsWorkflow(req.scope)
const { result, errors } = await updateGroups.run({
input: {
selector: { id: req.params.id },
update: req.validatedBody as CustomerGroupUpdatableFields,
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ customer_group: result[0] })
}
export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {
const id = req.params.id
const deleteCustomerGroups = deleteCustomerGroupsWorkflow(req.scope)
const { errors } = await deleteCustomerGroups.run({
input: { ids: [id] },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({
id,
object: "customer_group",
deleted: true,
})
}

View File

@@ -1,5 +1,6 @@
import { createCustomerGroupsWorkflow } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICustomerModuleService } from "@medusajs/types"
import { CreateCustomerGroupDTO, ICustomerModuleService } from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
@@ -17,8 +18,29 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
res.json({
count,
groups,
customer_groups: groups,
offset,
limit,
})
}
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
const createGroups = createCustomerGroupsWorkflow(req.scope)
const customersData = [
{
...(req.validatedBody as CreateCustomerGroupDTO),
created_by: req.user!.id,
},
]
const { result, errors } = await createGroups.run({
input: { customersData },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ customer_group: result[0] })
}

View File

@@ -71,7 +71,7 @@ export interface CreateCustomerGroupDTO {
created_by?: string
}
export interface CustomerGroupUpdatableFileds {
export interface CustomerGroupUpdatableFields {
name?: string
metadata?: Record<string, unknown> | null
}

View File

@@ -17,6 +17,7 @@ import {
CreateCustomerAddressDTO,
CreateCustomerDTO,
CreateCustomerGroupDTO,
CustomerGroupUpdatableFields,
CustomerUpdatableFields,
UpdateCustomerAddressDTO,
} from "./mutations"
@@ -75,17 +76,17 @@ export interface ICustomerModuleService extends IModuleService {
updateCustomerGroup(
groupId: string,
data: Partial<CreateCustomerGroupDTO>,
data: CustomerGroupUpdatableFields,
sharedContext?: Context
): Promise<CustomerGroupDTO>
updateCustomerGroup(
groupIds: string[],
data: Partial<CreateCustomerGroupDTO>,
data: CustomerGroupUpdatableFields,
sharedContext?: Context
): Promise<CustomerGroupDTO[]>
updateCustomerGroup(
selector: FilterableCustomerGroupProps,
data: Partial<CreateCustomerGroupDTO>,
data: CustomerGroupUpdatableFields,
sharedContext?: Context
): Promise<CustomerGroupDTO[]>