Your IP : 216.73.217.95


Current Path : /proc/thread-self/root/home/deltalab/PMS/partner-manager-backend/services/
Upload File :
Current File : //proc/thread-self/root/home/deltalab/PMS/partner-manager-backend/services/oms.js

/* eslint-disable no-param-reassign */
/* eslint-disable no-return-await */
/* eslint-disable no-use-before-define */
/* eslint-disable no-await-in-loop */
/**
 * Order Management Service
 *
 * Fetches orders from OMS Connector
 * and create/update the local copies to be served by mongoose.
 * See models/mongoose/order.js for PMS-FE queries.
 */

// DEPENDENCIES ===========================================
const { GraphQLClient, gql } = require('graphql-request');
const axios = require('axios');

// RESOURCES =======================================
const { orderModel } = require('../models/mongoose/order');
const { channelModel } = require('../models/mongoose/channel');
const { partnerModel } = require('../models/mongoose/partner');
const { productModel } = require('../models/mongoose/product');
const utility = require('./utility');
const sms = require('./sms');
const mail = require('./mail');

// INITIALIZATION =========================================
// Oms connection initialization
const omsUrl = process.env.OMS_CONN_URL;
const imsEndpoint = process.env.IMS_CONN_URL;
const omsClient = new GraphQLClient(omsUrl);

// Mutual exclusion for order checking process
let checkOrdersMutex = Promise.resolve();
let checkOrderMutex = Promise.resolve();

// PUBLIC FUNCTIONS =======================================

/**
 * Check orders wrapper to prevent concurrent access to the order synchronization phase
 * @returns mutex promise to await for
 */
async function checkOrders() {
  checkOrdersMutex = checkOrdersMutex
    .then(async () => {
      // the actual synchronization method
      await checkOrdersInternal();
    })
    .catch((error) => {
      console.log(error);
    });
  return checkOrdersMutex;
}

/**
 * Fetch fresh orders from the oms and populate the local versions.
 */
async function checkOrdersInternal() {
  const omsorders = await fetchOrdersRest()
    .catch((error) => {
      throw Error(error);
    });
  console.log(`checking ${omsorders.length} orders`);
  for (const omsorder of omsorders) {
    // checking a single order
    await checkOrderInternal(omsorder)
      .then((check) => {
        console.log(`order ${check.order.name} check success`);
      })
      .catch((error) => {
        console.log(`checking order ${omsorder.name}: ${error}`);
      });
  }
}

/**
 * check order wrapper to avoid concurrent access
 * @param {*} omsorder
 * @returns
 */
async function checkOrder(omsorder, instanceId, storeviewCode, action, activities) {
  checkOrderMutex = checkOrderMutex
    .then(async () => {
      console.log(`checking ${omsorder.name}`);
      // the actual synchronization method
      return await checkOrderInternal(omsorder, instanceId, storeviewCode, action, activities);
    });
  // .catch((error) => {
  //   console.log(error);
  // });
  return checkOrderMutex;
}

/**
 * Check if an OMS order exists locally.
 * If it doesn't exist a new one is created.
 * If it exists, it is updated.
 * @param {Order} order the OMS order data to copy locally
 * @return true if a new order was created
 */
async function checkOrderInternal(omsorder, instanceId, storeviewCode, action, activities) {
  const dborder = await createOrLoadDbOrder(omsorder, instanceId, storeviewCode);

  let isActivityBound = false;
  if (activities) {
    isActivityBound = true;
  }

  if (isActivityBound && activities.cancel) {
    console.log(`[CANCELLATION] Attempting to cancel order ${dborder.name}`);
    if (isActivityBound && !utility.checkActivity(dborder, utility.ACTIVITY_CANCEL)) {
      throw new Error(`There is an error in the ${utility.ACTIVITY_CANCEL} step. Check prerequisites.`);
    }
    // perform cancellation
    await utility.updateActivity(dborder, utility.ACTIVITY_CANCEL, true);
  }

  let toShip = dborder.status !== 'Fulfilled' // If already fullfilled, no need to ship it
    && !dborder.shipmentId // If a shipment is already on route, no need to create a new one
    && !omsorder.closed; // If order is closed no action is needed

  toShip = toShip && (!utility.isActivityDone(dborder, utility.ACTIVITY_CLOSE)) && (!utility.isActivityDone(dborder, utility.ACTIVITY_CANCEL));

  // If order is new, perform the callbacks
  console.log(`does this order need shipment? ${toShip}`);
  if (toShip) {
    console.log(`trying to ship order ${dborder.name}`);
    try {
      dborder.shipmentId = await createShipment(dborder, instanceId, storeviewCode, action, activities);
    } catch (error) {
      console.log(`cannot create shipment for order ${dborder.name}: ${error}`);
      dborder.fulfilled = false;
      await dborder.save();
    }
  }

  if (dborder.shipmentId) {
    dborder.fulfilled = true;
  }

  const registered = dborder.activities && dborder.activities.picking?.done;
  if (!registered) {
    await bookQuantities(dborder);
  }
  // Save data locally
  const savedOrder = await dborder.save();
  // Tell the caller if an order was created or not
  return { isNew: toShip, order: savedOrder, success: true };
}

