Feat: Bulk add customers to customer group (#1095)

* fix babel transform-runtime regenerator required for migrations

* add customer group model

* add migration for customer group

* add customer group model export

* add customer group repository

* add customer group service

* add CustomerGroupRepository to "withTransaction" in CustomerGroupService

* remove unnecessary argument to runtime plugin

* service export ordering

* add create customer group endpoint

* add customergroup to route index in admin

* add customer group service

* add customer groups test

* cleanup

* add customers batch initial

* batch creation of customer groups

* integration testing batch creation

* integration tests

* chaining existing customers creation in repo

* remove commented test

* update unit tests to reflect change in idempotent behavior

* ensure that exceptions are expected

* update idempotency behavior

* update formatting

* update format

* Update packages/medusa/src/repositories/customer-group.ts

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>

* pr feedback

* add In import

* add seperate model dto

* add integration test

* error handling in repository

* remove unused import

* jsdoc

* Update packages/medusa/src/api/routes/admin/customer-groups/add-customers-batch.ts

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>

* Update packages/medusa/src/api/routes/admin/customer-groups/add-customers-batch.ts

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>

* pr review comments

* rename variable

* fix: adds atomic phase clean up callback

* fix: call error handler in new transaction block too

* restore

* error handling

* fix: error handler in no isolation case

* add integration test for missing group on update

* final adjustments to test

* fix pr feedback

* cleanup core for pr

* remove console.log

* remove customergroupservice test from customers

* Apply suggestions from code review

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>

* add end bracket to customer tests

* remove comments

* change model decorator

* fix integration test merge

* onDelete cascade instead of cascade:true

* remove verbose flag

* fix: dedupe type

* add save to customer groups

* customer model delete cascade

* add await to asyncronous save operations

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>
This commit is contained in:
Philip Korsholm
2022-02-28 10:03:26 +01:00
committed by GitHub
parent 562a1b427a
commit 4b4463f0e2
21 changed files with 638 additions and 251 deletions

View File

@@ -130,7 +130,7 @@ describe("/admin/customer-groups", () => {
.catch((error) => {
expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual(
`CustomerGroup with ${id} was not found`
`CustomerGroup with id ${id} was not found`
)
})
})
@@ -193,6 +193,182 @@ describe("/admin/customer-groups", () => {
})
})
describe("POST /admin/customer-groups/{id}/customers/batch", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection)
await customerSeeder(dbConnection)
} catch (err) {
console.log(err)
throw err
}
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("adds multiple customers to a group", async () => {
const api = useApi()
const payload = {
customer_ids: [{ id: "test-customer-1" }, { id: "test-customer-2" }],
}
const batchAddResponse = await api.post(
"/admin/customer-groups/customer-group-1/customers/batch",
payload,
{
headers: {
Authorization: "Bearer test_token",
},
}
)
expect(batchAddResponse.status).toEqual(200)
expect(batchAddResponse.data.customer_group).toEqual(
expect.objectContaining({
name: "vip-customers",
})
)
const getCustomerResponse = await api.get(
"/admin/customers?expand=groups",
{
headers: { Authorization: "Bearer test_token" },
}
)
expect(getCustomerResponse.data.customers).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "test-customer-1",
groups: [
expect.objectContaining({
name: "vip-customers",
id: "customer-group-1",
}),
],
}),
expect.objectContaining({
id: "test-customer-2",
groups: [
expect.objectContaining({
name: "vip-customers",
id: "customer-group-1",
}),
],
}),
])
)
})
it("presents a descriptive error when presented with a non-existing group", async () => {
expect.assertions(2)
const api = useApi()
const payload = {
customer_ids: [{ id: "test-customer-1" }, { id: "test-customer-2" }],
}
await api
.post(
"/admin/customer-groups/non-existing-customer-group-1/customers/batch",
payload,
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
expect(err.response.data.type).toEqual("not_found")
expect(err.response.data.message).toEqual(
"CustomerGroup with id non-existing-customer-group-1 was not found"
)
})
})
it("adds customers to a group idempotently", async () => {
expect.assertions(3)
const api = useApi()
// add customer-1 to the customer group
const payload_1 = {
customer_ids: [{ id: "test-customer-1" }],
}
await api
.post(
"/admin/customer-groups/customer-group-1/customers/batch",
payload_1,
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => console.log(err))
// re-adding customer-1 to the customer group along with new addintion:
// customer-2 and some non-existing customers should cause the request to fail
const payload_2 = {
customer_ids: [
{ id: "test-customer-1" },
{ id: "test-customer-27" },
{ id: "test-customer-28" },
{ id: "test-customer-2" },
],
}
await api
.post(
"/admin/customer-groups/customer-group-1/customers/batch",
payload_2,
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
expect(err.response.data.type).toEqual("not_found")
expect(err.response.data.message).toEqual(
'The following customer ids do not exist: "test-customer-27, test-customer-28"'
)
})
// check that customer-1 is only added once and that customer-2 is added correctly
const getCustomerResponse = await api.get(
"/admin/customers?expand=groups",
{
headers: { Authorization: "Bearer test_token" },
}
)
expect(getCustomerResponse.data.customers).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "test-customer-1",
groups: [
expect.objectContaining({
name: "vip-customers",
id: "customer-group-1",
}),
],
}),
expect.objectContaining({
id: "test-customer-2",
groups: [],
}),
])
)
})
})
describe("POST /admin/customer-groups/:id", () => {
beforeEach(async () => {
try {
@@ -336,8 +512,6 @@ describe("/admin/customer-groups", () => {
})
it("throws error when a customer group doesn't exist", async () => {
expect.assertions(3)
const api = useApi()
const id = "test-group-000"
@@ -352,7 +526,7 @@ describe("/admin/customer-groups", () => {
expect(err.response.status).toEqual(404)
expect(err.response.data.type).toEqual("not_found")
expect(err.response.data.message).toEqual(
`CustomerGroup with ${id} was not found`
`CustomerGroup with id ${id} was not found`
)
})
})

View File

@@ -229,6 +229,32 @@ describe("/admin/customers", () => {
)
})
it("fails when adding a customer group which doesn't exist", async () => {
expect.assertions(3)
// Try adding a non existing group
const api = useApi()
await api
.post(
"/admin/customers/test-customer-3?expand=groups",
{
groups: [{ id: "fake-group-0" }],
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((error) => {
expect(error.response.status).toEqual(404)
expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual(
"Customer_group with customer_group_id fake-group-0 does not exist."
)
})
})
it("Correctly updates customer groups", async () => {
const api = useApi()
let response = await api
@@ -254,32 +280,6 @@ describe("/admin/customers", () => {
])
)
// 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

View File

@@ -882,8 +882,8 @@ describe("/admin/discounts", () => {
}
)
} catch (error) {
expect(error.response.data.message).toMatch(
/duplicate key value violates unique constraint/i
expect(error.response.data.message).toEqual(
"Discount with code TESTING already exists."
)
}
})

View File

@@ -1627,7 +1627,7 @@ describe("/admin/products", () => {
})
} catch (error) {
expect(error.response.data.message).toMatch(
/duplicate key value violates unique constraint/i
"Product with handle test-product already exists."
)
}
})
@@ -1699,7 +1699,7 @@ describe("/admin/products", () => {
})
} catch (error) {
expect(error.response.data.message).toMatch(
/duplicate key value violates unique constraint/i
"Product_collection with handle test-collection already exists."
)
}
})

