| Current Path : /proc/thread-self/root/home/deltalab/PMS/partner-manager-backend/services/ |
| 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,
};