const {INFO_PATH, DB, DEFAULT_QUERY_OPTIONS, ANCHORING_DOMAIN} = require('../constants');
const Manager = require("../../pdm-dsu-toolkit/managers/Manager");
const {functionCallIterator} = require('../services').utils;
const Stock = require('../model/Stock');
const Batch = require('../model/Batch');
const StockStatus = require('../model/StockStatus');
const StockManagementService = require("../services/StockManagementService");
const Page = require('../../pdm-dsu-toolkit/managers/Page');
const { toPage, paginate } = require('../../pdm-dsu-toolkit/managers/Page');
const ShipmentStatus = require("../model/ShipmentStatus");
/**
* Stock Manager Class
*
* Manager Classes in this context should do the bridge between the controllers
* and the services exposing only the necessary api to the controllers while encapsulating <strong>all</strong> business logic.
*
* All Manager Classes should be singletons.
*
* This complete separation of concerts is very beneficial for 2 reasons:
* <ul>
* <li>Allows for testing since there's no browser dependent code (i think) since the DSUStorage can be 'mocked'</li>
* <li>Allows for different controllers access different business logic when necessary (while benefiting from the singleton behaviour)</li>
* </ul>
*
* @param {ParticipantManager} participantManager
* @param {function(err, Manager)} [callback] optional callback for when the assurance that the table has already been indexed is required.
* @class StockManager
* @extends Manager
* @memberOf Managers
*/
class StockManager extends Manager{
constructor(participantManager, serialization, aggregation, callback) {
super(participantManager, DB.stock, ['name', 'gtin', 'manufName', 'quantity'], callback || aggregation);
this.serialization = serialization;
this.aggregation = callback ? aggregation : false;
this.productService = undefined;
this.batchService = undefined;
this.participantManager = participantManager;
}
_getProduct(gtin, callback){
if (!this.productService)
this.productService = new (require('../services/ProductService'))(ANCHORING_DOMAIN);
this.productService.getDeterministic(gtin, callback);
}
_getBatch(gtin, batch, callback){
if (!this.batchService)
this.batchService = new (require('../services/BatchService'))(ANCHORING_DOMAIN);
this.batchService.getDeterministic(gtin, batch, callback)
}
/**
* Creates a {@link Product} dsu
* @param {string|number} [gtin] the table key
* @param {Stock} stock
* @param {function(err, keySSI, string)} callback where the string is the mount path relative to the main DSU
*/
create(gtin, stock, callback) {
if (!callback) {
callback = stock;
stock = gtin;
gtin = stock.gtin;
}
let self = this;
stock.quantity = stock.getQuantity();
console.log(`Adding Stock for ${gtin} batches: ${stock.batches.map(b => b.batchNumber).join(', ')}`);
self.insertRecord(gtin, stock, (err) => {
if (err)
return self._err(`Could not insert record with gtin ${gtin} on table ${self.tableName}`, err, callback);
const path =`${self.tableName}/${gtin}`;
console.log(`Stock for Product ${gtin} created stored at '${path}'`);
callback(undefined, stock, path);
});
}
/**
* updates a product from the list
* @param {string|number} [gtin] the table key
* @param {Stock} newStock
* @param {function(err, Stock)} callback
* @override
*/
update(gtin, newStock, callback){
if (!callback){
callback = newStock;
newStock = gtin;
gtin = newStock.gtin;
}
let self = this;
newStock.quantity = newStock.getQuantity();
self.updateRecord(gtin, newStock, (err) => {
if (err)
return self._err(`Could not update stock with gtin ${gtin}: ${err.message}`, err, callback);
console.log(`Stock for Product ${gtin} updated`);
callback(undefined, newStock)
});
}
/**
*
* @param {Product} product
* @param {Batch} batch
* @param {function(err?, string[]?, Stock?)} callback
*/
manage(product, batch, callback){
const self = this;
if (batch.length === 0)
return callback();
const gtin = product.gtin || product;
const getBatch = function(gtin, batch, callback){
self._getBatch(gtin, batch.batchNumber, (err, batchFromDSU) => {
if (err)
return callback(err);
batch = new Batch({
batchNumber: batchFromDSU.batchNumber,
expiry: batchFromDSU.expiry,
serialNumbers: batch.serialNumbers,
quantity: batch.quantity,
batchStatus: batchFromDSU.batchStatus
})
callback(undefined, batch);
});
}
self.getOne(gtin, true, (err, stock) => {
if (err){
console.log('batch quantity err check', batch.quantity);
if (batch.quantity < 0)
return callback(`Trying to reduce from an unexisting stock`);
const cb = function(product){
const newStock = new Stock(product);
getBatch(product.gtin, batch, (err, mergedBatch) => {
if (err)
return callback(err);
newStock.batches = [mergedBatch];
return self.create(gtin, newStock, (err, created, path) => {
if (err)
return callback(err);
callback(undefined, batch.serialNumbers || batch.quantity, newStock);
});
});
}
if (typeof product !== 'string')
return cb(product);
return self._getProduct(product, (err, product) => err
? callback(err)
: cb(product));
}
getBatch(gtin, batch, (err, updatedBatch) => {
if (err)
return callback(err);
const sb = stock.batches.map((b,i) => ({batch: b, index: i})).find(b => b.batch.batchNumber === batch.batchNumber);
let serials;
if (!sb){
if (batch.getQuantity() < 0)
return callback(`Given a negative amount on a unnexisting stock`);
stock.batches.push(updatedBatch);
console.log(`Added batch ${updatedBatch.batchNumber} with ${updatedBatch.serialNumbers ? updatedBatch.serialNumbers.length : updatedBatch.getQuantity()} items`);
} else {
const newQuantity = sb.batch.getQuantity() + (updatedBatch.quantity || updatedBatch.getQuantity());
if (newQuantity < 0)
return callback(`Illegal quantity. Not enough Stock. requested ${batch.getQuantity() } of ${sb.batch.getQuantity() }`);
serials = sb.batch.manage(updatedBatch.getQuantity(), this.serialization);
stock.batches[sb.index] = new Batch({
batchNumber: updatedBatch.batchNumber,
expiry: updatedBatch.expiry,
batchStatus: updatedBatch.batchStatus,
quantity: newQuantity,
serialNumbers: sb.batch.serialNumbers
});
}
self.update(gtin, stock, (err, results) => {
if (err)
return self._err(`Could not manage stock for ${gtin}: ${err.message}`, err, callback);
console.log(`Updated Stock for ${gtin} batch ${batch.batchNumber}. ${self.serialization && serials ? serials.join(', ') : ''}`);
callback(undefined, (serials && serials.length ? serials : undefined) || batch.serialNumbers || batch.quantity, results);
});
});
});
}
/**
*
* @param {string} product gtin
* @param {Batch[]} batches
* @param {function(err?, {}?)} callback where {} as batchnumber as keys, as the added/removed serials as value
*/
manageAll(product, batches, callback){
const self = this;
const dbAction = function(product, batches, callback){
const iterator = function(product){
return function(batch, callback){
return self.manage(product, batch, (err, serials, stock) => {
if (err)
return callback(err);
callback(undefined, batch, serials, stock);
});
}
}
const cb = function(err, ...results){
if (err)
return self.cancelBatch(err2 => {
callback(err);
});
callback(undefined, ...results);
}
try {
self.beginBatch();
} catch (e){
return self.batchSchedule(() => dbAction(product, batches, callback));
//return callback(e);
}
functionCallIterator(iterator(product).bind(this), batches, (err, ...results) => {
if (err)
return cb(`Could not perform manage all on Stock beacause ${err}`);
self.commitBatch((err) => {
if(err)
return cb(err);
const newStocks = [];
const mergedResult = results.reduce((accum, result) => {
accum[result[0].batchNumber] = accum[result[0].batchNumber] || [];
try {
accum[result[0].batchNumber].push(...(Array.isArray(result[1]) ? result[1] : [result[1]]))
} catch (e) {
console.log(e)
}
if (result.length >= 3)
newStocks.push(result[2])
return accum;
}, {});
callback(undefined, mergedResult, newStocks);
});
});
}
dbAction(product, batches, callback);
}
/**
* updates a product from the list
* @param {string[]|number[]} [gtins] the table key
* @param {Stock[]} newStocks
* @param {function(err, Stock[])} callback
* @override
*/
updateAll(gtins, newStocks, callback){
if (!callback){
callback = newStocks;
newStocks = gtins;
gtins = newStocks.map(s => s.gtin);
}
let self = this;
super.updateAll(gtins, newStocks, (err) => {
if (err)
return self._err(`Could not update products`, err, callback);
console.log(`Products ${JSON.stringify(gtins)} updated`);
callback(undefined, newStocks)
});
}
/**
* reads ssi for that gtin in the db. loads is and reads the info at '/info'
* @param {string} gtin
* @param {boolean} [readDSU] defaults to true. decides if the manager loads and reads from the dsu or not
* @param {function(err, Stock|KeySSI, Archive)} callback returns the Product if readDSU and the dsu, the keySSI otherwise
* @override
*/
getOne(gtin, readDSU, callback) {
if (!callback){
callback = readDSU;
readDSU = true;
}
let self = this;
self.getRecord(gtin, (err, stock) => {
if (err)
return self._err(`Could not load record with key ${gtin} on table ${self._getTableName()}`, err, callback);
callback(undefined, new Stock(stock));
});
}
/**
* Lists all registered items according to query options provided
* @param {boolean} [readDSU] defaults to true. decides if the manager loads and reads from the dsu's {@link INFO_PATH} or not
* @param {object} [options] query options. defaults to {@link DEFAULT_QUERY_OPTIONS}
* @param {function(err, object[])} callback
* @override
*/
getAll(readDSU, options, callback){
const defaultOptions = () => Object.assign({}, DEFAULT_QUERY_OPTIONS, {
query: ['gtin like /.*/g']
});
if (!callback){
if (!options){
callback = readDSU;
options = defaultOptions();
readDSU = true;
}
if (typeof readDSU === 'boolean'){
callback = options;
options = defaultOptions();
}
if (typeof readDSU === 'object'){
callback = options;
options = readDSU;
readDSU = true;
}
}
options = options || defaultOptions();
let self = this;
self.query(options.query, options.sort, options.limit, (err, records) => {
if (err)
return self._err(`Could not perform query`, err, callback);
if (!readDSU)
return callback(undefined, records.map(r => r.pk));
callback(undefined, records.map(r => new Stock(r)));
});
}
/**
* Get partner stock products that were shipped by MAH/manufName
* @param { string } gtin
* @param {{manufName: string, batch: number}} options
* @param callback
*/
getStockTraceability(gtin, options, callback) {
let self = this;
if (!callback) {
callback = options;
options = {}
}
const {manufName, batch} = options;
if (!manufName) {
return self._getProduct(gtin, (err, product) => {
if (err)
return callback(err);
return self.getStockTraceability(gtin, {manufName: product.manufName, batch}, callback);
});
}
const identity = self.getIdentity();
if (identity.id !== manufName) {
return callback('Stock Traceability is only available for Marketing Authorization Holder')
}
try {
this.stockManager = this.stockManager || this.participantManager.getManager("StockManager");
this.shipmentLineManager = this.shipmentLineManager || this.participantManager.getManager("ShipmentLineManager");
this.receiptManager = this.receiptManager || this.participantManager.getManager("ReceiptManager");
} catch (e) {
return callback(e);
}
const stockManagementService = new StockManagementService(manufName, this.stockManager, this.shipmentLineManager, this.receiptManager);
stockManagementService.traceStockManagement(gtin, batch, callback)
}
toModel(filteredStock, model){
return Object.entries(filteredStock).map(([key, value]) => {
return {
gtin: key,
name: value.name,
batches: value.stock
}
});
}
}
/**
* @param {ParticipantManager} participantManager
* @param {boolean} [serialization] defaults to true.
* @param {function(err, Manager)} [callback] optional callback for when the assurance that the table has already been indexed is required.
* @returns {StockManager}
* @memberOf Managers
*/
const getStockManager = function (participantManager, serialization, callback) {
if (!callback){
callback = serialization;
serialization = true;
}
let manager;
try {
manager = participantManager.getManager(StockManager);
if (callback)
return callback(undefined, manager);
} catch (e){
manager = new StockManager(participantManager, serialization, callback);
}
return manager;
}
module.exports = getStockManager;
Source