Source

managers/IssuedOrderManager.js

const { DB, DEFAULT_QUERY_OPTIONS, SHIPMENT_PATH, INFO_PATH } = require('../constants');
const OrderManager = require("./OrderManager");
const {Order, OrderStatus, ShipmentStatus, Batch} = require('../model');
const Manager = require("../../pdm-dsu-toolkit/managers/Manager");
const {toPage, paginate} = require("../../pdm-dsu-toolkit/managers/Page");
const utils = require('../services').utils

/**
 * Issued Order Manager Class - concrete OrderManager for issuedOrders.
 *
 * 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 IssuedOrderManager
 * @extends Manager
 * @memberOf Managers
 */
class IssuedOrderManager extends OrderManager {
    constructor(participantManager, callback) {
        super(participantManager, DB.issuedOrders, ['orderId', 'senderId', 'shipmentId', 'status'], callback);
        this.stockManager = participantManager.stockManager;
    }

    /**
     * Must wrap the DB entry in an object like:
     * <pre>
     *     {
     *         index1: ...
     *         index2: ...
     *         value: item
     *     }
     * </pre>
     * so the DB can be queried by each of the indexes and still allow for lazy loading
     * @param {string} key
     * @param {Order} item
     * @param {string|object} record
     * @return {object} the indexed object to be stored in the db
     * @protected
     * @override
     */
    _indexItem(key, item, record) {
        return {...super._indexItem(key, item, record), senderId: item.senderId};
    }

    /**
     * Creates a {@link Order} dsu
     * @param {string|number} [orderId] the table key
     * @param {Order} order
     * @param {function(err, sReadSSI, dbPath)} callback where the dbPath follows a "tableName/orderId" template.
     */
    create(orderId, order, callback) {
        if (!callback){
            callback = order;
            order = orderId;
            orderId = order.orderId;
        }
        let self = this;

        self.orderService.create(order, (err, keySSI, orderLinesSSIs) => {
            if (err)
                return self._err(`Could not create product DSU for ${order}`, err, callback);                
            const keySSIStr = keySSI.getIdentifier();
            const sReadSSI = keySSI.derive();
            const sReadSSIStr = sReadSSI.getIdentifier();
            console.log("Order seedSSI="+keySSIStr+" sReadSSI="+sReadSSIStr);
            // storing the sReadSSI in base58
            self.insertRecord(super._genCompostKey(order.senderId, order.orderId), self._indexItem(orderId, order, keySSIStr), (err) => {
                if (err)
                    return self._err(`Could not insert record with orderId ${orderId} on table ${self.tableName}`, err, callback);
                const path = `${self.tableName}/${orderId}`;
                console.log(`Order ${orderId} created stored at DB '${path}'`);
                // send a message to senderId
                // TODO send the message before inserting record ? The message gives error if senderId does not exist/not listening.
                // TODO derive sReadSSI from keySSI
                this.sendMessage(order.senderId, DB.receivedOrders, sReadSSIStr, (err) => {
                    if (err)
                        return self._err(`Could not sent message to ${order.orderId} with ${DB.receivedOrders}`, err, callback);
                    console.log("Message sent to "+order.senderId+", "+DB.receivedOrders+", "+sReadSSIStr);
                    callback(undefined, keySSI, path);
                });
            });
        });
    }

    /**
     * Lists all issued orders.
     * @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, Order[])} callback
     */
    getAll(readDSU, options, callback) {
        const defaultOptions = () => Object.assign({}, DEFAULT_QUERY_OPTIONS);

        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;
        super.getAll(readDSU, options, (err, result) => {
            if (err)
                return self._err(`Could not parse IssuedOrders ${JSON.stringify(result)}`, err, callback);
            console.log(`Parsed ${result.length} orders`);
            callback(undefined, result);
        });
    }

    /**
     *
     * @param [order]
     * @override
     */
    refreshController(order) {
        const props = order ? {
                mode: 'issued',
                order: order
            } : undefined;
        super.refreshController(props);
    }

