From 5a1cbc68b721fe80d223e4ff611ebc81346333d7 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Tue, 13 Jul 2021 10:41:06 +0200 Subject: [PATCH] fix: allow updating billing address on customer --- .../api/__tests__/store/customer.js | 100 +++++++++++++++++- integration-tests/api/package.json | 6 +- integration-tests/api/yarn.lock | 48 ++++----- .../customers/__tests__/update-customer.js | 92 ++++++++++++++++ .../src/api/routes/store/customers/index.js | 2 +- .../routes/store/customers/update-customer.js | 1 + .../medusa/src/services/__tests__/customer.js | 8 +- packages/medusa/src/services/customer.js | 43 +++++++- 8 files changed, 267 insertions(+), 33 deletions(-) diff --git a/integration-tests/api/__tests__/store/customer.js b/integration-tests/api/__tests__/store/customer.js index bdbb40284a..2ed0f18ca4 100644 --- a/integration-tests/api/__tests__/store/customer.js +++ b/integration-tests/api/__tests__/store/customer.js @@ -1,6 +1,6 @@ const { dropDatabase } = require("pg-god"); const path = require("path"); -const { Customer } = require("@medusajs/medusa"); +const { Address, Customer } = require("@medusajs/medusa"); const setupServer = require("../../../helpers/setup-server"); const { useApi } = require("../../../helpers/use-api"); @@ -15,8 +15,8 @@ describe("/store/customers", () => { let dbConnection; const doAfterEach = async (manager) => { - await manager.query(`DELETE FROM "address"`); await manager.query(`DELETE FROM "customer"`); + await manager.query(`DELETE FROM "address"`); }; beforeAll(async () => { @@ -35,6 +35,7 @@ describe("/store/customers", () => { describe("POST /store/customers", () => { beforeEach(async () => { const manager = dbConnection.manager; + await manager.insert(Customer, { id: "test_customer", first_name: "John", @@ -82,6 +83,17 @@ describe("/store/customers", () => { describe("POST /store/customers/:id", () => { beforeEach(async () => { const manager = dbConnection.manager; + await manager.insert(Address, { + id: "addr_test", + first_name: "String", + last_name: "Stringson", + address_1: "String st", + city: "Stringville", + postal_code: "1236", + province: "ca", + country_code: "us", + }); + await manager.insert(Customer, { id: "test_customer", first_name: "John", @@ -130,5 +142,89 @@ describe("/store/customers", () => { }) ); }); + + it("updates customer billing address", async () => { + const api = useApi(); + + const authResponse = await api.post("/store/auth", { + email: "john@deere.com", + password: "test", + }); + + const customerId = authResponse.data.customer.id; + const [authCookie] = authResponse.headers["set-cookie"][0].split(";"); + + const response = await api.post( + `/store/customers/${customerId}`, + { + billing_address: { + first_name: "test", + last_name: "testson", + address_1: "Test st", + city: "Testion", + postal_code: "1235", + province: "ca", + country_code: "us", + }, + }, + { + headers: { + Cookie: authCookie, + }, + } + ); + + expect(response.status).toEqual(200); + expect(response.data.customer).not.toHaveProperty("password_hash"); + expect(response.data.customer.billing_address).toEqual( + expect.objectContaining({ + first_name: "test", + last_name: "testson", + address_1: "Test st", + city: "Testion", + postal_code: "1235", + province: "ca", + country_code: "us", + }) + ); + }); + + it("updates customer billing address with string", async () => { + const api = useApi(); + + const authResponse = await api.post("/store/auth", { + email: "john@deere.com", + password: "test", + }); + + const customerId = authResponse.data.customer.id; + const [authCookie] = authResponse.headers["set-cookie"][0].split(";"); + + const response = await api.post( + `/store/customers/${customerId}`, + { + billing_address: "addr_test", + }, + { + headers: { + Cookie: authCookie, + }, + } + ); + + expect(response.status).toEqual(200); + expect(response.data.customer).not.toHaveProperty("password_hash"); + expect(response.data.customer.billing_address).toEqual( + expect.objectContaining({ + first_name: "String", + last_name: "Stringson", + address_1: "String st", + city: "Stringville", + postal_code: "1236", + province: "ca", + country_code: "us", + }) + ); + }); }); }); diff --git a/integration-tests/api/package.json b/integration-tests/api/package.json index 19d49a250d..ce709d3978 100644 --- a/integration-tests/api/package.json +++ b/integration-tests/api/package.json @@ -8,15 +8,15 @@ "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { - "@medusajs/medusa": "1.1.28-dev-1624556551881", - "medusa-interfaces": "1.1.16-dev-1624556551881", + "@medusajs/medusa": "1.1.29-dev-1626162503472", + "medusa-interfaces": "1.1.17-dev-1626162503472", "typeorm": "^0.2.31" }, "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/node": "^7.12.10", - "babel-preset-medusa-package": "1.1.9-dev-1624556551881", + "babel-preset-medusa-package": "1.1.10-dev-1626162503472", "jest": "^26.6.3" } } diff --git a/integration-tests/api/yarn.lock b/integration-tests/api/yarn.lock index 0e907d8146..a99f579dee 100644 --- a/integration-tests/api/yarn.lock +++ b/integration-tests/api/yarn.lock @@ -1215,10 +1215,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@medusajs/medusa@1.1.28-dev-1624556551881": - version "1.1.28-dev-1624556551881" - resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.28-dev-1624556551881.tgz#263dac3aae36b656899dde61910e170e20647a1d" - integrity sha512-v6Rry8J7/z99dhn7uIWSt/IeFQ3o+O3zmdN1aYejLz2q0NWXdT9eSlicEEoznHFd/4EEZa1BYYqR4U1FqmGgUw== +"@medusajs/medusa@1.1.29-dev-1626162503472": + version "1.1.29-dev-1626162503472" + resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.29-dev-1626162503472.tgz#973ec19d02a66864c8cc11ac3e045cda2a82215d" + integrity sha512-8JDjTzOh056panREJIpN6uh2nwhauKqJHeGopG0Kdaw7sxOS3GJMBIfkwmUeNjju+cnpzj0nKlnJ56UgNMZSvA== dependencies: "@hapi/joi" "^16.1.8" "@types/lodash" "^4.14.168" @@ -1239,8 +1239,8 @@ joi "^17.3.0" joi-objectid "^3.0.1" jsonwebtoken "^8.5.1" - medusa-core-utils "1.1.15-dev-1624556551881" - medusa-test-utils "1.1.18-dev-1624556551881" + medusa-core-utils "1.1.16-dev-1626162503472" + medusa-test-utils "1.1.19-dev-1626162503472" morgan "^1.9.1" multer "^1.4.2" passport "^0.4.0" @@ -1696,10 +1696,10 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-preset-medusa-package@1.1.9-dev-1624556551881: - version "1.1.9-dev-1624556551881" - resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.9-dev-1624556551881.tgz#02631e1bc7ae0c6b28b6172d44f144dcc9ec9e0f" - integrity sha512-gbenDSqRQm0IoI4vqgwvL9DMsR/b3UgGu2ZzpTXZeM070wH4LsBeckNpXf0k5douOUIpqvhk5xhyIYBprZz5LQ== +babel-preset-medusa-package@1.1.10-dev-1626162503472: + version "1.1.10-dev-1626162503472" + resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.10-dev-1626162503472.tgz#65bba4e47361d9298b894fe9c08122fd60e0fd54" + integrity sha512-kQIZbFKnCnngCxxnPI3Ri+TC+6sadQOPgPGSMxd2X3yLm/W9RU+BLxRCLPEQcvvt6jeUA8dil8n0NUSl51cQnQ== dependencies: "@babel/plugin-proposal-class-properties" "^7.12.1" "@babel/plugin-proposal-decorators" "^7.12.1" @@ -4150,28 +4150,28 @@ media-typer@0.3.0: resolved "http://localhost:4873/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@1.1.15-dev-1624556551881: - version "1.1.15-dev-1624556551881" - resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.15-dev-1624556551881.tgz#3cd5ac7a40ecd870d6cf22873ddbcfe362b84215" - integrity sha512-KicW2VFP0nKNozJ/XvBQ7pGcML50cnj0IWThGBiak+S9+6+DBHPKmUVOWMwO4ocQgXUY9VV+ZjUzXYxKUrM0fA== +medusa-core-utils@1.1.16-dev-1626162503472: + version "1.1.16-dev-1626162503472" + resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.16-dev-1626162503472.tgz#f72029605508928f689df3e35969db8c90be9cfd" + integrity sha512-AsI8UNF2VaJIUppJHjipsQnO6o7O/HNjIx5yPamriZRHatevZpWnRAD3aCejz25gaPvUQHWZ66b+UunPz1YKmQ== dependencies: joi "^17.3.0" joi-objectid "^3.0.1" -medusa-interfaces@1.1.16-dev-1624556551881: - version "1.1.16-dev-1624556551881" - resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.16-dev-1624556551881.tgz#4644df88d49dac014a8c1c7170efa5fff45df15e" - integrity sha512-oWLD8qDGhByty3mIWnv4cIgdJ0sWDDy1/Yz4rL5fNhENtRhwzH0vvw5rEBvpFAFF5TZuwMap+Co61ldRyN6xBA== +medusa-interfaces@1.1.17-dev-1626162503472: + version "1.1.17-dev-1626162503472" + resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.17-dev-1626162503472.tgz#5cb72816c241a0074fbdbc64c2dfb0bedc073c03" + integrity sha512-aQcK39oMGBvb27aIHW3ko5sRdP2GRUAllXzrsTy3aQbUYzZxqnq3FHRlTjmBUWa9zbzwvfu3JLwdCEgZTfgr6Q== dependencies: - medusa-core-utils "1.1.15-dev-1624556551881" + medusa-core-utils "1.1.16-dev-1626162503472" -medusa-test-utils@1.1.18-dev-1624556551881: - version "1.1.18-dev-1624556551881" - resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.18-dev-1624556551881.tgz#fb2b2cd25755251c37545d1cfe725eecb96288af" - integrity sha512-lJyEvvSxM5mv+mxePSqt3Fcj/g+mkwSsN+NnURhiEG1vVi9s6t/kZf55mSu+IRqXQhs/FQMHzkgCHzUgmAjMqg== +medusa-test-utils@1.1.19-dev-1626162503472: + version "1.1.19-dev-1626162503472" + resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.19-dev-1626162503472.tgz#0a112fa9d5df2a2ce913312bb0527049ceb23e48" + integrity sha512-e9VsUYh0B1dzrmg0OAyHAMaEV+Ifrf2yqWl2ecGkwCX75whLgjfDxjR6dXlkahy+oL1Uqm3eGWZol04ZjIol7A== dependencies: "@babel/plugin-transform-classes" "^7.9.5" - medusa-core-utils "1.1.15-dev-1624556551881" + medusa-core-utils "1.1.16-dev-1626162503472" randomatic "^3.1.1" merge-descriptors@1.0.1: diff --git a/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js b/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js index 5b5887cdad..baab0d5261 100644 --- a/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js +++ b/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js @@ -56,6 +56,98 @@ describe("POST /store/customers/:id", () => { }) }) + describe("successfully updates a customer with billing address id", () => { + let subject + beforeAll(async () => { + subject = await request( + "POST", + `/store/customers/${IdMap.getId("lebron")}`, + { + payload: { + billing_address: "test", + }, + clientSession: { + jwt: { + customer_id: IdMap.getId("lebron"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls CustomerService update", () => { + expect(CustomerServiceMock.update).toHaveBeenCalledTimes(1) + expect(CustomerServiceMock.update).toHaveBeenCalledWith( + IdMap.getId("lebron"), + { + billing_address: "test", + } + ) + }) + + it("status code 200", () => { + expect(subject.status).toEqual(200) + }) + }) + + describe("successfully updates a customer with billing address object", () => { + let subject + beforeAll(async () => { + subject = await request( + "POST", + `/store/customers/${IdMap.getId("lebron")}`, + { + payload: { + billing_address: { + first_name: "Olli", + last_name: "Juhl", + address_1: "Laksegade", + city: "Copenhagen", + country_code: "dk", + postal_code: "2100", + phone: "+1 (222) 333 4444", + }, + }, + clientSession: { + jwt: { + customer_id: IdMap.getId("lebron"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls CustomerService update", () => { + expect(CustomerServiceMock.update).toHaveBeenCalledTimes(1) + expect(CustomerServiceMock.update).toHaveBeenCalledWith( + IdMap.getId("lebron"), + { + billing_address: { + first_name: "Olli", + last_name: "Juhl", + address_1: "Laksegade", + city: "Copenhagen", + country_code: "dk", + postal_code: "2100", + phone: "+1 (222) 333 4444", + }, + } + ) + }) + + it("status code 200", () => { + expect(subject.status).toEqual(200) + }) + }) + describe("fails if not authenticated", () => { let subject beforeAll(async () => { diff --git a/packages/medusa/src/api/routes/store/customers/index.js b/packages/medusa/src/api/routes/store/customers/index.js index 7c0b8a26a2..79938d1ac2 100644 --- a/packages/medusa/src/api/routes/store/customers/index.js +++ b/packages/medusa/src/api/routes/store/customers/index.js @@ -58,7 +58,7 @@ export default (app, container) => { return app } -export const defaultRelations = ["shipping_addresses"] +export const defaultRelations = ["shipping_addresses", "billing_address"] export const defaultFields = [ "id", diff --git a/packages/medusa/src/api/routes/store/customers/update-customer.js b/packages/medusa/src/api/routes/store/customers/update-customer.js index 5e9488c359..b30e1c7cfb 100644 --- a/packages/medusa/src/api/routes/store/customers/update-customer.js +++ b/packages/medusa/src/api/routes/store/customers/update-customer.js @@ -44,6 +44,7 @@ export default async (req, res) => { const { id } = req.params const schema = Validator.object().keys({ + billing_address: Validator.address().optional(), first_name: Validator.string().optional(), last_name: Validator.string().optional(), password: Validator.string().optional(), diff --git a/packages/medusa/src/services/__tests__/customer.js b/packages/medusa/src/services/__tests__/customer.js index d23c10dc52..e980cda5a3 100644 --- a/packages/medusa/src/services/__tests__/customer.js +++ b/packages/medusa/src/services/__tests__/customer.js @@ -168,8 +168,14 @@ describe("CustomerService", () => { }, }) + const addressRepository = MockRepository({ + create: data => data, + save: data => Promise.resolve(data), + }) + const customerService = new CustomerService({ manager: MockManager, + addressRepository, customerRepository, eventBusService, }) @@ -233,7 +239,7 @@ describe("CustomerService", () => { last_name: "Juhl", address_1: "Laksegade", city: "Copenhagen", - country_code: "DK", + country_code: "dk", postal_code: "2100", phone: "+1 (222) 333 4444", }, diff --git a/packages/medusa/src/services/customer.js b/packages/medusa/src/services/customer.js index de915a902e..f47c556527 100644 --- a/packages/medusa/src/services/customer.js +++ b/packages/medusa/src/services/customer.js @@ -367,6 +367,7 @@ class CustomerService extends BaseService { const customerRepository = manager.getCustomRepository( this.customerRepository_ ) + const addrRepo = manager.getCustomRepository(this.addressRepository_) const customer = await this.retrieve(customerId) @@ -375,6 +376,7 @@ class CustomerService extends BaseService { password, password_hash, billing_address, + billing_address_id, metadata, ...rest } = update @@ -387,8 +389,9 @@ class CustomerService extends BaseService { customer.email = this.validateEmail_(email) } - if (billing_address) { - customer.billing_address = this.validateBillingAddress_(billing_address) + if ("billing_address_id" in update || "billing_address" in update) { + const address = update.billing_address_id || update.billing_address + await this.updateBillingAddress_(customer, address, addrRepo) } for (const [key, value] of Object.entries(rest)) { @@ -400,6 +403,7 @@ class CustomerService extends BaseService { } const updated = await customerRepository.save(customer) + await this.eventBus_ .withTransaction(manager) .emit(CustomerService.Events.UPDATED, updated) @@ -407,6 +411,41 @@ class CustomerService extends BaseService { }) } + /** + * Updates the cart's billing address. + * @param {Customer} customer - the Customer to update + * @param {object} address - the value to set the billing address to + * @return {Promise} the result of the update operation + */ + async updateBillingAddress_(customer, addressOrId, addrRepo) { + if (typeof addressOrId === `string`) { + addressOrId = await addrRepo.findOne({ + where: { id: addressOrId }, + }) + } + + addressOrId.country_code = addressOrId.country_code.toLowerCase() + + if (addressOrId.id) { + customer.billing_address_id = addressOrId.id + customer.billing_address = addressOrId + } else { + if (customer.billing_address_id) { + const addr = await addrRepo.findOne({ + where: { id: customer.billing_address_id }, + }) + + await addrRepo.save({ ...addr, ...addressOrId }) + } else { + const created = addrRepo.create({ + ...addressOrId, + }) + const saved = await addrRepo.save(created) + customer.billing_address = saved + } + } + } + async updateAddress(customerId, addressId, address) { return this.atomicPhase_(async manager => { const addressRepo = manager.getCustomRepository(this.addressRepository_)