View File

@@ -28,7 +28,7 @@ describe("/admin/tax-rates", () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
try {
dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd, verbose: true })
medusaProcess = await setupServer({ cwd })
} catch (error) {
console.log(error)
}

View File

@@ -40,6 +40,7 @@ module.exports = async (connection, data = {}) => {
id: "test-customer-delete-cg",
email: "test-deletetion-cg@email.com",
})
await manager.save(deletionCustomer)
await manager.insert(CustomerGroup, {
id: "customer-group-1",
@@ -66,26 +67,31 @@ module.exports = async (connection, data = {}) => {
id: "test-customer-5",
email: "test5@email.com",
})
await manager.save(customer5)
const customer6 = manager.create(Customer, {
id: "test-customer-6",
email: "test6@email.com",
})
await manager.save(customer6)
const customer7 = manager.create(Customer, {
id: "test-customer-7",
email: "test7@email.com",
})
await manager.save(customer7)
const c_group_5 = manager.create(CustomerGroup, {
id: "test-group-5",
name: "test-group-5",
})
await manager.save(c_group_5)
const c_group_6 = manager.create(CustomerGroup, {
id: "test-group-6",
name: "test-group-6",
})
await manager.save(c_group_6)
customer5.groups = [c_group_5]
await manager.save(customer5)
@@ -100,6 +106,7 @@ module.exports = async (connection, data = {}) => {
id: "test-group-delete",
name: "test-group-delete",
})
await manager.save(c_group_delete)
deletionCustomer.groups = [c_group_delete]
await manager.save(deletionCustomer)

