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 KorsholmandSebastian Rindom authored and GitHub committed 2022-02-28 10:03:26 +01:00
1 parent 562a1b427a
commit 4b4463f0e2
21 files changed
+455 -68

No files matched your search

@@ -130,7 +130,7 @@ describe("/admin/customer-groups", () => {
.catch((error) => { .catch((error) => {
expect(error.response.data.type).toEqual("not_found") expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual( 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", () => { describe("POST /admin/customer-groups/:id", () => {
beforeEach(async () => { beforeEach(async () => {
try { try {
@@ -336,8 +512,6 @@ describe("/admin/customer-groups", () => {
}) })
it("throws error when a customer group doesn't exist", async () => { it("throws error when a customer group doesn't exist", async () => {
expect.assertions(3)
const api = useApi() const api = useApi()
const id = "test-group-000" const id = "test-group-000"
@@ -352,7 +526,7 @@ describe("/admin/customer-groups", () => {
expect(err.response.status).toEqual(404) expect(err.response.status).toEqual(404)
expect(err.response.data.type).toEqual("not_found") expect(err.response.data.type).toEqual("not_found")
expect(err.response.data.message).toEqual( expect(err.response.data.message).toEqual(
`CustomerGroup with ${id} was not found` `CustomerGroup with id ${id} was not found`
) )
}) })
}) })
@@ -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 () => { it("Correctly updates customer groups", async () => {
const api = useApi() const api = useApi()
let response = await api 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 // Delete all groups
response = await api response = await api
@@ -882,8 +882,8 @@ describe("/admin/discounts", () => {
} }
) )
} catch (error) { } catch (error) {
expect(error.response.data.message).toMatch( expect(error.response.data.message).toEqual(
/duplicate key value violates unique constraint/i "Discount with code TESTING already exists."
) )
} }
}) })
@@ -1627,7 +1627,7 @@ describe("/admin/products", () => {
}) })
} catch (error) { } catch (error) {
expect(error.response.data.message).toMatch( 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) { } catch (error) {
expect(error.response.data.message).toMatch( expect(error.response.data.message).toMatch(
/duplicate key value violates unique constraint/i "Product_collection with handle test-collection already exists."
) )
} }
}) })
@@ -28,7 +28,7 @@ describe("/admin/tax-rates", () => {
const cwd = path.resolve(path.join(__dirname, "..", "..")) const cwd = path.resolve(path.join(__dirname, "..", ".."))
try { try {
dbConnection = await initDb({ cwd }) dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd, verbose: true }) medusaProcess = await setupServer({ cwd })
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
@@ -40,6 +40,7 @@ module.exports = async (connection, data = {}) => {
id: "test-customer-delete-cg", id: "test-customer-delete-cg",
email: "test-deletetion-cg@email.com", email: "test-deletetion-cg@email.com",
}) })
await manager.save(deletionCustomer)
await manager.insert(CustomerGroup, { await manager.insert(CustomerGroup, {
id: "customer-group-1", id: "customer-group-1",
@@ -66,26 +67,31 @@ module.exports = async (connection, data = {}) => {
id: "test-customer-5", id: "test-customer-5",
email: "test5@email.com", email: "test5@email.com",
}) })
await manager.save(customer5)
const customer6 = manager.create(Customer, { const customer6 = manager.create(Customer, {
id: "test-customer-6", id: "test-customer-6",
email: "test6@email.com", email: "test6@email.com",
}) })
await manager.save(customer6)
const customer7 = manager.create(Customer, { const customer7 = manager.create(Customer, {
id: "test-customer-7", id: "test-customer-7",
email: "test7@email.com", email: "test7@email.com",
}) })
await manager.save(customer7)
const c_group_5 = manager.create(CustomerGroup, { const c_group_5 = manager.create(CustomerGroup, {
id: "test-group-5", id: "test-group-5",
name: "test-group-5", name: "test-group-5",
}) })
await manager.save(c_group_5)
const c_group_6 = manager.create(CustomerGroup, { const c_group_6 = manager.create(CustomerGroup, {
id: "test-group-6", id: "test-group-6",
name: "test-group-6", name: "test-group-6",
}) })
await manager.save(c_group_6)
customer5.groups = [c_group_5] customer5.groups = [c_group_5]
await manager.save(customer5) await manager.save(customer5)
@@ -100,6 +106,7 @@ module.exports = async (connection, data = {}) => {
id: "test-group-delete", id: "test-group-delete",
name: "test-group-delete", name: "test-group-delete",
}) })
await manager.save(c_group_delete)
deletionCustomer.groups = [c_group_delete] deletionCustomer.groups = [c_group_delete]
await manager.save(deletionCustomer) await manager.save(deletionCustomer)
@@ -46,7 +46,13 @@ export default () => {
statusCode = 500 statusCode = 500
errObj.code = API_ERROR errObj.code = API_ERROR
break break
case MedusaError.Types.UNEXPECTED_STATE:
case MedusaError.Types.INVALID_ARGUMENT:
break
default: default:
errObj.code = "unknown_error"
errObj.message = "An unknown error occurred."
errObj.type = "unknown_error"
break break
} }
@@ -33,9 +33,10 @@ describe("POST /admin/collections/:id/products/batch", () => {
it("product collection service update", () => { it("product collection service update", () => {
expect(ProductCollectionServiceMock.addProducts).toHaveBeenCalledTimes(1) expect(ProductCollectionServiceMock.addProducts).toHaveBeenCalledTimes(1)
expect( expect(ProductCollectionServiceMock.addProducts).toHaveBeenCalledWith(
ProductCollectionServiceMock.addProducts IdMap.getId("col"),
).toHaveBeenCalledWith(IdMap.getId("col"), ["prod_1", "prod_2"]) ["prod_1", "prod_2"]
)
}) })
}) })
@@ -60,7 +61,9 @@ describe("POST /admin/collections/:id/products/batch", () => {
}) })
it("throws error", () => { 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"
)
}) })
}) })
@@ -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[]
}
@@ -10,6 +10,10 @@ export default (app) => {
route.get("/:id", middlewares.wrap(require("./get-customer-group").default)) route.get("/:id", middlewares.wrap(require("./get-customer-group").default))
route.post("/", middlewares.wrap(require("./create-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( route.delete(
"/:id/customers/batch", "/:id/customers/batch",
middlewares.wrap(require("./delete-customers-batch").default) middlewares.wrap(require("./delete-customers-batch").default)
+3 -1
View File
@@ -69,7 +69,9 @@ export class Customer {
referencedColumnName: "id", referencedColumnName: "id",
}, },
}) })
@ManyToMany(() => CustomerGroup, (cg) => cg.customers, { cascade: true }) @ManyToMany(() => CustomerGroup, (cg) => cg.customers, {
onDelete: "CASCADE",
})
groups: CustomerGroup[] groups: CustomerGroup[]
@CreateDateColumn({ type: resolveDbType("timestamptz") }) @CreateDateColumn({ type: resolveDbType("timestamptz") })
@@ -3,6 +3,27 @@ import { CustomerGroup } from "../models/customer-group"
@EntityRepository(CustomerGroup) @EntityRepository(CustomerGroup)
export class CustomerGroupRepository extends Repository<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( async removeCustomers(
groupId: string, groupId: string,
customerIds: string[] customerIds: string[]
@@ -1,3 +1,4 @@
import { MedusaError } from "medusa-core-utils"
import { IdMap } from "medusa-test-utils" import { IdMap } from "medusa-test-utils"
export const ProductCollectionServiceMock = { export const ProductCollectionServiceMock = {
@@ -23,7 +24,10 @@ export const ProductCollectionServiceMock = {
products: product_ids.map((i) => ({ id: i })), 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()), removeProducts: jest.fn().mockReturnValue(Promise.resolve()),
list: jest.fn().mockImplementation((data) => { list: jest.fn().mockImplementation((data) => {
@@ -282,18 +282,6 @@ describe("CustomerService", () => {
jest.clearAllMocks() 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", () => { describe("updateAddress", () => {
const addressRepository = MockRepository({ const addressRepository = MockRepository({
findOne: (query) => { findOne: (query) => {
@@ -417,3 +405,4 @@ describe("CustomerService", () => {
}) })
}) })
}) })
})
+65 -1
View File
@@ -1,9 +1,11 @@
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
import { DeepPartial, EntityManager } from "typeorm" import { DeepPartial, EntityManager } from "typeorm"
import { CustomerService } from "."
import { CustomerGroup } from ".." import { CustomerGroup } from ".."
import { CustomerGroupRepository } from "../repositories/customer-group" import { CustomerGroupRepository } from "../repositories/customer-group"
import { FindConfig } from "../types/common" import { FindConfig } from "../types/common"
import { formatException } from "../utils/exception-formatter"
import { import {
CustomerGroupUpdate, CustomerGroupUpdate,
FilterableCustomerGroupProps, FilterableCustomerGroupProps,
@@ -12,21 +14,33 @@ import {
type CustomerGroupConstructorProps = { type CustomerGroupConstructorProps = {
manager: EntityManager manager: EntityManager
customerGroupRepository: typeof CustomerGroupRepository customerGroupRepository: typeof CustomerGroupRepository
customerService: CustomerService
} }
/**
* Provides layer to manipulate discounts.
* @implements {BaseService}
*/
class CustomerGroupService extends BaseService { class CustomerGroupService extends BaseService {
private manager_: EntityManager private manager_: EntityManager
private customerGroupRepository_: typeof CustomerGroupRepository private customerGroupRepository_: typeof CustomerGroupRepository
private customerService_: CustomerService
constructor({ constructor({
manager, manager,
customerGroupRepository, customerGroupRepository,
customerService,
}: CustomerGroupConstructorProps) { }: CustomerGroupConstructorProps) {
super() super()
this.manager_ = manager this.manager_ = manager
this.customerGroupRepository_ = customerGroupRepository this.customerGroupRepository_ = customerGroupRepository
/** @private @const {CustomerGroupService} */
this.customerService_ = customerService
} }
withTransaction(transactionManager: EntityManager): CustomerGroupService { withTransaction(transactionManager: EntityManager): CustomerGroupService {
@@ -37,6 +51,7 @@ class CustomerGroupService extends BaseService {
const cloned = new CustomerGroupService({ const cloned = new CustomerGroupService({
manager: transactionManager, manager: transactionManager,
customerGroupRepository: this.customerGroupRepository_, customerGroupRepository: this.customerGroupRepository_,
customerService: this.customerService_,
}) })
cloned.transactionManager_ = transactionManager cloned.transactionManager_ = transactionManager
@@ -56,7 +71,7 @@ class CustomerGroupService extends BaseService {
if (!customerGroup) { if (!customerGroup) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_FOUND, 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. * Update a customer group.
* *
* @param {string} customerGroupId - id of the customer group * @param {string} customerGroupId - id of the customer group
* @param {CustomerGroupUpdate} update - customer group partial data * @param {CustomerGroupUpdate} update - customer group partial data
* @returns resulting customer group
*/ */
async update( async update(
customerGroupId: string, customerGroupId: string,
+10 -10
View File
@@ -4,6 +4,7 @@ import { MedusaError, Validator } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
import Scrypt from "scrypt-kdf" import Scrypt from "scrypt-kdf"
import { Brackets, ILike } from "typeorm" import { Brackets, ILike } from "typeorm"
import { formatException } from "../utils/exception-formatter"
/** /**
* Provides layer to manipulate customers. * Provides layer to manipulate customers.
@@ -21,7 +22,6 @@ class CustomerService extends BaseService {
customerRepository, customerRepository,
eventBusService, eventBusService,
addressRepository, addressRepository,
customerGroupService,
}) { }) {
super() super()
@@ -36,8 +36,6 @@ class CustomerService extends BaseService {
/** @private @const {AddressRepository} */ /** @private @const {AddressRepository} */
this.addressRepository_ = addressRepository this.addressRepository_ = addressRepository
this.customerGroupService_ = customerGroupService
} }
withTransaction(transactionManager) { withTransaction(transactionManager) {
@@ -63,9 +61,7 @@ class CustomerService extends BaseService {
* @return {string} the validated email * @return {string} the validated email
*/ */
validateEmail_(email) { validateEmail_(email) {
const schema = Validator.string() const schema = Validator.string().email().required()
.email()
.required()
const { value, error } = schema.validate(email) const { value, error } = schema.validate(email)
if (error) { if (error) {
throw new MedusaError( throw new MedusaError(
@@ -382,7 +378,8 @@ class CustomerService extends BaseService {
* @return {Promise} resolves to the update result. * @return {Promise} resolves to the update result.
*/ */
async update(customerId, update) { async update(customerId, update) {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(
async (manager) => {
const customerRepository = manager.getCustomRepository( const customerRepository = manager.getCustomRepository(
this.customerRepository_ this.customerRepository_
) )
@@ -424,8 +421,7 @@ class CustomerService extends BaseService {
} }
if (groups) { if (groups) {
const id = groups.map((g) => g.id) customer.groups = groups
customer.groups = await this.customerGroupService_.list({ id })
} }
const updated = await customerRepository.save(customer) const updated = await customerRepository.save(customer)
@@ -434,7 +430,11 @@ class CustomerService extends BaseService {
.withTransaction(manager) .withTransaction(manager)
.emit(CustomerService.Events.UPDATED, updated) .emit(CustomerService.Events.UPDATED, updated)
return updated return updated
}) },
async (error) => {
throw formatException(error)
}
)
} }
/** /**
+5 -1
View File
@@ -2,6 +2,7 @@ import { BaseService } from "medusa-interfaces"
import { Validator, MedusaError } from "medusa-core-utils" import { Validator, MedusaError } from "medusa-core-utils"
import { parse, toSeconds } from "iso8601-duration" import { parse, toSeconds } from "iso8601-duration"
import { Brackets, ILike } from "typeorm" import { Brackets, ILike } from "typeorm"
import { formatException } from "../utils/exception-formatter"
/** /**
* Provides layer to manipulate discounts. * Provides layer to manipulate discounts.
@@ -182,7 +183,7 @@ class DiscountService extends BaseService {
"Fixed discounts can have one region" "Fixed discounts can have one region"
) )
} }
try {
if (discount.regions) { if (discount.regions) {
discount.regions = await Promise.all( discount.regions = await Promise.all(
discount.regions.map((regionId) => discount.regions.map((regionId) =>
@@ -200,6 +201,9 @@ class DiscountService extends BaseService {
const created = await discountRepo.create(discount) const created = await discountRepo.create(discount)
const result = await discountRepo.save(created) const result = await discountRepo.save(created)
return result return result
} catch (error) {
throw formatException(error)
}
}) })
} }
@@ -1,6 +1,7 @@
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
import { Brackets, ILike } from "typeorm" import { Brackets, ILike } from "typeorm"
import { formatException } from "../utils/exception-formatter"
/** /**
* Provides layer to manipulate product collections. * Provides layer to manipulate product collections.
@@ -106,8 +107,12 @@ class ProductCollectionService extends BaseService {
this.productCollectionRepository_ this.productCollectionRepository_
) )
const productCollection = collectionRepo.create(collection) try {
return collectionRepo.save(productCollection) const productCollection = await collectionRepo.create(collection)
return await collectionRepo.save(productCollection)
} catch (error) {
throw formatException(error)
}
}) })
} }
@@ -166,6 +171,7 @@ class ProductCollectionService extends BaseService {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_) const productRepo = manager.getCustomRepository(this.productRepository_)
try {
const { id } = await this.retrieve(collectionId, { select: ["id"] }) const { id } = await this.retrieve(collectionId, { select: ["id"] })
await productRepo.bulkAddToCollection(productIds, id) await productRepo.bulkAddToCollection(productIds, id)
@@ -173,6 +179,9 @@ class ProductCollectionService extends BaseService {
return await this.retrieve(id, { return await this.retrieve(id, {
relations: ["products"], relations: ["products"],
}) })
} catch (error) {
throw formatException(error)
}
}) })
} }
+8 -1
View File
@@ -1,6 +1,7 @@
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
import { Brackets } from "typeorm" import { Brackets } from "typeorm"
import { formatException } from "../utils/exception-formatter"
/** /**
* Provides layer to manipulate products. * Provides layer to manipulate products.
@@ -378,6 +379,7 @@ class ProductService extends BaseService {
rest.discountable = false rest.discountable = false
} }
try {
let product = productRepo.create(rest) let product = productRepo.create(rest)
if (images) { if (images) {
@@ -402,7 +404,9 @@ class ProductService extends BaseService {
}) })
) )
const result = await this.retrieve(product.id, { relations: ["options"] }) const result = await this.retrieve(product.id, {
relations: ["options"],
})
await this.eventBus_ await this.eventBus_
.withTransaction(manager) .withTransaction(manager)
@@ -410,6 +414,9 @@ class ProductService extends BaseService {
id: result.id, id: result.id,
}) })
return result return result
} catch (error) {
throw formatException(error)
}
}) })
} }
@@ -13,6 +13,7 @@ export class CustomerGroupsBatchCustomer {
@IsString() @IsString()
id: string id: string
} }
export class CustomerGroupUpdate { export class CustomerGroupUpdate {
name?: string name?: string
metadata?: object metadata?: object
@@ -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
}
}