    updateOrderByShipment(orderId, shipmentSSI, shipment, callback){
        const getOrderStatusByShipment = function(shipmentStatus){
            switch (shipmentStatus){
                case ShipmentStatus.CREATED:
                    return OrderStatus.ACKNOWLEDGED;
                default:
                    return shipmentStatus;
            }
        }

        /**
         * Message/ExtraInfo by default follows the model: `{SENDER_ID} {TIMESTAMP} {MESSAGE}`,
         * so need to be sanitized to remove {SENDER_ID} and {TIMESTAMP}, because {REQUESTER_ID}
         * just needs the message
         * @param {Status} status
         * @param {{}} extraInfo
         * @returns {string}
         */
        const getExtraInfoMsg = function (status, extraInfo) {
            if (!extraInfo)
                return '';

            const statusStr = status.status;
            if (!extraInfo.hasOwnProperty(statusStr))
                return '';

            const lastLog = status.log[status.log.length - 1]
            const extraInfoUpdated = extraInfo[statusStr].filter(_extraInfo => {
                // verify if extraInfo.timestamp ===log.timestamp
                return _extraInfo.split(' ')[1].trim() === lastLog.split(' ')[1].trim()
            })
            if (extraInfoUpdated.length > 0) {
                return extraInfoUpdated[0].split(' ').slice(2).join(' ').trim(); // sanitized
            } else {
                return '';
            }
        }

        const update = super.update.bind(this);

        console.log(`Updating order ${orderId} with shipment ${shipment.shipmentId}`);

        const self = this;
        const key = this._genCompostKey(shipment.senderId, orderId);
        self.getOne(key, true, (err, order) => {
            if (err)
                return self._err(`Could not load Order`, err, callback);
            order.status['status'] = getOrderStatusByShipment(shipment.status.status);
            order.status['extraInfo'] = getExtraInfoMsg(shipment.status, shipment.status.extraInfo);
            console.log(`Order Status for Issued Order ${key} to be updated to ${order.status.status}`);
            order.shipmentSSI = shipmentSSI;

            const dbAction = function(key, order, callback){

                try {
                    self.beginBatch();
                } catch (e){
                    return self.batchSchedule(() => dbAction(key, order, callback));
                    //return callback(e);
                }

                const cb = function(err, ...results){
                    if (err)
                        return self.cancelBatch(err2 => {
                            callback(err);
                        });
                    callback(undefined, ...results);
                }

                update(key, order, (err) => {
                    if (err)
                        return cb(`Could not update Order:\n${err.message}`);
                    self.commitBatch((err) => {
                        if(err)
                            return cb(err);

                        console.log(`Order Status for Issued Order ${key} updated to ${order.status}`);
                        self.refreshController(order);
                        return callback();
                    });         
                });
            }

            dbAction(key, order, callback);
        });
    }