View File

@@ -46,7 +46,13 @@ export default () => {
statusCode = 500
errObj.code = API_ERROR
break
case MedusaError.Types.UNEXPECTED_STATE:
case MedusaError.Types.INVALID_ARGUMENT:
break
default:
errObj.code = "unknown_error"
errObj.message = "An unknown error occurred."
errObj.type = "unknown_error"
break
}

View File

@@ -33,9 +33,10 @@ describe("POST /admin/collections/:id/products/batch", () => {
it("product collection service update", () => {
expect(ProductCollectionServiceMock.addProducts).toHaveBeenCalledTimes(1)
expect(
ProductCollectionServiceMock.addProducts
).toHaveBeenCalledWith(IdMap.getId("col"), ["prod_1", "prod_2"])
expect(ProductCollectionServiceMock.addProducts).toHaveBeenCalledWith(
IdMap.getId("col"),
["prod_1", "prod_2"]
)
})
})
@@ -60,7 +61,9 @@ describe("POST /admin/collections/:id/products/batch", () => {
})
it("throws error", () => {
expect(subject.body.message).toBe("Product collection not found")
expect(subject.body.message).toBe(
"Product collection with id: null was not found"
)
})
})

View File

@@ -0,0 +1,51 @@
import { Type } from "class-transformer"
import { ValidateNested } from "class-validator"
import { CustomerGroupService } from "../../../../services"
import { CustomerGroupsBatchCustomer } from "../../../../types/customer-groups"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /customer-groups/{id}/customers/batch
* operationId: "PostCustomerGroupsGroupCustomersBatch"
* summary: "Add a list of customers to a customer group "
* description: "Adds a list of customers, represented by id's, to a customer group."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The id of the customer group.
* - (body) customers=* {{id: string }[]} ids of the customers to add
* tags:
* - CustomerGroup
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* customerGroup:
* $ref: "#/components/schemas/customergroup"
*/
export default async (req, res) => {
const { id } = req.params
const validated = await validator(
AdminPostCustomerGroupsGroupCustomersBatchReq,
req.body
)
const customerGroupService: CustomerGroupService = req.scope.resolve(
"customerGroupService"
)
const customer_group = await customerGroupService.addCustomers(
id,
validated.customer_ids.map(({ id }) => id)
)
res.status(200).json({ customer_group })
}
export class AdminPostCustomerGroupsGroupCustomersBatchReq {
@ValidateNested({ each: true })
@Type(() => CustomerGroupsBatchCustomer)
customer_ids: CustomerGroupsBatchCustomer[]
}

View File

