From 65d3222973dec0d6c5df080648f02773b2a9cfbb Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:52:32 +0200 Subject: [PATCH] chore: Move customer + customer group integration tests and fixes issues (#7577) * chore: Move customer + customer group and fixes issues * remove /customer sendpoint --- .../api/__tests__/admin/customer-groups.js | 808 ------------------ .../api/__tests__/admin/customer.js | 441 ---------- .../admin/customer-group.spec.ts | 313 +++++++ .../__tests__/customer/admin/customer.spec.ts | 401 +++++++++ .../api/admin/customer-groups/[id]/route.ts | 19 +- .../api/admin/customer-groups/middlewares.ts | 6 +- .../api/admin/customer-groups/query-config.ts | 1 + .../api/admin/customer-groups/validators.ts | 4 +- .../src/api/admin/customers/[id]/route.ts | 2 +- .../src/api/admin/customers/query-config.ts | 18 + .../medusa/src/api/admin/customers/route.ts | 4 +- .../migrations/.snapshot-medusa-customer.json | 12 +- .../src/migrations/Migration20240602110946.ts | 19 + .../customer/src/models/customer-group.ts | 25 +- 14 files changed, 804 insertions(+), 1269 deletions(-) delete mode 100644 integration-tests/api/__tests__/admin/customer-groups.js delete mode 100644 integration-tests/api/__tests__/admin/customer.js create mode 100644 integration-tests/http/__tests__/customer-group/admin/customer-group.spec.ts create mode 100644 integration-tests/http/__tests__/customer/admin/customer.spec.ts create mode 100644 packages/modules/customer/src/migrations/Migration20240602110946.ts diff --git a/integration-tests/api/__tests__/admin/customer-groups.js b/integration-tests/api/__tests__/admin/customer-groups.js deleted file mode 100644 index 1a136c2966..0000000000 --- a/integration-tests/api/__tests__/admin/customer-groups.js +++ /dev/null @@ -1,808 +0,0 @@ -const path = require("path") - -const { IdMap } = require("medusa-test-utils") - -const setupServer = require("../../../environment-helpers/setup-server") -const { useApi } = require("../../../environment-helpers/use-api") -const { useDb, initDb } = require("../../../environment-helpers/use-db") - -const customerSeeder = require("../../../helpers/customer-seeder") -const adminSeeder = require("../../../helpers/admin-seeder") -const { - DiscountRuleType, - AllocationType, - DiscountConditionType, - DiscountConditionOperator, -} = require("@medusajs/medusa") -const { simpleDiscountFactory } = require("../../../factories") - -jest.setTimeout(30000) - -const adminReqConfig = { - headers: { - "x-medusa-access-token": "test_token", - }, -} - -describe("/admin/customer-groups", () => { - let medusaProcess - let dbConnection - - beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) - dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd }) - }) - - afterAll(async () => { - const db = useDb() - await db.shutdown() - medusaProcess.kill() - }) - - describe("POST /admin/customer-groups", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("creates customer group", async () => { - const api = useApi() - - const payload = { - name: "test group", - } - - const response = await api.post( - "/admin/customer-groups", - payload, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.customer_group).toEqual( - expect.objectContaining({ - name: "test group", - }) - ) - }) - - it("Fails to create duplciate customer group", async () => { - expect.assertions(3) - const api = useApi() - - const payload = { - name: "vip-customers", - } - - await api - .post("/admin/customer-groups", payload, adminReqConfig) - .catch((err) => { - expect(err.response.status).toEqual(422) - expect(err.response.data.type).toEqual("duplicate_error") - expect(err.response.data.message).toEqual( - "Key (name)=(vip-customers) already exists." - ) - }) - }) - }) - - describe("DELETE /admin/customer-groups", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("removes customer group from get endpoint", async () => { - expect.assertions(3) - - const api = useApi() - - const id = "customer-group-1" - - const deleteResponse = await api.delete( - `/admin/customer-groups/${id}`, - adminReqConfig - ) - - expect(deleteResponse.data).toEqual({ - id: id, - object: "customer_group", - deleted: true, - }) - - await api - .get(`/admin/customer-groups/${id}`, adminReqConfig) - .catch((error) => { - expect(error.response.data.type).toEqual("not_found") - expect(error.response.data.message).toEqual( - `CustomerGroup with id ${id} was not found` - ) - }) - }) - - it("removes customer group from customer upon deletion", async () => { - expect.assertions(3) - - const api = useApi() - - const id = "test-group-delete" - - const customerRes_preDeletion = await api.get( - `/admin/customers/test-customer-delete-cg?expand=groups`, - adminReqConfig - ) - - expect(customerRes_preDeletion.data.customer).toEqual( - expect.objectContaining({ - groups: [ - expect.objectContaining({ - id: "test-group-delete", - name: "test-group-delete", - }), - ], - }) - ) - - const deleteResponse = await api - .delete(`/admin/customer-groups/${id}`, adminReqConfig) - .catch((err) => console.log(err)) - - expect(deleteResponse.data).toEqual({ - id: id, - object: "customer_group", - deleted: true, - }) - - const customerRes = await api.get( - `/admin/customers/test-customer-delete-cg?expand=groups`, - adminReqConfig - ) - - expect(customerRes.data.customer).toEqual( - expect.objectContaining({ - groups: [], - }) - ) - }) - }) - - describe("GET /admin/customer-groups/{id}/customers", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("lists customers in group and count", async () => { - const api = useApi() - - const response = await api - .get("/admin/customer-groups/test-group-5/customers", adminReqConfig) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(3) - expect(response.data.customers).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-customer-5", - }), - expect.objectContaining({ - id: "test-customer-6", - }), - expect.objectContaining({ - id: "test-customer-7", - }), - ]) - ) - }) - }) - - describe("POST /admin/customer-groups/{id}/customers/batch", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - 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, - adminReqConfig - ) - - 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", - adminReqConfig - ) - - 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, - adminReqConfig - ) - .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, - adminReqConfig - ) - .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, - adminReqConfig - ) - .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", - adminReqConfig - ) - - 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 () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("updates group name & metadata", async () => { - const api = useApi() - - const id = "customer-group-2" - - const body = { - name: "vip-customers-v2", - metadata: { - metaKey1: `metaValue1`, - }, - } - - const response = await api.post( - `/admin/customer-groups/${id}`, - body, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.customer_group).toEqual( - expect.objectContaining({ - id: "customer-group-2", - name: "vip-customers-v2", - metadata: { - data1: "value1", - metaKey1: `metaValue1`, - }, - }) - ) - expect(response.data.customer_group).not.toHaveProperty("customers") - }) - - it("deletes `metadata` nested key", async () => { - const api = useApi() - - const id = "customer-group-2" - // already has some metadata initially - - const body = { - name: "vip-customers-v2", - metadata: { - data1: null, // delete - data2: "val2", // insert - }, - } - - const response = await api - .post( - `/admin/customer-groups/${id}?expand=customers`, - body, - adminReqConfig - ) - .catch(console.log) - - expect(response.status).toEqual(200) - expect(response.data.customer_group).toEqual( - expect.objectContaining({ - id: "customer-group-2", - name: "vip-customers-v2", - metadata: { data1: null, data2: "val2" }, - customers: [], - }) - ) - }) - }) - - describe("GET /admin/customer-groups", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("retreive a list of customer groups", async () => { - const api = useApi() - - const response = await api - .get( - `/admin/customer-groups?limit=5&offset=2&expand=customers&order=created_at`, - adminReqConfig - ) - .catch(console.log) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(7) - expect(response.data.customer_groups.length).toEqual(5) - expect(response.data.customer_groups[0]).toEqual( - expect.objectContaining({ id: "customer-group-3" }) - ) - expect(response.data.customer_groups[0]).toHaveProperty("customers") - }) - - it("retreive a list of customer groups filtered by name using `q` param", async () => { - const api = useApi() - - const response = await api.get( - `/admin/customer-groups?q=vip-customers`, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.customer_groups).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: "customer-group-1" }), - ]) - ) - expect(response.data.customer_groups[0]).not.toHaveProperty("customers") - }) - - it("lists customers in group filtered by discount condition id and count", async () => { - const api = useApi() - - const resCustomerGroup = await api.get( - "/admin/customer-groups", - adminReqConfig - ) - - const customerGroup1 = resCustomerGroup.data.customer_groups[0] - const customerGroup2 = resCustomerGroup.data.customer_groups[2] - - const buildDiscountData = (code, conditionId, groups) => { - return { - code, - rule: { - type: DiscountRuleType.PERCENTAGE, - value: 10, - allocation: AllocationType.TOTAL, - conditions: [ - { - id: conditionId, - type: DiscountConditionType.CUSTOMER_GROUPS, - operator: DiscountConditionOperator.IN, - customer_groups: groups, - }, - ], - }, - } - } - - const discountConditionId = IdMap.getId( - "discount-condition-customer-group-1" - ) - await simpleDiscountFactory( - dbConnection, - buildDiscountData("code-1", discountConditionId, [customerGroup1.id]) - ) - - const discountConditionId2 = IdMap.getId( - "discount-condition-customer-group-2" - ) - await simpleDiscountFactory( - dbConnection, - buildDiscountData("code-2", discountConditionId2, [customerGroup2.id]) - ) - - let res = await api.get( - `/admin/customer-groups?discount_condition_id=${discountConditionId}`, - adminReqConfig - ) - - expect(res.status).toEqual(200) - expect(res.data.customer_groups).toHaveLength(1) - expect(res.data.customer_groups).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: customerGroup1.id }), - ]) - ) - - res = await api.get( - `/admin/customer-groups?discount_condition_id=${discountConditionId2}`, - adminReqConfig - ) - - expect(res.status).toEqual(200) - expect(res.data.customer_groups).toHaveLength(1) - expect(res.data.customer_groups).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: customerGroup2.id }), - ]) - ) - - res = await api.get(`/admin/customer-groups`, adminReqConfig) - - expect(res.status).toEqual(200) - expect(res.data.customer_groups).toHaveLength(7) - expect(res.data.customer_groups).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: customerGroup1.id }), - expect.objectContaining({ id: customerGroup2.id }), - ]) - ) - }) - }) - - describe("GET /admin/customer-groups/:id", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("gets customer group", async () => { - const api = useApi() - - const id = "customer-group-1" - - const response = await api.get( - `/admin/customer-groups/${id}`, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.customer_group).toEqual( - expect.objectContaining({ - id: "customer-group-1", - name: "vip-customers", - }) - ) - expect(response.data.customer_group).not.toHaveProperty("customers") - }) - - it("gets customer group with `customers` prop", async () => { - const api = useApi() - - const id = "customer-group-1" - - const response = await api.get( - `/admin/customer-groups/${id}?expand=customers`, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.customer_group).toEqual( - expect.objectContaining({ - id: "customer-group-1", - name: "vip-customers", - }) - ) - expect(response.data.customer_group.customers).toEqual([]) - }) - - it("throws error when a customer group doesn't exist", async () => { - const api = useApi() - - const id = "test-group-000" - - await api - .get(`/admin/customer-groups/${id}`, adminReqConfig) - .catch((err) => { - expect(err.response.status).toEqual(404) - expect(err.response.data.type).toEqual("not_found") - expect(err.response.data.message).toEqual( - `CustomerGroup with id ${id} was not found` - ) - }) - }) - }) - - describe("DELETE /admin/customer-groups/{id}/batch", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("removes multiple customers from a group", async () => { - const api = useApi() - - const payload = { - customer_ids: [{ id: "test-customer-5" }, { id: "test-customer-6" }], - } - - const batchAddResponse = await api - .delete("/admin/customer-groups/test-group-5/customers/batch", { - ...adminReqConfig, - data: payload, - }) - .catch((err) => console.log(err)) - - expect(batchAddResponse.status).toEqual(200) - expect(batchAddResponse.data).toEqual({ - customer_group: expect.objectContaining({ - id: "test-group-5", - name: "test-group-5", - }), - }) - - const getCustomerResponse = await api.get( - "/admin/customers?expand=groups", - adminReqConfig - ) - - expect(getCustomerResponse.data.customers).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-customer-5", - groups: [], - }), - expect.objectContaining({ - id: "test-customer-6", - groups: [], - }), - ]) - ) - }) - - it("removes customers from only one group", async () => { - const api = useApi() - - const payload = { - customer_ids: [{ id: "test-customer-7" }], - } - - const batchAddResponse = await api - .delete("/admin/customer-groups/test-group-5/customers/batch", { - ...adminReqConfig, - data: payload, - }) - .catch((err) => console.log(err)) - - expect(batchAddResponse.status).toEqual(200) - expect(batchAddResponse.data).toEqual({ - customer_group: expect.objectContaining({ - id: "test-group-5", - name: "test-group-5", - }), - }) - - const getCustomerResponse = await api.get( - "/admin/customers/test-customer-7?expand=groups", - adminReqConfig - ) - - expect(getCustomerResponse.data.customer).toEqual( - expect.objectContaining({ - id: "test-customer-7", - groups: [ - expect.objectContaining({ - id: "test-group-6", - name: "test-group-6", - }), - ], - }) - ) - }) - - it("removes only select customers from a group", async () => { - const api = useApi() - - // 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 = { - customer_ids: [{ id: "test-customer-5" }], - } - - await api.delete("/admin/customer-groups/test-group-5/customers/batch", { - ...adminReqConfig, - data: payload, - }) - - // check that customer-1 is only added once and that customer-2 is added correctly - const getCustomerResponse = await api - .get("/admin/customers?expand=groups", adminReqConfig) - .catch((err) => console.log(err)) - - expect(getCustomerResponse.data.customers).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-customer-5", - groups: [], - }), - expect.objectContaining({ - id: "test-customer-6", - groups: [ - expect.objectContaining({ - name: "test-group-5", - id: "test-group-5", - }), - ], - }), - ]) - ) - }) - - it("removes customers from a group idempotently", async () => { - const api = useApi() - - // 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 = { - customer_ids: [{ id: "test-customer-5" }], - } - - await api.delete("/admin/customer-groups/test-group-5/customers/batch", { - ...adminReqConfig, - data: payload, - }) - - const idempotentRes = await api.delete( - "/admin/customer-groups/test-group-5/customers/batch", - { - ...adminReqConfig, - data: payload, - } - ) - - expect(idempotentRes.status).toEqual(200) - expect(idempotentRes.data).toEqual({ - customer_group: expect.objectContaining({ - id: "test-group-5", - name: "test-group-5", - }), - }) - }) - }) -}) diff --git a/integration-tests/api/__tests__/admin/customer.js b/integration-tests/api/__tests__/admin/customer.js deleted file mode 100644 index 94208001d8..0000000000 --- a/integration-tests/api/__tests__/admin/customer.js +++ /dev/null @@ -1,441 +0,0 @@ -const path = require("path") - -const setupServer = require("../../../environment-helpers/setup-server") -const { useApi } = require("../../../environment-helpers/use-api") -const { useDb, initDb } = require("../../../environment-helpers/use-db") - -const customerSeeder = require("../../../helpers/customer-seeder") -const adminSeeder = require("../../../helpers/admin-seeder") - -jest.setTimeout(30000) - -describe("/admin/customers", () => { - let medusaProcess - let dbConnection - - beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) - dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd }) - }) - - afterAll(async () => { - const db = useDb() - await db.shutdown() - - medusaProcess.kill() - }) - - describe("GET /admin/customers", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("lists customers and query count", async () => { - const api = useApi() - - const response = await api - .get("/admin/customers", { - headers: { - "x-medusa-access-token": "test_token", - }, - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(8) - expect(response.data.customers).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-customer-1", - }), - expect.objectContaining({ - id: "test-customer-2", - }), - expect.objectContaining({ - id: "test-customer-3", - }), - expect.objectContaining({ - id: "test-customer-has_account", - }), - ]) - ) - }) - - it("lists only registered customers", async () => { - const api = useApi() - - const response = await api - .get("/admin/customers?has_account=true", { - headers: { - "x-medusa-access-token": "test_token", - }, - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.customers).toEqual( - expect.not.arrayContaining([ - expect.objectContaining({ has_account: false }), - ]) - ) - }) - - it("lists customers in group and count", async () => { - const api = useApi() - - const response = await api - .get("/admin/customers?groups[]=test-group-5", { - headers: { - "x-medusa-access-token": "test_token", - }, - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(3) - expect(response.data.customers).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-customer-5", - }), - expect.objectContaining({ - id: "test-customer-6", - }), - expect.objectContaining({ - id: "test-customer-7", - }), - ]) - ) - }) - - it("lists customers with specific query", async () => { - const api = useApi() - - const response = await api - .get("/admin/customers?q=est2@", { - headers: { - "x-medusa-access-token": "test_token", - }, - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.customers).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-customer-2", - email: "test2@email.com", - }), - ]) - ) - }) - - it("lists customers with expand query", async () => { - const api = useApi() - - const response = await api - .get("/admin/customers?q=test1@email.com&expand=shipping_addresses", { - headers: { - "x-medusa-access-token": "test_token", - }, - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.customers).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-customer-1", - shipping_addresses: expect.arrayContaining([ - expect.objectContaining({ - id: "test-address", - first_name: "Lebron", - last_name: "James", - }), - ]), - }), - ]) - ) - }) - }) - - describe("POST /admin/customers", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("Correctly creates customer", async () => { - const api = useApi() - const response = await api - .post( - "/admin/customers", - { - first_name: "newf", - last_name: "newl", - email: "new@email.com", - password: "newpassword", - metadata: { foo: "bar" }, - }, - { - headers: { - "x-medusa-access-token": "test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(201) - expect(response.data.customer).toEqual( - expect.objectContaining({ - first_name: "newf", - last_name: "newl", - email: "new@email.com", - metadata: { foo: "bar" }, - }) - ) - }) - }) - - describe("POST /admin/customers/:id", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("Correctly updates customer", async () => { - const api = useApi() - const response = await api - .post( - "/admin/customers/test-customer-3", - { - first_name: "newf", - last_name: "newl", - email: "new@email.com", - metadata: { foo: "bar" }, - }, - { - headers: { - "x-medusa-access-token": "test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.customer).toEqual( - expect.objectContaining({ - first_name: "newf", - last_name: "newl", - email: "new@email.com", - metadata: { foo: "bar" }, - }) - ) - }) - - 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: { - "x-medusa-access-token": "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 - .post( - "/admin/customers/test-customer-3?expand=groups", - { - groups: [{ id: "test-group-4" }], - }, - { - headers: { - "x-medusa-access-token": "test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.customer.groups).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-group-4", - name: "test-group-4", - }), - ]) - ) - - // Delete all groups - - response = await api - .post( - "/admin/customers/test-customer-3?expand=groups", - { - groups: [], - }, - { - headers: { - "x-medusa-access-token": "test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.customer.groups.length).toEqual(0) - - // Adding a group to a customer with already existing groups. - - response = await api - .post( - "/admin/customers/test-customer-5?expand=groups", - { - groups: [{ id: "test-group-5" }, { id: "test-group-4" }], - }, - { - headers: { - "x-medusa-access-token": "test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.customer.groups.length).toEqual(2) - expect(response.data.customer.groups).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: "test-group-5", name: "test-group-5" }), - expect.objectContaining({ - id: "test-group-4", - name: "test-group-4", - }), - ]) - ) - }) - }) - - describe("GET /admin/customers/:id", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await customerSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("fetches a customer", async () => { - const api = useApi() - - const response = await api - .get("/admin/customers/test-customer-1", { - headers: { - "x-medusa-access-token": "test_token", - }, - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.customer).toMatchSnapshot({ - id: expect.any(String), - shipping_addresses: [ - { - id: "test-address", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - created_at: expect.any(String), - updated_at: expect.any(String), - }) - }) - - it("fetches a customer with expand query", async () => { - const api = useApi() - - const response = await api - .get("/admin/customers/test-customer-1?expand=billing_address,groups", { - headers: { - "x-medusa-access-token": "test_token", - }, - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.customer).toMatchSnapshot({ - id: "test-customer-1", - billing_address: { - id: "test-address", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - groups: [], - created_at: expect.any(String), - updated_at: expect.any(String), - }) - }) - }) -}) diff --git a/integration-tests/http/__tests__/customer-group/admin/customer-group.spec.ts b/integration-tests/http/__tests__/customer-group/admin/customer-group.spec.ts new file mode 100644 index 0000000000..766c7ea1ca --- /dev/null +++ b/integration-tests/http/__tests__/customer-group/admin/customer-group.spec.ts @@ -0,0 +1,313 @@ +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" + +jest.setTimeout(30000) + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, api, getContainer }) => { + let customer1 + let group + + beforeEach(async () => { + const appContainer = getContainer() + await createAdminUser(dbConnection, adminHeaders, appContainer) + + group = ( + await api.post( + "/admin/customer-groups", + { + name: "vip-customers", + metadata: { + data1: "value1", + }, + }, + adminHeaders + ) + ).data.customer_group + + customer1 = ( + await api.post( + "/admin/customers", + { + email: "test1@email.com", + }, + adminHeaders + ) + ).data.customer + }) + + describe("POST /admin/customer-groups", () => { + it("creates customer group", async () => { + const payload = { + name: "test group", + } + + const response = await api.post( + "/admin/customer-groups", + payload, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer_group).toEqual( + expect.objectContaining({ + name: "test group", + }) + ) + }) + + it("should fail to create duplicate customer group", async () => { + expect.assertions(3) + + const payload = { + name: "vip-customers", + } + + await api + .post("/admin/customer-groups", payload, adminHeaders) + .catch((err) => { + console.log(err) + // BREAKING: Duplicate error is now 400 + expect(err.response.status).toEqual(400) + expect(err.response.data.type).toEqual("invalid_data") + expect(err.response.data.message).toEqual( + "Customer group with name: vip-customers, already exists." + ) + }) + }) + }) + + describe("DELETE /admin/customer-groups", () => { + it("should remove customer group", async () => { + expect.assertions(3) + + const deleteResponse = await api.delete( + `/admin/customer-groups/${group.id}`, + adminHeaders + ) + + expect(deleteResponse.data).toEqual({ + id: group.id, + object: "customer_group", + deleted: true, + }) + + await api + .get(`/admin/customer-groups/${group.id}`, adminHeaders) + .catch((error) => { + expect(error.response.data.type).toEqual("not_found") + expect(error.response.data.message).toEqual( + `Customer group with id: ${group.id} not found` + ) + }) + }) + + it("should remove customer group from customer upon deletion", async () => { + expect.assertions(3) + + await api.post( + `/admin/customer-groups/${group.id}/customers`, + { add: [customer1.id] }, + adminHeaders + ) + + const customerPreDeletion = await api.get( + `/admin/customers/${customer1.id}?fields=*groups`, + adminHeaders + ) + + expect(customerPreDeletion.data.customer).toEqual( + expect.objectContaining({ + groups: [ + expect.objectContaining({ + id: group.id, + }), + ], + }) + ) + + const deleteResponse = await api + .delete(`/admin/customer-groups/${group.id}`, adminHeaders) + .catch((err) => console.log(err)) + + expect(deleteResponse.data).toEqual({ + id: group.id, + object: "customer_group", + deleted: true, + }) + + const customerRes = await api.get( + `/admin/customers/${customer1.id}?fields=*groups`, + adminHeaders + ) + + expect(customerRes.data.customer).toEqual( + expect.objectContaining({ + groups: [], + }) + ) + }) + }) + + // BREAKING: This endpoint has been removed in favor of `GET /admin/customers?customer_group_id=...` + // Keeping this test to keep a record of it + describe("GET /admin/customer-groups/{id}/customers", () => { + it("should list customers in group and count", async () => { + await api.post( + `/admin/customer-groups/${group.id}/customers`, + { add: [customer1.id] }, + adminHeaders + ) + + const response = await api.get( + `/admin/customers?groups=${group.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.customers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: customer1.id, + }), + ]) + ) + }) + }) + + describe("POST /admin/customer-groups/:id", () => { + it("should update group name & metadata", async () => { + const body = { + name: "vip-customers-v2", + metadata: { + metaKey1: `metaValue1`, + }, + } + + const response = await api.post( + `/admin/customer-groups/${group.id}`, + body, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer_group).toEqual( + expect.objectContaining({ + id: group.id, + name: "vip-customers-v2", + metadata: { + data1: "value1", + metaKey1: `metaValue1`, + }, + }) + ) + }) + + it("should delete `metadata` nested key", async () => { + const body = { + name: "vip-customers-v2", + metadata: { + data1: null, // delete + data2: "val2", // insert + }, + } + + const response = await api.post( + `/admin/customer-groups/${group.id}?fields=*customers`, + body, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer_group).toEqual( + expect.objectContaining({ + id: group.id, + name: "vip-customers-v2", + metadata: { data1: null, data2: "val2" }, + customers: [], + }) + ) + }) + }) + + describe("GET /admin/customer-groups", () => { + it("should retreive a list of customer groups", async () => { + const response = await api.get( + `/admin/customer-groups?fields=*customers`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.customer_groups[0]).toEqual( + expect.objectContaining({ id: group.id }) + ) + expect(response.data.customer_groups[0]).toHaveProperty("customers") + }) + + it("should retrieve a list of customer groups filtered by name using `q` param", async () => { + const response = await api.get( + `/admin/customer-groups?q=vip-customers`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.customer_groups).toEqual([ + expect.objectContaining({ id: group.id }), + ]) + expect(response.data.customer_groups[0]).not.toHaveProperty("customers") + }) + }) + + describe("GET /admin/customer-groups/:id", () => { + it("should get customer group", async () => { + const response = await api.get( + `/admin/customer-groups/${group.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer_group).toEqual( + expect.objectContaining({ + id: group.id, + name: "vip-customers", + }) + ) + expect(response.data.customer_group).not.toHaveProperty("customers") + }) + + it("gets customer group with `customers` prop", async () => { + const response = await api.get( + `/admin/customer-groups/${group.id}?fields=*customers`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer_group).toEqual( + expect.objectContaining({ + id: group.id, + name: "vip-customers", + }) + ) + expect(response.data.customer_group.customers).toEqual([]) + }) + + it("throws error when a customer group doesn't exist", async () => { + await api + .get(`/admin/customer-groups/does-not-exist`, adminHeaders) + .catch((err) => { + expect(err.response.status).toEqual(404) + expect(err.response.data.type).toEqual("not_found") + expect(err.response.data.message).toEqual( + `Customer group with id: does-not-exist not found` + ) + }) + }) + }) + }, +}) diff --git a/integration-tests/http/__tests__/customer/admin/customer.spec.ts b/integration-tests/http/__tests__/customer/admin/customer.spec.ts new file mode 100644 index 0000000000..56f7713de5 --- /dev/null +++ b/integration-tests/http/__tests__/customer/admin/customer.spec.ts @@ -0,0 +1,401 @@ +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" + +jest.setTimeout(30000) + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, api, getContainer }) => { + let customer1 + let customer2 + let customer3 + let customer4 + let customer5 + beforeEach(async () => { + const appContainer = getContainer() + await createAdminUser(dbConnection, adminHeaders, appContainer) + + customer1 = ( + await api.post( + "/admin/customers", + { + email: "test1@email.com", + }, + adminHeaders + ) + ).data.customer + + customer2 = ( + await api.post( + "/admin/customers", + { + email: "test2@email.com", + }, + adminHeaders + ) + ).data.customer + + customer3 = ( + await api.post( + "/admin/customers", + { + email: "test3@email.com", + }, + adminHeaders + ) + ).data.customer + + customer4 = ( + await api.post( + "/admin/customers", + { + email: "test4@email.com", + }, + adminHeaders + ) + ).data.customer + }) + + describe("GET /admin/customers", () => { + it("should list customers and query count", async () => { + const response = await api + .get("/admin/customers", adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(4) + expect(response.data.customers).toEqual([ + expect.objectContaining({ + email: "test1@email.com", + }), + expect.objectContaining({ + email: "test2@email.com", + }), + expect.objectContaining({ + email: "test3@email.com", + }), + expect.objectContaining({ + email: "test4@email.com", + // BREAKING: You cannot create customers with an account directly. This will happen upon customer registration. + // has_account: true, + }), + ]) + }) + + it("should list customers in group and count", async () => { + const customerGroup = ( + await api.post( + "/admin/customer-groups", + { + name: "VIP", + }, + adminHeaders + ) + ).data.customer_group + + await api.post( + `/admin/customer-groups/${customerGroup.id}/customers`, + { + add: [customer1.id, customer2.id, customer3.id], + }, + adminHeaders + ) + + const response = await api.get( + `/admin/customers?groups[]=${customerGroup.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(3) + expect(response.data.customers).toEqual([ + expect.objectContaining({ + id: customer1.id, + }), + expect.objectContaining({ + id: customer2.id, + }), + expect.objectContaining({ + id: customer3.id, + }), + ]) + }) + + it("should list customers with specific query", async () => { + const response = await api.get("/admin/customers?q=est2@", adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.customers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + email: "test2@email.com", + }), + ]) + ) + }) + + it("should list customers with expand query", async () => { + await api.post( + `/admin/customers/${customer1.id}/addresses`, + { + first_name: "Lebron", + last_name: "James", + }, + adminHeaders + ) + // BREAKING: Customers no longer carry shipping_addresses, they have addresses + const response = await api.get( + "/admin/customers?q=test1@email.com&fields=*addresses", + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.customers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: customer1.id, + addresses: expect.arrayContaining([ + expect.objectContaining({ + first_name: "Lebron", + last_name: "James", + }), + ]), + }), + ]) + ) + }) + }) + + describe("POST /admin/customers", () => { + it("should create a customer", async () => { + const response = await api + .post( + "/admin/customers", + { + first_name: "newf", + last_name: "newl", + email: "new@email.com", + password: "newpassword", + metadata: { foo: "bar" }, + }, + adminHeaders + ) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.customer).toEqual( + expect.objectContaining({ + first_name: "newf", + last_name: "newl", + email: "new@email.com", + metadata: { foo: "bar" }, + }) + ) + }) + }) + + describe("POST /admin/customers/:id", () => { + it("should correctly update customer", async () => { + const response = await api + .post( + `/admin/customers/${customer3.id}`, + { + first_name: "newf", + last_name: "newl", + email: "new@email.com", + metadata: { foo: "bar" }, + }, + adminHeaders + ) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.customer).toEqual( + expect.objectContaining({ + first_name: "newf", + last_name: "newl", + email: "new@email.com", + metadata: { foo: "bar" }, + }) + ) + }) + + // BREAKING: You can no longer update groups on a customer directly. Use dedicated customer groups endpoint + it("should fail when adding a customer group which doesn't exist", async () => { + expect.assertions(3) + + await api + .post( + "/admin/customer-groups/does-not-exist/customers", + { + add: [customer1.id], + }, + adminHeaders + ) + .catch((error) => { + expect(error.response.status).toEqual(404) + expect(error.response.data.type).toEqual("not_found") + expect(error.response.data.message).toEqual( + "You tried to set relationship customer_group_id: does-not-exist, but such entity does not exist" + ) + }) + }) + + // BREAKING: You can no longer update groups on a customer directly. Use dedicated customer groups endpoint + it("should correctly update customer groups", async () => { + const customerGroup = ( + await api.post( + "/admin/customer-groups", + { + name: "VIP", + }, + adminHeaders + ) + ).data.customer_group + + await api.post( + `/admin/customer-groups/${customerGroup.id}/customers`, + { + add: [customer3.id], + }, + adminHeaders + ) + + let response = await api.get( + `/admin/customers/${customer3.id}?fields=*groups`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer.groups).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: customerGroup.id, + }), + ]) + ) + + // Adding a group to a customer with already existing groups. + + const otherCustomerGroup = ( + await api.post( + "/admin/customer-groups", + { + name: "Other VIP", + }, + adminHeaders + ) + ).data.customer_group + + await api.post( + `/admin/customer-groups/${otherCustomerGroup.id}/customers`, + { + add: [customer3.id], + }, + adminHeaders + ) + + response = await api.get( + `/admin/customers/${customer3.id}?fields=*groups`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer.groups.length).toEqual(2) + expect(response.data.customer.groups).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: customerGroup.id, + }), + expect.objectContaining({ + id: otherCustomerGroup.id, + }), + ]) + ) + + // Remove groups + await api.post( + `/admin/customer-groups/${customerGroup.id}/customers`, + { + remove: [customer3.id], + }, + adminHeaders + ) + await api.post( + `/admin/customer-groups/${otherCustomerGroup.id}/customers`, + { + remove: [customer3.id], + }, + adminHeaders + ) + + response = await api.get( + `/admin/customers/${customer3.id}?fields=*groups`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer.groups.length).toEqual(0) + }) + }) + + describe("GET /admin/customers/:id", () => { + it("should fetch a customer", async () => { + const response = await api.get( + `/admin/customers/${customer1.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer).toEqual( + expect.objectContaining({ + id: customer1.id, + }) + ) + }) + + it("should fetch a customer with expand query", async () => { + await api.post( + `/admin/customers/${customer1.id}/addresses`, + { + first_name: "Lebron", + last_name: "James", + is_default_billing: true, + }, + adminHeaders + ) + + // BREAKING: Customers no longer carry billing_address, they have addresses + const response = await api.get( + `/admin/customers/${customer1.id}?fields=*addresses,*groups`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.customer).toEqual( + expect.objectContaining({ + id: customer1.id, + addresses: [ + expect.objectContaining({ + is_default_billing: true, + first_name: "Lebron", + last_name: "James", + }), + ], + groups: [], + }) + ) + }) + }) + }, +}) diff --git a/packages/medusa/src/api/admin/customer-groups/[id]/route.ts b/packages/medusa/src/api/admin/customer-groups/[id]/route.ts index 9c977f6caa..ff8e20a737 100644 --- a/packages/medusa/src/api/admin/customer-groups/[id]/route.ts +++ b/packages/medusa/src/api/admin/customer-groups/[id]/route.ts @@ -1,12 +1,13 @@ -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "../../../../types/routing" import { deleteCustomerGroupsWorkflow, updateCustomerGroupsWorkflow, } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" +import { MedusaError } from "@medusajs/utils" import { refetchCustomerGroup } from "../helpers" import { AdminUpdateCustomerGroupType } from "../validators" @@ -20,6 +21,13 @@ export const GET = async ( req.remoteQueryConfig.fields ) + if (!customerGroup) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Customer group with id: ${req.params.id} not found` + ) + } + res.status(200).json({ customer_group: customerGroup }) } @@ -27,8 +35,7 @@ export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const updateGroups = updateCustomerGroupsWorkflow(req.scope) - await updateGroups.run({ + await updateCustomerGroupsWorkflow(req.scope).run({ input: { selector: { id: req.params.id }, update: req.validatedBody, diff --git a/packages/medusa/src/api/admin/customer-groups/middlewares.ts b/packages/medusa/src/api/admin/customer-groups/middlewares.ts index 7d03715798..61363e157a 100644 --- a/packages/medusa/src/api/admin/customer-groups/middlewares.ts +++ b/packages/medusa/src/api/admin/customer-groups/middlewares.ts @@ -1,14 +1,14 @@ -import * as QueryConfig from "./query-config" import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformQuery } from "../../utils/validate-query" +import { createLinkBody } from "../../utils/validators" +import * as QueryConfig from "./query-config" import { AdminCreateCustomerGroup, AdminGetCustomerGroupParams, AdminGetCustomerGroupsParams, AdminUpdateCustomerGroup, } from "./validators" -import { validateAndTransformBody } from "../../utils/validate-body" -import { createLinkBody } from "../../utils/validators" export const adminCustomerGroupRoutesMiddlewares: MiddlewareRoute[] = [ { diff --git a/packages/medusa/src/api/admin/customer-groups/query-config.ts b/packages/medusa/src/api/admin/customer-groups/query-config.ts index dc78491cc9..498a4a248f 100644 --- a/packages/medusa/src/api/admin/customer-groups/query-config.ts +++ b/packages/medusa/src/api/admin/customer-groups/query-config.ts @@ -5,6 +5,7 @@ export const defaultAdminCustomerGroupFields = [ "created_at", "updated_at", "deleted_at", + "metadata", ] export const retrieveTransformQueryConfig = { diff --git a/packages/medusa/src/api/admin/customer-groups/validators.ts b/packages/medusa/src/api/admin/customer-groups/validators.ts index 2b6ab506bd..07aac78c84 100644 --- a/packages/medusa/src/api/admin/customer-groups/validators.ts +++ b/packages/medusa/src/api/admin/customer-groups/validators.ts @@ -1,9 +1,9 @@ +import { z } from "zod" import { createFindParams, createOperatorMap, createSelectParams, } from "../../utils/validators" -import { z } from "zod" export type AdminGetCustomerGroupParamsType = z.infer< typeof AdminGetCustomerGroupParams @@ -58,6 +58,7 @@ export type AdminCreateCustomerGroupType = z.infer< > export const AdminCreateCustomerGroup = z.object({ name: z.string(), + metadata: z.record(z.any()).optional(), }) export type AdminUpdateCustomerGroupType = z.infer< @@ -65,4 +66,5 @@ export type AdminUpdateCustomerGroupType = z.infer< > export const AdminUpdateCustomerGroup = z.object({ name: z.string(), + metadata: z.record(z.any()).optional(), }) diff --git a/packages/medusa/src/api/admin/customers/[id]/route.ts b/packages/medusa/src/api/admin/customers/[id]/route.ts index 9a117857d5..dbbfbf3f79 100644 --- a/packages/medusa/src/api/admin/customers/[id]/route.ts +++ b/packages/medusa/src/api/admin/customers/[id]/route.ts @@ -2,6 +2,7 @@ import { deleteCustomersWorkflow, updateCustomersWorkflow, } from "@medusajs/core-flows" +import { AdminCustomer } from "@medusajs/types" import { MedusaError } from "@medusajs/utils" import { AuthenticatedMedusaRequest, @@ -9,7 +10,6 @@ import { } from "../../../../types/routing" import { refetchCustomer } from "../helpers" import { AdminUpdateCustomerType } from "../validators" -import { AdminCustomer } from "@medusajs/types" export const GET = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/medusa/src/api/admin/customers/query-config.ts b/packages/medusa/src/api/admin/customers/query-config.ts index 842e87e12b..0265bba30f 100644 --- a/packages/medusa/src/api/admin/customers/query-config.ts +++ b/packages/medusa/src/api/admin/customers/query-config.ts @@ -13,8 +13,26 @@ export const defaultAdminCustomerFields = [ "deleted_at", ] +export const allowed = [ + "id", + "company_name", + "first_name", + "last_name", + "email", + "phone", + "metadata", + "has_account", + "created_by", + "created_at", + "updated_at", + "deleted_at", + "addresses", + "groups", +] + export const retrieveTransformQueryConfig = { defaults: defaultAdminCustomerFields, + allowed, isList: false, } diff --git a/packages/medusa/src/api/admin/customers/route.ts b/packages/medusa/src/api/admin/customers/route.ts index 5fb4af733d..183001fe0c 100644 --- a/packages/medusa/src/api/admin/customers/route.ts +++ b/packages/medusa/src/api/admin/customers/route.ts @@ -1,5 +1,6 @@ import { createCustomersWorkflow } from "@medusajs/core-flows" +import { AdminCustomer, PaginatedResponse } from "@medusajs/types" import { ContainerRegistrationKeys, remoteQueryObjectFromString, @@ -8,9 +9,8 @@ import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../types/routing" -import { AdminCreateCustomerType } from "./validators" import { refetchCustomer } from "./helpers" -import { AdminCustomer, PaginatedResponse } from "@medusajs/types" +import { AdminCreateCustomerType } from "./validators" export const GET = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/modules/customer/src/migrations/.snapshot-medusa-customer.json b/packages/modules/customer/src/migrations/.snapshot-medusa-customer.json index 0d05e9c4bd..f906791ac2 100644 --- a/packages/modules/customer/src/migrations/.snapshot-medusa-customer.json +++ b/packages/modules/customer/src/migrations/.snapshot-medusa-customer.json @@ -388,7 +388,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "mappedType": "text" }, "metadata": { @@ -445,6 +445,16 @@ "name": "customer_group", "schema": "public", "indexes": [ + { + "keyName": "IDX_customer_group_name_unique", + "columnNames": [ + "name" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_customer_group_name_unique\" ON \"customer_group\" (name) WHERE deleted_at IS NULL" + }, { "keyName": "customer_group_pkey", "columnNames": [ diff --git a/packages/modules/customer/src/migrations/Migration20240602110946.ts b/packages/modules/customer/src/migrations/Migration20240602110946.ts new file mode 100644 index 0000000000..e2ca6df517 --- /dev/null +++ b/packages/modules/customer/src/migrations/Migration20240602110946.ts @@ -0,0 +1,19 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20240602110946 extends Migration { + async up(): Promise { + this.addSql( + 'ALTER TABLE IF EXISTS "customer_group" ALTER COLUMN "name" SET NOT NULL;' + ) + this.addSql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_customer_group_name_unique" ON "customer_group" (name) WHERE deleted_at IS NULL;' + ) + } + + async down(): Promise { + this.addSql( + 'ALTER TABLE IF EXISTS "customer_group" ALTER COLUMN "name" DROP NOT NULL;' + ) + this.addSql('drop index if exists "IDX_customer_group_name_unique";') + } +} diff --git a/packages/modules/customer/src/models/customer-group.ts b/packages/modules/customer/src/models/customer-group.ts index 54dce4eb69..d2ee6dfa0f 100644 --- a/packages/modules/customer/src/models/customer-group.ts +++ b/packages/modules/customer/src/models/customer-group.ts @@ -1,21 +1,33 @@ import { DAL } from "@medusajs/types" -import { DALUtils, Searchable, generateEntityId } from "@medusajs/utils" +import { + DALUtils, + Searchable, + createPsqlIndexStatementHelper, + generateEntityId, +} from "@medusajs/utils" import { BeforeCreate, + Collection, Entity, + Filter, + ManyToMany, OnInit, OptionalProps, PrimaryKey, Property, - ManyToMany, - Collection, - Filter, } from "@mikro-orm/core" import Customer from "./customer" import CustomerGroupCustomer from "./customer-group-customer" type OptionalGroupProps = DAL.SoftDeletableEntityDateColumns // TODO: To be revisited when more clear +const CustomerGroupUniqueName = createPsqlIndexStatementHelper({ + tableName: "customer_group", + columns: ["name"], + unique: true, + where: "deleted_at IS NULL", +}) + @Entity({ tableName: "customer_group" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class CustomerGroup { @@ -25,8 +37,9 @@ export default class CustomerGroup { id!: string @Searchable() - @Property({ columnType: "text", nullable: true }) - name: string | null = null + @CustomerGroupUniqueName.MikroORMIndex() + @Property({ columnType: "text" }) + name: string @ManyToMany({ entity: () => Customer,