committed by
GitHub
parent
cb727c8689
commit
cc2e87754d
37
packages/medusa/src/models/__mocks__/customer.js
Normal file
37
packages/medusa/src/models/__mocks__/customer.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const customers = {
|
||||
testCustomer: {
|
||||
_id: IdMap.getId("testCustomer"),
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
billingAddress: {},
|
||||
password_hash: "123456789",
|
||||
},
|
||||
deleteCustomer: {
|
||||
_id: IdMap.getId("deleteId"),
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
billingAddress: {},
|
||||
password_hash: "123456789",
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomerModelMock = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
updateOne: jest.fn().mockImplementation((query, update) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
findOne: jest.fn().mockImplementation(query => {
|
||||
if (query._id === IdMap.getId("testCustomer")) {
|
||||
return Promise.resolve(customers.testCustomer)
|
||||
}
|
||||
if (query._id === IdMap.getId("deleteId")) {
|
||||
return Promise.resolve(customers.deleteCustomer)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
@@ -14,7 +14,6 @@ class CustomerModel extends BaseModel {
|
||||
first_name: { type: String, required: true },
|
||||
last_name: { type: String, required: true },
|
||||
billing_address: { type: AddressSchema },
|
||||
password_hash: { type: String },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
}
|
||||
}
|
||||
|
||||
277
packages/medusa/src/services/__tests__/customer.js
Normal file
277
packages/medusa/src/services/__tests__/customer.js
Normal file
@@ -0,0 +1,277 @@
|
||||
import mongoose from "mongoose"
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import CustomerService from "../customer"
|
||||
import { CustomerModelMock, customers } from "../../models/__mocks__/customer"
|
||||
|
||||
describe("CustomerService", () => {
|
||||
describe("retrieve", () => {
|
||||
let result
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
})
|
||||
result = await customerService.retrieve(IdMap.getId("testCustomer"))
|
||||
})
|
||||
|
||||
it("calls customer model functions", () => {
|
||||
expect(CustomerModelMock.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerModelMock.findOne).toHaveBeenCalledWith({
|
||||
_id: IdMap.getId("testCustomer"),
|
||||
})
|
||||
})
|
||||
|
||||
it("returns the customer", () => {
|
||||
expect(result).toEqual(customers.testCustomer)
|
||||
})
|
||||
})
|
||||
|
||||
describe("setMetadata", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls updateOne with correct params", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
await customerService.setMetadata(`${id}`, "metadata", "testMetadata")
|
||||
|
||||
expect(CustomerModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(CustomerModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: `${id}` },
|
||||
{ $set: { "metadata.metadata": "testMetadata" } }
|
||||
)
|
||||
})
|
||||
|
||||
it("throw error on invalid key type", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
|
||||
try {
|
||||
await customerService.setMetadata(`${id}`, 1234, "nono")
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls model layer create", () => {
|
||||
customerService.create({
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
})
|
||||
|
||||
expect(CustomerModelMock.create).toBeCalledTimes(1)
|
||||
expect(CustomerModelMock.create).toBeCalledWith({
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
})
|
||||
})
|
||||
|
||||
it("fails if email is in incorrect format", () => {
|
||||
try {
|
||||
customerService.create({
|
||||
email: "olivermedusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("The email is not valid")
|
||||
}
|
||||
})
|
||||
|
||||
it("fails if billing address is in incorrect format", () => {
|
||||
try {
|
||||
customerService.create({
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
billing_address: {
|
||||
first_name: 1234,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("The address is not valid")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully updates a customer", async () => {
|
||||
await customerService.update(IdMap.getId("testCustomer"), {
|
||||
first_name: "Olli",
|
||||
last_name: "Test",
|
||||
})
|
||||
|
||||
expect(CustomerModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(CustomerModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testCustomer") },
|
||||
{ $set: { first_name: "Olli", last_name: "Test" } },
|
||||
{ runValidators: true }
|
||||
)
|
||||
})
|
||||
|
||||
it("fails if metadata updates are attempted", async () => {
|
||||
try {
|
||||
await customerService.update(IdMap.getId("testCustomer"), {
|
||||
metadata: "Nononono",
|
||||
})
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual("Use setMetadata to update metadata fields")
|
||||
}
|
||||
})
|
||||
|
||||
it("fails if billing address updates are attempted", async () => {
|
||||
try {
|
||||
await customerService.update(IdMap.getId("testCustomer"), {
|
||||
billing_address: {
|
||||
last_name: "nnonono",
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"Use updateBillingAddress to update billing address"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateEmail", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully updates an email", async () => {
|
||||
await customerService.updateEmail(
|
||||
IdMap.getId("testCustomer"),
|
||||
"oliver@medusa2.com"
|
||||
)
|
||||
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("testCustomer"),
|
||||
},
|
||||
{
|
||||
$set: { email: "oliver@medusa2.com" },
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("throws on invalid email", async () => {
|
||||
try {
|
||||
await customerService.updateEmail(
|
||||
IdMap.getId("testCustomer"),
|
||||
"olivermedusa"
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual("The email is not valid")
|
||||
}
|
||||
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
describe("updateBillingAddress", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully updates billing address", async () => {
|
||||
await customerService.updateBillingAddress(
|
||||
IdMap.getId("testCustomer"),
|
||||
{
|
||||
first_name: "Olli",
|
||||
last_name: "Juhl",
|
||||
address_1: "Laksegade",
|
||||
city: "Copenhagen",
|
||||
country_code: "DK",
|
||||
postal_code: "2100",
|
||||
}
|
||||
)
|
||||
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("testCustomer"),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
billing_address: {
|
||||
first_name: "Olli",
|
||||
last_name: "Juhl",
|
||||
address_1: "Laksegade",
|
||||
city: "Copenhagen",
|
||||
country_code: "DK",
|
||||
postal_code: "2100",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("throws on invalid address", async () => {
|
||||
try {
|
||||
await customerService.updateBillingAddress(
|
||||
IdMap.getId("testCustomer"),
|
||||
{
|
||||
first_name: 1234,
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual("The address is not valid")
|
||||
}
|
||||
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("delete", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("deletes customer successfully", async () => {
|
||||
await customerService.delete(IdMap.getId("deleteId"))
|
||||
|
||||
expect(CustomerModelMock.deleteOne).toBeCalledTimes(1)
|
||||
expect(CustomerModelMock.deleteOne).toBeCalledWith({
|
||||
_id: IdMap.getId("deleteId"),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -167,7 +167,7 @@ class CartService extends BaseService {
|
||||
*/
|
||||
async decorate(cart, fields, expandFields = []) {
|
||||
const requiredFields = ["_id", "metadata"]
|
||||
const decorated = _.pick(product, fields.concat(requiredFields))
|
||||
const decorated = _.pick(cart, fields.concat(requiredFields))
|
||||
return decorated
|
||||
}
|
||||
|
||||
|
||||
260
packages/medusa/src/services/customer.js
Normal file
260
packages/medusa/src/services/customer.js
Normal file
@@ -0,0 +1,260 @@
|
||||
import mongoose from "mongoose"
|
||||
import _ from "lodash"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate customers.
|
||||
* @implements BaseService
|
||||
*/
|
||||
class CustomerService extends BaseService {
|
||||
constructor({ customerModel, eventBusService }) {
|
||||
super()
|
||||
|
||||
/** @private @const {CustomerModel} */
|
||||
this.customerModel_ = customerModel
|
||||
|
||||
/** @private @const {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate customer ids. Throws an error if the cast fails
|
||||
* @param {string} rawId - the raw customer id to validate.
|
||||
* @return {string} the validated id
|
||||
*/
|
||||
validateId_(rawId) {
|
||||
const schema = Validator.objectId()
|
||||
const { value, error } = schema.validate(rawId)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"The customerId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate customer email.
|
||||
* @param {string} email - email to validate
|
||||
* @return {string} the validated email
|
||||
*/
|
||||
validateEmail_(email) {
|
||||
const schema = Validator.string()
|
||||
.email()
|
||||
.required()
|
||||
const { value, error } = schema.validate(email)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The email is not valid"
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
validateBillingAddress_(address) {
|
||||
const { value, error } = Validator.address().validate(address)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The address is not valid"
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} selector - the query object for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
list(selector) {
|
||||
return this.customerModel_.find(selector)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a customer by id.
|
||||
* @param {string} customerId - the id of the customer to get.
|
||||
* @return {Promise<Customer>} the customer document.
|
||||
*/
|
||||
retrieve(customerId) {
|
||||
const validatedId = this.validateId_(customerId)
|
||||
return this.customerModel_.findOne({ _id: validatedId }).catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a customer with email and billing address
|
||||
* (if provided) being validated.
|
||||
* @param {object} customer - the customer to create
|
||||
* @return {Promise} the result of create
|
||||
*/
|
||||
create(customer) {
|
||||
const { email, billing_address } = customer
|
||||
this.validateEmail_(email)
|
||||
if (billing_address) {
|
||||
this.validateBillingAddress_(billing_address)
|
||||
}
|
||||
this.customerModel_.create(customer).catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the email of a customer
|
||||
* @param {string} customerId - the id of the customer to update
|
||||
* @param {string} email - the email to add to customer
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async updateEmail(customerId, email) {
|
||||
const customer = await this.retrieve(customerId)
|
||||
if (!customer) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Customer with ${customerId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
this.validateEmail_(email)
|
||||
|
||||
return this.customerModel_.updateOne(
|
||||
{
|
||||
_id: customerId,
|
||||
},
|
||||
{
|
||||
$set: { email },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the billing address of a customer
|
||||
* @param {*} customerId - the customer to update address on
|
||||
* @param {*} address - the new address to replace the current one
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async updateBillingAddress(customerId, address) {
|
||||
const customer = await this.retrieve(customerId)
|
||||
if (!customer) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Customer with ${customerId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
this.validateBillingAddress_(address)
|
||||
|
||||
return this.customerModel_.updateOne(
|
||||
{
|
||||
_id: customerId,
|
||||
},
|
||||
{
|
||||
$set: { billing_address: address },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a customer. Metadata updates and address updates should
|
||||
* use dedicated methods, e.g. `setMetadata`, etc. The function
|
||||
* will throw errors if metadata updates and address updates are attempted.
|
||||
* @param {string} variantId - the id of the variant. Must be a string that
|
||||
* can be casted to an ObjectId
|
||||
* @param {object} update - an object with the update values.
|
||||
* @return {Promise} resolves to the update result.
|
||||
*/
|
||||
async update(customerId, update) {
|
||||
const customer = await this.retrieve(customerId)
|
||||
if (!customer) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Customer with ${customerId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
if (update.metadata) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Use setMetadata to update metadata fields"
|
||||
)
|
||||
}
|
||||
|
||||
if (update.billing_address) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Use updateBillingAddress to update billing address"
|
||||
)
|
||||
}
|
||||
|
||||
return this.customerModel_
|
||||
.updateOne({ _id: customerId }, { $set: update }, { runValidators: true })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a customer from a given customer id.
|
||||
* @param {string} customerId - the id of the customer to delete. Must be
|
||||
* castable as an ObjectId
|
||||
* @return {Promise} the result of the delete operation.
|
||||
*/
|
||||
async delete(customerId) {
|
||||
const customer = await this.retrieve(customerId)
|
||||
// Delete is idempotent, but we return a promise to allow then-chaining
|
||||
if (!customer) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return this.customerModel_.deleteOne({ _id: customer._id }).catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates a customer.
|
||||
* @param {Customer} customer - the cart to decorate.
|
||||
* @param {string[]} fields - the fields to include.
|
||||
* @param {string[]} expandFields - fields to expand.
|
||||
* @return {Customer} return the decorated customer.
|
||||
*/
|
||||
async decorate(customer, fields, expandFields = []) {
|
||||
const requiredFields = ["_id", "metadata"]
|
||||
const decorated = _.pick(customer, fields.concat(requiredFields))
|
||||
return decorated
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to set metadata for a customer.
|
||||
* To ensure that plugins does not overwrite each
|
||||
* others metadata fields, setMetadata is provided.
|
||||
* @param {string} customerId - the customer to apply metadata to.
|
||||
* @param {string} key - key for metadata field
|
||||
* @param {string} value - value for metadata field.
|
||||
* @return {Promise} resolves to the updated result.
|
||||
*/
|
||||
setMetadata(customerId, key, value) {
|
||||
const validatedId = this.validateId_(customerId)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
|
||||
const keyPath = `metadata.${key}`
|
||||
return this.customerModel_
|
||||
.updateOne({ _id: validatedId }, { $set: { [keyPath]: value } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomerService
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user