Source

managers/TraceabilityManager.js

const {INFO_PATH, DB, DEFAULT_QUERY_OPTIONS, ANCHORING_DOMAIN} = require('../constants');
const Manager = require("../../pdm-dsu-toolkit/managers/Manager");
const TraceabilityService = require('../services/TraceabilityService');
const getShipmentLineManager = require('./ShipmentLineManager');
const getReceiptManager = require('./ReceiptManager');
const IndividualProduct = require('../model/IndividualProduct');

const ACTION = {
    REQUEST: 'request',
    RESPONSE: 'response'
}

class RequestCache {
    cache = {};

    submitRequest(id, callback){
        if (id in this.cache)
            return callback(`Id already Exists!`);
        this.cache[id] = callback;
        console.log(`Tracking request ${id} submitted`);
    }

    getRequest(id){
        if (!(id in this.cache))
            throw new Error(`Id does not exist in cache!`);
        const cb = this.cache[id];
        delete this.cache[id];
        return cb;
    }
}

const convertForJson = function(startNode, endNode){
    startNode = Object.assign({}, startNode);
    endNode = Object.assign({}, endNode);

    const nodeIterator = function(node, accumulator = {}){
        node.children = node.children || [];
        node.parents = node.parents || [];

        node.parents.reduce((accum, n) => nodeIterator(n, accum), accumulator);

        node.children = node.children.map(n => n.id);
        node.parents = node.parents.map(n => n.id);
        accumulator[node.id] = accumulator[node.id] || node;
        return accumulator;
    }

    const nodeList = nodeIterator(startNode);

    return {
        startNode: startNode,
        endNode: endNode,
        nodeList: nodeList
    }
}

class TrackMessage {
    id;
    action;
    message;
    requesterId;
    error;

    constructor(trackMessage){
        if (typeof trackMessage !== undefined)
            for (let prop in trackMessage)
                if (trackMessage.hasOwnProperty(prop))
                    this[prop] = trackMessage[prop];
        if (!this.action || !this.message)
            throw new Error(`Needs id, action and message`);
        if (this.action === ACTION.REQUEST && !this.id)
            this.id = Date.now();
        if (this.action === ACTION.REQUEST && !this.requesterId)
            throw new Error("Needs a requester Id for that action");
    }
}

/**
 * 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 TraceabilityManager
 * @extends Manager
 * @memberOf Managers
 */
class TraceabilityManager extends Manager{
    constructor(participantManager, callback) {
        super(participantManager, DB.traceability, [], (err, manager) => {
            if (err)
                return callback ? callback(err) : console.log(err);
            manager.registerMessageListener((message, cb) => {
                manager.processMessageRecord(message, (err) => {
                    cb(err);
                });
            });
            if (callback)
                callback(undefined, manager);
        });
        this.participantManager = participantManager;
        this.requestCache = new RequestCache();
    }

    _processMessageRecord(message, callback) {
        let self = this;
        if (!message)
            return callback(`No message received. Skipping record.`);

        try {
            message = new TrackMessage(message);
        } catch (e) {
            return callback(e);
        }

        switch (message.action){
            case ACTION.REQUEST:
                return self._trackObj(message.requesterId, message.message, (err, startNode, endNode, nodeList) => {
                    self._replyToMessage(message.id, message.requesterId, startNode, endNode, nodeList, err, self._messageCallback)
                    callback();
                });
            case ACTION.RESPONSE:
                let cb;
                try {
                    cb = self.requestCache.getRequest(message.id);
                } catch (e) {
                    return callback(e);
                }
                cb(message.error, message.message.startNode, message.message.endNode, message.message.nodeList);
                return callback();
            default:
                return callback(`Invalid Action request received: ${message.action}`);
        }
    };

