const {INFO_PATH, DB, DEFAULT_QUERY_OPTIONS, ANCHORING_DOMAIN} = require('../constants');
const Manager = require("../../pdm-dsu-toolkit/managers/Manager");
const Sale = require('../model/Sale');
const Batch = require('../model/Batch');
const BatchStatus = require('../model/BatchStatus');
const IndividualProduct = require('../model/IndividualProduct');
const {toPage, paginate} = require("../../pdm-dsu-toolkit/managers/Page");
const utils = require('../services').utils;
/**
* 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 SaleManager
* @extends Manager
* @memberOf Managers
*/
class SaleManager extends Manager{
constructor(participantManager, callback) {
super(participantManager, DB.sales, ['id', 'products', 'sellerId'], callback);
this.stockManager = participantManager.stockManager;
this.saleService = new (require('../services').SaleService)(ANCHORING_DOMAIN);
}
/**
*
* @param key
* @param item
* @param {Sale} record
* @return {{}}
* @private
*/
_indexItem(key, item, record) {
if (!record){
record = item;
item = undefined;
if (!record){
record = key;
key = record.id
}
}
return Object.assign(record, {
products: record.productList
.map(ip => `${ip.gtin}-${ip.batchNumber}-${ip.serialNumber}`)
.join(',')})
}
/**
* Verify if the sale can be done. Some conditions that are checked:
* - GTIN and BATCH need exists in stock
* - Check if product qty in stock is enough
* - Check if sale is not duplicated (product already sold)
* - Check if product status is not recalled or quarantined
* @param {{}} aggStock: key is a GTIN, value is an array of BATCHES
* @param {{}} productList: key is a manufName identifier, value is an array of IndividualProduct
* @param callback
*/
_checkStockAvailability(aggStock, productList, callback) {
const qtySoldByGtinBatch = {}; // qty sold by gtin and batch, cannot sale more than exists on stock
const qtySoldBySn = {}; // qty sold by serial number, cannot sale same product more than once
const aggBatchesByGtin = {}
const aggIndividualProductsByMAH = {};
const iterator = (_aggStock, _productList, _callback) => {
const productSold = _productList.shift();
if (!productSold)
return _callback(undefined, aggBatchesByGtin, aggIndividualProductsByMAH);
const gtinBatchNumber = `${productSold.gtin}-${productSold.batchNumber}`;
if(!(_aggStock.hasOwnProperty(gtinBatchNumber)))
return _callback(`Product gtin ${productSold.gtin}, batchNumber ${productSold.batchNumber} not found in stock.`);
// check if selling the same product more than once
const indProductId = `${productSold.gtin}-${productSold.batchNumber}-${productSold.serialNumber}`;
qtySoldBySn[indProductId] = 1 + (qtySoldBySn[indProductId] || 0);
if (qtySoldBySn[indProductId] > 1)
return _callback(`Product ${productSold.gtin}: trying to sold a product more than once.`);
const stockProduct = _aggStock[gtinBatchNumber];
// check if sale qty is available in stock
qtySoldByGtinBatch[gtinBatchNumber] = 1 + (qtySoldByGtinBatch[gtinBatchNumber] || 0);
if (stockProduct.batch.quantity - qtySoldByGtinBatch[gtinBatchNumber] < 0)
return _callback(`Product ${productSold.gtin}: quantity not enough in stock.`);
if (stockProduct.batch.batchStatus.status !== BatchStatus.COMMISSIONED)
return _callback(`Product gtin ${productSold.gtin}, batch ${productSold.batchNumber}: is not available for sale, because batchStatus is ${stockProduct.batch.batchStatus.status}. `)
const individualProductSold = new IndividualProduct({
gtin: productSold.gtin,
batchNumber: productSold.batchNumber,
serialNumber: productSold.serialNumber,
name: stockProduct.name,
manufName: stockProduct.manufName,
expiry: stockProduct.batch.expiry,
status: stockProduct.batch.batchStatus.status
});
// add to aggIndividualProductsByMAH
const mah = stockProduct.manufName;
if (aggIndividualProductsByMAH.hasOwnProperty(mah))
aggIndividualProductsByMAH[mah].push(individualProductSold);
else
aggIndividualProductsByMAH[mah] = [individualProductSold];
// add to aggBatchesByGtin
if (aggBatchesByGtin.hasOwnProperty(productSold.gtin)) {
const batch = aggBatchesByGtin[productSold.gtin].find((batch) => batch.batchNumber === productSold.batchNumber);
batch.serialNumbers.push(productSold.serialNumber);
batch.quantity = batch.getQuantity() * -1; // update qty because when create a Batch, there is a qty validation
} else {
const batch = new Batch({
batchNumber: productSold.batchNumber,
serialNumbers: [productSold.serialNumber],
});
batch.quantity = batch.getQuantity() * -1; // update qty because when create a Batch, there is a qty validation
aggBatchesByGtin[productSold.gtin] = [batch]
}
iterator(_aggStock, _productList, _callback);
}
iterator(aggStock, productList.slice(), callback);
}
/**
* Creates a {@link Sale} entry
* @param {Sale} sale
* @param {function(err, keySSI?, string?)} callback where the string is the mount path relative to the main DSU
*/
create(sale, callback) {
let self = this;
if (!(sale instanceof Sale))
sale = new Sale(sale);
const query = {query: [`gtin like /${sale.productList.map(il => il.gtin).join('|')}/g`]};
self.stockManager.getAll(true, query, (err, stocks) => {
if (err)
return self._err(`Could not get stocks for sale`, err, callback);
if (!stocks || stocks.length === 0)
return callback(`Not available stock for sale.`);
const cb = function(err, ...results){
if (err)
return self.cancelBatch(_ => callback(err));
callback(undefined, ...results);
}
const aggStockByGtinBatch = (accum, _stock, _callback) => {
const stock = _stock.shift();
if (!stock)
return _callback(accum);
const batches = stock.batches.reduce((_accum, batch) => {
_accum[`${stock.gtin}-${batch.batchNumber}`] = {
gtin: stock.gtin,
name: stock.name,
manufName: stock.manufName,
batch: batch
};
return _accum;
}, {});
accum = {...accum, ...batches};
aggStockByGtinBatch(accum, _stock, _callback);
}
aggStockByGtinBatch({}, stocks.slice(), (resultAggStockByGtinBatch) => {
self._checkStockAvailability(resultAggStockByGtinBatch, sale.productList, (err, aggBatchesByGtin, aggIndividualProductsByMAH) => {
if (err)
return callback(err);
const dbAction = function(sale, aggBatchesByGtin, aggIndividualProductsByMAH, _callback) {
try {
self.beginBatch();
} catch (e){
return self.batchSchedule(() => dbAction(sale, aggBatchesByGtin, aggIndividualProductsByMAH, _callback));
}
const removeFromStock = function(gtins, _aggBatchesByGtin, _callback){
const gtin = gtins.shift();
if (!gtin)
return _callback(undefined);
self.batchAllow(self.stockManager);
self.stockManager.manageAll(gtin, _aggBatchesByGtin[gtin].slice(), (err, serials, stocks) => {
self.batchDisallow(self.stockManager);
if (err)
return _callback(err);
removeFromStock(gtins, _aggBatchesByGtin, _callback);
});
}
removeFromStock(Object.keys(aggBatchesByGtin), aggBatchesByGtin, (err) => {
if (err)
return cb(err);
console.log(`Creating sale entry for: ${sale.productList.map(p => `${p.gtin}-${p.batchNumber}-${p.serialNumber}`).join(', ')}`);
self._addSale(sale.id, aggIndividualProductsByMAH, (err, readSSis, insertedSale, ) => {
if (err)
return cb(`Could not Crease Sales DSUs`);
self.insertRecord(insertedSale.id, self._indexItem(insertedSale), (err) => {
if (err)
return cb(`Could not insert record with id ${insertedSale.id} on table ${self.tableName}`);
self.commitBatch((err) => {
if(err)
return cb(err);
const path =`${self.tableName}/${insertedSale.id}`;
console.log(`Sale stored at '${path}'`);
_callback(undefined, insertedSale, path, readSSis);
});
});
});
});
}
dbAction(sale, aggBatchesByGtin, aggIndividualProductsByMAH, callback);
});
})
}); // stockManager.getAll end
}
_addSale(saleId, aggIndividualProductsByMAH, callback){
const self = this;
const sellerId = self.getIdentity().id;
const insertedSale = new Sale({
id: saleId,
sellerId: sellerId,
productList: []
});
const saleReadSSIs = [];
const createIterator = function(products, accumulator, _callback){
const saleByMAH = new Sale({
id: saleId,
sellerId: sellerId,
productList: products
});
const saleErr = saleByMAH.validate();
if (saleErr)
return self._err(`Sale validate error`, saleErr, _callback);
insertedSale.productList.push(...saleByMAH.productList);
self.saleService.create(saleByMAH, (err, keySSI, dsu) => {
if (err)
return self._err(`Could not create Sale DSU`, err, _callback);
accumulator.push(keySSI.getIdentifier());
console.log(`Created Sale with SSI ${keySSI.getIdentifier()}`);
_callback(undefined, accumulator);
});
}
const createAndNotifyIterator = function(mahs, accumulator, _callback){
const mah = mahs.shift();
if (!mah)
return _callback(undefined, saleReadSSIs, insertedSale);
createIterator(aggIndividualProductsByMAH[mah].slice(), [], (err, keySSIs) => {
if (err)
return _callback(err);
accumulator[mah] = keySSIs;
const keySSISpace = utils.getKeySSISpace();
let readSSIs;
try {
readSSIs = keySSIs.map(k => keySSISpace.parse(k).derive().getIdentifier())
saleReadSSIs.push(...readSSIs);
} catch(e) {
return _callback(`Invalid keys found`);
}
self.sendMessage(mah, DB.receipts, readSSIs, err =>
self._messageCallback(err ? `Could not send message` : `Message to Mah ${mah} sent with sales`));
createAndNotifyIterator(mahs, accumulator, _callback);
});
}
createAndNotifyIterator(Object.keys(aggIndividualProductsByMAH), {}, callback);
}
/**
* updates a product from the list
* @param {string|number} [id] the table key
* @param {Sale} newSale
* @param {function(err, Sale?)} callback
* @override
*/
update(id, newSale, callback){
return callback(`All sales are final`);
}
/**
* reads ssi for that gtin in the db. loads is and reads the info at '/info'
* @param {string} id
* @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(id, readDSU, callback) {
if (!callback){
callback = readDSU;
readDSU = true;
}
let self = this;
self.getRecord(id, (err, sale) => {
if (err)
return self._err(`Could not load record with key ${id} on table ${self._getTableName()}`, err, callback);
callback(undefined, new Sale(sale));
});
}
/**
* 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: [
"__timestamp > 0",
'id like /.*/g'
],
sort: "dsc"
});
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 Sale(r)));
});
}
}
/**
* @param {ParticipantManager} participantManager
* @param {function(err, Manager)} [callback] optional callback for when the assurance that the table has already been indexed is required.
* @returns {SaleManager}
* @memberOf Managers
*/
const getSaleManager = function (participantManager, callback) {
let manager;
try {
manager = participantManager.getManager(SaleManager);
if (callback)
return callback(undefined, manager);
} catch (e){
manager = new SaleManager(participantManager, callback);
}
return manager;
}
module.exports = getSaleManager;
Source