@@ -10,6 +10,10 @@ export default (app) => {
route.get("/:id", middlewares.wrap(require("./get-customer-group").default))
route.post("/", middlewares.wrap(require("./create-customer-group").default))
route.post(
"/:id/customers/batch",
middlewares.wrap(require("./add-customers-batch").default)
)
route.delete(
"/:id/customers/batch",
middlewares.wrap(require("./delete-customers-batch").default)

View File

@@ -69,7 +69,9 @@ export class Customer {
referencedColumnName: "id",
},
})
@ManyToMany(() => CustomerGroup, (cg) => cg.customers, { cascade: true })
@ManyToMany(() => CustomerGroup, (cg) => cg.customers, {
onDelete: "CASCADE",
})
groups: CustomerGroup[]
@CreateDateColumn({ type: resolveDbType("timestamptz") })

View File

@@ -3,6 +3,27 @@ import { CustomerGroup } from "../models/customer-group"
@EntityRepository(CustomerGroup)
export class CustomerGroupRepository extends Repository<CustomerGroup> {
async addCustomers(
groupId: string,
customerIds: string[]
): Promise<CustomerGroup> {
const customerGroup = await this.findOne(groupId)
await this.createQueryBuilder()
.insert()
.into("customer_group_customers")
.values(
customerIds.map((id) => ({
customer_id: id,
customer_group_id: groupId,
}))
)
.orIgnore()
.execute()
return customerGroup as CustomerGroup
}
async removeCustomers(
groupId: string,
customerIds: string[]

View File

@@ -1,7 +1,8 @@
import { MedusaError } from "medusa-core-utils"
import { IdMap } from "medusa-test-utils"
export const ProductCollectionServiceMock = {
withTransaction: function() {
withTransaction: function () {
return this
},
create: jest.fn().mockImplementation((data) => {
@@ -23,7 +24,10 @@ export const ProductCollectionServiceMock = {
products: product_ids.map((i) => ({ id: i })),
})
}
throw new Error("Product collection not found")
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product collection with id: ${id} was not found`
)
}),
removeProducts: jest.fn().mockReturnValue(Promise.resolve()),
list: jest.fn().mockImplementation((data) => {

View File

@@ -3,13 +3,13 @@ import CustomerService from "../customer"
const eventBusService = {
emit: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const customerGroupService = {
withTransaction: function() {
withTransaction: function () {
return this
},
list: jest.fn().mockImplementation(() => Promise.resolve()),
@@ -282,42 +282,43 @@ describe("CustomerService", () => {
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" }],
describe("updateAddress", () => {
const addressRepository = MockRepository({
findOne: (query) => {
return Promise.resolve({
id: IdMap.getId("hollywood-boulevard"),
address_1: "Hollywood Boulevard 2",
})
},
})
expect(customerGroupService.list).toBeCalledTimes(1)
expect(customerGroupService.list).toBeCalledWith({ id: ["group-id"] })
const customerService = new CustomerService({
manager: MockManager,
addressRepository,
})
expect(customerRepository.save).toBeCalledTimes(1)
})
})
beforeEach(async () => {
jest.clearAllMocks()
})
describe("updateAddress", () => {
const addressRepository = MockRepository({
findOne: (query) => {
return Promise.resolve({
it("successfully updates address", async () => {
await customerService.updateAddress(
IdMap.getId("ironman"),
IdMap.getId("hollywood-boulevard"),
{
first_name: "Tony",
last_name: "Stark",
address_1: "Hollywood Boulevard 1",
city: "Los Angeles",
country_code: "us",
postal_code: "90046",
phone: "+1 (222) 333 4444",
}
)
expect(addressRepository.save).toBeCalledTimes(1)
expect(addressRepository.save).toBeCalledWith({
id: IdMap.getId("hollywood-boulevard"),
address_1: "Hollywood Boulevard 2",
})
},
})
const customerService = new CustomerService({
manager: MockManager,
addressRepository,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully updates address", async () => {
await customerService.updateAddress(
IdMap.getId("ironman"),
IdMap.getId("hollywood-boulevard"),
{
first_name: "Tony",
last_name: "Stark",
address_1: "Hollywood Boulevard 1",
@@ -325,94 +326,82 @@ describe("CustomerService", () => {
country_code: "us",
postal_code: "90046",
phone: "+1 (222) 333 4444",
}
)
})
})
expect(addressRepository.save).toBeCalledTimes(1)
expect(addressRepository.save).toBeCalledWith({
id: IdMap.getId("hollywood-boulevard"),
first_name: "Tony",
last_name: "Stark",
address_1: "Hollywood Boulevard 1",
city: "Los Angeles",
country_code: "us",
postal_code: "90046",
phone: "+1 (222) 333 4444",
it("throws on invalid address", async () => {
await expect(
customerService.updateAddress(
IdMap.getId("ironman"),
IdMap.getId("hollywood-boulevard"),
{
first_name: "Tony",
last_name: "Stark",
country_code: "us",
unknown: "key",
address_1: "Hollywood",
}
)
).rejects.toThrow("The address is not valid")
})
})
it("throws on invalid address", async () => {
await expect(
customerService.updateAddress(
IdMap.getId("ironman"),
IdMap.getId("hollywood-boulevard"),
{
first_name: "Tony",
last_name: "Stark",
country_code: "us",
unknown: "key",
address_1: "Hollywood",
}
)
).rejects.toThrow("The address is not valid")
})
})
describe("removeAddress", () => {
const addressRepository = MockRepository({
findOne: (query) => {
return Promise.resolve({
id: IdMap.getId("hollywood-boulevard"),
address_1: "Hollywood Boulevard 2",
})
},
})
describe("removeAddress", () => {
const addressRepository = MockRepository({
findOne: (query) => {
return Promise.resolve({
const customerService = new CustomerService({
manager: MockManager,
addressRepository,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully deletes address", async () => {
await customerService.removeAddress(
IdMap.getId("ironman"),
IdMap.getId("hollywood-boulevard")
)
expect(addressRepository.softRemove).toBeCalledTimes(1)
expect(addressRepository.softRemove).toBeCalledWith({
id: IdMap.getId("hollywood-boulevard"),
address_1: "Hollywood Boulevard 2",
})
},
})
const customerService = new CustomerService({
manager: MockManager,
addressRepository,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully deletes address", async () => {
await customerService.removeAddress(
IdMap.getId("ironman"),
IdMap.getId("hollywood-boulevard")
)
expect(addressRepository.softRemove).toBeCalledTimes(1)
expect(addressRepository.softRemove).toBeCalledWith({
id: IdMap.getId("hollywood-boulevard"),
address_1: "Hollywood Boulevard 2",
})
})
})
describe("delete", () => {
const customerRepository = MockRepository({
findOne: (query) => {
return Promise.resolve({ id: IdMap.getId("ironman") })
},
})
describe("delete", () => {
const customerRepository = MockRepository({
findOne: (query) => {
return Promise.resolve({ id: IdMap.getId("ironman") })
},
})
const customerService = new CustomerService({
manager: MockManager,
customerRepository,
})
const customerService = new CustomerService({
manager: MockManager,
customerRepository,
})
beforeEach(async () => {
jest.clearAllMocks()
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully deletes customer", async () => {
await customerService.delete(IdMap.getId("ironman"))
it("successfully deletes customer", async () => {
await customerService.delete(IdMap.getId("ironman"))
expect(customerRepository.softRemove).toBeCalledTimes(1)
expect(customerRepository.softRemove).toBeCalledWith({
id: IdMap.getId("ironman"),
expect(customerRepository.softRemove).toBeCalledTimes(1)
expect(customerRepository.softRemove).toBeCalledWith({
id: IdMap.getId("ironman"),
})
})
})
})

View File

@@ -1,9 +1,11 @@
import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { DeepPartial, EntityManager } from "typeorm"
import { CustomerService } from "."
import { CustomerGroup } from ".."
import { CustomerGroupRepository } from "../repositories/customer-group"
import { FindConfig } from "../types/common"
import { formatException } from "../utils/exception-formatter"
import {
CustomerGroupUpdate,
FilterableCustomerGroupProps,
@@ -12,21 +14,33 @@ import {
type CustomerGroupConstructorProps = {
manager: EntityManager
customerGroupRepository: typeof CustomerGroupRepository
customerService: CustomerService
}
/**
* Provides layer to manipulate discounts.
* @implements {BaseService}
*/
class CustomerGroupService extends BaseService {
private manager_: EntityManager
private customerGroupRepository_: typeof CustomerGroupRepository
private customerService_: CustomerService
constructor({
manager,
customerGroupRepository,
customerService,
}: CustomerGroupConstructorProps) {
super()
this.manager_ = manager
this.customerGroupRepository_ = customerGroupRepository
/** @private @const {CustomerGroupService} */
this.customerService_ = customerService
}
withTransaction(transactionManager: EntityManager): CustomerGroupService {
@@ -37,6 +51,7 @@ class CustomerGroupService extends BaseService {
const cloned = new CustomerGroupService({
manager: transactionManager,
customerGroupRepository: this.customerGroupRepository_,
customerService: this.customerService_,
})
cloned.transactionManager_ = transactionManager
@@ -56,7 +71,7 @@ class CustomerGroupService extends BaseService {
if (!customerGroup) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`CustomerGroup with ${id} was not found`
`CustomerGroup with id ${id} was not found`
)
}
@@ -89,11 +104,60 @@ class CustomerGroupService extends BaseService {
})
}
/**
* Add a batch of customers to a customer group at once
* @param {string} id id of the customer group to add customers to
* @param {string[]} customerIds customer id's to add to the group
* @return {Promise<CustomerGroup>} the customer group after insertion
*/
async addCustomers(
id: string,
customerIds: string | string[]
): Promise<CustomerGroup> {
let ids: string[]
if (typeof customerIds === "string") {
ids = [customerIds]
} else {
ids = customerIds
}
return this.atomicPhase_(
async (manager) => {
const cgRepo: CustomerGroupRepository = manager.getCustomRepository(
this.customerGroupRepository_
)
return await cgRepo.addCustomers(id, ids)
},
async (error) => {
if (error.code === "23503") {
await this.retrieve(id)
const existingCustomers = await this.customerService_.list({
id: ids,
})
const nonExistingCustomers = ids.filter(
(cId) => existingCustomers.findIndex((el) => el.id === cId) === -1
)
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following customer ids do not exist: ${JSON.stringify(
nonExistingCustomers.join(", ")
)}`
)
}
throw formatException(error)
}
)
}
/**
* Update a customer group.
*
* @param {string} customerGroupId - id of the customer group
* @param {CustomerGroupUpdate} update - customer group partial data
* @returns resulting customer group
*/
async update(
customerGroupId: string,

View File

@@ -4,6 +4,7 @@ import { MedusaError, Validator } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import Scrypt from "scrypt-kdf"
import { Brackets, ILike } from "typeorm"
import { formatException } from "../utils/exception-formatter"
/**
* Provides layer to manipulate customers.
@@ -21,7 +22,6 @@ class CustomerService extends BaseService {
customerRepository,
eventBusService,
addressRepository,
customerGroupService,
}) {
super()
@@ -36,8 +36,6 @@ class CustomerService extends BaseService {
/** @private @const {AddressRepository} */
this.addressRepository_ = addressRepository
this.customerGroupService_ = customerGroupService
}
withTransaction(transactionManager) {
@@ -63,9 +61,7 @@ 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(
@@ -382,59 +378,63 @@ class CustomerService extends BaseService {
* @return {Promise} resolves to the update result.
*/
async update(customerId, update) {
return this.atomicPhase_(async (manager) => {
const customerRepository = manager.getCustomRepository(
this.customerRepository_
)
const addrRepo = manager.getCustomRepository(this.addressRepository_)
return this.atomicPhase_(
async (manager) => {
const customerRepository = manager.getCustomRepository(
this.customerRepository_
)
const addrRepo = manager.getCustomRepository(this.addressRepository_)
const customer = await this.retrieve(customerId)
const customer = await this.retrieve(customerId)
const {
email,
password,
metadata,
billing_address,
billing_address_id,
groups,
...rest
} = update
const {
email,
password,
metadata,
billing_address,
billing_address_id,
groups,
...rest
} = update
if (metadata) {
customer.metadata = this.setMetadata_(customer, metadata)
}
if (email) {
customer.email = this.validateEmail_(email)
}
if ("billing_address_id" in update || "billing_address" in update) {
const address = billing_address_id || billing_address
if (typeof address !== "undefined") {
await this.updateBillingAddress_(customer, address, addrRepo)
if (metadata) {
customer.metadata = this.setMetadata_(customer, metadata)
}
if (email) {
customer.email = this.validateEmail_(email)
}
if ("billing_address_id" in update || "billing_address" in update) {
const address = billing_address_id || billing_address
if (typeof address !== "undefined") {
await this.updateBillingAddress_(customer, address, addrRepo)
}
}
for (const [key, value] of Object.entries(rest)) {
customer[key] = value
}
if (password) {
customer.password_hash = await this.hashPassword_(password)
}
if (groups) {
customer.groups = groups
}
const updated = await customerRepository.save(customer)
await this.eventBus_
.withTransaction(manager)
.emit(CustomerService.Events.UPDATED, updated)
return updated
},
async (error) => {
throw formatException(error)
}
for (const [key, value] of Object.entries(rest)) {
customer[key] = value
}
if (password) {
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_
.withTransaction(manager)
.emit(CustomerService.Events.UPDATED, updated)
return updated
})
)
}
/**

View File

@@ -2,6 +2,7 @@ import { BaseService } from "medusa-interfaces"
import { Validator, MedusaError } from "medusa-core-utils"
import { parse, toSeconds } from "iso8601-duration"
import { Brackets, ILike } from "typeorm"
import { formatException } from "../utils/exception-formatter"
/**
* Provides layer to manipulate discounts.
@@ -182,24 +183,27 @@ class DiscountService extends BaseService {
"Fixed discounts can have one region"
)
}
if (discount.regions) {
discount.regions = await Promise.all(
discount.regions.map((regionId) =>
this.regionService_.withTransaction(manager).retrieve(regionId)
try {
if (discount.regions) {
discount.regions = await Promise.all(
discount.regions.map((regionId) =>
this.regionService_.withTransaction(manager).retrieve(regionId)
)
)
)
}
const discountRule = await ruleRepo.create(validatedRule)
const createdDiscountRule = await ruleRepo.save(discountRule)
discount.code = discount.code.toUpperCase()
discount.rule = createdDiscountRule
const created = await discountRepo.create(discount)
const result = await discountRepo.save(created)
return result
} catch (error) {
throw formatException(error)
}
const discountRule = await ruleRepo.create(validatedRule)
const createdDiscountRule = await ruleRepo.save(discountRule)
discount.code = discount.code.toUpperCase()
discount.rule = createdDiscountRule
const created = await discountRepo.create(discount)
const result = await discountRepo.save(created)
return result
})
}

View File

@@ -1,6 +1,7 @@
import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { Brackets, ILike } from "typeorm"
import { formatException } from "../utils/exception-formatter"
/**
* Provides layer to manipulate product collections.
@@ -106,8 +107,12 @@ class ProductCollectionService extends BaseService {
this.productCollectionRepository_
)
const productCollection = collectionRepo.create(collection)
return collectionRepo.save(productCollection)
try {
const productCollection = await collectionRepo.create(collection)
return await collectionRepo.save(productCollection)
} catch (error) {
throw formatException(error)
}
})
}
@@ -166,13 +171,17 @@ class ProductCollectionService extends BaseService {
return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_)
const { id } = await this.retrieve(collectionId, { select: ["id"] })
try {
const { id } = await this.retrieve(collectionId, { select: ["id"] })
await productRepo.bulkAddToCollection(productIds, id)
await productRepo.bulkAddToCollection(productIds, id)
return await this.retrieve(id, {
relations: ["products"],
})
return await this.retrieve(id, {
relations: ["products"],
})
} catch (error) {
throw formatException(error)
}
})
}

View File

@@ -1,6 +1,7 @@
import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { Brackets } from "typeorm"
import { formatException } from "../utils/exception-formatter"
/**
* Provides layer to manipulate products.
@@ -378,38 +379,44 @@ class ProductService extends BaseService {
rest.discountable = false
}
let product = productRepo.create(rest)
try {
let product = productRepo.create(rest)
if (images) {
product.images = await this.upsertImages_(images)
}
if (images) {
product.images = await this.upsertImages_(images)
}
if (tags) {
product.tags = await this.upsertProductTags_(tags)
}
if (tags) {
product.tags = await this.upsertProductTags_(tags)
}
if (typeof type !== `undefined`) {
product.type_id = await this.upsertProductType_(type)
}
if (typeof type !== `undefined`) {
product.type_id = await this.upsertProductType_(type)
}
product = await productRepo.save(product)
product = await productRepo.save(product)
product.options = await Promise.all(
options.map(async (o) => {
const res = optionRepo.create({ ...o, product_id: product.id })
await optionRepo.save(res)
return res
product.options = await Promise.all(
options.map(async (o) => {
const res = optionRepo.create({ ...o, product_id: product.id })
await optionRepo.save(res)
return res
})
)
const result = await this.retrieve(product.id, {
relations: ["options"],
})
)
const result = await this.retrieve(product.id, { relations: ["options"] })
await this.eventBus_
.withTransaction(manager)
.emit(ProductService.Events.CREATED, {
id: result.id,
})
return result
await this.eventBus_
.withTransaction(manager)
.emit(ProductService.Events.CREATED, {
id: result.id,
})
return result
} catch (error) {
throw formatException(error)
}
})
}

View File

@@ -13,6 +13,7 @@ export class CustomerGroupsBatchCustomer {
@IsString()
id: string
}
export class CustomerGroupUpdate {
name?: string
metadata?: object

View File

@@ -0,0 +1,41 @@
import { MedusaError } from "medusa-core-utils"
export enum PostgresError {
DUPLICATE_ERROR = "23505",
FOREIGN_KEY_ERROR = "23503",
}
export const formatException = (err): Error => {
switch (err.code) {
case PostgresError.DUPLICATE_ERROR:
return new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
`${err.table.charAt(0).toUpperCase()}${err.table.slice(
1
)} with ${err.detail
.slice(4)
.replace(/[()=]/g, (s) => (s === "=" ? " " : ""))}`
)
case PostgresError.FOREIGN_KEY_ERROR: {
const matches =
/Key \(([\w-\d]+)\)=\(([\w-\d]+)\) is not present in table "(\w+)"/g.exec(
err.detail
)
if (matches?.length !== 4) {
return new MedusaError(
MedusaError.Types.NOT_FOUND,
JSON.stringify(matches)
)
}
return new MedusaError(
MedusaError.Types.NOT_FOUND,
`${matches[3]?.charAt(0).toUpperCase()}${matches[3]?.slice(1)} with ${
matches[1]
} ${matches[2]} does not exist.`
)
}
default:
return err
}
}