From 269d137df5a280682f77b9fa2a3f6fcf73bc8a6e Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Wed, 26 Jun 2024 08:44:06 +0200 Subject: [PATCH] OAM-218: improve the scalability of local storage, improve login performance --- .../local-storage.factory.js | 270 +++++++++++ .../local-storage.factory.spec.js | 256 ++++++++++ src/referencedata-lot/lot.service.js | 106 ++++ src/referencedata-lot/lot.service.spec.js | 184 +++++++ .../orderable-fulfills.service.js | 111 +++++ .../orderable-fulfills.service.spec.js | 103 ++++ src/referencedata-program/program.service.js | 222 +++++++++ .../program.service.spec.js | 283 +++++++++++ .../program.service.decorator.js | 56 +++ .../program.service.decorator.spec.js | 101 ++++ .../requisition-search-cache.run.js | 37 ++ .../requisition-search-cache.run.spec.js | 92 ++++ .../requisition-search.service.js | 234 +++++++++ .../requisition-search.service.spec.js | 457 ++++++++++++++++++ 14 files changed, 2512 insertions(+) create mode 100644 src/openlmis-local-storage/local-storage.factory.js create mode 100644 src/openlmis-local-storage/local-storage.factory.spec.js create mode 100644 src/referencedata-lot/lot.service.js create mode 100644 src/referencedata-lot/lot.service.spec.js create mode 100644 src/referencedata-orderable-fulfills/orderable-fulfills.service.js create mode 100644 src/referencedata-orderable-fulfills/orderable-fulfills.service.spec.js create mode 100644 src/referencedata-program/program.service.js create mode 100644 src/referencedata-program/program.service.spec.js create mode 100644 src/referencedata-user-programs-cache/program.service.decorator.js create mode 100644 src/referencedata-user-programs-cache/program.service.decorator.spec.js create mode 100644 src/requisition-search/requisition-search-cache.run.js create mode 100644 src/requisition-search/requisition-search-cache.run.spec.js create mode 100644 src/requisition-search/requisition-search.service.js create mode 100644 src/requisition-search/requisition-search.service.spec.js diff --git a/src/openlmis-local-storage/local-storage.factory.js b/src/openlmis-local-storage/local-storage.factory.js new file mode 100644 index 0000000..085b653 --- /dev/null +++ b/src/openlmis-local-storage/local-storage.factory.js @@ -0,0 +1,270 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc service + * @name openlmis-local-storage.localStorageFactory + * + * @description + * It stores objects in browser cache to make them accessible offline. + * Each stored or retrieved object is copied. + */ + angular + .module('openlmis-local-storage') + .factory('localStorageFactory', factory); + + factory.$inject = ['localStorageService', '$filter']; + + function factory(localStorageService, $filter) { + var resources = {}; + + return LocalStorageFactory; + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name LocalStorageFactory + * + * @description + * Creates array in local storage that is named with resourceName. + * It returns object with methods to operate on this resource. + * + * @param {String} resourceName Name of resource to be stored + * @return {Object} Object with methods to save/get resource objects + */ + function LocalStorageFactory(resourceName) { + if (resources[resourceName]) { + return resources[resourceName]; + } + + var items = getFromStorage(resourceName); + + if (!items) { + items = []; + } + + var resource = { + put: put, + putAll: putAll, + getBy: getBy, + getAll: getAll, + search: search, + remove: remove, + removeBy: removeBy, + clearAll: clearAll, + contains: contains + }; + resources[resourceName] = resource; + return resource; + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name put + * + * @description + * Stores given object in local storage. + * + * @param {Object} object Object to store + */ + function put(item) { + if (item && !contains(item)) { + executeWithStorageUpdate(function() { + if (item.id) { + removeItemBy('id', item.id); + } + items.push(typeof item === 'object' ? JSON.parse(JSON.stringify(item)) : item); + }); + } + } + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name putAll + * + * @description + * Stores all objects in local storage. + * + * @param {Array} collectionToStore Array of objects to store + */ + function putAll(collectionToStore) { + if (Array.isArray(collectionToStore) && collectionToStore.length > 0) { + executeWithStorageUpdate(function() { + items = collectionToStore.map(function(item) { + if (item.id) { + removeItemBy('id', item.id); + } + return typeof item === 'object' ? JSON.parse(JSON.stringify(item)) : item; + }); + }); + } + } + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name getBy + * + * @description + * It searches for all objects that given property value is equal + * and returns first from the list. If there is no results method returns undefined. + * + * @param {String} property Name of object property to be compared + * @param {Object} value Value of property (equals found object property value) + * @return {Object} First found object with equal property value + */ + function getBy(property, value) { + var filtered = searchItems(toParams(property, value)); + return filtered.length ? JSON.parse(JSON.stringify(filtered[0])) : undefined; + } + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name getAll + * + * @description + * Method returns all resource objects stored in local storage. + * + * @return {Array} All objects stored in this resource + */ + function getAll() { + return angular.copy(items); + } + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name search + * + * @description + * It takes params and passes it to filter. If filter name is + * not defined the default filter will be used. + * + * @param {Object} params Criteria passed to filter + * @param {String} filter Name of the filter(optional) + * @return {Array} Filtered objects + */ + function search(params, filter) { + return angular.copy(searchItems(params, filter)); + } + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name remove + * + * @description + * Removes a specific object from local storage + * + * @param {Object} item Object to remove + */ + function remove(item) { + executeWithStorageUpdate(function() { + removeItem(item); + }); + } + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name removeBy + * + * @description + * It searches for all objects that given property value is equal + * and removes first from the list. If there is no results method returns undefined. + * + * @param {String} property Name of object property to be compared + * @param {Object} value Value of property (equals found object property value) + */ + function removeBy(property, value) { + executeWithStorageUpdate(function() { + removeItemBy(property, value); + }); + } + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name clearAll + * + * @description + * Removes all items from resource. + */ + function clearAll() { + executeWithStorageUpdate(function() { + items.splice(0, items.length); + }); + } + + /** + * @ngdoc method + * @methodOf openlmis-local-storage.localStorageFactory + * @name contains + * + * @description + * Check if exactly same object exist in storage and if so + * returns true, otherwise false. + * + * @param {Object} object Object to compare + * @return {Boolean} if objects exist a in storage + */ + function contains(object) { + return items.indexOf(object) !== -1; + } + + function removeItem(item) { + items.splice(items.indexOf(item), 1); + } + + function removeItemBy(property, value) { + var filtered = searchItems(toParams(property, value)); + if (filtered.length) { + removeItem(filtered[0]); + } + } + + function searchItems(params, filter) { + return $filter(filter ? filter : 'filter')(items, params); + } + + function toParams(property, value) { + var params = {}; + params[property] = value; + return params; + } + + function executeWithStorageUpdate(functionToExecute) { + functionToExecute(); + updateStorage(resourceName, items); + } + } + + function getFromStorage(key) { + var json = localStorageService.get(key); + return json ? angular.fromJson(json) : undefined; + } + + function updateStorage(key, data) { + localStorageService.add(key, angular.toJson(data)); + } + + } +})(); diff --git a/src/openlmis-local-storage/local-storage.factory.spec.js b/src/openlmis-local-storage/local-storage.factory.spec.js new file mode 100644 index 0000000..49c3368 --- /dev/null +++ b/src/openlmis-local-storage/local-storage.factory.spec.js @@ -0,0 +1,256 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('localStorageFactory', function() { + + beforeEach(function() { + module('openlmis-local-storage'); + + inject(function($injector) { + this.localStorageService = $injector.get('localStorageService'); + this.localStorageFactory = $injector.get('localStorageFactory'); + }); + + this.items = [{ + id: 1, + name: 'item1' + }, + { + id: 2, + name: 'item2' + }]; + + var items = this.items; + spyOn(this.localStorageService, 'add'); + spyOn(this.localStorageService, 'get').andCallFake(function(resourceName) { + return resourceName === 'items' ? items : undefined; + }); + + this.itemsLocalStorage = this.localStorageFactory('items'); + + this.compare = function(a, b) { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + }; + }); + + describe('put', function() { + + beforeEach(function() { + this.item = { + id: 3, + name: 'item3' + }; + }); + + it('should put item', function() { + this.itemsLocalStorage.put(this.item); + + expect(this.items.length).toBe(3); + expect(this.items[2]).toEqual(this.item); + }); + + it('should update object if item with the same id exist', function() { + this.item.id = 1; + + this.itemsLocalStorage.put(this.item); + + expect(this.items.length).toBe(2); + expect(this.items[1]).toEqual(this.item); + }); + + it('should update storage', function() { + this.itemsLocalStorage.put(this.item); + + expect(this.localStorageService.add).toHaveBeenCalledWith('items', angular.toJson(this.items)); + }); + + it('should not modify the storage object, unless the object is explicitly updated', function() { + this.itemsLocalStorage.put(this.item); + + var firstItem = this.itemsLocalStorage.getBy('id', 3); + + expect(firstItem.name).toEqual('item3'); + + // don't save this change + firstItem.name = 'foo bar'; + + var secondItem = this.itemsLocalStorage.getBy('id', 3); + + expect(secondItem.name).toEqual('item3'); + + secondItem.name = 'foo baz'; + // just saved the item + this.itemsLocalStorage.put(secondItem); + + var thirdItem = this.itemsLocalStorage.getBy('id', 3); + // a newly pulled item got the changes + expect(thirdItem.name).toEqual('foo baz'); + }); + + }); + + describe('putAll', function() { + + beforeEach(function() { + this.putAllItems = [{ + id: 3, + name: 'item3' + }, + { + id: 4, + name: 'item4' + }]; + }); + + it('should put all items', function() { + this.itemsLocalStorage.putAll(this.putAllItems); + + expect(this.putAllItems.length).toBe(2); + }); + + it('should update storage', function() { + this.itemsLocalStorage.putAll(this.putAllItems); + + expect(this.localStorageService.add).toHaveBeenCalledWith('items', angular.toJson(this.putAllItems)); + }); + }); + + describe('getBy', function() { + + it('should get item by id', function() { + var result = this.itemsLocalStorage.getBy('id', 1); + + expect(result).toEqual(this.items[0]); + }); + + it('should get item by name', function() { + var result = this.itemsLocalStorage.getBy('name', 'item2'); + + expect(result).toEqual(this.items[1]); + }); + + }); + + describe('clearAll', function() { + + it('should remove all items', function() { + this.itemsLocalStorage.clearAll(); + + expect(this.items.length).toBe(0); + }); + + it('should update storage', function() { + this.itemsLocalStorage.clearAll(); + + expect(this.localStorageService.add).toHaveBeenCalledWith('items', angular.toJson(this.items)); + }); + + }); + + describe('getAll', function() { + + it('should get all items', function() { + var result = this.itemsLocalStorage.getAll(); + result.sort(this.compare); + + expect(result.length).toBe(2); + expect(result[0]).toEqual(this.items[0]); + expect(result[1]).toEqual(this.items[1]); + }); + }); + + describe('search', function() { + + it('should search with default filter by id', function() { + var result = this.itemsLocalStorage.search({ + id: this.items[0].id + }); + + expect(result.length).toBe(1); + expect(result[0]).toEqual(this.items[0]); + }); + + it('should search with default filter by name', function() { + var result = this.itemsLocalStorage.search({ + name: this.items[1].name + }); + + expect(result.length).toBe(1); + expect(result[0]).toEqual(this.items[1]); + }); + + it('should search with default filter by name and id', function() { + var result = this.itemsLocalStorage.search({ + name: this.items[1].name, + id: this.items[1].id + }); + + expect(result.length).toBe(1); + expect(result[0]).toEqual(this.items[1]); + }); + + it('should failed to search with default filter by wrong name', function() { + var result = this.itemsLocalStorage.search({ + name: this.items[0].name, + id: this.items[1].id + }); + + expect(result.length).toBe(0); + }); + }); + + describe('removeBy', function() { + + it('should remove item by id', function() { + var result, + id = this.items[0].id; + + this.itemsLocalStorage.removeBy('id', id); + result = this.itemsLocalStorage.getAll(); + + expect(this.itemsLocalStorage.getBy('id', id)).toBe(undefined); + expect(result.length).toBe(1); + }); + + it('should remove item by name', function() { + var result, + name = this.items[0].name; + + this.itemsLocalStorage.removeBy('name', name); + result = this.itemsLocalStorage.getAll(); + + expect(this.itemsLocalStorage.getBy('name', name)).toBe(undefined); + expect(result.length).toBe(1); + }); + + it('should not remove item with wrong value', function() { + var result; + + this.itemsLocalStorage.removeBy('name', 'otherName'); + result = this.itemsLocalStorage.getAll(); + result.sort(this.compare); + + expect(result.length).toBe(2); + expect(result[0]).toEqual(this.items[0]); + expect(result[1]).toEqual(this.items[1]); + }); + }); +}); diff --git a/src/referencedata-lot/lot.service.js b/src/referencedata-lot/lot.service.js new file mode 100644 index 0000000..b41e89e --- /dev/null +++ b/src/referencedata-lot/lot.service.js @@ -0,0 +1,106 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc service + * @name referencedata-lot.lotService + * + * @description + * Responsible for retrieving lots information from server. + */ + angular + .module('referencedata-lot') + .service('lotService', service); + + service.$inject = [ + 'LotResource', 'localStorageFactory', 'offlineService', '$q', 'alertService' + ]; + + function service(LotResource, localStorageFactory, offlineService, $q, alertService) { + + var lotsOffline = localStorageFactory('lots'); + var lotResource = new LotResource(); + + this.query = query; + this.clearLotsOffline = clearLotsOffline; + + /** + * @ngdoc method + * @methodOf referencedata-lot.lotService + * @name query + * + * @description + * Retrieves lots that match given params. + * If user is online it stores lots into offline storage. + * + * @param {String} queryParams the search parameters + */ + function query(queryParams) { + if (offlineService.isOffline()) { + return getFromLocalStorage(queryParams); + } + + return lotResource.query(queryParams) + .then(function(lots) { + lotsOffline.putAll(lots.content); + return lots; + }); + } + + function getFromLocalStorage(queryParams) { + var lots = {}, + deferred = $q.defer(), + paramName = Object.keys(queryParams)[0]; + lots.content = []; + + queryParams[paramName].forEach(function(param) { + return getByParamFromLocalStorage(lots, deferred, paramName, param); + }); + + deferred.resolve(lots); + return deferred.promise; + } + + function getByParamFromLocalStorage(lots, deferred, paramName, param) { + if (paramName === 'tradeItemId') { + var cachedLots = lotsOffline.search({ + tradeItemId: param + }); + cachedLots.forEach(function(offlineLot) { + lots.content.push(offlineLot); + }); + if (lots.content.length === 0) { + alertService.error('referencedataLot.offlineMessage'); + deferred.reject(); + } + } else { + var offlineLot = lotsOffline.getBy(paramName, param); + if (!offlineLot) { + alertService.error('referencedataLot.offlineMessage'); + deferred.reject(); + } + lots.content.push(offlineLot); + } + } + + function clearLotsOffline() { + lotsOffline.clearAll(); + } + } +})(); diff --git a/src/referencedata-lot/lot.service.spec.js b/src/referencedata-lot/lot.service.spec.js new file mode 100644 index 0000000..9110fec --- /dev/null +++ b/src/referencedata-lot/lot.service.spec.js @@ -0,0 +1,184 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('lotService', function() { + + beforeEach(function() { + + this.offlineService = jasmine.createSpyObj('offlineService', ['isOffline', 'checkConnection']); + this.lotsStorage = jasmine.createSpyObj('lotsStorage', ['put', 'putAll', 'getBy', 'search']); + + var offlineService = this.offlineService, + lotsStorage = this.lotsStorage; + + module('referencedata-lot', function($provide) { + $provide.service('localStorageFactory', function() { + return jasmine.createSpy('localStorageFactory').andReturn(lotsStorage); + }); + + $provide.service('offlineService', function() { + return offlineService; + }); + }); + + inject(function($injector) { + this.$httpBackend = $injector.get('$httpBackend'); + this.$q = $injector.get('$q'); + this.$rootScope = $injector.get('$rootScope'); + this.referencedataUrlFactory = $injector.get('referencedataUrlFactory'); + this.lotService = $injector.get('lotService'); + this.alertService = $injector.get('alertService'); + this.LotDataBuilder = $injector.get('LotDataBuilder'); + this.PageDataBuilder = $injector.get('PageDataBuilder'); + }); + + this.offlineService.isOffline.andReturn(false); + + this.lots = [ + new this.LotDataBuilder() + .withId(1) + .withTradeItemId('tradeItemId-1') + .build(), + new this.LotDataBuilder() + .withTradeItemId('tradeItemId-2') + .withId(2) + .build(), + new this.LotDataBuilder() + .withId(3) + .build(), + new this.LotDataBuilder() + .withId(4) + .build() + ]; + + this.lotsPage = new this.PageDataBuilder() + .withContent(this.lots) + .build(); + + this.page = 0; + this.size = 2; + + this.params = { + page: this.page, + size: this.size + }; + }); + + describe('query', function() { + + it('should get all lots and save them to storage', function() { + this.$httpBackend + .expectGET(this.referencedataUrlFactory('/api/lots')) + .respond(200, this.lotsPage); + var result; + this.lotService + .query() + .then(function(paginatedObject) { + result = paginatedObject; + }); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(result.content).toEqual(this.lots); + expect(this.lotsStorage.putAll.callCount).toEqual(1); + }); + + it('should get a proper lot from local storage', function() { + this.offlineService.isOffline.andReturn(true); + this.lotsStorage.getBy.andReturn(this.lots[0]); + var params = { + id: [this.lots[0].id] + }; + + var result; + this.lotService + .query(params) + .then(function(paginatedObject) { + result = paginatedObject; + }); + this.$rootScope.$apply(); + + expect(this.offlineService.isOffline).toHaveBeenCalled(); + expect(result.content[0]).toEqual(this.lots[0]); + expect(this.lotsStorage.put).not.toHaveBeenCalled(); + }); + + it('should get a proper lot from local storage by tradeItemId', function() { + this.offlineService.isOffline.andReturn(true); + this.lotsStorage.search.andReturn(this.lots); + var params = { + tradeItemId: [this.lots[0].tradeItemId] + }; + + var result; + this.lotService + .query(params) + .then(function(paginatedObject) { + result = paginatedObject; + }); + this.$rootScope.$apply(); + + expect(this.offlineService.isOffline).toHaveBeenCalled(); + expect(result.content[0]).toEqual(this.lots[0]); + expect(this.lotsStorage.put).not.toHaveBeenCalled(); + }); + + it('should reject if lot not found by id in local storage', function() { + spyOn(this.alertService, 'error'); + + this.offlineService.isOffline.andReturn(true); + this.lotsStorage.getBy.andReturn(undefined); + var params = { + id: [this.lots[0].id] + }; + + var result; + this.lotService + .query(params) + .then(function(paginatedObject) { + result = paginatedObject; + }); + this.$rootScope.$apply(); + + expect(result).toBeUndefined(); + expect(this.offlineService.isOffline).toHaveBeenCalled(); + expect(this.lotsStorage.put).not.toHaveBeenCalled(); + expect(this.alertService.error).toHaveBeenCalledWith('referencedataLot.offlineMessage'); + }); + + it('should reject if lot not found by tradeItemId in local storage', function() { + spyOn(this.alertService, 'error'); + + this.offlineService.isOffline.andReturn(true); + this.lotsStorage.search.andReturn([]); + var params = { + tradeItemId: [this.lots[0].tradeItemId] + }; + + var result; + this.lotService + .query(params) + .then(function(paginatedObject) { + result = paginatedObject; + }); + this.$rootScope.$apply(); + + expect(result).toBeUndefined(); + expect(this.offlineService.isOffline).toHaveBeenCalled(); + expect(this.lotsStorage.put).not.toHaveBeenCalled(); + expect(this.alertService.error).toHaveBeenCalledWith('referencedataLot.offlineMessage'); + }); + }); +}); \ No newline at end of file diff --git a/src/referencedata-orderable-fulfills/orderable-fulfills.service.js b/src/referencedata-orderable-fulfills/orderable-fulfills.service.js new file mode 100644 index 0000000..1ad8876 --- /dev/null +++ b/src/referencedata-orderable-fulfills/orderable-fulfills.service.js @@ -0,0 +1,111 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc service + * @name referencedata-orderable-fulfills.orderableFulfillsService + * + * @description + * Responsible for retrieving orderableFulFills information from server. + */ + angular + .module('referencedata-orderable-fulfills') + .service('orderableFulfillsService', service); + + service.$inject = [ + 'OrderableFulfillsResource', 'localStorageFactory', 'offlineService', '$q', 'alertService' + ]; + + function service(OrderableFulfillsResource, localStorageFactory, offlineService, $q, alertService) { + + var orderableFulfillsOffline = localStorageFactory('orderableFulfills'); + var orderableFulfillsResource = new OrderableFulfillsResource(); + + this.query = query; + this.clearOrderableFulfillsOffline = clearOrderableFulfillsOffline; + + /** + * @ngdoc method + * @methodOf referencedata-orderable-fulfills.orderableFulfillsService + * @name query + * + * @description + * Retrieves orderableFulFills that match given params. + * If user is online it stores orderableFulFills into offline storage. + * + * @param {String} queryParams the search parameters + */ + function query(queryParams) { + if (offlineService.isOffline()) { + return getFromLocalStorage(queryParams); + } + + return orderableFulfillsResource.query(queryParams) + .then(function(orderableFulfills) { + var orderableFulfillsToStore = Object.keys( + orderableFulfills + ).map(function(item) { + if (!isPromiseAttribute(item)) { + var orderableFulfillJson = { + id: item + }; + Object.entries(orderableFulfills[item]).forEach( + function(entry) { + orderableFulfillJson[entry[0]] = entry[1]; + } + ); + + return orderableFulfillJson; + } + }); + + orderableFulfillsOffline.putAll(orderableFulfillsToStore); + + return orderableFulfills; + }); + } + + function getFromLocalStorage(queryParams) { + var orderableFulfills = [], + paramName = Object.keys(queryParams)[0]; + + queryParams[paramName].forEach(function(param) { + var cachedOrderableFulfill = orderableFulfillsOffline.getBy(paramName, param); + + if (cachedOrderableFulfill) { + orderableFulfills[param] = cachedOrderableFulfill; + } + }); + + if (Object.keys(orderableFulfills).length === 0) { + alertService.error('referencedataOrderableFulfills.offlineMessage'); + return $q.reject(); + } + return $q.resolve(orderableFulfills); + } + + function clearOrderableFulfillsOffline() { + return orderableFulfillsOffline.clearAll(); + } + + function isPromiseAttribute(item) { + return item === '$promise' || item === '$resolved'; + } + } +})(); diff --git a/src/referencedata-orderable-fulfills/orderable-fulfills.service.spec.js b/src/referencedata-orderable-fulfills/orderable-fulfills.service.spec.js new file mode 100644 index 0000000..93830e9 --- /dev/null +++ b/src/referencedata-orderable-fulfills/orderable-fulfills.service.spec.js @@ -0,0 +1,103 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('orderableFulfillsService', function() { + + beforeEach(function() { + + this.offlineService = jasmine.createSpyObj('offlineService', ['isOffline', 'checkConnection']); + this.orderableFulfillsStorage = jasmine.createSpyObj('orderableFulfillsStorage', ['put', 'putAll', 'getBy']); + + var offlineService = this.offlineService, + orderableFulfillsStorage = this.orderableFulfillsStorage; + + module('referencedata-orderable-fulfills', function($provide) { + $provide.service('localStorageFactory', function() { + return jasmine.createSpy('localStorageFactory').andReturn(orderableFulfillsStorage); + }); + + $provide.service('offlineService', function() { + return offlineService; + }); + }); + + inject(function($injector) { + this.$httpBackend = $injector.get('$httpBackend'); + this.$rootScope = $injector.get('$rootScope'); + this.referencedataUrlFactory = $injector.get('referencedataUrlFactory'); + this.orderableFulfillsService = $injector.get('orderableFulfillsService'); + this.PageDataBuilder = $injector.get('PageDataBuilder'); + this.alertService = $injector.get('alertService'); + }); + + this.offlineService.isOffline.andReturn(false); + + this.orderableFulfills = { + idOne: { + canFulfillForMe: [1, 2], + canBeFulfilledByMe: [] + }, + idTwo: { + canFulfillForMe: [], + canBeFulfilledByMe: [] + } + }; + }); + + describe('query', function() { + + it('should get all orderableFulfills and save them to storage', function() { + this.$httpBackend + .expectGET(this.referencedataUrlFactory('/api/orderableFulfills')) + .respond(200, this.orderableFulfills); + var result; + this.orderableFulfillsService + .query() + .then(function(items) { + result = items; + }); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(result['idOne']).toEqual(this.orderableFulfills['idOne']); + expect(result['idTwo']).toEqual(this.orderableFulfills['idTwo']); + expect(this.orderableFulfillsStorage.putAll.callCount).toEqual(1); + }); + + it('should reject if orderable fulfills not found in the local storage', function() { + spyOn(this.alertService, 'error'); + + this.offlineService.isOffline.andReturn(true); + this.orderableFulfillsStorage.getBy.andReturn(undefined); + + var params = { + id: ['id-1'] + }; + + var result; + this.orderableFulfillsService + .query(params) + .then(function(items) { + result = items; + }); + this.$rootScope.$apply(); + + expect(result).toBeUndefined(); + expect(this.offlineService.isOffline).toHaveBeenCalled(); + expect(this.orderableFulfillsStorage.put).not.toHaveBeenCalled(); + expect(this.alertService.error).toHaveBeenCalledWith('referencedataOrderableFulfills.offlineMessage'); + }); + }); +}); \ No newline at end of file diff --git a/src/referencedata-program/program.service.js b/src/referencedata-program/program.service.js new file mode 100644 index 0000000..09d313f --- /dev/null +++ b/src/referencedata-program/program.service.js @@ -0,0 +1,222 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc service + * @name referencedata-program.programService + * + * @description + * Responsible for retrieving programs from the server. + */ + angular + .module('referencedata-program') + .factory('programService', service); + + service.$inject = ['openlmisUrlFactory', '$resource', '$q', 'localStorageFactory', 'localStorageService']; + + function service(openlmisUrlFactory, $resource, $q, localStorageFactory, localStorageService) { + + var resource = $resource(openlmisUrlFactory('/api/programs/:id'), {}, { + getAll: { + url: openlmisUrlFactory('/api/programs'), + method: 'GET', + isArray: true + }, + getUserPrograms: { + url: openlmisUrlFactory('api/users/:userId/programs'), + method: 'GET', + isArray: true + }, + update: { + method: 'PUT' + }, + getUserSupportedPrograms: { + url: openlmisUrlFactory('api/users/:userId/supportedPrograms'), + method: 'GET', + isArray: true + } + }), + userProgramsCache = localStorageFactory('userPrograms'), + programsCache = localStorageFactory('programs'); + + return { + get: get, + getAll: getAll, + getUserPrograms: getUserPrograms, + getUserSupportedPrograms: getUserSupportedPrograms, + update: update, + create: create, + clearProgramsCache: clearProgramsCache + }; + + /** + * @ngdoc method + * @methodOf referencedata-program.programService + * @name get + * + * @description + * Gets program by id. + * + * @param {String} id Program UUID + * @return {Promise} Program info + */ + function get(id) { + var cachedProgram = programsCache.getBy('id', id); + + if (cachedProgram) { + return $q.resolve(cachedProgram); + } + + return resource.get({ + id: id + }) + .$promise + .then(function(program) { + programsCache.put(program); + return program; + }) + .catch(function() { + return $q.reject(); + }); + } + + /** + * @ngdoc method + * @methodOf referencedata-program.programService + * @name getAll + * + * @description + * Gets all programs. + * + * @return {Promise} Array of all programs + */ + function getAll() { + return resource.getAll().$promise; + } + + /** + * @ngdoc method + * @methodOf referencedata-program.programService + * @name update + * + * @description + * Updates program. + * + * @param {Object} program Program to be updated + * @return {Promise} Updated program + */ + function update(program) { + return resource.update({ + id: program.id + }, program) + .$promise + .then(function(program) { + programsCache.put(program); + return program; + }) + .catch(function() { + return $q.reject(); + }); + } + + /** + * @ngdoc method + * @methodOf referencedata-program.programService + * @name create + * + * @description + * Creates new program. + * + * @param {Object} program Program to be created + * @return {Promise} Updated program + */ + function create(program) { + return resource.save(null, program).$promise; + } + + /** + * @ngdoc method + * @methodOf referencedata-program.programService + * @name getUserPrograms + * + * @description + * Retrieves programs for the current user and saves them in the local storage. + * If the user is offline program are retrieved from the local storage. + * + * @param {String} userId User UUID + * @return {Promise} Array of programs + */ + function getUserPrograms(userId) { + var cachedPrograms = userProgramsCache.search({ + userIdOffline: userId + }); + + if (cachedPrograms && cachedPrograms.length) { + return $q.resolve(cachedPrograms); + } + + return resource.getUserPrograms({ + userId: userId + }) + .$promise + .then(function(programs) { + var programsToStore = programs.map(function(program) { + program.userIdOffline = userId; + return program; + }); + + userProgramsCache.putAll(programsToStore); + + return programs; + }) + .catch(function() { + return $q.reject(); + }); + } + + /** + * @ngdoc method + * @methodOf referencedata-program.programService + * @name getUserSupportedPrograms + * + * @description + * Retrieves programs for the given user. + * + * @param {String} userId User UUID + * @return {Promise} Array of programs + */ + function getUserSupportedPrograms(userId) { + return resource.getUserSupportedPrograms({ + userId: userId + }).$promise; + } + + /** + * @ngdoc method + * @methodOf referencedata-program.programService + * @name clearProgramsCache + * + * @description + * Deletes programs stored in the browser cache. + */ + function clearProgramsCache() { + localStorageService.remove('programs'); + } + } +})(); diff --git a/src/referencedata-program/program.service.spec.js b/src/referencedata-program/program.service.spec.js new file mode 100644 index 0000000..55b74a9 --- /dev/null +++ b/src/referencedata-program/program.service.spec.js @@ -0,0 +1,283 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('programService', function() { + + beforeEach(function() { + this.programsStorage = jasmine.createSpyObj('programsStorage', ['getBy', 'putAll', 'getAll', 'put', 'search']); + this.offlineService = jasmine.createSpyObj('offlineService', ['isOffline', 'checkConnection']); + + var programsStorage = this.programsStorage, + offlineService = this.offlineService; + module('referencedata-program', function($provide) { + $provide.service('localStorageFactory', function() { + return function() { + return programsStorage; + }; + }); + $provide.service('offlineService', function() { + return offlineService; + }); + }); + + inject(function($injector) { + this.$httpBackend = $injector.get('$httpBackend'); + this.$rootScope = $injector.get('$rootScope'); + this.$q = $injector.get('$q'); + this.openlmisUrlFactory = $injector.get('openlmisUrlFactory'); + this.programService = $injector.get('programService'); + this.ProgramDataBuilder = $injector.get('ProgramDataBuilder'); + }); + + this.program1 = new this.ProgramDataBuilder().build(); + this.program2 = new this.ProgramDataBuilder().build(); + + this.programs = [ + this.program1, + this.program2 + ]; + + this.userId = 'userId'; + this.userIdOffline = { + userIdOffline: this.userId + }; + + this.programsStorage.search.andReturn([]); + }); + + it('should get program by id and save it to storage', function() { + this.$httpBackend + .expectGET(this.openlmisUrlFactory('/api/programs/' + this.program1.id)) + .respond(200, this.program1); + + var result; + this.programService + .get(this.program1.id) + .then(function(response) { + result = response; + }); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(angular.toJson(result)).toEqual(angular.toJson(this.program1)); + expect(this.programsStorage.put).toHaveBeenCalled(); + }); + + it('should get program by id from storage while offline', function() { + this.programsStorage.getBy.andReturn(this.program1); + this.offlineService.isOffline.andReturn(true); + + var result; + this.programService.get(this.program1.id).then(function(program) { + result = program; + }); + this.$rootScope.$apply(); + + expect(angular.toJson(result)).toBe(angular.toJson(this.program1)); + }); + + it('should get all programs', function() { + + this.$httpBackend + .expectGET(this.openlmisUrlFactory('/api/programs')) + .respond(200, this.programs); + + var result; + this.programService.getAll().then(function(response) { + result = response; + }); + + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(angular.toJson(result)).toEqual(angular.toJson(this.programs)); + }); + + it('should get user programs from storage while offline', function() { + this.programsStorage.search.andReturn([this.program1]); + this.offlineService.isOffline.andReturn(true); + + var result, + isForHomeFacility = '2'; + this.programService + .getUserPrograms(this.userId, isForHomeFacility) + .then(function(response) { + result = response; + }); + this.$rootScope.$apply(); + + expect(result).toEqual([this.program1]); + }); + + it('should get user programs and save them to storage', function() { + this.offlineService.isOffline.andReturn(false); + + this.$httpBackend + .expectGET(this.openlmisUrlFactory('api/users/' + this.userId + '/programs')) + .respond(200, this.programs); + + var result; + this.programService + .getUserPrograms(this.userId) + .then(function(response) { + result = response; + }); + + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + var expectedProgram1 = _.extend({}, this.program1, this.userIdOffline), + expectedProgram2 = _.extend({}, this.program2, this.userIdOffline); + + expect(angular.toJson(result)).toEqual(angular.toJson([ + expectedProgram1, + expectedProgram2 + ])); + }); + + it('should update program and save it to storage', function() { + + this.$httpBackend + .expectPUT(this.openlmisUrlFactory('/api/programs/' + this.program1.id)) + .respond(200, this.program2); + + var result; + this.programService.update(this.program1).then(function(response) { + result = response; + }); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(angular.toJson(result)).toEqual(angular.toJson(this.program2)); + expect(this.programsStorage.put).toHaveBeenCalled(); + }); + + describe('getUserPrograms', function() { + + it('will get a list of all the users programs', function() { + this.$httpBackend + .expectGET(this.openlmisUrlFactory('api/users/' + this.userId + '/programs')) + .respond(200, [this.program1]); + + var result; + this.programService + .getUserPrograms(this.userId) + .then(function(programs) { + result = programs; + }); + + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(angular.toJson(result)).toEqual(angular.toJson([ + _.extend({}, this.program1, this.userIdOffline) + ])); + }); + + it('will cache the first successful request', function() { + this.$httpBackend + .expectGET(this.openlmisUrlFactory('api/users/' + this.userId + '/programs')) + .respond(200, this.programs); + + this.programService.getUserPrograms(this.userId); + + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(angular.toJson(this.programsStorage.putAll.calls[0].args[0])) + .toEqual(angular.toJson([ + _.extend({}, this.program1, this.userIdOffline), + _.extend({}, this.program2, this.userIdOffline) + ])); + }); + + it('will not cache an unsuccessful request', function() { + this.$httpBackend + .expectGET(this.openlmisUrlFactory('api/users/' + this.userId + '/programs')) + .respond(400); + + var rejected; + this.programService + .getUserPrograms(this.userId) + .catch(function() { + rejected = true; + }); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(rejected).toEqual(true); + expect(this.programsStorage.put).not.toHaveBeenCalled(); + }); + + it('will return a cached response instead of making another request', function() { + var cachedPrograms = [new this.ProgramDataBuilder().build()]; + + this.programsStorage.search.andReturn(cachedPrograms); + + var result; + this.programService + .getUserPrograms(this.userId) + .then(function(programs) { + result = programs; + }); + this.$rootScope.$apply(); + + expect(result).toEqual(cachedPrograms); + }); + }); + + describe('getSupportedUserPrograms', function() { + + it('should get a list of all the users programs', function() { + this.$httpBackend + .expectGET(this.openlmisUrlFactory('api/users/' + this.userId + '/supportedPrograms')) + .respond(200, this.programs); + + var result; + this.programService + .getUserSupportedPrograms(this.userId) + .then(function(programs) { + result = programs; + }); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(angular.toJson(result)).toEqual(angular.toJson(this.programs)); + }); + + it('should reject promise on unsuccessful request', function() { + this.$httpBackend + .expectGET(this.openlmisUrlFactory('api/users/' + this.userId + '/supportedPrograms')) + .respond(400); + + var rejected; + this.programService + .getUserSupportedPrograms(this.userId) + .catch(function() { + rejected = true; + }); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(rejected).toBe(true); + expect(this.programsStorage.put).not.toHaveBeenCalled(); + }); + }); + + afterEach(function() { + this.$httpBackend.verifyNoOutstandingExpectation(); + this.$httpBackend.verifyNoOutstandingRequest(); + }); +}); diff --git a/src/referencedata-user-programs-cache/program.service.decorator.js b/src/referencedata-user-programs-cache/program.service.decorator.js new file mode 100644 index 0000000..5866201 --- /dev/null +++ b/src/referencedata-user-programs-cache/program.service.decorator.js @@ -0,0 +1,56 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc service + * @name referencedata-user-programs-cache.programService + * + * @description + * Decorates programService with clearing cache method. + */ + angular.module('referencedata-user-programs-cache') + .config(config); + + config.$inject = ['$provide']; + + function config($provide) { + $provide.decorator('programService', decorator); + } + + decorator.$inject = ['$delegate', 'localStorageFactory']; + function decorator($delegate, localStorageFactory) { + var userProgramsCache = localStorageFactory('userPrograms'); + + $delegate.clearUserProgramsCache = clearCache; + + return $delegate; + + /** + * @ngdoc method + * @methodOf referencedata-user-programs-cache.programService + * @name clearUserProgramsCache + * + * @description + * Deletes user programs stored in the browser cache. + */ + function clearCache() { + userProgramsCache.clearAll(); + } + } +})(); diff --git a/src/referencedata-user-programs-cache/program.service.decorator.spec.js b/src/referencedata-user-programs-cache/program.service.decorator.spec.js new file mode 100644 index 0000000..a67a750 --- /dev/null +++ b/src/referencedata-user-programs-cache/program.service.decorator.spec.js @@ -0,0 +1,101 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('ProgramService getUserPrograms decorator', function() { + + beforeEach(function() { + this.cache = jasmine.createSpyObj('cache', ['getBy', 'put', 'putAll', 'clearAll', 'getAll', 'search']); + var cache = this.cache; + module('referencedata-user'); + module('referencedata-user-programs-cache', function($provide) { + $provide.factory('localStorageFactory', function() { + cache.getAll.andReturn([]); + return function() { + return cache; + }; + }); + }); + + inject(function($injector) { + this.programService = $injector.get('programService'); + this.$rootScope = $injector.get('$rootScope'); + this.$httpBackend = $injector.get('$httpBackend'); + this.openlmisUrlFactory = $injector.get('openlmisUrlFactory'); + this.UserDataBuilder = $injector.get('UserDataBuilder'); + this.ProgramDataBuilder = $injector.get('ProgramDataBuilder'); + }); + + this.user = new this.UserDataBuilder().buildReferenceDataUserJson(); + + this.programs = [ + new this.ProgramDataBuilder().build(), + new this.ProgramDataBuilder().build() + ]; + + this.userIdOffline = { + userIdOffline: this.user.id + }; + }); + + it('should return a cached programs if available', function() { + this.cache.search.andReturn(this.programs); + + var result; + this.programService + .getUserPrograms(this.user.id) + .then(function(response) { + result = response; + }); + this.$rootScope.$apply(); + + expect(result).toEqual(this.programs); + expect(this.cache.search).toHaveBeenCalled(); + }); + + it('should send original request if there is no user programs cached', function() { + this.$httpBackend + .expectGET(this.openlmisUrlFactory('api/users/' + this.user.id + '/programs')) + .respond(200, this.programs); + + this.cache.search.andReturn(undefined); + + var result; + this.programService + .getUserPrograms(this.user.id) + .then(function(response) { + result = response; + }); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + + expect(this.cache.putAll.callCount).toEqual(1); + expect(angular.toJson(result)).toEqual(angular.toJson([ + _.extend({}, this.programs[0], this.userIdOffline), + _.extend({}, this.programs[1], this.userIdOffline) + ])); + }); + + it('should clear user programs cache', function() { + this.programService.clearUserProgramsCache(); + + expect(this.cache.clearAll).toHaveBeenCalled(); + }); + + afterEach(function() { + this.$httpBackend.verifyNoOutstandingExpectation(); + this.$httpBackend.verifyNoOutstandingRequest(); + }); + +}); diff --git a/src/requisition-search/requisition-search-cache.run.js b/src/requisition-search/requisition-search-cache.run.js new file mode 100644 index 0000000..805937a --- /dev/null +++ b/src/requisition-search/requisition-search-cache.run.js @@ -0,0 +1,37 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + angular + .module('requisition-search') + .run(routes); + + routes.$inject = ['loginService', 'requisitionSearchService']; + + function routes(loginService, requisitionSearchService) { + + loginService.registerPostLoginAction(function() { + requisitionSearchService.getFacilities(); + }); + + loginService.registerPostLogoutAction(function() { + return requisitionSearchService.clearCachedFacilities(); + }); + } + +})(); diff --git a/src/requisition-search/requisition-search-cache.run.spec.js b/src/requisition-search/requisition-search-cache.run.spec.js new file mode 100644 index 0000000..e3ba15a --- /dev/null +++ b/src/requisition-search/requisition-search-cache.run.spec.js @@ -0,0 +1,92 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('requisition-search run', function() { + + var loginServiceSpy, requisitionSearchServiceSpy, postLoginAction, postLogoutAction, $q, $rootScope; + + beforeEach(function() { + module('requisition-search', function($provide) { + loginServiceSpy = jasmine.createSpyObj('loginService', [ + 'registerPostLoginAction', 'registerPostLogoutAction' + ]); + $provide.value('loginService', loginServiceSpy); + + requisitionSearchServiceSpy = jasmine.createSpyObj('requisitionSearchService', [ + 'getFacilities', 'clearCachedFacilities' + ]); + $provide.value('requisitionSearchService', requisitionSearchServiceSpy); + }); + + inject(function($injector) { + $rootScope = $injector.get('$rootScope'); + $q = $injector.get('$q'); + }); + + postLoginAction = getLastCall(loginServiceSpy.registerPostLoginAction).args[0]; + postLogoutAction = getLastCall(loginServiceSpy.registerPostLogoutAction).args[0]; + }); + + describe('run block', function() { + + beforeEach(function() { + inject(); + }); + + it('should register post login action', function() { + expect(loginServiceSpy.registerPostLoginAction).toHaveBeenCalled(); + }); + + it('should register post logout action', function() { + expect(loginServiceSpy.registerPostLogoutAction).toHaveBeenCalled(); + }); + + }); + + describe('post login action', function() { + + it('should cache facilities', function() { + requisitionSearchServiceSpy.getFacilities.andReturn($q.resolve()); + + postLoginAction(); + + expect(requisitionSearchServiceSpy.getFacilities).toHaveBeenCalled(); + }); + + }); + + describe('post logout action', function() { + + it('should clear current user cache', function() { + requisitionSearchServiceSpy.clearCachedFacilities.andReturn($q.resolve()); + + var success; + postLogoutAction() + .then(function() { + success = true; + }); + $rootScope.$apply(); + + expect(success).toBe(true); + expect(requisitionSearchServiceSpy.clearCachedFacilities).toHaveBeenCalled(); + }); + + }); + + function getLastCall(method) { + return method.calls[method.calls.length - 1]; + } + +}); \ No newline at end of file diff --git a/src/requisition-search/requisition-search.service.js b/src/requisition-search/requisition-search.service.js new file mode 100644 index 0000000..16aa5b1 --- /dev/null +++ b/src/requisition-search/requisition-search.service.js @@ -0,0 +1,234 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc service + * @name requisition-search.requisitionSearchService + * + * @description + * Prepares data for the Requisition Search view. + */ + angular + .module('requisition-search') + .service('requisitionSearchService', requisitionSearchService); + + requisitionSearchService.$inject = [ + 'facilityFactory', 'authorizationService', 'currentUserService', 'SupervisoryNodeResource', 'RoleResource', + 'RequisitionGroupResource', '$q', 'OpenlmisArrayDecorator', 'REQUISITION_RIGHTS', 'localStorageService' + ]; + + function requisitionSearchService(facilityFactory, authorizationService, currentUserService, + SupervisoryNodeResource, RoleResource, RequisitionGroupResource, $q, + OpenlmisArrayDecorator, REQUISITION_RIGHTS, localStorageService) { + + var promise, + REQUISITION_SEARCH_FACILITIES = 'requisitionSearchFacilities'; + + this.getFacilities = getFacilities; + this.clearCachedFacilities = clearCachedFacilities; + + /** + * @ngdoc method + * @methodOf requisition-search.requisitionSearchService + * @name getFacilities + * + * @description + * Prepares the list of facilities to be displayed on the Requisition Search view. + * + * @return {Array} the list of facilities based on both permission strings and role assignments for partner + * nodes + */ + function getFacilities() { + if (promise) { + return promise; + } + + var user = authorizationService.getUser(); + + var cachedFacilities = localStorageService.get(REQUISITION_SEARCH_FACILITIES); + if (cachedFacilities) { + promise = $q.resolve(angular.fromJson(cachedFacilities)); + } else { + promise = $q + .all([ + facilityFactory.getAllUserFacilities(user.user_id), + getFacilitiesBasedOnPartnerNodes() + ]) + .then(mergeFacilityLists) + .then(getUniqueSortedByName) + .then(cacheFacilities); + } + + return promise; + } + + function clearCachedFacilities() { + promise = undefined; + localStorageService.remove(REQUISITION_SEARCH_FACILITIES); + } + + function getFacilitiesBasedOnPartnerNodes() { + return $q + .all([ + fetchSupervisoryNodes(), + new RequisitionGroupResource().query() + ]) + .then(fetchFacilitiesForRequsitionGroupsRelatedWithSupervisoryNodes) + .then(mergeFacilityLists); + } + + function fetchSupervisoryNodes() { + return $q + .all([ + getRoleIdsForRolesWithRequisitionViewRight(), + currentUserService.getUserInfo() + ]) + .then(getSupervisoryNodeIdsForMatchingRoleAssignments) + .then(fetchSupervisoryNodesByIds) + .then(fetchSupervisoryNodesForPartners); + } + + function getRoleIdsForRolesWithRequisitionViewRight() { + return new RoleResource() + .query() + .then(function(roles) { + return roles + .filter(hasRequisitionViewRight) + .map(toProperty('id')); + }); + } + + function hasRequisitionViewRight(role) { + return role.rights.filter(function(right) { + return right.name === REQUISITION_RIGHTS.REQUISITION_VIEW; + }).length > 0; + } + + function getSupervisoryNodeIdsForMatchingRoleAssignments(responses) { + var roleIds = responses[0], + user = responses[1]; + + return user.roleAssignments + .filter(outByUndefinedProperty('supervisoryNodeId')) + .filter(filterOutNotInBy(roleIds, 'roleId')) + .map(toProperty('supervisoryNodeId')); + } + + function fetchSupervisoryNodesByIds(supervisoryNodeIds) { + if (supervisoryNodeIds.length === 0) { + return []; + } + + return new SupervisoryNodeResource().query({ + id: supervisoryNodeIds + }); + } + + function fetchSupervisoryNodesForPartners(nodesPage) { + if (nodesPage.content === undefined || nodesPage.content.length === 0) { + return []; + } + + var partnerNodeIds = getPartnerOfIdsFromNodes(nodesPage.content); + + if (partnerNodeIds.length === 0) { + return []; + } + + return new SupervisoryNodeResource().query({ + id: partnerNodeIds + }); + } + + function getPartnerOfIdsFromNodes(nodes) { + return nodes + .filter(outByUndefinedProperty('partnerNodeOf')) + .map(toProperty('partnerNodeOf')) + .map(toProperty('id')); + } + + function fetchFacilitiesForRequsitionGroupsRelatedWithSupervisoryNodes(responses) { + var nodes = responses[0].content, + groups = responses[1], + requisitionGroupIds; + + if (nodes === undefined) { + requisitionGroupIds = []; + } else { + requisitionGroupIds = getRequisitionGroupIdsFromNodes(nodes); + } + + return groups + .filter(filterOutNotInBy(requisitionGroupIds, 'id')) + .map(toProperty('memberFacilities')); + } + + function filterOutNotInBy(array, propertyName) { + return function(item) { + return array.indexOf(item[propertyName]) > -1; + }; + } + + function getRequisitionGroupIdsFromNodes(nodes) { + return nodes + .filter(outByUndefinedProperty('requisitionGroup')) + .map(toProperty('requisitionGroup')) + .map(toProperty('id')); + } + + function mergeFacilityLists(facilityLists) { + var facilities = []; + + facilityLists.forEach(function(facilityList) { + facilityList.forEach(function(facility) { + facilities.push(facility); + }); + }); + + return facilities; + } + + function getUniqueSortedByName(facilities) { + var decoratedFacilityList = new OpenlmisArrayDecorator(facilities); + + decoratedFacilityList.sortBy('name'); + + return decoratedFacilityList.getAllWithUniqueIds(); + } + + function cacheFacilities(facilities) { + localStorageService.add(REQUISITION_SEARCH_FACILITIES, angular.toJson(facilities)); + return facilities; + } + + function outByUndefinedProperty(propertyName) { + return function(item) { + return item[propertyName]; + }; + } + + function toProperty(propertyName) { + return function(item) { + return item[propertyName]; + }; + } + + } + +})(); \ No newline at end of file diff --git a/src/requisition-search/requisition-search.service.spec.js b/src/requisition-search/requisition-search.service.spec.js new file mode 100644 index 0000000..d23123a --- /dev/null +++ b/src/requisition-search/requisition-search.service.spec.js @@ -0,0 +1,457 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('RequisitionSearchService', function() { + + beforeEach(function() { + module('requisition-search'); + + this.prepareFacilities = prepareFacilities; + this.prepareRequisitionGroups = prepareRequisitionGroups; + this.prepareSupervisoryNodes = prepareSupervisoryNodes; + this.preparePartnerNodes = preparePartnerNodes; + this.prepareRights = prepareRights; + this.prepareRoles = prepareRoles; + this.prepareUser = prepareUser; + this.prepareUserWithoutHomeFacility = prepareUserWithoutHomeFacility; + this.prepareUserWithoutPartnerNodes = prepareUserWithoutPartnerNodes; + + inject(function($injector) { + this.SupervisoryNodeDataBuilder = $injector.get('SupervisoryNodeDataBuilder'); + this.FacilityDataBuilder = $injector.get('FacilityDataBuilder'); + this.RequisitionGroupDataBuilder = $injector.get('RequisitionGroupDataBuilder'); + this.AuthUserDataBuilder = $injector.get('AuthUserDataBuilder'); + this.UserDataBuilder = $injector.get('UserDataBuilder'); + this.RightDataBuilder = $injector.get('RightDataBuilder'); + this.RoleDataBuilder = $injector.get('RoleDataBuilder'); + this.PageDataBuilder = $injector.get('PageDataBuilder'); + this.currentUserService = $injector.get('currentUserService'); + this.SupervisoryNodeResource = $injector.get('SupervisoryNodeResource'); + this.facilityFactory = $injector.get('facilityFactory'); + this.RequisitionGroupResource = $injector.get('RequisitionGroupResource'); + this.authorizationService = $injector.get('authorizationService'); + this.REQUISITION_RIGHTS = $injector.get('REQUISITION_RIGHTS'); + this.$q = $injector.get('$q'); + this.$rootScope = $injector.get('$rootScope'); + this.requisitionSearchService = $injector.get('requisitionSearchService'); + this.RoleResource = $injector.get('RoleResource'); + this.localStorageService = $injector.get('localStorageService'); + }); + + this.prepareFacilities(); + this.prepareRequisitionGroups(); + this.prepareSupervisoryNodes(); + this.preparePartnerNodes(); + this.prepareRights(); + this.prepareRoles(); + this.prepareUser(); + + spyOn(this.currentUserService, 'getUserInfo').andReturn(this.$q.resolve(this.user)); + spyOn(this.RoleResource.prototype, 'query').andReturn(this.$q.resolve(this.roles)); + spyOn(this.RequisitionGroupResource.prototype, 'query').andReturn(this.$q.resolve([ + this.requisitionGroupA, + this.requisitionGroupB, + this.requisitionGroupC + ])); + spyOn(this.facilityFactory, 'getAllUserFacilities').andReturn(this.$q.resolve([])); + spyOn(this.localStorageService, 'get'); + spyOn(this.localStorageService, 'add'); + spyOn(this.localStorageService, 'remove'); + + var context = this; + spyOn(this.SupervisoryNodeResource.prototype, 'query').andCallFake(function(params) { + var nodes = params.id.map(function(id) { + return context.nodesMap[id]; + }); + + var page = new context.PageDataBuilder() + .withContent(nodes) + .build(); + return context.$q.resolve(page); + }); + }); + + describe('getFacilities', function() { + + it('should handle null requisition group', function() { + this.supervisoryNodeA.requisitionGroup = undefined; + + var result; + this.requisitionSearchService + .getFacilities() + .then(function(facilities) { + result = facilities; + }); + this.$rootScope.$apply(); + + expect(result).toEqual([ + this.facilityF + ]); + + }); + + it('should not return duplicated facilities', function() { + //this facility comes from both permission strings and role assignments + this.facilityFactory.getAllUserFacilities.andReturn([ + this.facilityF + ]); + + var result; + this.requisitionSearchService + .getFacilities() + .then(function(facilities) { + result = facilities; + }); + this.$rootScope.$apply(); + + expect(result.indexOf(this.facilityF)).toEqual(result.lastIndexOf(this.facilityF)); + }); + + it('should return facilities sorted alphabetically', function() { + var result; + this.requisitionSearchService + .getFacilities() + .then(function(facilities) { + result = facilities; + }); + this.$rootScope.$apply(); + + expect(result).toEqual([ + this.facilityA, + this.facilityB, + this.facilityC, + this.facilityF + ]); + }); + + it('should reject if preparing partner facilities fails', function() { + this.facilityFactory.getAllUserFacilities.andReturn(this.$q.reject()); + + var rejected; + this.requisitionSearchService + .getFacilities() + .catch(function() { + rejected = true; + }); + this.$rootScope.$apply(); + + expect(rejected).toBe(true); + }); + + it('should reject if fetching requisition groups fails', function() { + this.RequisitionGroupResource.prototype.query.andReturn(this.$q.reject()); + + var rejected; + this.requisitionSearchService + .getFacilities() + .catch(function() { + rejected = true; + }); + this.$rootScope.$apply(); + + expect(rejected).toBe(true); + }); + + it('should reject if fetching supervisory nodes fails', function() { + this.SupervisoryNodeResource.prototype.query.andReturn(this.$q.reject()); + + var rejected; + this.requisitionSearchService + .getFacilities() + .catch(function() { + rejected = true; + }); + this.$rootScope.$apply(); + + expect(rejected).toBe(true); + }); + + it('should reject if retrieving user fails', function() { + this.currentUserService.getUserInfo.andReturn(this.$q.reject()); + + var rejected; + this.requisitionSearchService + .getFacilities() + .catch(function() { + rejected = true; + }); + this.$rootScope.$apply(); + + expect(rejected).toBe(true); + }); + + it('should not duplicate calls for subsequent calls', function() { + this.requisitionSearchService.getFacilities(); + this.requisitionSearchService.getFacilities(); + + this.$rootScope.$apply(); + + expect(this.currentUserService.getUserInfo.callCount).toEqual(1); + expect(this.facilityFactory.getAllUserFacilities.callCount).toEqual(1); + expect(this.RequisitionGroupResource.prototype.query.callCount).toEqual(1); + expect(this.RoleResource.prototype.query.callCount).toEqual(1); + expect(this.SupervisoryNodeResource.prototype.query.callCount).toEqual(2); + }); + + it('should fetch data from the local storage after refreshing the page', function() { + this.localStorageService.get.andReturn(angular.toJson([ + this.facilityA, + this.facilityF + ])); + + var result; + this.requisitionSearchService.getFacilities() + .then(function(facilities) { + result = facilities; + }); + this.$rootScope.$apply(); + + expect(this.localStorageService.get).toHaveBeenCalledWith('requisitionSearchFacilities'); + expect(angular.toJson(result)).toEqual(angular.toJson([ + this.facilityA, + this.facilityF + ])); + }); + + it('should not call backend if there is data in the local storage', function() { + this.localStorageService.get.andReturn(angular.toJson([ + this.facilityA, + this.facilityF + ])); + + this.requisitionSearchService.getFacilities(); + this.$rootScope.$apply(); + + expect(this.currentUserService.getUserInfo).not.toHaveBeenCalled(); + expect(this.facilityFactory.getAllUserFacilities).not.toHaveBeenCalled(); + expect(this.RequisitionGroupResource.prototype.query).not.toHaveBeenCalled(); + expect(this.RoleResource.prototype.query).not.toHaveBeenCalled(); + expect(this.SupervisoryNodeResource.prototype.query).not.toHaveBeenCalled(); + }); + + it('should not call for supervisory nodes if user has no role with any', function() { + this.prepareUserWithoutHomeFacility(); + this.currentUserService.getUserInfo.andReturn(this.$q.resolve(this.user)); + + this.requisitionSearchService.getFacilities(); + this.$rootScope.$apply(); + + expect(this.SupervisoryNodeResource.prototype.query).not.toHaveBeenCalled(); + }); + + it('should not call for partner supervisory nodes if there is none for the facilities', function() { + this.prepareUserWithoutPartnerNodes(); + this.currentUserService.getUserInfo.andReturn(this.$q.resolve(this.user)); + + this.requisitionSearchService.getFacilities(); + this.$rootScope.$apply(); + + expect(this.SupervisoryNodeResource.prototype.query.callCount).toEqual(1); + expect(this.SupervisoryNodeResource.prototype.query).toHaveBeenCalledWith({ + id: [ this.supervisoryNodeA.id, this.supervisoryNodeC.id ] + }); + }); + + it('should cache facilities in the local storage', function() { + this.requisitionSearchService.getFacilities(); + this.$rootScope.$apply(); + + expect(this.localStorageService.add).toHaveBeenCalledWith('requisitionSearchFacilities', angular.toJson([ + this.facilityA, + this.facilityB, + this.facilityC, + this.facilityF + ])); + }); + + }); + + describe('clearCachedFacilities', function() { + + it('should clear cached facilities', function() { + this.requisitionSearchService.getFacilities(); + this.$rootScope.$apply(); + + expect(this.currentUserService.getUserInfo.callCount).toEqual(1); + expect(this.facilityFactory.getAllUserFacilities.callCount).toEqual(1); + expect(this.RequisitionGroupResource.prototype.query.callCount).toEqual(1); + expect(this.RoleResource.prototype.query.callCount).toEqual(1); + expect(this.SupervisoryNodeResource.prototype.query.callCount).toEqual(2); + + this.requisitionSearchService.clearCachedFacilities(); + + expect(this.localStorageService.remove).toHaveBeenCalledWith('requisitionSearchFacilities'); + + this.requisitionSearchService.getFacilities(); + this.$rootScope.$apply(); + + expect(this.currentUserService.getUserInfo.callCount).toEqual(2); + expect(this.facilityFactory.getAllUserFacilities.callCount).toEqual(2); + expect(this.RequisitionGroupResource.prototype.query.callCount).toEqual(2); + expect(this.RoleResource.prototype.query.callCount).toEqual(2); + expect(this.SupervisoryNodeResource.prototype.query.callCount).toEqual(4); + }); + + }); + + function prepareFacilities() { + this.facilityA = new this.FacilityDataBuilder() + .withName('Facility A') + .build(); + + this.facilityB = new this.FacilityDataBuilder() + .withName('Facility B') + .build(); + + this.facilityC = new this.FacilityDataBuilder() + .withName('Facility C') + .build(); + + this.facilityD = new this.FacilityDataBuilder() + .withName('Facility D') + .build(); + + this.facilityE = new this.FacilityDataBuilder() + .withName('Facility E') + .build(); + + this.facilityF = new this.FacilityDataBuilder() + .withName('Facility F') + .build(); + + this.facilityG = new this.FacilityDataBuilder() + .withName('Facility G') + .build(); + + this.facilityH = new this.FacilityDataBuilder() + .withName('Facility H') + .build(); + } + + function prepareRequisitionGroups() { + this.requisitionGroupA = new this.RequisitionGroupDataBuilder() + .withMemberFacilities([ + this.facilityC, + this.facilityA, + this.facilityB + ]) + .buildJson(); + + this.requisitionGroupB = new this.RequisitionGroupDataBuilder() + .withMemberFacilities([ + this.facilityF + ]) + .buildJson(); + + this.requisitionGroupC = new this.RequisitionGroupDataBuilder() + .withMemberFacilities([ + this.facilityD + ]) + .buildJson(); + } + + function prepareSupervisoryNodes() { + this.nodesMap = {}; + + this.supervisoryNodeA = new this.SupervisoryNodeDataBuilder() + .withRequisitionGroup(this.requisitionGroupA) + .build(); + this.nodesMap[this.supervisoryNodeA.id] = this.supervisoryNodeA; + + this.supervisoryNodeB = new this.SupervisoryNodeDataBuilder() + .withRequisitionGroup(this.requisitionGroupB) + .build(); + this.nodesMap[this.supervisoryNodeB.id] = this.supervisoryNodeB; + + this.supervisoryNodeC = new this.SupervisoryNodeDataBuilder() + .withRequisitionGroup(this.requisitionGroupC) + .build(); + this.nodesMap[this.supervisoryNodeC.id] = this.supervisoryNodeC; + } + + function preparePartnerNodes() { + this.partnerNodeA = new this.SupervisoryNodeDataBuilder() + .withPartnerNodeOf(this.supervisoryNodeA) + .build(); + this.nodesMap[this.partnerNodeA.id] = this.partnerNodeA; + + this.partnerNodeB = new this.SupervisoryNodeDataBuilder() + .withPartnerNodeOf(this.supervisoryNodeB) + .build(); + this.nodesMap[this.partnerNodeB.id] = this.partnerNodeB; + + this.partnerNodeC = new this.SupervisoryNodeDataBuilder() + .withPartnerNodeOf(this.supervisoryNodeC) + .build(); + this.nodesMap[this.partnerNodeC.id] = this.partnerNodeC; + } + + function prepareRights() { + this.rightA = new this.RightDataBuilder() + .withName(this.REQUISITION_RIGHTS.REQUISITION_VIEW) + .build(); + + this.rightB = new this.RightDataBuilder() + .withName(this.REQUISITION_RIGHTS.REQUISITION_CREATE) + .build(); + } + + function prepareRoles() { + this.roleA = new this.RoleDataBuilder() + .withRight(this.rightA) + .build(); + + this.roleB = new this.RoleDataBuilder() + .withRight(this.rightB) + .build(); + + this.roles = [ + this.roleB, + this.roleA + ]; + } + + function prepareUser() { + this.programId = 'program-id'; + + this.user = new this.UserDataBuilder() + .withSupervisionRoleAssignment(this.roleA.id, this.partnerNodeA.id, this.programId) + .withSupervisionRoleAssignment(this.roleA.id, this.partnerNodeB.id, this.programId) + .withSupervisionRoleAssignment(this.roleB.id, this.partnerNodeC.id, this.programId) + .withSupervisionRoleAssignment(this.roleA.id, this.supervisoryNodeC.id, this.programId) + .withSupervisionRoleAssignment(this.roleA.id, null, this.programId) + .buildReferenceDataUserJson(); + } + + function prepareUserWithoutHomeFacility() { + this.programId = 'program-id'; + + this.user = new this.UserDataBuilder() + .withSupervisionRoleAssignment(this.roleA.id, null, this.programId) + .withSupervisionRoleAssignment(this.roleB.id, null, this.programId) + .withoutHomeFacilityId() + .buildReferenceDataUserJson(); + } + + function prepareUserWithoutPartnerNodes() { + this.programId = 'program-id'; + + this.user = new this.UserDataBuilder() + .withSupervisionRoleAssignment(this.roleA.id, this.supervisoryNodeA.id, this.programId) + .withSupervisionRoleAssignment(this.roleA.id, this.supervisoryNodeC.id, this.programId) + .buildReferenceDataUserJson(); + } + +}); \ No newline at end of file