diff --git a/back/models/country.json b/back/models/country.json index bf8486b4a..8364636fc 100644 --- a/back/models/country.json +++ b/back/models/country.json @@ -4,36 +4,39 @@ "base": "VnModel", "options": { "mysql": { - "table": "country" + "table": "country" } }, "properties": { "id": { - "type": "Number", - "id": true, - "description": "Identifier" + "type": "Number", + "id": true, + "description": "Identifier" }, "country": { - "type": "string", - "required": true + "type": "string", + "required": true }, "code": { - "type": "string" + "type": "string" + }, + "isUeeMember": { + "type": "Boolean" } }, "relations": { "currency": { - "type": "belongsTo", - "model": "Currency", - "foreignKey": "currencyFk" + "type": "belongsTo", + "model": "Currency", + "foreignKey": "currencyFk" } }, "acls": [ { - "accessType": "READ", - "principalType": "ROLE", - "principalId": "$everyone", - "permission": "ALLOW" + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" } ] } \ No newline at end of file diff --git a/db/changes/10140-kings/00-ACL.sql b/db/changes/10140-kings/00-ACL.sql index fa507a3c3..fe1cbeb24 100644 --- a/db/changes/10140-kings/00-ACL.sql +++ b/db/changes/10140-kings/00-ACL.sql @@ -1,3 +1,3 @@ INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Thermograph', '*', '*', 'ALLOW', 'ROLE', 'buyer'); INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('TravelThermograph', '*', '*', 'ALLOW', 'ROLE', 'buyer'); -INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Entry', '*', '*', 'ALLOW', 'ROLE', 'buyer'); +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Entry', '*', '*', 'ALLOW', 'ROLE', 'buyer'); \ No newline at end of file diff --git a/db/changes/10141-kings/00-ACL.sql b/db/changes/10141-kings/00-ACL.sql new file mode 100644 index 000000000..9a43990d0 --- /dev/null +++ b/db/changes/10141-kings/00-ACL.sql @@ -0,0 +1 @@ +REPLACE INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('CustomsAgent', '*', '*', 'ALLOW', 'ROLE', 'employee'); diff --git a/db/changes/10141-kings/01-customsAgent.sql b/db/changes/10141-kings/01-customsAgent.sql new file mode 100644 index 000000000..34f77f20f --- /dev/null +++ b/db/changes/10141-kings/01-customsAgent.sql @@ -0,0 +1,11 @@ +CREATE TABLE `vn`.`customsAgent` ( + `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, + `fiscalName` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `street` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `nif` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `phone` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +ALTER TABLE `vn`.`customsAgent` + ADD UNIQUE KEY `nif_UNIQUE` (`nif`); \ No newline at end of file diff --git a/db/changes/10141-kings/02-incoterms.sql b/db/changes/10141-kings/02-incoterms.sql new file mode 100644 index 000000000..3e31b0c89 --- /dev/null +++ b/db/changes/10141-kings/02-incoterms.sql @@ -0,0 +1,10 @@ +CREATE TABLE `vn`.`incoterms` ( + `code` varchar(3) COLLATE utf8_unicode_ci DEFAULT NULL, + `name` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='Internacional Commercial Terms'; + +ALTER TABLE `vn`.`incoterms` + ADD PRIMARY KEY (`code`); + +REPLACE INTO `vn`.`incoterms` (`code`, `name`) VALUES +('FAS', 'Free Alongside Ship'); diff --git a/db/changes/10141-kings/03-address.sql b/db/changes/10141-kings/03-address.sql new file mode 100644 index 000000000..042c2e17e --- /dev/null +++ b/db/changes/10141-kings/03-address.sql @@ -0,0 +1,26 @@ +ALTER TABLE `vn`.`address` +ADD COLUMN `customsAgentFk` INT NULL DEFAULT NULL AFTER `isEqualizated`; + +ALTER TABLE `vn`.`address` +ADD COLUMN `incotermsFk` VARCHAR(3) NULL DEFAULT NULL AFTER `customsAgentFk`; + + +ALTER TABLE `vn`.`address` +ADD INDEX `address_customsAgentFk_idx` (`customsAgentFk` ASC); + +ALTER TABLE `vn`.`address` +ADD INDEX `address_incotermsFk_idx` (`incotermsFk` ASC); + +ALTER TABLE `vn`.`address` +ADD CONSTRAINT `address_customsAgentFk` + FOREIGN KEY (`customsAgentFk`) + REFERENCES `vn`.`customsAgent` (`id`) + ON DELETE RESTRICT + ON UPDATE CASCADE; + +ALTER TABLE `vn`.`address` +ADD CONSTRAINT `address_incotermsFk` + FOREIGN KEY (`incotermsFk`) + REFERENCES `vn`.`incoterms` (`code`) + ON DELETE RESTRICT + ON UPDATE CASCADE; \ No newline at end of file diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 87b915ec0..7b7651a9d 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -78,12 +78,13 @@ INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossF INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`) VALUES - (1, 'España', 0, 'ES', 1, 24), + (1, 'España', 1, 'ES', 1, 24), (2, 'Italia', 1, 'IT', 1, 27), (3, 'Alemania', 1, 'DE', 1, 22), (4, 'Rumania', 1, 'RO', 1, 24), (5, 'Holanda', 1, 'NL', 1, 18), (8, 'Portugal', 1, 'PT', 1, 27), + (13,'Ecuador', 0, 'EC', 1, 24), (19,'Francia', 1, 'FR', 1, 27), (30,'Canarias', 1, 'IC', 1, 24); @@ -188,8 +189,8 @@ INSERT INTO `vn`.`province`(`id`, `name`, `countryFk`, `warehouseFk`) (1, 'Province one', 1, NULL), (2, 'Province two', 1, NULL), (3, 'Province three', 1, NULL), - (4, 'Province four', 1, NULL), - (5, 'Province five', 1, NULL); + (4, 'Province four', 2, NULL), + (5, 'Province five', 13, NULL); INSERT INTO `vn`.`town`(`id`, `name`, `provinceFk`) VALUES @@ -1960,4 +1961,13 @@ INSERT INTO `vn`.`travelThermograph`(`thermographFk`, `created`, `warehouseFk`, ('TL.BBA85422', CURDATE(), 2, 1, 'COOL', 'can not read the temperature', NULL), ('TZ1905012010', CURDATE(), 1, 1, 'WARM', 'Temperature in range', 5), ('138350-0', DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 1, 'WARM', NULL, 5), - ('138350-0', CURDATE(), 1, NULL, 'COOL', NULL, NULL); \ No newline at end of file + ('138350-0', CURDATE(), 1, NULL, 'COOL', NULL, NULL); + +REPLACE INTO `vn`.`incoterms` (`code`, `name`) + VALUES + ('FAS', 'Free Alongside Ship'); + +REPLACE INTO `vn`.`customsAgent` (`id`, `fiscalName`, `street`, `nif`, `phone`, `email`) + VALUES + (1, 'Agent one', '1007 Mountain Drive, Gotham', 'N1111111111', '111111111', 'agentone@gotham.com'), + (2, 'Agent two', '1007 Mountain Drive, Gotham', 'N2222222222', '222222222', 'agenttwo@gotham.com'); \ No newline at end of file diff --git a/e2e/helpers/puppeteer.js b/e2e/helpers/puppeteer.js index cd445bdbe..01496be20 100644 --- a/e2e/helpers/puppeteer.js +++ b/e2e/helpers/puppeteer.js @@ -6,6 +6,7 @@ import {url as defaultURL} from './config'; export async function getBrowser() { const browser = await Puppeteer.launch({ args: [ + '--no-sandbox', `--window-size=${ 1920 },${ 1080 }` ], defaultViewport: null, diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 5a076c6c4..019a3a252 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -103,11 +103,13 @@ export default { streetAddressInput: '[ng-model="$ctrl.address.street"]', postcodeInput: '[ng-model="$ctrl.address.postalCode"]', cityInput: '[ng-model="$ctrl.address.city"]', - provinceAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.provinceFk"]', - agencyAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.agencyModeFk"]', + provinceAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.provinceId"]', + agencyAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.agencyModeId"]', phoneInput: '[ng-model="$ctrl.address.phone"]', mobileInput: '[ng-model="$ctrl.address.mobile"]', defaultAddress: 'vn-client-address-index div:nth-child(1) div[name="street"]', + incotermsAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.incotermsId"]', + customsAgentAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.customsAgentId"]', secondMakeDefaultStar: 'vn-client-address-index vn-card div:nth-child(2) vn-icon-button[icon="star_border"]', firstEditAddress: 'vn-client-address-index div:nth-child(1) > a', secondEditAddress: 'vn-client-address-index div:nth-child(2) > a', diff --git a/e2e/paths/02-client-module/01_create_client.spec.js b/e2e/paths/02-client-module/01_create_client.spec.js index 01a30da13..ea5ffb17d 100644 --- a/e2e/paths/02-client-module/01_create_client.spec.js +++ b/e2e/paths/02-client-module/01_create_client.spec.js @@ -74,7 +74,7 @@ describe('Client create path', async() => { await page.waitToClick(selectors.createClientView.createButton); const result = await page.waitForLastSnackbar(); - expect(result).toEqual(`The postcode doesn't exists. Ensure you put the correct format`); + expect(result).toEqual(`The postcode doesn't exist. Please enter a correct one`); }); it(`should check for autocompleted city, province and country`, async() => { diff --git a/e2e/paths/02-client-module/05_add_address.spec.js b/e2e/paths/02-client-module/05_add_address.spec.js index 67afbfdbe..8eb4a86bf 100644 --- a/e2e/paths/02-client-module/05_add_address.spec.js +++ b/e2e/paths/02-client-module/05_add_address.spec.js @@ -26,7 +26,7 @@ describe('Client Add address path', () => { it('should receive an error after clicking save button as consignee, street and town fields are empty', async() => { await page.waitToClick(selectors.clientAddresses.defaultCheckboxInput); - await page.autocompleteSearch(selectors.clientAddresses.provinceAutocomplete, 'Province one'); + await page.autocompleteSearch(selectors.clientAddresses.provinceAutocomplete, 'Province five'); await page.write(selectors.clientAddresses.cityInput, 'Valencia'); await page.write(selectors.clientAddresses.postcodeInput, '46000'); await page.autocompleteSearch(selectors.clientAddresses.agencyAutocomplete, 'Entanglement'); @@ -38,12 +38,29 @@ describe('Client Add address path', () => { expect(result).toEqual('Some fields are invalid'); }); - it(`should create a new address with all it's data`, async() => { + + it(`should receive an error after clicking save button as consignee, incoterms and customsAgent are empty`, async() => { await page.write(selectors.clientAddresses.consigneeInput, 'Bruce Bunner'); await page.write(selectors.clientAddresses.streetAddressInput, '320 Park Avenue New York'); await page.waitToClick(selectors.clientAddresses.saveButton); const result = await page.waitForLastSnackbar(); + expect(result).toEqual('Incoterms is required for a non UEE member'); + }); + + it(`should receive an error after clicking save button as consignee, incoterms and customsAgent are empty`, async() => { + await page.autocompleteSearch(selectors.clientAddresses.incotermsAutocomplete, 'Free Alongside Ship'); + await page.waitToClick(selectors.clientAddresses.saveButton); + const result = await page.waitForLastSnackbar(); + + expect(result).toEqual('Customs agent is required for a non UEE member'); + }); + + it(`should create a new address with all it's data`, async() => { + await page.autocompleteSearch(selectors.clientAddresses.customsAgentAutocomplete, 'Agent one'); + await page.waitToClick(selectors.clientAddresses.saveButton); + const result = await page.waitForLastSnackbar(); + expect(result).toEqual('Data saved!'); }); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 70d06c9bd..2ea2b1a3a 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -54,12 +54,14 @@ "This ticket can not be modified": "This ticket can not be modified", "You can't delete a confirmed order": "You can't delete a confirmed order", "Value has an invalid format": "Value has an invalid format", - "The postcode doesn't exists. Ensure you put the correct format": "The postcode doesn't exists. Ensure you put the correct format", + "The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one", "Can't create stowaway for this ticket": "Can't create stowaway for this ticket", "Has deleted the ticket id": "Has deleted the ticket id [#{{id}}]({{{url}}})", "Swift / BIC can't be empty": "Swift / BIC can't be empty", "MESSAGE_BOUGHT_UNITS": "Bought {{quantity}} units of {{concept}} (#{{itemId}}) for the ticket id [#{{ticketId}}]({{{url}}})", "MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} (#{{clientId}})]({{{url}}}) to *{{credit}} €*", "MESSAGE_CHANGED_PAYMETHOD": "I have changed the pay method for client [{{clientName}} (#{{clientId}})]({{{url}}})", - "MESSAGE_CLAIM_ITEM_REGULARIZE": "I sent *{{quantity}}* units of [{{concept}} (#{{itemId}})]({{{itemUrl}}}) to {{nickname}} coming from ticket id [#{{ticketId}}]({{{ticketUrl}}})" + "MESSAGE_CLAIM_ITEM_REGULARIZE": "I sent *{{quantity}}* units of [{{concept}} (#{{itemId}})]({{{itemUrl}}}) to {{nickname}} coming from ticket id [#{{ticketId}}]({{{ticketUrl}}})", + "Customs agent is required for a non UEE member": "Customs agent is required for a non UEE member", + "Incoterms is required for a non UEE member": "Incoterms is required for a non UEE member" } \ No newline at end of file diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 3de0b1626..a3490b372 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -106,7 +106,7 @@ "Invalid quantity": "Cantidad invalida", "This postal code is not valid": "This postal code is not valid", "is invalid": "is invalid", - "The postcode doesn't exists. Ensure you put the correct format": "El código postal no existe. Asegúrate de ponerlo con el formato correcto", + "The postcode doesn't exist. Please enter a correct one": "El código postal no existe. Por favor, introduce uno correcto", "The department name can't be repeated": "El nombre del departamento no puede repetirse", "This phone already exists": "Este teléfono ya existe", "You cannot move a parent to its own sons": "No puedes mover un elemento padre a uno de sus hijos", @@ -119,6 +119,8 @@ "Start date should be lower than end date": "La fecha de inicio debe ser menor que la fecha de fín", "You should mark at least one week day": "Debes marcar al menos un día de la semana", "Swift / BIC can't be empty": "Swift / BIC no puede estar vacío", + "Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios", + "Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios", "MESSAGE_BOUGHT_UNITS": "Se ha comprado {{quantity}} unidades de {{concept}} (#{{itemId}}) para el ticket id [#{{ticketId}}]({{{url}}})", "MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} (#{{clientId}})]({{{url}}}) a *{{credit}} €*", "MESSAGE_CHANGED_PAYMETHOD": "He cambiado la forma de pago del cliente [{{clientName}} (#{{clientId}})]({{{url}}})", diff --git a/modules/client/back/methods/address/createDefaultAddress.js b/modules/client/back/methods/address/createDefaultAddress.js deleted file mode 100644 index e524a6017..000000000 --- a/modules/client/back/methods/address/createDefaultAddress.js +++ /dev/null @@ -1,44 +0,0 @@ -module.exports = function(Self) { - Self.remoteMethod('createDefaultAddress', { - description: 'Creates both client and its web account', - accepts: { - arg: 'data', - type: 'object', - http: {source: 'body'} - }, - returns: { - root: true, - type: 'Object' - }, - http: { - verb: 'post', - path: '/createDefaultAddress' - } - }); - - Self.createDefaultAddress = async data => { - const Address = Self.app.models.Address; - const Client = Self.app.models.Client; - const tx = await Address.beginTransaction({}); - - try { - let options = {transaction: tx}; - - let address = data.address; - let newAddress = await Address.create(address, options); - let client = await Client.findById(address.clientFk, null, options); - - if (data.isDefaultAddress) { - await client.updateAttributes({ - defaultAddressFk: newAddress.id - }, options); - } - - await tx.commit(); - return newAddress; - } catch (e) { - await tx.rollback(); - throw e; - } - }; -}; diff --git a/modules/client/back/methods/address/specs/createDefaultAddress.spec.js b/modules/client/back/methods/address/specs/createDefaultAddress.spec.js deleted file mode 100644 index 452d9c9b7..000000000 --- a/modules/client/back/methods/address/specs/createDefaultAddress.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -const app = require('vn-loopback/server/server'); - -describe('Address createDefaultAddress', () => { - let address; - let client; - - afterAll(async done => { - await client.updateAttributes({defaultAddressFk: 1}); - await address.destroy(); - - done(); - }); - - it('should verify that client defaultAddressFk is untainted', async() => { - client = await app.models.Client.findById(101); - - expect(client.defaultAddressFk).toEqual(1); - }); - - it('should create a new address and set as a client default address', async() => { - let data = { - address: { - clientFk: 101, - nickname: 'My address', - street: 'Wall Street', - city: 'New York', - - }, - isDefaultAddress: true - }; - - address = await app.models.Address.createDefaultAddress(data); - client = await app.models.Client.findById(101); - - expect(client.defaultAddressFk).toEqual(address.id); - }); -}); diff --git a/modules/client/back/methods/client/createAddress.js b/modules/client/back/methods/client/createAddress.js new file mode 100644 index 000000000..0319fc386 --- /dev/null +++ b/modules/client/back/methods/client/createAddress.js @@ -0,0 +1,121 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = function(Self) { + Self.remoteMethodCtx('createAddress', { + description: 'Creates client address updating default address', + accepts: [{ + arg: 'id', + type: 'Number', + description: 'The client id', + http: {source: 'path'} + }, + { + arg: 'nickname', + type: 'String', + required: true + }, + { + arg: 'city', + type: 'String', + required: true + }, + { + arg: 'street', + type: 'String', + required: true + }, + { + arg: 'phone', + type: 'String' + }, + { + arg: 'mobile', + type: 'String' + }, + { + arg: 'postalCode', + type: 'String' + }, + { + arg: 'provinceId', + type: 'Number' + }, + { + arg: 'agencyModeId', + type: 'Number' + }, + { + arg: 'incotermsId', + type: 'String' + }, + { + arg: 'customsAgentId', + type: 'Number' + }, + { + arg: 'isActive', + type: 'Boolean' + }, + { + arg: 'isDefaultAddress', + type: 'Boolean' + }], + returns: { + root: true, + type: 'Object' + }, + http: { + verb: 'post', + path: '/:id/createAddress' + } + }); + + Self.createAddress = async(ctx, clientId) => { + const models = Self.app.models; + const args = ctx.args; + const tx = await models.Address.beginTransaction({}); + + try { + const options = {transaction: tx}; + const province = await models.Province.findById(args.provinceId, { + include: { + relation: 'country' + } + }, options); + + const isUeeMember = province.country().isUeeMember; + if (!isUeeMember && !args.incotermsId) + throw new UserError(`Incoterms is required for a non UEE member`); + + if (!isUeeMember && !args.customsAgentId) + throw new UserError(`Customs agent is required for a non UEE member`); + + const newAddress = await models.Address.create({ + clientFk: clientId, + nickname: args.nickname, + incotermsFk: args.incotermsId, + customsAgentFk: args.customsAgentId, + city: args.city, + street: args.street, + phone: args.phone, + postalCode: args.postalCode, + provinceFk: args.provinceId, + agencyModeFk: args.agencyModeId, + isActive: args.isActive + }, options); + const client = await Self.findById(clientId, null, options); + + if (args.isDefaultAddress) { + await client.updateAttributes({ + defaultAddressFk: newAddress.id + }, options); + } + + await tx.commit(); + return newAddress; + } catch (e) { + await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/client/back/methods/client/specs/createAddress.spec.js b/modules/client/back/methods/client/specs/createAddress.spec.js new file mode 100644 index 000000000..29f9e145f --- /dev/null +++ b/modules/client/back/methods/client/specs/createAddress.spec.js @@ -0,0 +1,87 @@ +const app = require('vn-loopback/server/server'); + +describe('Address createAddress', () => { + const clientId = 101; + const provinceId = 5; + const incotermsId = 'FAS'; + const customAgentOneId = 1; + let address; + let client; + + afterAll(async done => { + await client.updateAttributes({defaultAddressFk: 1}); + await address.destroy(); + + done(); + }); + + it('should throw a non uee member error if no incoterms is defined', async() => { + const expectedResult = 'My edited address'; + const ctx = { + args: { + provinceId: provinceId, + nickname: expectedResult, + street: 'Wall Street', + city: 'New York', + customsAgentId: customAgentOneId + } + }; + + try { + await app.models.Client.createAddress(ctx, clientId); + } catch (e) { + err = e; + } + + expect(err).toBeDefined(); + expect(err.message).toEqual('Incoterms is required for a non UEE member'); + }); + + it('should throw a non uee member error if no customsAgent is defined', async() => { + const expectedResult = 'My edited address'; + const ctx = { + args: { + provinceId: provinceId, + nickname: expectedResult, + street: 'Wall Street', + city: 'New York', + incotermsId: incotermsId + } + }; + + + try { + await app.models.Client.createAddress(ctx, clientId); + } catch (e) { + err = e; + } + + expect(err).toBeDefined(); + expect(err.message).toEqual('Customs agent is required for a non UEE member'); + }); + + it('should verify that client defaultAddressFk is untainted', async() => { + client = await app.models.Client.findById(clientId); + + expect(client.defaultAddressFk).toEqual(1); + }); + + it('should create a new address and set as a client default address', async() => { + const ctx = { + args: { + provinceId: 1, + nickname: 'My address', + street: 'Wall Street', + city: 'New York', + incotermsId: incotermsId, + customsAgentId: customAgentOneId, + isDefaultAddress: true + } + }; + + address = await app.models.Client.createAddress(ctx, clientId); + client = await app.models.Client.findById(clientId); + + expect(client.defaultAddressFk).toEqual(address.id); + }); +}); diff --git a/modules/client/back/methods/client/specs/updateAddress.spec.js b/modules/client/back/methods/client/specs/updateAddress.spec.js new file mode 100644 index 000000000..7dca0883e --- /dev/null +++ b/modules/client/back/methods/client/specs/updateAddress.spec.js @@ -0,0 +1,100 @@ +const app = require('vn-loopback/server/server'); + +describe('Address updateAddress', () => { + const clientId = 101; + const addressId = 1; + const provinceId = 5; + const incotermsId = 'FAS'; + const customAgentOneId = 1; + let oldAddress; + let address; + + afterAll(async done => { + await address.updateAttributes({ + nickname: oldAddress.nickname, + provinceFk: 1, + customsAgentFk: null, + incotermsFk: null + }); + + done(); + }); + + it('should throw a non uee member error if no incoterms is defined', async() => { + const expectedResult = 'My edited address'; + const ctx = { + args: { + provinceFk: provinceId, + nickname: expectedResult, + customsAgentFk: customAgentOneId + } + }; + + try { + await app.models.Client.updateAddress(ctx, clientId, addressId); + } catch (e) { + err = e; + } + + expect(err).toBeDefined(); + expect(err.message).toEqual('Incoterms is required for a non UEE member'); + }); + + it('should throw a non uee member error if no customsAgent is defined', async() => { + const expectedResult = 'My edited address'; + const ctx = { + args: { + provinceFk: provinceId, + nickname: expectedResult, + incotermsFk: incotermsId + } + }; + + + try { + await app.models.Client.updateAddress(ctx, clientId, addressId); + } catch (e) { + err = e; + } + + expect(err).toBeDefined(); + expect(err.message).toEqual('Customs agent is required for a non UEE member'); + }); + + it('should update the adress from a non uee member with no error thrown', async() => { + const expectedResult = 'My edited address'; + const ctx = { + args: { + provinceFk: provinceId, + nickname: expectedResult, + incotermsFk: incotermsId, + customsAgentFk: customAgentOneId + } + }; + + oldAddress = await app.models.Address.findById(addressId); + + await app.models.Client.updateAddress(ctx, clientId, addressId); + + address = await app.models.Address.findById(addressId); + + expect(address.nickname).toEqual(expectedResult); + }); + + it('should update the address', async() => { + const expectedResult = 'My second time edited address'; + const ctx = { + args: { + nickname: expectedResult + } + }; + + oldAddress = await app.models.Address.findById(addressId); + + await app.models.Client.updateAddress(ctx, clientId, addressId); + + address = await app.models.Address.findById(addressId); + + expect(address.nickname).toEqual(expectedResult); + }); +}); diff --git a/modules/client/back/methods/client/updateAddress.js b/modules/client/back/methods/client/updateAddress.js new file mode 100644 index 000000000..b9270600f --- /dev/null +++ b/modules/client/back/methods/client/updateAddress.js @@ -0,0 +1,119 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = function(Self) { + Self.remoteMethod('updateAddress', { + description: 'Updates a client address updating default address', + accepts: [{ + arg: 'ctx', + type: 'Object', + http: {source: 'context'} + }, + { + arg: 'clientId', + type: 'Number', + description: 'The client id', + http: {source: 'path'} + }, + { + arg: 'addressId', + type: 'Number', + description: 'The address id', + http: {source: 'path'} + }, + { + arg: 'nickname', + type: 'String' + }, + { + arg: 'city', + type: 'String' + }, + { + arg: 'street', + type: 'String' + }, + { + arg: 'phone', + type: 'String' + }, + { + arg: 'mobile', + type: 'String' + }, + { + arg: 'postalCode', + type: 'String' + }, + { + arg: 'provinceFk', + type: 'Number' + }, + { + arg: 'agencyModeFk', + type: 'Number' + }, + { + arg: 'incotermsFk', + type: 'String' + }, + { + arg: 'customsAgentFk', + type: 'Number' + }, + { + arg: 'isActive', + type: 'Boolean' + }, + { + arg: 'isEqualizated', + type: 'Boolean' + }], + returns: { + root: true, + type: 'Object' + }, + http: { + verb: 'patch', + path: '/:clientId/updateAddress/:addressId' + } + }); + + Self.updateAddress = async(ctx, clientId, addressId) => { + const models = Self.app.models; + const args = ctx.args; + const tx = await models.Address.beginTransaction({}); + try { + const options = {transaction: tx}; + const address = await models.Address.findOne({ + where: { + id: addressId, + clientFk: clientId + } + }); + const provinceId = args.provinceFk || address.provinceFk; + const province = await models.Province.findById(provinceId, { + include: { + relation: 'country' + } + }, options); + + const isUeeMember = province.country().isUeeMember; + const incotermsId = args.incotermsFk || address.incotermsFk; + if (!isUeeMember && !incotermsId) + throw new UserError(`Incoterms is required for a non UEE member`); + + const customsAgentId = args.customsAgentFk || address.customsAgentFk; + if (!isUeeMember && !customsAgentId) + throw new UserError(`Customs agent is required for a non UEE member`); + + delete args.ctx; // Remove unwanted properties + const updatedAddress = await address.updateAttributes(ctx.args, options); + + await tx.commit(); + return updatedAddress; + } catch (e) { + await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/client/back/model-config.json b/modules/client/back/model-config.json index 670b7d2ca..cdb54f6b6 100644 --- a/modules/client/back/model-config.json +++ b/modules/client/back/model-config.json @@ -97,5 +97,11 @@ }, "ClientDms": { "dataSource": "vn" + }, + "CustomsAgent": { + "dataSource": "vn" + }, + "Incoterms": { + "dataSource": "vn" } } diff --git a/modules/client/back/models/address.js b/modules/client/back/models/address.js index afdacdee5..384a2e686 100644 --- a/modules/client/back/models/address.js +++ b/modules/client/back/models/address.js @@ -3,9 +3,6 @@ let getFinalState = require('vn-loopback/util/hook').getFinalState; let isMultiple = require('vn-loopback/util/hook').isMultiple; module.exports = Self => { - // Methods - require('../methods/address/createDefaultAddress')(Self); - Self.validateAsync('isEqualizated', cannotHaveET, { message: 'Cannot check Equalization Tax in this NIF/CIF' }); @@ -25,6 +22,22 @@ module.exports = Self => { done(); } + Self.validateAsync('postalCode', hasValidPostcode, { + message: `The postcode doesn't exist. Please enter a correct one` + }); + + async function hasValidPostcode(err, done) { + if (!this.postalCode) + return done(); + + const models = Self.app.models; + const postcode = await models.Postcode.findById(this.postalCode); + + if (!postcode) err(); + done(); + } + + Self.beforeRemote('findById', function(ctx, modelInstance, next) { ctx.args.filter = { include: [{ @@ -42,21 +55,6 @@ module.exports = Self => { next(); }); - Self.validateAsync('postalCode', hasValidPostcode, { - message: `The postcode doesn't exists. Ensure you put the correct format` - }); - - async function hasValidPostcode(err, done) { - if (!this.postalCode) - return done(); - - const models = Self.app.models; - const postcode = await models.Postcode.findById(this.postalCode); - - if (!postcode) err(); - done(); - } - // Helpers Self.observe('before save', async function(ctx) { diff --git a/modules/client/back/models/address.json b/modules/client/back/models/address.json index ffc80c49b..8daac0466 100644 --- a/modules/client/back/models/address.json +++ b/modules/client/back/models/address.json @@ -1,78 +1,88 @@ { - "name": "Address", - "description": "Client addresses", - "base": "Loggable", - "log": { - "model": "ClientLog", - "relation": "client", - "showField": "nickname" - }, - "options": { - "mysql": { - "table": "address" + "name": "Address", + "description": "Client addresses", + "base": "Loggable", + "log": { + "model": "ClientLog", + "relation": "client", + "showField": "nickname" + }, + "options": { + "mysql": { + "table": "address" + } + }, + "properties": { + "id": { + "type": "Number", + "id": true, + "description": "Identifier" + }, + "nickname": { + "type": "string", + "required": true + }, + "street": { + "type": "string", + "required": true + }, + "city": { + "type": "string", + "required": true + }, + "postalCode": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "longitude": { + "type": "Number" + }, + "latitude": { + "type": "Number" + }, + "isEqualizated": { + "type": "boolean" + } + }, + "validations": [], + "relations": { + "province": { + "type": "belongsTo", + "model": "Province", + "foreignKey": "provinceFk" + }, + "client": { + "type": "belongsTo", + "model": "Client", + "foreignKey": "clientFk" + }, + "agencyMode": { + "type": "belongsTo", + "model": "AgencyMode", + "foreignKey": "agencyModeFk" + }, + "observations": { + "type": "hasMany", + "model": "AddressObservation", + "foreignKey": "addressFk" + }, + "incoterms": { + "type": "belongsTo", + "model": "Incoterms", + "foreignKey": "incotermsFk" + }, + "customsAgent": { + "type": "belongsTo", + "model": "CustomsAgent", + "foreignKey": "customsAgentFk" + } } - }, - "properties": { - "id": { - "type": "Number", - "id": true, - "description": "Identifier" - }, - "nickname": { - "type": "string", - "required": true - }, - "street": { - "type": "string", - "required": true - }, - "city": { - "type": "string", - "required": true - }, - "postalCode": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "mobile": { - "type": "string" - }, - "isActive": { - "type": "boolean" - }, - "longitude": { - "type": "Number" - }, - "latitude": { - "type": "Number" - }, - "isEqualizated": { - "type": "boolean" - } - }, - "validations": [], - "relations": { - "province": { - "type": "belongsTo", - "model": "Province", - "foreignKey": "provinceFk" - }, - "client": { - "type": "belongsTo", - "model": "Client", - "foreignKey": "clientFk" - }, - "agencyMode": { - "type": "belongsTo", - "model": "AgencyMode", - "foreignKey": "agencyModeFk" - }, - "observations": { - "type": "hasMany", - "model": "AddressObservation", - "foreignKey": "addressFk" - } - } -} +} \ No newline at end of file diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index 1dba6c6db..367e0f0eb 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -25,6 +25,8 @@ module.exports = Self => { require('../methods/client/uploadFile')(Self); require('../methods/client/lastActiveTickets')(Self); require('../methods/client/sendSms')(Self); + require('../methods/client/createAddress')(Self); + require('../methods/client/updateAddress')(Self); // Validations @@ -156,7 +158,7 @@ module.exports = Self => { } Self.validateAsync('postCode', hasValidPostcode, { - message: `The postcode doesn't exists. Ensure you put the correct format` + message: `The postcode doesn't exist. Please enter a correct one` }); async function hasValidPostcode(err, done) { diff --git a/modules/client/back/models/customs-agent.json b/modules/client/back/models/customs-agent.json new file mode 100644 index 000000000..f72d7bf28 --- /dev/null +++ b/modules/client/back/models/customs-agent.json @@ -0,0 +1,33 @@ +{ + "name": "CustomsAgent", + "base": "VnModel", + "options": { + "mysql": { + "table": "customsAgent" + } + }, + "properties": { + "id": { + "type": "Number", + "description": "Identifier", + "id": true + }, + "fiscalName": { + "type": "String", + "required": true + }, + "street": { + "type": "String" + }, + "nif": { + "type": "String", + "required": true + }, + "phone": { + "type": "String" + }, + "email": { + "type": "String" + } + } +} \ No newline at end of file diff --git a/modules/client/back/models/incoterms.json b/modules/client/back/models/incoterms.json new file mode 100644 index 000000000..915a5b59a --- /dev/null +++ b/modules/client/back/models/incoterms.json @@ -0,0 +1,27 @@ +{ + "name": "Incoterms", + "base": "VnModel", + "options": { + "mysql": { + "table": "incoterms" + } + }, + "properties": { + "code": { + "type": "String", + "description": "Identifier", + "id": true + }, + "name": { + "type": "String" + } + }, + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] +} \ No newline at end of file diff --git a/modules/client/back/models/specs/address.spec.js b/modules/client/back/models/specs/address.spec.js index 49fb709bf..685acc80d 100644 --- a/modules/client/back/models/specs/address.spec.js +++ b/modules/client/back/models/specs/address.spec.js @@ -2,9 +2,10 @@ const app = require('vn-loopback/server/server'); describe('loopback model address', () => { let createdAddressId; + const clientId = 101; afterAll(async done => { - let client = await app.models.Client.findById(101); + let client = await app.models.Client.findById(clientId); await app.models.Address.destroyById(createdAddressId); await client.updateAttribute('isEqualizated', false); @@ -28,14 +29,14 @@ describe('loopback model address', () => { }); it('should set isEqualizated to true of a given Client to trigger any new address to have it', async() => { - let client = await app.models.Client.findById(101); + let client = await app.models.Client.findById(clientId); expect(client.isEqualizated).toBeFalsy(); await client.updateAttribute('isEqualizated', true); let newAddress = await app.models.Address.create({ - clientFk: 101, + clientFk: clientId, agencyModeFk: 5, city: 'here', isActive: true, @@ -44,7 +45,9 @@ describe('loopback model address', () => { phone: '555555555', postalCode: '46000', provinceFk: 1, - street: 'Test address' + street: 'Test address', + incotermsFk: 'FAS', + customsAgentFk: 1 }); expect(newAddress.isEqualizated).toBeTruthy(); diff --git a/modules/client/front/address/create/index.html b/modules/client/front/address/create/index.html index 43f6e3589..1c70a1cbd 100644 --- a/modules/client/front/address/create/index.html +++ b/modules/client/front/address/create/index.html @@ -1,8 +1,9 @@ @@ -19,7 +20,7 @@ + label="Default" ng-model="$ctrl.address.isDefaultAddress"> @@ -41,7 +42,7 @@ + + + + + + + + + + @@ -130,3 +154,38 @@ + + + + +
New customs agent
+ + + + + + + + + + + + +
+ + + + +
\ No newline at end of file diff --git a/modules/client/front/address/create/index.js b/modules/client/front/address/create/index.js index 3cd8af614..f21ec73ff 100644 --- a/modules/client/front/address/create/index.js +++ b/modules/client/front/address/create/index.js @@ -1,17 +1,14 @@ import ngModule from '../../module'; +import Component from 'core/lib/component'; -export default class Controller { - constructor($, $state) { - this.$ = $; - this.$state = $state; - this.data = { - address: { - clientFk: parseInt($state.params.id), - isActive: true - }, +export default class Controller extends Component { + constructor($element, $) { + super($element, $); + + this.address = { + isActive: true, isDefaultAddress: false }; - this.address = this.data.address; } get postcodeSelection() { @@ -36,15 +33,27 @@ export default class Controller { onSubmit() { this.$.watcher.submit().then(res => { - if (res.data && this.data.isDefaultAddress) + if (this.address.isDefaultAddress) this.client.defaultAddressFk = res.data.id; this.$state.go('client.card.address.index'); }); } + + showCustomAgent(event) { + if (event.defaultPrevented) return; + event.preventDefault(); + + this.$.customAgent.show(); + } + + onCustomAgentAccept() { + return this.$http.post(`CustomsAgents`, this.newCustomsAgent) + .then(res => this.address.customsAgentFk = res.data.id); + } } -Controller.$inject = ['$scope', '$state']; +Controller.$inject = ['$element', '$scope']; ngModule.component('vnClientAddressCreate', { template: require('./index.html'), diff --git a/modules/client/front/address/create/index.spec.js b/modules/client/front/address/create/index.spec.js index e840ec2d8..6bd53cb72 100644 --- a/modules/client/front/address/create/index.spec.js +++ b/modules/client/front/address/create/index.spec.js @@ -3,17 +3,21 @@ import watcher from 'core/mocks/watcher'; describe('Client', () => { describe('Component vnClientAddressCreate', () => { + let $scope; let controller; - let $componentController; + let $httpBackend; + let $element; let $state; beforeEach(ngModule('client')); - beforeEach(angular.mock.inject((_$componentController_, _$state_) => { - $componentController = _$componentController_; + beforeEach(angular.mock.inject(($componentController, $rootScope, _$state_, _$httpBackend_) => { + $scope = $rootScope.$new(); + $httpBackend = _$httpBackend_; $state = _$state_; $state.params.id = '1234'; - controller = $componentController('vnClientAddressCreate', {$state}); + $element = angular.element(''); + controller = $componentController('vnClientAddressCreate', {$element, $scope}); controller.$.watcher = watcher; controller.$.watcher.submit = () => { return { @@ -26,14 +30,13 @@ describe('Client', () => { })); it('should define and set address property', () => { - expect(controller.data.address.clientFk).toBe(1234); - expect(controller.data.address.isActive).toBe(true); + expect(controller.address.isActive).toBe(true); }); describe('onSubmit()', () => { it('should perform a PATCH and not set value to defaultAddressFk property', () => { spyOn(controller.$state, 'go'); - controller.data.isDefaultAddress = false; + controller.address.isDefaultAddress = false; controller.onSubmit(); expect(controller.client.defaultAddressFk).toEqual(121); @@ -42,7 +45,7 @@ describe('Client', () => { it('should perform a PATCH and set a value to defaultAddressFk property', () => { spyOn(controller.$state, 'go'); - controller.data.isDefaultAddress = true; + controller.address.isDefaultAddress = true; controller.onSubmit(); expect(controller.client.defaultAddressFk).toEqual(124); @@ -73,5 +76,16 @@ describe('Client', () => { expect(controller.address.provinceFk).toEqual(1); }); }); + + describe('onCustomAgentAccept()', () => { + it(`should create a new customs agent and then set the customsAgentFk property on the address`, () => { + const expectedResult = {id: 1, fiscalName: 'Customs agent one'}; + $httpBackend.when('POST', 'CustomsAgents').respond(200, expectedResult); + controller.onCustomAgentAccept(); + $httpBackend.flush(); + + expect(controller.address.customsAgentFk).toEqual(1); + }); + }); }); }); diff --git a/modules/client/front/address/create/locale/es.yml b/modules/client/front/address/create/locale/es.yml deleted file mode 100644 index 922d758d5..000000000 --- a/modules/client/front/address/create/locale/es.yml +++ /dev/null @@ -1,9 +0,0 @@ -Street address: Dirección postal -Default: Predeterminado -Consignee: Consignatario -Postcode: Código postal -Town/City: Ciudad -Province: Provincia -Agency: Agencia -Phone: Teléfono -Mobile: Móvil \ No newline at end of file diff --git a/modules/client/front/address/edit/index.html b/modules/client/front/address/edit/index.html index ed3758e33..035608120 100644 --- a/modules/client/front/address/edit/index.html +++ b/modules/client/front/address/edit/index.html @@ -6,16 +6,16 @@ @@ -99,8 +99,8 @@ ng-model="$ctrl.address.postalCode" rule> - + + +
New customs agent
+ + + + + + + + + + + + +
+ + + + +
\ No newline at end of file diff --git a/modules/client/front/address/edit/index.js b/modules/client/front/address/edit/index.js index 7c7b274a6..4e5ed7237 100644 --- a/modules/client/front/address/edit/index.js +++ b/modules/client/front/address/edit/index.js @@ -1,12 +1,7 @@ import ngModule from '../../module'; +import Component from 'core/lib/component'; -export default class Controller { - constructor($scope, $state) { - this.$ = $scope; - this.$state = $state; - this.$stateParams = $state.params; - } - +export default class Controller extends Component { removeObservation(index) { this.$.watcher.setDirty(); this.$.model.remove(index); @@ -25,17 +20,26 @@ export default class Controller { } onSubmit() { - this.$.watcher.check(); - this.$.watcher.realSubmit() + this.$.watcher.submit() .then(() => this.$.model.save(true)) .then(() => { - this.$.watcher.notifySaved(); this.card.reload(); this.goToIndex(); }); } + + showCustomAgent(event) { + if (event.defaultPrevented) return; + event.preventDefault(); + + this.$.customAgent.show(); + } + + onCustomAgentAccept() { + return this.$http.post(`CustomsAgents`, this.newCustomsAgent) + .then(res => this.address.customsAgentFk = res.data.id); + } } -Controller.$inject = ['$scope', '$state']; ngModule.component('vnClientAddressEdit', { template: require('./index.html'), diff --git a/modules/client/front/address/edit/index.spec.js b/modules/client/front/address/edit/index.spec.js index aa1f59669..3d91ad440 100644 --- a/modules/client/front/address/edit/index.spec.js +++ b/modules/client/front/address/edit/index.spec.js @@ -2,15 +2,22 @@ import './index'; describe('Client', () => { describe('Component vnClientAddressEdit', () => { - let $state; + let $scope; let controller; + let $httpBackend; + let $element; + let $state; beforeEach(ngModule('client')); - beforeEach(angular.mock.inject(($componentController, _$state_) => { + beforeEach(angular.mock.inject(($componentController, $rootScope, _$state_, _$httpBackend_) => { + $scope = $rootScope.$new(); + $httpBackend = _$httpBackend_; $state = _$state_; $state.params.addressId = '1'; - controller = $componentController('vnClientAddressEdit', {$state}); + $element = angular.element(''); + controller = $componentController('vnClientAddressEdit', {$element, $scope}); + controller.address = {id: 1, customsAgentFk: null}; controller.$.watcher = { setDirty: () => {}, setPristine: () => {}, @@ -55,5 +62,16 @@ describe('Client', () => { expect(controller.$state.go).toHaveBeenCalledWith('client.card.address.index'); }); }); + + describe('onCustomAgentAccept()', () => { + it(`should create a new customs agent and then set the customsAgentFk property on the address`, () => { + const expectedResult = {id: 1, fiscalName: 'Customs agent one'}; + $httpBackend.when('POST', 'CustomsAgents').respond(200, expectedResult); + controller.onCustomAgentAccept(); + $httpBackend.flush(); + + expect(controller.address.customsAgentFk).toEqual(1); + }); + }); }); }); diff --git a/modules/client/front/address/edit/locale/es.yml b/modules/client/front/address/edit/locale/es.yml deleted file mode 100644 index f1aa52834..000000000 --- a/modules/client/front/address/edit/locale/es.yml +++ /dev/null @@ -1,7 +0,0 @@ -Enabled: Activo -Is equalizated: Recargo de equivalencia -Observation type: Tipo de observación -Description: Descripción -The observation type must be unique: El tipo de observación ha de ser único -Remove note: Quitar nota -Add note: Añadir nota \ No newline at end of file diff --git a/modules/client/front/address/index/locale/es.yml b/modules/client/front/address/index/locale/es.yml deleted file mode 100644 index 5df8b3275..000000000 --- a/modules/client/front/address/index/locale/es.yml +++ /dev/null @@ -1,2 +0,0 @@ -Set as default: Establecer como predeterminado -Active first to set as default: Active primero para marcar como predeterminado \ No newline at end of file diff --git a/modules/client/front/address/locale/es.yml b/modules/client/front/address/locale/es.yml new file mode 100644 index 000000000..dc39175d6 --- /dev/null +++ b/modules/client/front/address/locale/es.yml @@ -0,0 +1,27 @@ +# Index +Set as default: Establecer como predeterminado +Active first to set as default: Active primero para marcar como predeterminado +# Edit +Enabled: Activo +Is equalizated: Recargo de equivalencia +Observation type: Tipo de observación +Description: Descripción +The observation type must be unique: El tipo de observación ha de ser único +Remove note: Quitar nota +Add note: Añadir nota +Customs agent: Agente de aduanas +New customs agent: Nuevo agente de aduanas +# Create +Street address: Dirección postal +Default: Predeterminado +Consignee: Consignatario +Postcode: Código postal +Town/City: Ciudad +Province: Provincia +Agency: Agencia +Phone: Teléfono +Mobile: Móvil + +# Common +Fiscal name: Nombre fiscal +Street: Dirección fiscal \ No newline at end of file