|
3 | 3 |
|
4 | 4 | angular = 'default' in angular ? angular['default'] : angular;
|
5 | 5 |
|
6 |
| - var versionDateFormat = "YYYY-MM-DD"; |
7 |
| - var config = { |
8 |
| - versionDateFormat: versionDateFormat |
| 6 | + var versions = [{ "version": 1, "date": "2016-01-01", "coefficients": { "product:2-reconst-syg": { "wastage": 1.1, "coverage": 0.87 }, "product:5-reconst-syg": { "wastage": 1.1, "coverage": 0.87 }, "product:ad-syg": { "wastage": 1.05, "coverage": 0.87 }, "product:bcg": { "wastage": 2, "coverage": 0.9, "doses": 1 }, "product:bcg-syg": { "coverage": 0.87 }, "product:diluent-bcg": {}, "product:diluent-mv": {}, "product:diluent-yf": {}, "product:hep-b": { "wastage": 1.3333333333333333, "coverage": 0.9, "doses": 1 }, "product:hpv": { "wastage": 1.1, "coverage": 0.9, "doses": 2 }, "product:ipv": { "wastage": 1.05, "coverage": 0.87, "doses": 1 }, "product:mv": { "wastage": 1.4285714285714286, "coverage": 0.9, "doses": 1 }, "product:opv": { "wastage": 1.3333333333333333, "coverage": 0.9, "doses": 4 }, "product:pcv": { "wastage": 1.05, "coverage": 0.87, "doses": 3 }, "product:penta": { "wastage": 1.3333333333333333, "coverage": 0.87, "doses": 3 }, "product:rota": { "wastage": 1.05, "coverage": 0.87, "doses": 3 }, "product:safety-boxes": { "wastage": 1.05 }, "product:td": { "wastage": 1.3333333333333333, "coverage": 0.87, "doses": 2 }, "product:yf": { "wastage": 1.4285714285714286, "coverage": 0.9, "doses": 1 } } }]; |
| 7 | + var defaultCoefficients = { |
| 8 | + versions: versions |
9 | 9 | };
|
10 | 10 |
|
11 |
| - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); |
12 |
| - |
13 |
| - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } |
14 |
| - |
15 | 11 | // TODO: replace with Array#find ponyfill
|
16 |
| - var find = function find(list, match) { |
| 12 | + function find(list, match) { |
17 | 13 | for (var i = 0; i < list.length; i++) {
|
18 | 14 | if (match(list[i])) {
|
19 | 15 | return list[i];
|
20 | 16 | }
|
21 | 17 | }
|
22 | 18 | return undefined;
|
| 19 | + } |
| 20 | + |
| 21 | + function somethingIsWrong(msg) { |
| 22 | + if (console) { |
| 23 | + console.warn("angular-nav-thresholds: " + msg); |
| 24 | + } |
| 25 | + // TODO: log it to Google Analytics (#28) |
| 26 | + } |
| 27 | + |
| 28 | + var versionDateFormat = "YYYY-MM-DD"; |
| 29 | + var config = { |
| 30 | + versionDateFormat: versionDateFormat |
| 31 | + }; |
| 32 | + |
| 33 | + var formulae = { |
| 34 | + 'product:bcg': 'regular', |
| 35 | + 'product:mv': 'regular', |
| 36 | + 'product:yf': 'regular', |
| 37 | + 'product:opv': 'regular', |
| 38 | + 'product:td': 'regular', |
| 39 | + 'product:penta': 'regular', |
| 40 | + 'product:hep-b': 'regular', |
| 41 | + 'product:pcv': 'regular', |
| 42 | + 'product:ipv': 'regular', |
| 43 | + 'product:rota': 'regular', |
| 44 | + 'product:hpv': 'regular', |
| 45 | + 'product:ad-syg': 'adSyg', |
| 46 | + 'product:bcg-syg': 'bcg', |
| 47 | + 'product:5-reconst-syg': 'fiveReconst', |
| 48 | + 'product:2-reconst-syg': 'twoReconst', |
| 49 | + 'product:safety-boxes': 'safetyBoxes', |
| 50 | + 'product:diluent-bcg': 'diluentBcg', |
| 51 | + 'product:diluent-yf': 'diluentYf', |
| 52 | + 'product:diluent-mv': 'diluentMv' |
| 53 | + }; |
| 54 | + |
| 55 | + var calculator = {}; |
| 56 | + calculator.regular = function (allocations, coefficient, targetPopulation) { |
| 57 | + return targetPopulation * coefficient.doses * coefficient.coverage * coefficient.wastage; |
| 58 | + }; |
| 59 | + |
| 60 | + calculator.adSyg = function (allocations) { |
| 61 | + return allocations['product:penta'] + allocations['product:mv'] + allocations['product:yf'] + allocations['product:hep-b'] + allocations['product:td'] + allocations['product:pcv'] + allocations['product:ipv'] + allocations['product:hpv']; |
| 62 | + }; |
| 63 | + |
| 64 | + calculator.bcg = function (allocations) { |
| 65 | + return allocations['product:bcg']; |
| 66 | + }; |
| 67 | + |
| 68 | + calculator.fiveReconst = function (allocations, coefficient) { |
| 69 | + return (allocations['product:mv'] + allocations['product:yf']) / 10 * coefficient.wastage; |
23 | 70 | };
|
24 | 71 |
|
| 72 | + calculator.twoReconst = function (allocations, coefficient) { |
| 73 | + return allocations['product:bcg'] / 20 * coefficient.wastage; |
| 74 | + }; |
| 75 | + |
| 76 | + calculator.safetyBoxes = function (allocations, coefficient) { |
| 77 | + return (allocations['product:ad-syg'] + allocations['product:bcg-syg'] + allocations['product:5-reconst-syg'] + allocations['product:2-reconst-syg']) / 100 * coefficient.wastage; |
| 78 | + }; |
| 79 | + |
| 80 | + calculator.diluentBcg = function (allocations) { |
| 81 | + return allocations['product:bcg']; |
| 82 | + }; |
| 83 | + calculator.diluentYf = function (allocations) { |
| 84 | + return allocations['product:yf']; |
| 85 | + }; |
| 86 | + calculator.diluentMv = function (allocations) { |
| 87 | + return allocations['product:mv']; |
| 88 | + }; |
| 89 | + |
| 90 | + var calculateForProduct = function calculateForProduct(monthlyTargetPopulations, coefficients, allocations, productId) { |
| 91 | + var coefficient = coefficients[productId]; |
| 92 | + var monthlyTargetPopulation = monthlyTargetPopulations[productId]; |
| 93 | + |
| 94 | + allocations[productId] = 0; |
| 95 | + if (typeof monthlyTargetPopulation !== 'undefined' && coefficient) { |
| 96 | + allocations[productId] = calculator[formulae[productId]](allocations, coefficient, monthlyTargetPopulation / 4); |
| 97 | + } |
| 98 | + return allocations; |
| 99 | + }; |
| 100 | + |
| 101 | + var calculateWeeklyLevels = (function (monthlyTargetPopulations, coefficients) { |
| 102 | + return Object.keys(formulae).reduce(calculateForProduct.bind(null, monthlyTargetPopulations, coefficients), {}); |
| 103 | + }); |
| 104 | + |
25 | 105 | var isVersion = function isVersion(date, version) {
|
26 | 106 | var momentDate = moment().isoWeekYear(date.year).isoWeek(date.week).isoWeekday(1).startOf('day');
|
27 | 107 | var momentVersionStartDate = moment(version.date, config.versionDateFormat).startOf('isoWeek').startOf('day');
|
28 | 108 | return momentDate.isSameOrAfter(momentVersionStartDate);
|
29 | 109 | };
|
30 | 110 |
|
31 |
| - var isId = function isId(id, item) { |
32 |
| - return item._id === id; |
33 |
| - }; |
34 |
| - |
35 | 111 | var getFactor = function getFactor(versions, date) {
|
36 | 112 | var reverseVersions = versions.slice(0).reverse();
|
37 | 113 | var factor = find(reverseVersions, isVersion.bind(null, date));
|
|
42 | 118 | return factor;
|
43 | 119 | };
|
44 | 120 |
|
45 |
| - var getFactors = function getFactors(stockCount, location) { |
46 |
| - // centralized for whenever we implement #16 |
47 |
| - var somethingIsWrong = function somethingIsWrong() { |
48 |
| - return undefined; |
49 |
| - }; |
| 121 | + var getCoefficients = function getCoefficients(productCoefficients, date) { |
| 122 | + if (!(productCoefficients && productCoefficients.versions && productCoefficients.versions.length)) { |
| 123 | + throw new Error('missing productCoefficients or productCoefficients.versions'); |
| 124 | + } |
50 | 125 |
|
51 |
| - var getWeeklyLevels = function getWeeklyLevels() { |
52 |
| - if (!(location.allocations && location.allocations.length)) { |
53 |
| - somethingIsWrong(); |
54 |
| - } |
| 126 | + var version = getFactor(productCoefficients.versions, date); |
| 127 | + if (!(version && version.coefficients)) { |
| 128 | + throw new Error('cannot find version of coefficients for date ' + date); |
| 129 | + } |
| 130 | + return version.coefficients; |
| 131 | + }; |
55 | 132 |
|
56 |
| - var allocations = getFactor(location.allocations, stockCount.date); |
57 |
| - return allocations && allocations.weeklyLevels; |
58 |
| - }; |
| 133 | + var getWeeksOfStock = function getWeeksOfStock(location, date) { |
| 134 | + if (!(location.plans && location.plans.length)) { |
| 135 | + throw new Error('missing plans on location ' + location._id); |
| 136 | + } |
59 | 137 |
|
60 |
| - var getWeeksOfStock = function getWeeksOfStock() { |
61 |
| - if (!(location.plans && location.plans.length)) { |
62 |
| - somethingIsWrong(); |
63 |
| - } |
| 138 | + var plans = getFactor(location.plans, date); |
| 139 | + if (!(plans && plans.weeksOfStock)) { |
| 140 | + throw new Error('cannot find version of weeksOfStock for location ' + location._id + ' and date ' + date); |
| 141 | + } |
| 142 | + return plans.weeksOfStock; |
| 143 | + }; |
64 | 144 |
|
65 |
| - var plans = getFactor(location.plans, stockCount.date); |
66 |
| - return plans && plans.weeksOfStock; |
67 |
| - }; |
| 145 | + var getTargetPopulations = function getTargetPopulations(location, date) { |
| 146 | + if (location.targetPopulations && location.targetPopulations.length) { |
| 147 | + var targetPopulations = getFactor(location.targetPopulations, date); |
68 | 148 |
|
69 |
| - var getMonthlyTargetPopulations = function getMonthlyTargetPopulations() { |
70 |
| - var monthlyTargetPopulations = void 0; |
71 |
| - if (location.targetPopulations) { |
72 |
| - if (!location.targetPopulations.length) { |
73 |
| - somethingIsWrong(); |
74 |
| - } |
| 149 | + return { |
| 150 | + version: targetPopulations.version, |
| 151 | + monthlyTargetPopulations: targetPopulations && targetPopulations.monthlyTargetPopulations |
| 152 | + }; |
| 153 | + } |
75 | 154 |
|
76 |
| - var targetPopulations = getFactor(location.targetPopulations, stockCount.date); |
77 |
| - monthlyTargetPopulations = targetPopulations && targetPopulations.monthlyTargetPopulations; |
78 |
| - } else { |
79 |
| - // For backwards compatibility with the old style location docs, |
80 |
| - // since we have no control about when the dashboards are going |
81 |
| - // to replicate the new location docs |
82 |
| - if (!(location.targetPopulation && location.targetPopulation.length)) { |
83 |
| - somethingIsWrong(); |
84 |
| - } |
85 |
| - monthlyTargetPopulations = location.targetPopulation; |
86 |
| - } |
87 |
| - return monthlyTargetPopulations; |
88 |
| - }; |
| 155 | + // For backwards compatibility to version before introducing `targetPopulations`, |
| 156 | + // since we have no control about when the dashboards are going |
| 157 | + // to replicate the new location docs |
| 158 | + if (!(location.targetPopulation && Object.keys(location.targetPopulation).length)) { |
| 159 | + return { |
| 160 | + version: 1 |
| 161 | + }; |
| 162 | + } |
89 | 163 |
|
90 | 164 | return {
|
91 |
| - weeksOfStock: getWeeksOfStock(), |
92 |
| - weeklyLevels: getWeeklyLevels(), |
93 |
| - targetPopulations: getMonthlyTargetPopulations() |
| 165 | + version: 1, |
| 166 | + monthlyTargetPopulations: location.targetPopulation |
94 | 167 | };
|
95 | 168 | };
|
96 | 169 |
|
| 170 | + var getWeeklyLevels = function getWeeklyLevels(location, date) { |
| 171 | + if (!(location.allocations && location.allocations.length)) { |
| 172 | + throw new Error('missing allocations on location ' + location._id); |
| 173 | + } |
| 174 | + |
| 175 | + var allocations = getFactor(location.allocations, date); |
| 176 | + if (!(allocations && allocations.weeklyLevels)) { |
| 177 | + throw new Error('cannot find version of weeklyLevels for location ' + location._id + ' and date ' + date); |
| 178 | + } |
| 179 | + return allocations.weeklyLevels; |
| 180 | + }; |
| 181 | + |
| 182 | + var getFactors = (function (location, productCoefficients, date) { |
| 183 | + var weeksOfStock = getWeeksOfStock(location, date); |
| 184 | + |
| 185 | + var _getTargetPopulations = getTargetPopulations(location, date), |
| 186 | + version = _getTargetPopulations.version, |
| 187 | + monthlyTargetPopulations = _getTargetPopulations.monthlyTargetPopulations; |
| 188 | + |
| 189 | + // For backwards compatibility to version before introducing `targetPopulations`, |
| 190 | + // since for that version `weeklyAllocations` were not always calculated |
| 191 | + // based on target population |
| 192 | + |
| 193 | + |
| 194 | + if (version === 1) { |
| 195 | + return { |
| 196 | + weeklyLevels: getWeeklyLevels(location, date), |
| 197 | + weeksOfStock: weeksOfStock, |
| 198 | + monthlyTargetPopulations: monthlyTargetPopulations |
| 199 | + }; |
| 200 | + } |
| 201 | + |
| 202 | + var coefficients = getCoefficients(productCoefficients, date); |
| 203 | + var weeklyLevels = calculateWeeklyLevels(monthlyTargetPopulations, coefficients); |
| 204 | + |
| 205 | + return { |
| 206 | + weeklyLevels: weeklyLevels, |
| 207 | + weeksOfStock: weeksOfStock, |
| 208 | + monthlyTargetPopulations: monthlyTargetPopulations |
| 209 | + }; |
| 210 | + }); |
| 211 | + |
| 212 | + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); |
| 213 | + |
| 214 | + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } |
| 215 | + |
97 | 216 | var ThresholdsService = function () {
|
98 | 217 | function ThresholdsService($q, smartId, lgasService, statesService) {
|
99 | 218 | _classCallCheck(this, ThresholdsService);
|
|
107 | 226 | // For zones the thresholds are based on the state store required allocation for
|
108 | 227 | // the week, that information is passed as an optional param (`requiredStateStoresAllocation`).
|
109 | 228 | // That param is only used for zones.
|
| 229 | + // |
| 230 | + // Passing the coefficientVersions as a param so that it can be adapted later to use the database doc |
110 | 231 |
|
111 | 232 |
|
112 | 233 | _createClass(ThresholdsService, [{
|
113 | 234 | key: 'calculateThresholds',
|
114 | 235 | value: function calculateThresholds(location, stockCount, products) {
|
115 | 236 | var requiredStateStoresAllocation = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
| 237 | + var productCoefficients = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : defaultCoefficients; |
116 | 238 |
|
117 |
| - if (!(stockCount && stockCount.date)) { |
118 |
| - return; |
| 239 | + if (!stockCount) { |
| 240 | + var locationId = location && location._id ? location._id : 'with unknown id'; |
| 241 | + return somethingIsWrong('missing mandatory param stock count for location ' + locationId); |
| 242 | + } |
| 243 | + if (!stockCount.date) { |
| 244 | + return somethingIsWrong('missing date on stock count ' + stockCount._id); |
119 | 245 | }
|
120 | 246 |
|
121 |
| - if (!(location && location.level)) { |
122 |
| - return; |
| 247 | + if (!location) { |
| 248 | + var stockCountId = stockCount && stockCount._id ? stockCount._id : 'with unknown id'; |
| 249 | + return somethingIsWrong('missing mandatory param location for stock count ' + stockCountId); |
| 250 | + } |
| 251 | + if (!location.level) { |
| 252 | + return somethingIsWrong('missing level on location ' + location._id); |
123 | 253 | }
|
124 | 254 |
|
125 | 255 | if (!(products && products.length)) {
|
126 |
| - return; |
| 256 | + return somethingIsWrong('missing mandatory param products'); |
127 | 257 | }
|
128 | 258 |
|
129 |
| - var _getFactors = getFactors(stockCount, location), |
130 |
| - weeklyLevels = _getFactors.weeklyLevels, |
131 |
| - weeksOfStock = _getFactors.weeksOfStock, |
132 |
| - targetPopulations = _getFactors.targetPopulations; |
133 |
| - |
134 |
| - if (!(weeklyLevels && weeksOfStock)) { |
| 259 | + var locationFactors = void 0; |
| 260 | + try { |
| 261 | + locationFactors = getFactors(location, productCoefficients, stockCount.date); |
| 262 | + } catch (e) { |
| 263 | + somethingIsWrong(e.message); |
135 | 264 | return;
|
136 | 265 | }
|
137 | 266 |
|
138 |
| - return Object.keys(weeklyLevels).reduce(function (index, productId) { |
139 |
| - index[productId] = Object.keys(weeksOfStock).reduce(function (productThresholds, threshold) { |
140 |
| - var level = weeklyLevels[productId] * weeksOfStock[threshold]; |
141 |
| - var product = find(products, isId.bind(null, productId)); |
| 267 | + var _locationFactors = locationFactors, |
| 268 | + weeksOfStock = _locationFactors.weeksOfStock, |
| 269 | + weeklyLevels = _locationFactors.weeklyLevels, |
| 270 | + monthlyTargetPopulations = _locationFactors.monthlyTargetPopulations; |
142 | 271 |
|
143 |
| - // Default rounding used in VSPMD and highest possible presentation |
144 |
| - var presentation = 20; |
145 | 272 |
|
146 |
| - if (product && product.presentation) { |
147 |
| - // TODO: product presentations should be ints, not strings |
148 |
| - presentation = parseInt(product.presentation, 10); |
149 |
| - } |
| 273 | + return products.reduce(function (index, product) { |
| 274 | + var productId = product._id; |
| 275 | + var weeklyLevel = weeklyLevels[productId]; |
| 276 | + |
| 277 | + // Default rounding used in VSPMD and highest possible presentation |
| 278 | + var presentation = 20; |
| 279 | + |
| 280 | + if (product && product.presentation) { |
| 281 | + // TODO: product presentations should be ints, not strings |
| 282 | + presentation = parseInt(product.presentation, 10); |
| 283 | + } |
150 | 284 |
|
| 285 | + index[productId] = Object.keys(weeksOfStock).reduce(function (productThresholds, threshold) { |
| 286 | + var level = weeklyLevel * weeksOfStock[threshold]; |
151 | 287 | var roundedLevel = Math.ceil(level / presentation) * presentation;
|
152 | 288 | productThresholds[threshold] = roundedLevel;
|
153 | 289 |
|
|
158 | 294 | return productThresholds;
|
159 | 295 | }, {});
|
160 | 296 |
|
161 |
| - if (targetPopulations) { |
162 |
| - // old (and new?) zone docs have no target population doc |
163 |
| - index[productId].targetPopulation = targetPopulations[productId]; |
| 297 | + index[productId].weeklyLevel = weeklyLevel; |
| 298 | + |
| 299 | + if (monthlyTargetPopulations) { |
| 300 | + // old zone docs have no target population |
| 301 | + index[productId].targetPopulation = monthlyTargetPopulations[productId]; |
164 | 302 | }
|
165 | 303 |
|
166 | 304 | return index;
|
|
171 | 309 | value: function getThresholdsFor(stockCounts, products) {
|
172 | 310 | var _this = this;
|
173 | 311 |
|
| 312 | + var productCoefficients = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : defaultCoefficients; |
| 313 | + |
| 314 | + var isId = function isId(id, item) { |
| 315 | + return item._id === id; |
| 316 | + }; |
| 317 | + |
174 | 318 | // TODO: make it work for zones too.
|
175 | 319 | // For making it work with zones, we need to take into account the amount of stock
|
176 | 320 | // to be allocated to the zone state stores in a particular week
|
|
206 | 350 | Object.keys(index).forEach(function (key) {
|
207 | 351 | var item = index[key];
|
208 | 352 | var location = find(promisesRes[item.type], isId.bind(null, key));
|
209 |
| - item.thresholds = _this.calculateThresholds(location, item, products); |
| 353 | + item.thresholds = _this.calculateThresholds(location, item, products, null, productCoefficients); |
210 | 354 | delete item.type;
|
211 | 355 | });
|
212 | 356 |
|
|
0 commit comments