    _trackObj(requesterId, obj, callback){
        if (!callback) { // compatibility
            callback = obj;
            obj = requesterId;
            requesterId = undefined;
        }

        if (!this.getIdentity().id.startsWith("MAH")) // TODO: Bad hack (the other one was worse). maybe we just split this in two files to split the api
            return callback(`Only manufacturers can access this`);

        const self = this;

        const track = function(callback){
            const tracker = new TraceabilityService(self.shipmentLineManager, self.receiptManager, requesterId);
            const method = !!obj.serialNumber ? tracker.fromProduct : tracker.fromBatch;
            method(obj, (err, startNode, endNode) => {
                if (err)
                    return callback(err);
                console.log(`Tracking for product ${obj.gtin}, batch ${obj.batchNumber} and Serial ${obj.serialNumber} complete. Start and end Nodes:`, startNode, endNode);
                const message = convertForJson(startNode, endNode);
                callback(undefined, message.startNode, message.endNode, message.nodeList);
            });
        }

        const cacheManagers = function(callback){
            getShipmentLineManager(self.participantManager, (err, shipmentLineManager) => {
                if (err)
                    return callback(err);
                self.shipmentLineManager = shipmentLineManager;
                getReceiptManager(self.participantManager, (err, receiptManager) => {
                    if (err)
                        return callback(err);
                    self.receiptManager = receiptManager;
                    callback();
                });
            });
        }

        if (this.shipmentLineManager && this.receiptManager)
            return track(callback);

        cacheManagers((err) => {
            if (err)
                return callback(err);
            track(callback);
        });
    }

    _validate(obj) {
        const errors = [];
        if (!obj.gtin)
            errors.push('GTIN is required.');

        if (!obj.batchNumber)
            errors.push('batchNumber is required.');
        return errors.join(' ');
    }

    _replyToMessage(requestId, requesterId, startNode, endNode, nodeList, error, callback){
        const self = this;

        const reply = new TrackMessage({
            id: requestId,
            action: ACTION.RESPONSE,
            message: {
                startNode: startNode,
                endNode: endNode,
                nodeList: nodeList
            },
            error: error ? error.message || error : undefined
        });
        self.sendMessage(requesterId, reply, callback);
    }

    _getProduct(gtin, callback){
        if (!this.productService)
            this.productService = new (require('../services/ProductService'))(ANCHORING_DOMAIN);
        this.productService.getDeterministic(gtin, callback);
    }

    /**
     * @param {string} key
     * @param {*} obj
     * @param {function(err?, keySSI?, string?)} callback where the string is the mount path relative to the main DSU
     */
    create(key, obj, callback) {
        callback(`Traceability cannot be created`);
    }

    /**
     * @param {string|number} [key] the table key
     * @param {{}} obj
     * @param {function(err?, Stock?)} callback
     * @override
     */
    update(key, obj, callback){
        callback(`Traceability cannot be updated`);
    }

    /**
     * @param {IndividualProduct} obj
     * @param {function(err?, Node?, Node?)} callback
     * @override
     */
    getOne(obj,  callback) {
        let self = this;
        if (!(obj instanceof IndividualProduct))
            obj = new IndividualProduct(obj);

        const _err = self._validate(obj);
        if (_err)
            return callback(`Invalid Object Provided. ${_err}`);

        if (!obj.manufName)
            return self._getProduct(obj.gtin, (err, p) => {
                if (err)
                    return callback(err);
                obj.manufName = p.manufName;
                self.getOne(obj, callback);
            });

        const identity = self.getIdentity();
        if (identity.id === obj.manufName)
            return self._trackObj(identity.id, obj, callback);

        const message = new TrackMessage({
            id: identity.id + Date.now(),
            action: ACTION.REQUEST,
            message: obj,
            requesterId: identity.id
        });
        self.requestCache.submitRequest(message.id, callback);
        self.sendMessage(obj.manufName, message, self._messageCallback)
    }

    /**
     * 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){
        callback(`Not the way tracking works`);
    }
}


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

    return manager;
}

module.exports = getTraceabilityManager;