    /**
     * updates an item
     *
     * @param {string} [key] key is optional so child classes can override them
     * @param {Order} order
     * @param {function(err, Order?, Archive?)} callback
     */
    update(key, order, callback){
        if (!callback){
            callback = order;
            order = key;
            key = this._genCompostKey(order.senderId, order.orderId);
        }

        const update = super.update.bind(this);

        const self = this;

        self.getOne(key, false,(err, record) => {
            if (err)
                return callback(err);
                
            const dbAction = function(key, order, record, callback){
                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(key, order, record, callback));
                    //return callback(e);
                }

                update(key, order, (err, updatedOrder, dsu) => {
                    if (err)
                        return cb(err);

                    const sendMessages = function(){
                        const sReadSSIStr = utils.getKeySSISpace().parse(record).derive().getIdentifier();
                        self.sendMessagesAsync(order, sReadSSIStr);
                        callback(undefined, updatedOrder, dsu);
                    }

                    if (order.status.status !== OrderStatus.CONFIRMED)
                        return self.commitBatch((err) => {
                            if(err)
                                return cb(err);
                            sendMessages();
                        });

                    // Get all the shipmentLines from the shipment so we can add it to the stock
                    dsu.readFile(`${SHIPMENT_PATH}${INFO_PATH}`, (err, data) => {
                        if (err)
                            return cb(`Could not get ShipmentLines SSI`);

                        let shipment;
                        try {
                            shipment = JSON.parse(data);
                        } catch (e) {
                            return cb(e);
                        }
                        const gtins = shipment.shipmentLines.map(sl => sl.gtin);
                        const batchesToAdd = shipment.shipmentLines.reduce((accum, sl) => {
                            accum[sl.gtin] = accum[sl.gtin] || [];
                            accum[sl.gtin].push(new Batch({
                                batchNumber: sl.batch,
                                quantity: sl.quantity,
                                serialNumbers: sl.serialNumbers
                            }))
                            return accum;
                        }, {});

                        const result = {};

                        const gtinIterator = function(gtins, batchObj, callback){
                            const gtin = gtins.shift();
                            if (!gtin)
                                return callback(undefined, result);
                            const batches = batchObj[gtin];
                            self.batchAllow(self.stockManager);
                            self.stockManager.manageAll(gtin, batches, (err, newStocks) => {
                                self.batchDisallow(self.stockManager);
                                if (err)
                                    return callback(err);
                                result[gtin] = result[gtin] || [];
                                result[gtin].push(newStocks);
                                gtinIterator(gtins, batchObj, callback);
                            });
                        }

                        gtinIterator(gtins.slice(), batchesToAdd, (err, result) => {
                            if (err)
                                return cb(`Could not update Stock`);

                            self.commitBatch((err) => {
                                if(err)
                                    return cb(err);

                                console.log(`Stocks updated`, result);
                                sendMessages();
                            });
                        })
                    });
                });
            }

            dbAction(key, order, record, callback);
        });
    }

    sendMessagesAsync(order, orderSSI){
        const self = this;
        self.sendMessage(order.senderId, DB.receivedOrders, orderSSI, (err) =>
            self._messageCallback(err ?
                `Could not sent message to ${order.orderId} with ${DB.receivedOrders}` :
                "Message sent to "+order.senderId+", "+DB.receivedOrders+", "+orderSSI));
    }

    /**
     * Creates a blank {@link Order} with some specific initializations.
     * Uses the participantManager to obtain some data.
     * @param {function(err, order)} callback
     */
    newBlank(callback) {
        let self = this;
        self.getIdentity((err, participant) => {
            if (err) {
                return callback(err);
            }
            let orderId = Date.now(); // TODO sequential unique numbering ? It should comes from the ERP anyway.
            let requesterId = participant.id;
            let senderId = '';
            let shipToAddress = participant.address;
            let order = new Order(orderId, requesterId, senderId, shipToAddress, OrderStatus.CREATED, []);
            callback(undefined, order);
        });
    }

    /**
     * Convert an Order into a OrderControler view model. 
     * The order.orderLines are converted to a special format. See locale.js
     * @param {Order} object the business model object
     * @param model the Controller's model object
     * @returns {{}}
     */
    toModel(object, model) {
        model = model || {};
        for (let prop in object) {
            //console.log("prop", prop, "=='orderLines'", prop=="orderLines");
            if (object.hasOwnProperty(prop)) {
                if (!model[prop])
                    model[prop] = {};
                if (prop == "orderLines") {
                    model[prop].value = "";
                    let sep = "";
                    object[prop].forEach((orderLine) => {
                        model[prop].value += sep + orderLine.gtin + "," + orderLine.quantity;
                        sep = ";";
                    });
                } else {
                    model[prop].value = object[prop];
                }
            }
        }
        return model;
    }
}

/**
 * @param {ParticipantManager} participantManager
 * @param {function(err, Manager)} [callback] optional callback for when the assurance that the table has already been indexed is required.
 * @returns {IssuedOrderManager}
 * @memberOf Managers
 */
const getIssuedOrderManager = function (participantManager, callback) {
    let manager;
    try {
        manager = participantManager.getManager(IssuedOrderManager);
        if (callback)
            return callback(undefined, manager);
    } catch (e){
        manager = new IssuedOrderManager(participantManager, callback);
    }

    return manager;
}

module.exports = getIssuedOrderManager;