async function createOrLoadDbOrder(omsorder, instanceId, storeviewCode) {
  console.log('Fase 1, salvataggio ordine su PMS');
  // sanity check
  if (!omsorder) {
    throw new Error('cannot checkOrder: order is null');
  }
  // look in the db for a matching order
  const foundOrder = await orderModel.findOne({ omsgid: omsorder.omsgid, instanceId, storeName: storeviewCode });
  console.log(foundOrder);
  // copy data over to the local version
  const dborder = new orderModel(omsorder);
  // check if order has to be created or updated
  dborder.activities = {};
  if (foundOrder) {
    console.log(`updating existing order ${omsorder.name}`);
    // Update the same entry
    dborder._id = foundOrder._id;
    dborder.shipmentId = foundOrder.shipmentId;
    dborder.partnerId = foundOrder.partnerId;
    dborder.activities = foundOrder.activities;
    dborder.isNew = false; // force update
  }
  console.log(omsorder.storeId, instanceId, storeviewCode);
  // assign the partnerId
  dborder.partnerId = await getChannelManagerId(omsorder.storeId, instanceId, storeviewCode);
  // assign the instanceId
  dborder.instanceId = instanceId;
  dborder.storeName = storeviewCode;
  // sanity check
  if (!dborder.partnerId) {
    console.log('cerco il partner dal manager canale');
    dborder.partnerId = await getPartnerId(dborder);
  }
  // sanity check
  if (!dborder.partnerId) {
    throw new Error(`cannot checkOrder ${dborder.name}: cannot identify order partner`);
  }
  // load the partner id
  dborder.partner = await partnerModel.findById(dborder.partnerId);
  // sanity check
  if (!dborder.partner) {
    throw new Error(`cannot checkOrder ${dborder.name}: no such partner ${dborder.partnerId}`);
  }
  await utility.updateActivity(dborder, utility.ACTIVITY_REGISTER, true);

  // DEBUG
  // Check if the order has to be shipped
  return dborder;
}

/**
 * Process an order booking the quantityt involved into items.
 * @param {Order} order the order data
 * @return true if a new order was created
 */
async function bookQuantities(order) {
  if (!order.fullyBooked) {
    // do not look for the best warehouse, this step will be done on shipment phase only.
    console.log(`order has ${order.items.length} items`);
    for (let i = 0; i < order.items.length; ++i) {
      const item = order.items[i];
      const product = await productModel.findOne({ sku: item.sku });
      if (product) {
        product.bookedQuantity += item.quantity;
        await product.save();
      }
    }
    order.fullyBooked = true;
  }
  return order;
}

async function getOrdersByChannel(channel) {
  const channelOrders = await fetchOrdersByChannelRest(channel);
  return channelOrders;
}

// INTERNAL FUNCTIONS ======================================

/**
 * Create a new shipment for the given order if suitable
 * @param {Order} order the order to be shipped
 * @return the shipment id created or an exception is thrown
 */
async function createShipment(order, instanceId, storeviewCode, action, activities) {
  // Sanity check
  if (!order) {
    throw new Error('cannot create shipment for order: order is null');
  }
  // Verify if a shipment already exists for this order
  if (order.shipmentId) {
    throw new Error(`cannot create shipment: order ${order.name} has already a shipment ${order.shipmentId}`);
  }
  // Check the status to prevent desync
  if (order.status === 'Fulfilled' || order.fulfilled) { // TODO: use enum
    throw new Error(`cannot create shipment: order ${order.name} is already fulfilled`);
  }
  // Create the shipment
  const shipmentId = await sms.checkShipment(order, instanceId, storeviewCode, action, activities);
  console.log(`Shipment ${shipmentId} created for order ${order.name}`);
  return shipmentId;
}

/**
 * Look for a valid partner id among the order items
 *
 * This is based on the assumption that products will always
 * be selected from a single partner.
 * @param {orderModel} order
 * @return the order partnerid, if any was found
 */
async function getPartnerId(order) {
  let i = 0;
  // Iterate over the order items
  // as long as the partner is not found
  for (; i < order.items.length
    && !order.partnerId; ++i) {
    const item = order.items[i];
    // Look in the database for a matching product based on the IMS id
    await productModel.findOne({ 'offers.sku': item.sku })
      .then((product) => {
        // If a product is found, carry the partner over
        if (product) {
          order.partnerId = product.partnerId;
          console.log(`product ${product.sku} partner id is ${order.partnerId}`);
        }
      });
  }
  return order.partnerId;
}

