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:
1 parent
562a1b427a
commit
4b4463f0e2
21 files changed
+638
-251
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)
|
||||||
|
|||||||
@@ -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,7 +1,8 @@
|
|||||||
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
export const ProductCollectionServiceMock = {
|
export const ProductCollectionServiceMock = {
|
||||||
withTransaction: function() {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
create: jest.fn().mockImplementation((data) => {
|
create: jest.fn().mockImplementation((data) => {
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import CustomerService from "../customer"
|
|||||||
|
|
||||||
const eventBusService = {
|
const eventBusService = {
|
||||||
emit: jest.fn(),
|
emit: jest.fn(),
|
||||||
withTransaction: function() {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerGroupService = {
|
const customerGroupService = {
|
||||||
withTransaction: function() {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
list: jest.fn().mockImplementation(() => Promise.resolve()),
|
list: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
@@ -282,42 +282,43 @@ describe("CustomerService", () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calls `customerGroupService.list` if `groups` prop is received as a param", async () => {
|
describe("updateAddress", () => {
|
||||||
await customerService.update(IdMap.getId("ironman"), {
|
const addressRepository = MockRepository({
|
||||||
groups: [{ id: "group-id" }],
|
findOne: (query) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
id: IdMap.getId("hollywood-boulevard"),
|
||||||
|
address_1: "Hollywood Boulevard 2",
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(customerGroupService.list).toBeCalledTimes(1)
|
const customerService = new CustomerService({
|
||||||
expect(customerGroupService.list).toBeCalledWith({ id: ["group-id"] })
|
manager: MockManager,
|
||||||
|
addressRepository,
|
||||||
|
})
|
||||||
|
|
||||||
expect(customerRepository.save).toBeCalledTimes(1)
|
beforeEach(async () => {
|
||||||
})
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("updateAddress", () => {
|
it("successfully updates address", async () => {
|
||||||
const addressRepository = MockRepository({
|
await customerService.updateAddress(
|
||||||
findOne: (query) => {
|
IdMap.getId("ironman"),
|
||||||
return Promise.resolve({
|
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"),
|
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",
|
first_name: "Tony",
|
||||||
last_name: "Stark",
|
last_name: "Stark",
|
||||||
address_1: "Hollywood Boulevard 1",
|
address_1: "Hollywood Boulevard 1",
|
||||||
@@ -325,94 +326,82 @@ describe("CustomerService", () => {
|
|||||||
country_code: "us",
|
country_code: "us",
|
||||||
postal_code: "90046",
|
postal_code: "90046",
|
||||||
phone: "+1 (222) 333 4444",
|
phone: "+1 (222) 333 4444",
|
||||||
}
|
})
|
||||||
)
|
})
|
||||||
|
|
||||||
expect(addressRepository.save).toBeCalledTimes(1)
|
it("throws on invalid address", async () => {
|
||||||
expect(addressRepository.save).toBeCalledWith({
|
await expect(
|
||||||
id: IdMap.getId("hollywood-boulevard"),
|
customerService.updateAddress(
|
||||||
first_name: "Tony",
|
IdMap.getId("ironman"),
|
||||||
last_name: "Stark",
|
IdMap.getId("hollywood-boulevard"),
|
||||||
address_1: "Hollywood Boulevard 1",
|
{
|
||||||
city: "Los Angeles",
|
first_name: "Tony",
|
||||||
country_code: "us",
|
last_name: "Stark",
|
||||||
postal_code: "90046",
|
country_code: "us",
|
||||||
phone: "+1 (222) 333 4444",
|
unknown: "key",
|
||||||
|
address_1: "Hollywood",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).rejects.toThrow("The address is not valid")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throws on invalid address", async () => {
|
describe("removeAddress", () => {
|
||||||
await expect(
|
const addressRepository = MockRepository({
|
||||||
customerService.updateAddress(
|
findOne: (query) => {
|
||||||
IdMap.getId("ironman"),
|
return Promise.resolve({
|
||||||
IdMap.getId("hollywood-boulevard"),
|
id: IdMap.getId("hollywood-boulevard"),
|
||||||
{
|
address_1: "Hollywood Boulevard 2",
|
||||||
first_name: "Tony",
|
})
|
||||||
last_name: "Stark",
|
},
|
||||||
country_code: "us",
|
})
|
||||||
unknown: "key",
|
|
||||||
address_1: "Hollywood",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).rejects.toThrow("The address is not valid")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("removeAddress", () => {
|
const customerService = new CustomerService({
|
||||||
const addressRepository = MockRepository({
|
manager: MockManager,
|
||||||
findOne: (query) => {
|
addressRepository,
|
||||||
return Promise.resolve({
|
})
|
||||||
|
|
||||||
|
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"),
|
id: IdMap.getId("hollywood-boulevard"),
|
||||||
address_1: "Hollywood Boulevard 2",
|
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", () => {
|
describe("delete", () => {
|
||||||
const customerRepository = MockRepository({
|
const customerRepository = MockRepository({
|
||||||
findOne: (query) => {
|
findOne: (query) => {
|
||||||
return Promise.resolve({ id: IdMap.getId("ironman") })
|
return Promise.resolve({ id: IdMap.getId("ironman") })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const customerService = new CustomerService({
|
const customerService = new CustomerService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
customerRepository,
|
customerRepository,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully deletes customer", async () => {
|
it("successfully deletes customer", async () => {
|
||||||
await customerService.delete(IdMap.getId("ironman"))
|
await customerService.delete(IdMap.getId("ironman"))
|
||||||
|
|
||||||
expect(customerRepository.softRemove).toBeCalledTimes(1)
|
expect(customerRepository.softRemove).toBeCalledTimes(1)
|
||||||
expect(customerRepository.softRemove).toBeCalledWith({
|
expect(customerRepository.softRemove).toBeCalledWith({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,59 +378,63 @@ 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_(
|
||||||
const customerRepository = manager.getCustomRepository(
|
async (manager) => {
|
||||||
this.customerRepository_
|
const customerRepository = manager.getCustomRepository(
|
||||||
)
|
this.customerRepository_
|
||||||
const addrRepo = manager.getCustomRepository(this.addressRepository_)
|
)
|
||||||
|
const addrRepo = manager.getCustomRepository(this.addressRepository_)
|
||||||
|
|
||||||
const customer = await this.retrieve(customerId)
|
const customer = await this.retrieve(customerId)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
metadata,
|
metadata,
|
||||||
billing_address,
|
billing_address,
|
||||||
billing_address_id,
|
billing_address_id,
|
||||||
groups,
|
groups,
|
||||||
...rest
|
...rest
|
||||||
} = update
|
} = update
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
customer.metadata = this.setMetadata_(customer, 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 (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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,24 +183,27 @@ 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) =>
|
||||||
this.regionService_.withTransaction(manager).retrieve(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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,13 +171,17 @@ 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_)
|
||||||
|
|
||||||
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, {
|
return await this.retrieve(id, {
|
||||||
relations: ["products"],
|
relations: ["products"],
|
||||||
})
|
})
|
||||||
|
} 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 } from "typeorm"
|
import { Brackets } from "typeorm"
|
||||||
|
import { formatException } from "../utils/exception-formatter"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides layer to manipulate products.
|
* Provides layer to manipulate products.
|
||||||
@@ -378,38 +379,44 @@ class ProductService extends BaseService {
|
|||||||
rest.discountable = false
|
rest.discountable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let product = productRepo.create(rest)
|
try {
|
||||||
|
let product = productRepo.create(rest)
|
||||||
|
|
||||||
if (images) {
|
if (images) {
|
||||||
product.images = await this.upsertImages_(images)
|
product.images = await this.upsertImages_(images)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags) {
|
if (tags) {
|
||||||
product.tags = await this.upsertProductTags_(tags)
|
product.tags = await this.upsertProductTags_(tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof type !== `undefined`) {
|
if (typeof type !== `undefined`) {
|
||||||
product.type_id = await this.upsertProductType_(type)
|
product.type_id = await this.upsertProductType_(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
product = await productRepo.save(product)
|
product = await productRepo.save(product)
|
||||||
|
|
||||||
product.options = await Promise.all(
|
product.options = await Promise.all(
|
||||||
options.map(async (o) => {
|
options.map(async (o) => {
|
||||||
const res = optionRepo.create({ ...o, product_id: product.id })
|
const res = optionRepo.create({ ...o, product_id: product.id })
|
||||||
await optionRepo.save(res)
|
await optionRepo.save(res)
|
||||||
return 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)
|
||||||
await this.eventBus_
|
.emit(ProductService.Events.CREATED, {
|
||||||
.withTransaction(manager)
|
id: result.id,
|
||||||
.emit(ProductService.Events.CREATED, {
|
})
|
||||||
id: result.id,
|
return result
|
||||||
})
|
} catch (error) {
|
||||||
return result
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user