feat: update customer groups (#1075)
* update customer service prep * expand on the update customer groups test case * add the test case for customer group update * docs for `groups` prop on update customer endpoint * refactor, update comments * expend on integration tests, add customer mock with a group * refactor to use `customerGroupService.list` method * update units * remove `updateCustomerGroups` * fix rebase conflict * fix customer seed data, add JoinTable on groups * group retrieval using the expand param * fix: use `buildQuery_` * fix: validation for `groups`, enable `expand`param on update customer endpoint * fix: remove fileds form the `FilterableCustomerGroupProps` * fix: spearate body/query validation Co-authored-by: fPolic <frane@medusajs.com>
This commit is contained in:
@@ -62,6 +62,7 @@ Object {
|
||||
"deleted_at": null,
|
||||
"email": "test1@email.com",
|
||||
"first_name": null,
|
||||
"groups": Array [],
|
||||
"has_account": false,
|
||||
"id": "test-customer-1",
|
||||
"last_name": null,
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("/admin/customers", () => {
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.count).toEqual(4)
|
||||
expect(response.data.count).toEqual(5)
|
||||
expect(response.data.customers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -228,6 +228,106 @@ describe("/admin/customers", () => {
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("Correctly updates customer groups", async () => {
|
||||
const api = useApi()
|
||||
let response = await api
|
||||
.post(
|
||||
"/admin/customers/test-customer-3?expand=groups",
|
||||
{
|
||||
groups: [{ id: "test-group-4" }],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.customer.groups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: "test-group-4", name: "test-group-4" }),
|
||||
])
|
||||
)
|
||||
|
||||
// Try adding a non existing group
|
||||
|
||||
response = await api
|
||||
.post(
|
||||
"/admin/customers/test-customer-3?expand=groups",
|
||||
{
|
||||
groups: [{ id: "test-group-4" }, { id: "fake-group-0" }],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.customer.groups.length).toEqual(1)
|
||||
expect(response.data.customer.groups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: "test-group-4", name: "test-group-4" }),
|
||||
])
|
||||
)
|
||||
|
||||
// Delete all groups
|
||||
|
||||
response = await api
|
||||
.post(
|
||||
"/admin/customers/test-customer-3?expand=groups",
|
||||
{
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.customer.groups.length).toEqual(0)
|
||||
|
||||
// Adding a group to a customer with already existing groups.
|
||||
|
||||
response = await api
|
||||
.post(
|
||||
"/admin/customers/test-customer-5?expand=groups",
|
||||
{
|
||||
groups: [{ id: "test-group-5" }, { id: "test-group-4" }],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.customer.groups.length).toEqual(2)
|
||||
expect(response.data.customer.groups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: "test-group-4", name: "test-group-4" }),
|
||||
expect.objectContaining({ id: "test-group-5", name: "test-group-5" }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /admin/customers/:id", () => {
|
||||
@@ -278,7 +378,7 @@ describe("/admin/customers", () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api
|
||||
.get("/admin/customers/test-customer-1?expand=billing_address", {
|
||||
.get("/admin/customers/test-customer-1?expand=billing_address,groups", {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
@@ -295,6 +395,7 @@ describe("/admin/customers", () => {
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
groups: [],
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
|
||||
@@ -36,8 +36,24 @@ module.exports = async (connection, data = {}) => {
|
||||
has_account: true,
|
||||
})
|
||||
|
||||
await manager.insert(Customer, {
|
||||
id: "test-customer-5",
|
||||
email: "test5@email.com",
|
||||
groups: [{ id: "test-group-5", name: "test-group-5" }],
|
||||
})
|
||||
|
||||
await manager.insert(CustomerGroup, {
|
||||
id: "customer-group-1",
|
||||
name: "vip-customers",
|
||||
})
|
||||
|
||||
await manager.insert(CustomerGroup, {
|
||||
id: "test-group-4",
|
||||
name: "test-group-4",
|
||||
})
|
||||
|
||||
await manager.insert(CustomerGroup, {
|
||||
id: "test-group-5",
|
||||
name: "test-group-5",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { IsEmail, IsObject, IsOptional, IsString } from "class-validator"
|
||||
import {
|
||||
IsArray,
|
||||
IsEmail,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import CustomerService from "../../../../services/customer"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { defaultAdminCustomersRelations } from "."
|
||||
import { Type } from "class-transformer"
|
||||
import { FindParams } from "../../../../types/common"
|
||||
|
||||
/**
|
||||
* @oas [post] /customers/{id}
|
||||
@@ -31,6 +41,16 @@ import { validator } from "../../../../utils/validator"
|
||||
* password:
|
||||
* type: string
|
||||
* description: The Customer's password.
|
||||
* groups:
|
||||
* type: array
|
||||
* description: A list of customer groups to which the customer belongs.
|
||||
* items:
|
||||
* required:
|
||||
* - id
|
||||
* properties:
|
||||
* id:
|
||||
* description: The id of a customer group
|
||||
* type: string
|
||||
* metadata:
|
||||
* type: object
|
||||
* description: Metadata for the customer.
|
||||
@@ -49,27 +69,43 @@ import { validator } from "../../../../utils/validator"
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const validated = await validator(AdminPostCustomersCustomerReq, req.body)
|
||||
const validatedBody = await validator(AdminPostCustomersCustomerReq, req.body)
|
||||
const validatedQuery = await validator(FindParams, req.query)
|
||||
|
||||
const customerService: CustomerService = req.scope.resolve("customerService")
|
||||
|
||||
let customer = await customerService.retrieve(id)
|
||||
|
||||
if (validated.email && customer.has_account) {
|
||||
if (validatedBody.email && customer.has_account) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Email cannot be changed when the user has registered their account"
|
||||
)
|
||||
}
|
||||
|
||||
await customerService.update(id, validated)
|
||||
await customerService.update(id, validatedBody)
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (validatedQuery.expand) {
|
||||
expandFields = validatedQuery.expand.split(",")
|
||||
}
|
||||
|
||||
const findConfig = {
|
||||
relations: expandFields.length
|
||||
? expandFields
|
||||
: defaultAdminCustomersRelations,
|
||||
}
|
||||
|
||||
customer = await customerService.retrieve(id, findConfig)
|
||||
|
||||
customer = await customerService.retrieve(id, {
|
||||
relations: ["orders"],
|
||||
})
|
||||
res.status(200).json({ customer })
|
||||
}
|
||||
|
||||
class Group {
|
||||
@IsString()
|
||||
id: string
|
||||
}
|
||||
|
||||
export class AdminPostCustomersCustomerReq {
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@@ -94,4 +130,10 @@ export class AdminPostCustomersCustomerReq {
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: object
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@Type(() => Group)
|
||||
@ValidateNested({ each: true })
|
||||
groups?: Group[]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,10 @@ export class Customer {
|
||||
@JoinColumn({ name: "billing_address_id" })
|
||||
billing_address: Address
|
||||
|
||||
@OneToMany(() => Address, (address) => address.customer)
|
||||
@OneToMany(
|
||||
() => Address,
|
||||
(address) => address.customer
|
||||
)
|
||||
shipping_addresses: Address[]
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
@@ -55,9 +58,23 @@ export class Customer {
|
||||
@Column({ default: false })
|
||||
has_account: boolean
|
||||
|
||||
@OneToMany(() => Order, (order) => order.customer)
|
||||
@OneToMany(
|
||||
() => Order,
|
||||
(order) => order.customer
|
||||
)
|
||||
orders: Order[]
|
||||
|
||||
@JoinTable({
|
||||
name: "customer_group_customers",
|
||||
inverseJoinColumn: {
|
||||
name: "customer_group_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
joinColumn: {
|
||||
name: "customer_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
@ManyToMany(() => CustomerGroup, { cascade: true })
|
||||
groups: CustomerGroup[]
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@ const eventBusService = {
|
||||
},
|
||||
}
|
||||
|
||||
const customerGroupService = {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
list: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
}
|
||||
|
||||
describe("CustomerService", () => {
|
||||
describe("retrieve", () => {
|
||||
const customerRepository = MockRepository({
|
||||
@@ -86,7 +93,7 @@ describe("CustomerService", () => {
|
||||
|
||||
describe("create", () => {
|
||||
const customerRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
if (query.where.email === "tony@stark.com") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("exists"),
|
||||
@@ -162,14 +169,14 @@ describe("CustomerService", () => {
|
||||
|
||||
describe("update", () => {
|
||||
const customerRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
return Promise.resolve({ id: IdMap.getId("ironman") })
|
||||
},
|
||||
})
|
||||
|
||||
const addressRepository = MockRepository({
|
||||
create: data => data,
|
||||
save: data => Promise.resolve(data),
|
||||
create: (data) => data,
|
||||
save: (data) => Promise.resolve(data),
|
||||
})
|
||||
|
||||
const customerService = new CustomerService({
|
||||
@@ -246,9 +253,50 @@ describe("CustomerService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("update customer groups", () => {
|
||||
const customerRepository = MockRepository({
|
||||
findOne: (query) => {
|
||||
return Promise.resolve({ id: IdMap.getId("ironman") })
|
||||
},
|
||||
})
|
||||
|
||||
const addressRepository = MockRepository({
|
||||
create: (data) => data,
|
||||
save: (data) => Promise.resolve(data),
|
||||
})
|
||||
|
||||
const customerGroupRepository = MockRepository({
|
||||
findByIds: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
})
|
||||
|
||||
const customerService = new CustomerService({
|
||||
manager: MockManager,
|
||||
addressRepository,
|
||||
customerRepository,
|
||||
customerGroupRepository,
|
||||
customerGroupService,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls `customerGroupService.list` if `groups` prop is received as a param", async () => {
|
||||
await customerService.update(IdMap.getId("ironman"), {
|
||||
groups: [{ id: "group-id" }],
|
||||
})
|
||||
|
||||
expect(customerGroupService.list).toBeCalledTimes(1)
|
||||
expect(customerGroupService.list).toBeCalledWith({ id: ["group-id"] })
|
||||
|
||||
expect(customerRepository.save).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateAddress", () => {
|
||||
const addressRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("hollywood-boulevard"),
|
||||
address_1: "Hollywood Boulevard 2",
|
||||
@@ -312,7 +360,7 @@ describe("CustomerService", () => {
|
||||
|
||||
describe("removeAddress", () => {
|
||||
const addressRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("hollywood-boulevard"),
|
||||
address_1: "Hollywood Boulevard 2",
|
||||
@@ -345,7 +393,7 @@ describe("CustomerService", () => {
|
||||
|
||||
describe("delete", () => {
|
||||
const customerRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
return Promise.resolve({ id: IdMap.getId("ironman") })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,8 @@ import { BaseService } from "medusa-interfaces"
|
||||
import { DeepPartial, EntityManager } from "typeorm"
|
||||
import { CustomerGroup } from ".."
|
||||
import { CustomerGroupRepository } from "../repositories/customer-group"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { FilterableCustomerGroupProps } from "../types/customer-groups"
|
||||
|
||||
type CustomerGroupConstructorProps = {
|
||||
manager: EntityManager
|
||||
@@ -64,6 +66,25 @@ class CustomerGroupService extends BaseService {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List customer groups.
|
||||
*
|
||||
* @param {Object} selector - the query object for find
|
||||
* @param {Object} config - the config to be used for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async list(
|
||||
selector: FilterableCustomerGroupProps = {},
|
||||
config: FindConfig<CustomerGroup>
|
||||
): Promise<CustomerGroup[]> {
|
||||
const cgRepo: CustomerGroupRepository = this.manager_.getCustomRepository(
|
||||
this.customerGroupRepository_
|
||||
)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return await cgRepo.find(query)
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomerGroupService
|
||||
|
||||
@@ -21,6 +21,7 @@ class CustomerService extends BaseService {
|
||||
customerRepository,
|
||||
eventBusService,
|
||||
addressRepository,
|
||||
customerGroupService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -35,6 +36,8 @@ class CustomerService extends BaseService {
|
||||
|
||||
/** @private @const {AddressRepository} */
|
||||
this.addressRepository_ = addressRepository
|
||||
|
||||
this.customerGroupService_ = customerGroupService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
@@ -60,7 +63,9 @@ class CustomerService extends BaseService {
|
||||
* @return {string} the validated email
|
||||
*/
|
||||
validateEmail_(email) {
|
||||
const schema = Validator.string().email().required()
|
||||
const schema = Validator.string()
|
||||
.email()
|
||||
.required()
|
||||
const { value, error } = schema.validate(email)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
@@ -391,6 +396,7 @@ class CustomerService extends BaseService {
|
||||
metadata,
|
||||
billing_address,
|
||||
billing_address_id,
|
||||
groups,
|
||||
...rest
|
||||
} = update
|
||||
|
||||
@@ -417,6 +423,11 @@ class CustomerService extends BaseService {
|
||||
customer.password_hash = await this.hashPassword_(password)
|
||||
}
|
||||
|
||||
if (groups) {
|
||||
const id = groups.map((g) => g.id)
|
||||
customer.groups = await this.customerGroupService_.list({ id })
|
||||
}
|
||||
|
||||
const updated = await customerRepository.save(customer)
|
||||
|
||||
await this.eventBus_
|
||||
|
||||
10
packages/medusa/src/types/customer-groups.ts
Normal file
10
packages/medusa/src/types/customer-groups.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ValidateNested } from "class-validator"
|
||||
import { IsType } from "../utils/validators/is-type"
|
||||
|
||||
import { StringComparisonOperator } from "./common"
|
||||
|
||||
export class FilterableCustomerGroupProps {
|
||||
@ValidateNested()
|
||||
@IsType([String, [String], StringComparisonOperator])
|
||||
id?: string | string[] | StringComparisonOperator
|
||||
}
|
||||
Reference in New Issue
Block a user