/**
 * Retrieve orders from the OMS Connector
 * @return a list of the orders
 */
async function fetchOrders() {
  // OrderMany query and variables
  // TODO: create appropriate models for this
  const orderManyQuery = gql`
  query {
    orderMany {
      omsgid,
      name,
      customer {
        displayName
      },
      shippingAddress {
        name,
        address1,
        address2,
        city,
        province,
        zip,
        country, 
        phone
      },
      totalPriceSet {
        amount,
        currencyCode
      },
      items {
        imsgid,
        name,
        sku,
        quantity,
        totalPriceSet {
          amount,
          currencyCode
        },
        unitPriceSet {
          amount,
          currencyCode
        }
      },
      fullyPaid,
      createdAt,
      partnerId,
      status
    }
  }`;

  const variables = {
  };

  console.log('contacting OMS for orders...');
  // TODO: implement pagination, something like hasNext? next!
  // Sending the request to the OMS Connector
  const response = await omsClient.request(orderManyQuery, variables);
  // Sanity check
  if (response) {
    // Return the list of orders
    return response.orderMany;
  }
}

async function fetchOrdersRest() {
  const response = await axios.get(`${imsEndpoint}/rest/orders`);
  if (!response.data.success) {
    throw new Error('could not find orders');
  }
  return response.data.data;
}

async function fetchOrdersByChannelRest(channel) {
  try {
    console.log('[FETCH-ORDERS] Getting orders from channel...');
    const response = await axios.get(`${channel.imsAddress}/rest/orders/store/${channel.storeName}`);
    if (!response.data.success) {
      throw new Error('could not find orders');
    }
    return response.data.data;
  } catch (e) {
    return [];
  }
}

async function fetchOrderRest(id, instanceId, storeviewCode) {
  try {
    console.log('[FETCH-ORDER] Getting order from store...');
    const channel = await channelModel.findOne({ instanceId, storeName: storeviewCode });
    console.log(channel);
    const response = await axios.get(`${channel.imsAddress}/rest/orders/${id}`);
    if (!response.data.success) {
      throw new Error('could not find orders');
    }
    return response.data.data;
  } catch (e) {
    console.log(e);
    return [];
  }
}

async function getChannelManagerId(storeId, instanceId, storeviewCode) {
  const channelInstance = await channelModel.findOne({ instanceId, storeName: storeviewCode });
  const response = await axios.get(`${channelInstance.imsAddress}/rest/orders/stores/${storeId}`);
  if (!response.data.success) {
    throw new Error('could not find orders');
  }
  const channel = await channelModel.findOne({ parentStore: response.data.data.code }).exec();
  if (channel) {
    return channel.managerId;
  }
  return null;
}

async function sendRecapEmail(orderList, instanceId, storeviewCode) {
  const orderTableBySeller = {};

  for (let i = 0; i < orderList.length; i++) {
    console.log(`order to be evaluated ${orderList[i]}`);
    const omsorder = await fetchOrderRest(orderList[i], instanceId, storeviewCode).catch((error) => {
      // send message to error queue
      console.log(error);
      console.log('ORDINE NON TROVATO');
    });
    if (!omsorder) {
      // order not valid, continue
      console.log('Ordine specificato non trovato');
      continue;
    }
    const dborder = await createOrLoadDbOrder(omsorder[0], instanceId, storeviewCode);
    // now I should have the order
    for (let j = 0; j < dborder.items.length; j++) {
      const item = dborder.items[j];
      const product = await productModel.findOne({ 'offers.sku': item.sku });
      if (product) {
        if (!orderTableBySeller[product.partnerId]) {
          orderTableBySeller[product.partnerId] = {};
        }
        if (!orderTableBySeller[product.partnerId][dborder.omsgid]) {
          orderTableBySeller[product.partnerId][dborder.omsgid] = {};
        }

        if (!orderTableBySeller[product.partnerId][dborder.omsgid][item.sku]) {
          orderTableBySeller[product.partnerId][dborder.omsgid][item.sku] = { quantity: item.quantity };
        } else {
          orderTableBySeller[product.partnerId][dborder.omsgid][item.sku].quantity += item.quantity;
        }
      }
    }
  }

  // send email to each partner

  await mail.sendMailToPartners(orderTableBySeller);
  console.log(JSON.stringify(orderTableBySeller));
  if (!orderTableBySeller) {
    return false;
  }
  return true;
}

// Exporting public functions ------------------------------
module.exports = {
  checkOrders,
  checkOrder,
  getOrdersByChannel,
  fetchOrderRest,
  fetchOrdersRest,
  fetchOrdersByChannelRest,
  bookQuantities,
  getChannelManagerId,
  sendRecapEmail,
};