Your IP : 216.73.216.220


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/sms.js

/* eslint-disable no-param-reassign */
/* eslint-disable prefer-destructuring */
/* eslint-disable no-continue */
/* eslint-disable new-cap */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-use-before-define */
/* eslint-disable max-len */
/* eslint-disable no-restricted-syntax */
/**
 * Shipment Management Service
 *
 * Fetches shipments from SMS Connector
 * and create/update the local copies to be served by mongoose.
 * See models/mongoose/shipment.js for PMS-FE queries.
 */

// DEPENDENCIES ===========================================
const { dotenv } = require('dotenv').config();
const { GraphQLClient, gql } = require('graphql-request');
const ims = require('./ims');
const { countryCodes } = require('./utils/countrycodes');
const mail = require('./mail');
const utility = require('./utility');

// RESOURCES ==============================================
const { createShipmentInfo } = require('../models/mongoose/shipment');
const { createShipmentValueInfo } = require('../models/mongoose/shipment');
const { shipmentModel } = require('../models/mongoose/shipment');
const { warehouseModel } = require('../models/mongoose/warehouse');
const { channelModel } = require('../models/mongoose/channel');
const { addressModel } = require('../models/mongoose/address');
const { bookPickupModel } = require('../models/mongoose/pickup');
const { shipmentBaseInfoModel } = require('../models/mongoose/shipment');
const { parcelBaseModel } = require('../models/mongoose/parcel');
const { parcelModel } = require('../models/mongoose/parcel');
const { boxModel } = require('../models/mongoose/parcel');
const { productModel } = require('../models/mongoose/product');
const { carrierModel } = require('../models/mongoose/carrier');
const { warehouseJournalModel } = require('../models/mongoose/warehouse-journal');

// INITIALIZATION =========================================
// SMS connection initialization
const smsUrl = process.env.SMS_CONN_URL;
const smsClient = new GraphQLClient(smsUrl);

// UTILITIES =============================================

async function isRefrigerated(order) {
  for (const item of order.items) {
    const product = await productModel.findOne({ sku: item.sku });
    if (product) {
      console.log(`product found, is it refrigerated?  ${product.refrigerated}`);
    }
    if (product && product.refrigerated) {
      return true;
    }
  }
  return false;
}

function isLocalOrder(order) {
  console.log(`Da dove proviene questo ordine? ${order.shippingAddress.province}`);
  return order.shippingAddress.province === 'Trento' || order.shippingAddress.province === 'TN';
}

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

/**
 * Start the order shipping process
 *
 * @param {OrderSchema} order what to dispatch and to where
 * @param {Address} fromaddress the warehouse address where the order will be shipped from
 * @return the backend shipment id to be used for future references
 */
async function checkShipment(order, instanceId, storeviewCode, action, activities) {
  // check if activities are specified
  let isActivityBound = false;
  if (activities) {
    isActivityBound = true;
  }

  console.log(`Checking shipment for order ${order.name}`);

  // 1) select shipment warehouse by channel
  console.log('Fase 2: Selezione magazzino');
  const warehouses = await getChannelShipmentWarehouses(order, instanceId, storeviewCode);
  if (!warehouses) {
    throw new Error(`Cannot ship ${order.name}: no shipment warehouses found for the involved channel ${instanceId} ${storeviewCode}`);
  }

  // 2) check if it already contains all items (otherwise create transfers)
  const checkedWarehouses = await checkOrderAvailability(order, warehouses, instanceId, storeviewCode, activities);
  if (checkedWarehouses.okWarehouses.length === 0) {
    // TODO: choose the shipment warehouse by relevant criteria
    console.log('creo i trasferimenti per gli ordini');
    if (!order.shipmentWarehouse) {
      order.shipmentWarehouse = checkedWarehouses.invalidWarehouses[0];
      await utility.updateActivity(order, utility.ACTIVITY_WAREHOUSE, true);
    }
    await order.save();

    console.log('Fase 3: Impegno quantità');
    console.log('Fase 4: Creazione trasferimenti');
    await createWarehouseBookingsAndTransfers(order, order.shipmentWarehouse ? order.shipmentWarehouse : checkedWarehouses.invalidWarehouses[0], instanceId, storeviewCode, action, activities);
    throw new Error(`cannot create shipment for order ${order.name}: no warehouse contains all the items required. Created transfers.`);
  }

  // if order.shipmentWarehouse is specified, this order has already been processed and a suitable warehouse was already specified
  const bestWarehouseAddress = await getBestWarehouseAddress(order, order.shipmentWarehouse ? [order.shipmentWarehouse] : checkedWarehouses.okWarehouses);
  if (!bestWarehouseAddress.from) {
    throw new Error(`Cannot ship ${order.name}: no valid warehouse address found`);
  }
  if (!order.shipmentWarehouse) {
    order.shipmentWarehouse = checkedWarehouses.invalidWarehouses[0];
    await utility.updateActivity(order, utility.ACTIVITY_WAREHOUSE, true);
    await order.save();
  }
  await utility.updateActivity(order, utility.ACTIVITY_WAREHOUSE, true);


  //TODO: controllare funzionamento per evitare doppio trasferimento
  if ((!isActivityBound && action === utility.ACTION_SURF) || (isActivityBound && action === utility.ACTION_SURF && activities.booking)) {
    if (isActivityBound && !utility.checkActivity(order, utility.ACTIVITY_BOOKING)) {
      throw new Error(`There is an error in the ${utility.ACTIVITY_BOOKING} step. Check prerequisites.`);
    }
    // perform booking, now it's needed for each order (even though I could have to ship it immediately)
    await createWarehouseBookingsAndTransfers(order, bestWarehouseAddress.warehouse, instanceId, storeviewCode, action, activities);
    await utility.updateActivity(order, utility.ACTIVITY_BOOKING, true);
  }

  console.log(`shipping origin address is ${bestWarehouseAddress.from}`);

  let parcels = null;
  if (!isActivityBound || (isActivityBound && activities.pickingList)) {
    console.log('Fase 5: Creazione picking list');
    if (isActivityBound && !utility.checkActivity(order, utility.ACTIVITY_PICKING_LIST)) {
      throw new Error(`There is an error in the ${utility.ACTIVITY_PICKING_LIST} step. Check prerequisites.`);
    }
    parcels = await createParcels(order);
    await utility.updateActivity(order, utility.ACTIVITY_PICKING_LIST, true);
  }

  // Explicitly create the shipment
  // const orderId = order.name.substring(1); // CUT # FOR SHOPIFY ORDER
  let shipmentCreationData = null;
  if (!isActivityBound || (isActivityBound && activities.shipmentShippyPro)) {
    if (isActivityBound && !utility.checkActivity(order, utility.ACTIVITY_SHIPMENT_SHIPPYPRO)) {
      throw new Error(`There is an error in the ${utility.ACTIVITY_SHIPMENT_SHIPPYPRO} step. Check prerequisites.`);
    }
    // Prepare the shipment basic info using the order as input
    if (!parcels) {
      //if parcels is not created (i.e. picking list was executed in the past)
      parcels = await createParcels(order);
    }
    const shipmentBaseInfo = new shipmentBaseInfoModel();
    shipmentBaseInfo.from = bestWarehouseAddress.from;
    shipmentBaseInfo.from.email = bestWarehouseAddress.warehouse.email[0]; // AG1263
    shipmentBaseInfo.to = getShippingAddress(order);
    shipmentBaseInfo.parcels = parcels;
    shipmentBaseInfo.shipmentValueInfo = createShipmentValueInfo(
      order.totalPriceSet.amount,
      order.totalPriceSet.currencyCode,
      order.cashOnDelivery,
    );
    // Retrieve the available rates
    const rates = await fetchRates(shipmentBaseInfo);
    const refrigeratedOrder = await isRefrigerated(order);
    const localOrder = isLocalOrder(order);
    const carrierIds = await getCarrierIds(order.partnerId, refrigeratedOrder, localOrder);
    const rate = getBestRate(rates, carrierIds);
    if (!rate) {
      console.log(`no rate found for order ${order.name}`);
      throw new Error(`cannot create shipment for order ${order.name}: no valid rate found`);
    }

    console.log('Fase 6: creazione lettera di vettura');
    const orderId = order.name;
    shipmentCreationData = await createShipment(orderId, rate.carrier.smsid, rate.rateId, shipmentBaseInfo);
    await utility.updateActivity(order, utility.ACTIVITY_SHIPMENT_SHIPPYPRO, true);

    console.log(`[SHIPMENT-SHIPPYPRO] shipment created for order ${order.name}`);
    console.log(`[SHIPMENT-SHIPPYPRO] shipment creation data ${JSON.stringify(shipmentCreationData)}`);
  }

  if (isActivityBound && activities.shipmentSurf) {
    // surf is not used in default behaviour
    if (isActivityBound && !utility.checkActivity(order, utility.ACTIVITY_SHIPMENT_SURF)) {
      throw new Error(`There is an error in the ${utility.ACTIVITY_SHIPMENT_SURF} step. Check prerequisites.`);
    }
    console.log('Fase 6: creazione lettera di vettura SURF');
    await utility.updateActivity(order, utility.ACTIVITY_SHIPMENT_SURF, true);

    console.log(`[SHIPMENT-SURF] shipment created for order ${order.name}`);
    console.log(`[SHIPMENT-SURF] shipment creation data ${JSON.stringify(shipmentCreationData)}`);
  }
  // DEBUG -----------------------

  // Book a pickup for this shipment
  const pickupDate = new Date(); // Today
  pickupDate.setDate(getPickupDate(pickupDate).getDate()); // Next working day

  // console.log(pickupDate);
  // // forcing pickup date to today
  // const customPickupDate = new Date();
  // customPickupDate.setHours(15);
  // customPickupDate.setMinutes(30);
  // console.log(customPickupDate);
  // const pickupConfirmation = await sms.bookPickup(customRate.carrier.smsid, shipmentBaseInfo, customPickupDate);

  if (!isActivityBound || (isActivityBound && activities.mail)) {
    if (isActivityBound && !utility.checkActivity(order, utility.ACTIVITY_MAIL)) {
      throw new Error(`There is an error in the ${utility.ACTIVITY_MAIL} step. Check prerequisites.`);
    }
    console.log('Fase 7: invio mail');
    console.log(`sending mail to ${bestWarehouseAddress.warehouse.email}`);

    if (!parcels) {
      //if parcels is not created (i.e. picking list was executed in the past)
      parcels = await createParcels(order);
    }
    // use warehouseAddress.from
    const emails = bestWarehouseAddress.warehouse.email;
    let emailAddresses = '';
    emails.forEach((email) => {
      emailAddresses += email;
      emailAddresses += ';';
    });
    mail.sendPickingEmail(emailAddresses, order, parcels, shipmentCreationData ? shipmentCreationData.labelUrl : [], pickupDate);
    await utility.updateActivity(order, utility.ACTIVITY_MAIL, true);
  }
  let shipment = null;
  if (!isActivityBound || (isActivityBound && activities.picking)) {
    if (isActivityBound && !utility.checkActivity(order, utility.ACTIVITY_PICKING)) {
      throw new Error(`There is an error in the ${utility.ACTIVITY_PICKING} step. Check prerequisites.`);
    }
    shipment = new shipmentModel();
    // const orderIds = shipmentCreationData.shipmentId; // not managing multiple orders in one shipment at the moment, otherwise format is XXX;YYY;ZZZ;
    // once order is confirmed, subtract order quantities from bookedQuantity and InventoryLevel
    console.log('Fase 8: pick quantità');
    await pickQuantities(order, bestWarehouseAddress);

    if (shipmentCreationData) {
      // transform the shipment id number to a string
      const shipmentId = shipmentCreationData.shipmentId.toString();
      // Create the shipping manifest
      await createManifest(shipmentId);
      // DEBUG -----------------------
      console.log(`manifest created for shipment ${order.name}`);
      // Persist the shipment locally
      shipment.smsid = shipmentCreationData.shipmentId;
      shipment.labelUrl = shipmentCreationData.labelUrl;
      shipment.orderId = order._id;
      shipment.trackingCarrier = shipmentCreationData.trackingCarrier;
      shipment.trackingNumber = shipmentCreationData.trackingNumber;
      // shipment.pickupId = pickupId; //not provided yet, since pickup is not created from PMS
      shipment.partnerId = order.partnerId;
      shipment.createdAt = new Date();
      await shipment.save((err) => {
        if (err) {
          throw new Error(`Cannot save shipment for order ${order.name}: ${err}`);
        }
      });
      console.log(`shipment ${shipment._id} created successfully for order ${order.name}`);
    }

    console.log('saving all products involved');

    await utility.updateActivity(order, utility.ACTIVITY_PICKING, true);
  }

  await saveReferences(order);
  if (!isActivityBound || (isActivityBound && activities.close)) {
    if (isActivityBound && !utility.checkActivity(order, utility.ACTIVITY_CLOSE)) {
      throw new Error(`There is an error in the ${utility.ACTIVITY_CLOSE} step. Check prerequisites.`);
    }
    await utility.updateActivity(order, utility.ACTIVITY_CLOSE, true);
    return shipment._id;
  }
}

// EXPORTS =================================================
module.exports = {
  checkShipment,
  getBestWarehouseAddress,
  findBestFittingBox,
  sortBoxes,
  sortItems,
  estimateParcels,
  createParcels,
  fetchRates,
  getBestRate,
  getShippingAddress,
  createShipmentValueInfo,
  createShipment,
  bookPickup,
  getCarrierIds,
  getPickupDate,
  pickQuantities,
};

// PRIVATE FUNCTIONS =======================================

/**
 * Find the best warehouse for the order shipment
 *
 * @param {*} order the order to ship
 * @param {*} warehouses a list of valid warehouses
 */
async function getBestWarehouseAddress(order, warehouses) {
  const bestRates = [];
  // Prepare the shipment basic info used to fetch rates
  const shipmentBaseInfo = new shipmentBaseInfoModel();
  shipmentBaseInfo.to = getShippingAddress(order);
  shipmentBaseInfo.parcels = await createParcels(order);
  shipmentBaseInfo.shipmentValueInfo = createShipmentValueInfo(
    order.totalPriceSet.amount,
    order.totalPriceSet.currencyCode,
    order.cashOnDelivery,
  );
  const refrigeratedOrder = await isRefrigerated(order);
  const localOrder = isLocalOrder(order);
  const carrierIds = await getCarrierIds(order.partnerId, refrigeratedOrder, localOrder);

  console.log(carrierIds);
  // Compute the rate for all the warehouses
  for (const warehouse of warehouses) {
    // Update the shipment origin
    shipmentBaseInfo.from = getShipmentWarehouseAddress(warehouse.address);
    // Retrieve the available rates for the shipment
    const rates = await fetchRates(shipmentBaseInfo);
    // Get the best rate out of the given list of available carriers
    const rate = getBestRate(rates, carrierIds);
    if (!rate) {
      console.log(`no rate found for order ${order.name} shipping from ${warehouse.address}`);
    } else {
      // Add original warehouse to the rate
      rate.warehouse = warehouse;
      // Add the warehouse address
      rate.from = shipmentBaseInfo.from;
      bestRates.push(rate);
    }
  }
  // Get the best rate among all the best rates
  const bestRate = getBestRate(bestRates, carrierIds);

  // Sanity check
  if (!bestRate) return null;

  if (!order.shipmentWarehouse) {
    // save bestRate warehouse in order if not already
    order.shipmentWarehouse = bestRate.warehouses;
    await order.save();
  }
  // Return the associated warehouse address
  return bestRate;
}

/**
 * Retrieve the matching product for the given order item
 * @param {orderItemModel} orderItem
 * @return the matching productModel or null if no product matches the given item
 */
async function getValidProduct(orderItem, partnerId) {
  let product;
  if (orderItem.sku) {
    // Look for product based on item imsgid and order partnerId
    product = await productModel.findOne({ 'offers.sku': orderItem.sku }); // controllare se il prodotto va bene
  } else {
    // Look for product based on item sku, name and order partnerId
    product = await productModel.findOne({ 'offers.sku': orderItem.sku, name: orderItem.name }); // controllare se il prodotto va bene
  }
  // Sanity check
  if (!product) {
    console.log(`no such product for sku ${orderItem.sku} (partner ${partnerId})`);
    return null;
  }
  // Do not look for inventory levels for non-phisical products, if we are shipping the product
  // Return product data in availability calculations
  if (!product.requiresShipping) {
    console.log(`product ${product.sku} doesn't require shipping`);
    return null;
  }
  return product;
}

async function getChannelShipmentWarehouses(order, instanceId, storeviewCode) {
  console.log('cerco se ci sono magazzini adatti per la spedizione');
  const orderChannel = await channelModel.findOne({ instanceId, storeName: storeviewCode });
  if (orderChannel) {
    const warehouses = await warehouseModel.find({ channelAssignments: { $elemMatch: { channelId: orderChannel._id, shipmentWarehouse: true } } });
    if (warehouses) {
      console.log(`ce ne sono ${warehouses.length}`);
      return warehouses;
    }
  }
  return [];
}

async function checkOrderAvailability(order, warehouses, instanceId, storeviewCode, activities) {
  // for each warehouse, check if one has all the required items
  // if not, loop through all the relevant warehouses of the product and create transfer orders to the warehouse for the difference (if any);
  const okWarehouses = [];
  const invalidWarehouses = [];
  for (const warehouse of warehouses) {
    let validOrder = true;
    for (const orderItem of order.items) {
      const reference = await getValidProduct(orderItem, order.partnerId);
      for (const offer of reference.offers) {
        const orderChannel = await channelModel.findOne({ instanceId, storeName: storeviewCode });
        if (!offer.channelId.equals(orderChannel._id)) {
          console.log('non è una offerta di questo canale');
          continue;
        }

        console.log(`extracting ${orderItem.name}`);
        console.log(`is product null?  ${reference === null}`);
        // In case no such product has to be shipped, skip this item
        if (!reference) {
          console.log('Il prodotto non esiste');
          continue;
        }

        // same thing if it's not to be shipped
        if (!reference.requiresShipping) {
          console.log('Il prodotto non deve essere spedito');
          continue;
        }

        // same thing if it's not tracked in the inventory
        if (!reference.trackInventory) {
          console.log('Il prodotto non traccia la giacenza');
          continue;
        }

        // Check the product inventory levels
        // TODO: search for quantity in the warehouse;
        console.log('Verifico se il prodotto è presente in un magazzino nella quantità richiesta');
        let warehouseFound = false;
        for (const inventoryLevel of reference.inventoryLevels) {
          if (inventoryLevel.warehouseId.equals(warehouse._id)) {
            console.log(`il magazzino ${warehouse.name} contiene il prodotto`);
            warehouseFound = true;
            let bookedQuantity = 0;
            for (let i = 0; i < inventoryLevel.bookings.length; i++) {
              const booking = inventoryLevel.bookings[i];
              if (booking.omsgid === order.omsgid && !booking.fulfilled) {
                bookedQuantity += booking.amount;
              }
            }

            const remainingQuantity = orderItem.quantity - bookedQuantity;
            if (inventoryLevel.amount < remainingQuantity && !reference.sellBelowZero) {
              console.log('...ma non in quantità sufficiente');
              // if warehouse does not contain enough stock, it's not eligible for shipping at the moment
              validOrder = false;
              break;
            }
          }
        }
        if (!warehouseFound) {
          console.log(`il magazzino ${warehouse.name} non contiene il prodotto`);
          // an item does not have stock in eligible warehouses, the whole order must fail for now
          validOrder = false;
        }
      }
    }
    if (validOrder) {
      console.log('Il magazzino ha stock disponibile');
      okWarehouses.push(warehouse);
    } else {
      console.log('Il magazzino è nella lista degli indisponibili');
      invalidWarehouses.push(warehouse);
    }
  }
  return { okWarehouses, invalidWarehouses };
}

async function createWarehouseBookingsAndTransfers(order, warehouse, instanceId, storeviewCode, action, activities) {
  let isActivityBound = false;
  if (activities) {
    isActivityBound = true;
  }

  const requestBody = {
    omsgid: order.omsgid,
    confirmed: false,
  };

  const orderPendingTransfers = await warehouseJournalModel.find(requestBody);
  if (orderPendingTransfers.length > 0) {
    console.log('there are some transfers yet to be confirmed. Check them first');
    return;
  }

  for (const orderItem of order.items) {
    const reference = await getValidProduct(orderItem, order.partnerId);
    // Check the product inventory levels
    let requiredQuantity = orderItem.quantity;
    console.log(`starting loop with ${requiredQuantity} pieces`);
    for (const offer of reference.offers) {
      const orderChannel = await channelModel.findOne({ instanceId, storeName: storeviewCode });
      console.log(`extracting ${orderItem.name}`);

      if (requiredQuantity <= 0) {
        console.log('all available quantity picked, exiting');
        break;
      }

      if (!reference) {
        console.log('no reference');
        continue;
      }

      // same thing if it's not to be shipped
      if (!reference.requiresShipping) {
        console.log('no shipping needed');
        continue;
      }

      // same thing if it's not tracked in the inventory
      if (!reference.trackInventory) {
        console.log('no tracked inventory');
        continue;
      }

      const offerQuantity = await getOfferQuantity(reference, offer, order.omsgid);
      if (offerQuantity < orderItem.quantity && !reference.sellBelowZero) {
        console.log('the entire stock of this product is not enough to fulfill the order');
        // if warehouse does not contain enough stock, it's not eligible for shipping at the moment
        break;
      }

      // first loop through inventoryLevels in order to find the quantity already in the warehouse
      for (const inventoryLevel of reference.inventoryLevels) {
        console.log(`looking for ${requiredQuantity} pieces in warehouses`);
        if (requiredQuantity <= 0) {
          break;
        }

        if (!reference.sellBelowZero && inventoryLevel.amount <= 0) {
          // no stock, no transfer
          console.log('zero stock, no available transfers');
          continue;
        }

        if (inventoryLevel.warehouseId.equals(warehouse._id) && inventoryLevel.amount > 0) {
          console.log(`same warehouse, no need to transfer ${inventoryLevel.amount} quantity since it's already there`);
          if (!inventoryLevel.bookings) {
            inventoryLevel.bookings = [];
          }
          if (inventoryLevel.amount >= requiredQuantity) {
            inventoryLevel.bookings.push({ amount: requiredQuantity, omsgid: order.omsgid, channelId: orderChannel._id });
            inventoryLevel.amount -= requiredQuantity;
            await utility.updateActivity(order, utility.ACTIVITY_BOOKING, true);
            requiredQuantity = 0;
          } else {
            inventoryLevel.bookings.push({ amount: inventoryLevel.amount, omsgid: order.omsgid, channelId: orderChannel._id });
            requiredQuantity -= inventoryLevel.amount;
            inventoryLevel.amount = 0;
          }
          await reference.save();
          await ims.referenceUpdate(reference, reference.sku);
          break;
        }
      }

      for (const inventoryLevel of reference.inventoryLevels) {
        if (requiredQuantity <= 0) {
          break;
        }

        if (!reference.sellBelowZero && inventoryLevel.amount <= 0) {
          // no stock, no transfer
          console.log('zero stock, no available transfers');
          continue;
        }

        // check if warehouse is eligible on channel
        if (!isChannelWarehouse(inventoryLevel, orderChannel)) {
          continue;
        }

        console.log(inventoryLevel.warehouseId);
        if (inventoryLevel.warehouseId.equals(warehouse._id)) {
          console.log('same warehouse');
          // don't consider the same warehouse in the loop
          continue;
        }

        const externalWarehouse = reference.partnerId !== warehouse.partnerId.toString();
        console.log('there is enough stock to fulfill this order');
        const warehouseFrom = await warehouseModel.findById(inventoryLevel.warehouseId);

        if (inventoryLevel.amount >= requiredQuantity || reference.sellBelowZero) {
          console.log('################################################## LEVEL ENOUGH #############################################################');
          console.log('... and there is enough in this warehouse');
          inventoryLevel.amount -= requiredQuantity;
          ims.addEntryToWarehouseJournal(inventoryLevel.warehouseId, reference._id, reference.partnerId, requiredQuantity, requiredQuantity, 'DECREASE', reference.refrigerated, !externalWarehouse, false, order.omsgid);
          // TODO: ACTIVITY (journal non necessita di conferma se activity.transfer == FALSE) AND ADD BOOKING nel magazzino di destinazione
          if (isActivityBound && action === utility.ACTION_SURF) {
            if (isActivityBound && !utility.checkActivity(order, utility.ACTIVITY_BOOKING)) {
              throw new Error(`There is an error in the ${utility.ACTIVITY_BOOKING} step. Check prerequisites.`);
            }
            console.log('No transfer needed, just confirm them right away');
            ims.addEntryToWarehouseJournal(warehouse, reference._id, reference.partnerId, requiredQuantity, requiredQuantity, 'DUMMY', reference.refrigerated, !externalWarehouse, true, order.omsgid, inventoryLevel.warehouseId);
            ims.createReferenceBooking(reference._id, warehouse._id, requiredQuantity, order.omsgid);
            await utility.updateActivity(order, utility.ACTIVITY_BOOKING, true);
          } else {
            console.log('Normal behaviour, insert a transfer to be confirmed later');
            ims.addEntryToWarehouseJournal(warehouse, reference._id, reference.partnerId, requiredQuantity, requiredQuantity, 'INCREASE', reference.refrigerated, !externalWarehouse, true, order.omsgid, inventoryLevel.warehouseId);
            mail.sendWarehouseTransferEmail(warehouseFrom, warehouse, offer, requiredQuantity);
            await utility.updateActivity(order, utility.ACTIVITY_TRANSFER, true);
          }
          await reference.save();
          requiredQuantity = 0;
          await ims.referenceUpdate(reference, reference.sku);
          break;
        } else {
          console.log('################################################## LEVEL NOT ENOUGH #############################################################');
          console.log('...and there is some here');
          requiredQuantity -= inventoryLevel.amount;
          mail.sendWarehouseTransferEmail(warehouseFrom, warehouse, offer, inventoryLevel.amount);
          console.log(`${requiredQuantity} remaining`);
          ims.addEntryToWarehouseJournal(inventoryLevel.warehouseId, reference._id, reference.partnerId, inventoryLevel.amount, 0, 'DECREASE', reference.refrigerated, !externalWarehouse, false, order.omsgid);
          // ACTIVITY (journal non necessita di conferma se activity.transfer == FALSE) AND ADD BOOKING nel magazzino di destinazione
          if (isActivityBound && action === utility.ACTION_SURF) {
            if (isActivityBound && !utility.checkActivity(order, utility.ACTIVITY_BOOKING)) {
              throw new Error(`There is an error in the ${utility.ACTIVITY_BOOKING} step. Check prerequisites.`);
            }
            console.log('No transfer needed, just confirm them right away');
            ims.addEntryToWarehouseJournal(warehouse, reference._id, reference.partnerId, inventoryLevel.amount, inventoryLevel.amount, 'DUMMY', reference.refrigerated, !externalWarehouse, true, order.omsgid, inventoryLevel.warehouseId);
            ims.createReferenceBooking(reference._id, warehouse._id, inventoryLevel.amount, order.omsgid);
            await utility.updateActivity(order, utility.ACTIVITY_BOOKING, true);
          } else {
            console.log('Normal behaviour, insert a transfer to be confirmed later');
            ims.addEntryToWarehouseJournal(warehouse, reference._id, reference.partnerId, inventoryLevel.amount, inventoryLevel.amount, 'INCREASE', reference.refrigerated, !externalWarehouse, true, order.omsgid, inventoryLevel.warehouseId);
            await utility.updateActivity(order, utility.ACTIVITY_BOOKING, true);
            await utility.updateActivity(order, utility.ACTIVITY_TRANSFER, true);
          }

          inventoryLevel.amount = 0;
          // TODO: Manca la mail per la quantità corretta
          await reference.save();
          await ims.referenceUpdate(reference, reference.sku);
        }

        // split stock
      }
    }
  }
}

async function isChannelWarehouse(inventoryLevel, orderChannel) {
  const inventoryWarehouse = await warehouseModel.findById(inventoryLevel.warehouseId);
  for (const channelAssignment of inventoryWarehouse.channelAssignments) {
    if (channelAssignment.channelId.equals(orderChannel)) {
      return true;
    }
  }
  return false;
}

async function getOfferQuantity(orderItem, offer, omsgid = undefined) {
  const channel = await channelModel.findById(offer.channelId);
  return ims.getQuantityByChannel(orderItem, channel, omsgid);
}

async function isValidWarehouse(inventoryLevel, instanceId) {
  const warehouse = await warehouseModel.findById(inventoryLevel.warehouseId);
  const channel = await channelModel.findOne({ instanceId });
  if (warehouse) {
    for (let j = 0; j < warehouse.channelAssignments.length; j++) {
      if (warehouse.channelAssignments[j].channelId.equals(channel._id)) {
        return true;
      }
    }
  }
  return false;
}

/**
 * Adapt the warehouse address to a shipping valid address format
 * @param {*} warehouseAddress
 */
function getShipmentWarehouseAddress(warehouseAddress) {
  const smsAddress = new addressModel();
  smsAddress.name = warehouseAddress.name;
  smsAddress.company = warehouseAddress.company;
  smsAddress.street1 = warehouseAddress.street1;
  smsAddress.street2 = warehouseAddress.street2;
  smsAddress.city = warehouseAddress.city;
  smsAddress.zip = warehouseAddress.zip;
  smsAddress.state = warehouseAddress.country;
  smsAddress.country = warehouseAddress.country.length === 2 ? warehouseAddress.country : countryCodes[warehouseAddress.country];
  smsAddress.phone = warehouseAddress.phone;
  smsAddress.email = warehouseAddress.email;
  return smsAddress;
}

/**
 * Extract a suitable shipping address for the given order
 * @param {orderModel} order
 * @returns the shipping address for this order properly configured
 */
function getShippingAddress(order) {
  console.log(`shipping address for order ${order.name} is ${order.shippingAddress.name}`);
  const smsAddress = new addressModel();
  smsAddress.name = order.shippingAddress.name;
  smsAddress.company = order.shippingAddress.company ? order.shippingAddress.company : order.shippingAddress.name;
  smsAddress.street1 = order.shippingAddress.address1;
  smsAddress.street2 = order.shippingAddress.address2;
  smsAddress.city = order.shippingAddress.city;
  smsAddress.zip = order.shippingAddress.zip;
  smsAddress.state = order.shippingAddress.province ?? order.shippingAddress.country;
  console.log(`STATE IS ${smsAddress.state}`);
  smsAddress.phone = order.shippingAddress.phone;

  smsAddress.country = order.shippingAddress.country.length === 2 ? order.shippingAddress.country : countryCodes[order.shippingAddress.country]; // country to ISO
  if (order.customer) {
    // smsAddress.phone = order.customer.phone;
    smsAddress.email = order.customer.email;
  } else {
    // smsAddress.phone = "";
    smsAddress.email = '';
  }
  // TODO: ensure phone is not empty as DHL doesnt like it
  return smsAddress;
}

/**
 * Creates a boxModel with the given parameters
 * @param {*} name
 * @param {*} height
 * @param {*} length
 * @param {*} width
 * @param {*} maxWeight grams
 * @param {*} weight grams of the box itself
 * @returns a boxModel
 */
function prepareBox(name, height, length, width, maxWeight, weight) {
  const box = new boxModel();
  box.name = name;
  box.height = height;
  box.length = length;
  box.width = width;
  box.maxWeight = maxWeight; // grams
  box.weight = weight; // box intrinsic weight
  return box;
}

/**
 * Create the parcels for the given order.
 *
 * Parcels are loosely based on the concept of order item
 * summarizing size, weight of the objects to be shipped
 *
 * @param {Order} order
 * @return {[Parcel]} parcels
 */
async function createParcels(order) {
  const boxes = [];
  // FIXME: hardcoded boxes as instructed
  boxes.push(prepareBox('monosized box', 800, 700, 500, 12000, 150));
  // retrieve the items to be shipped
  const items = await getIndividualItems(order);
  // perform a best estimate the number and type of parcels to prepare for the order items
  const estimatedParcels = estimateParcels(items, boxes);
  // upcast the parcel objects to their base model to fit in the expected shipment model
  console.log(`estimated ${estimatedParcels.length} parcels for order ${order.name}`);
  const parcels = [];
  let i = 0;
  for (const estimatedParcel of estimatedParcels) {
    ++i;
    console.log(`${i}) parcel ${estimatedParcel.box.name} ${estimatedParcel.box.maxWeight}g with ${estimatedParcel.items.length} items for a total of ${estimatedParcel.weight}g`);
    // upcast to the base version, compatible with shipmentinfo
    const parcel = new parcelBaseModel(estimatedParcel);
    parcels.push(parcel);
  }
  return parcels;
}

/**
 * See if the given item can fit in any of the specified parcels (First Fit algorithm)
 * @param {[parcelModel]} parcels
 * @param {orderItemModel} item
 * @return if item has been added to an existing parcel or not
 */
function fitInExistingParcels(parcels, item) {
  // prevent items with bad weight to fit into boxes
  if (!item.weight || item.weight <= 0) return false;
  const parcelsLen = parcels.length;
  for (let i = 0; i < parcelsLen; ++i) {
    const parcel = parcels[i];
    if (parcel.weight + item.weight < parcel.box.maxWeight) {
      parcels[i].addItem(item);
      return true;
    }
  }
  return false;
}

/**
 * Sort a list of boxModel based on their maxWeight ascending, the smaller first
 * This is required to optimize the greedy parcel algorithm
 * @param {[boxModel]} boxes the boxes to sort
 * @return boxes array sorted by maxWeight
 */
function sortBoxes(boxes) {
  return boxes.sort((first, second) => first.maxWeight - second.maxWeight);
}

/**
 * Sort the given list of items based on their weight, the heavier first
 * This is required to optimize the greedy parcel algorithm
 * @param {[orderItemModel]} items the items to sort
 * @param {string} mode either DESC or ASC, for descending or ascending. DESC is by default, if anything else is specified ASC is assumed
 * @returns sorted items be weight
 */
function sortItems(items, mode = 'DESC') {
  return items.sort((first, second) => (mode === 'DESC'
    ? second.weight - first.weight
    : first.weight - second.weight));
}

/**
 * Find the box with the minimum maxWeight bigger than weight
 * This is used to see in which box to put items
 * @param {[boxModel]} boxes a list of available boxes sorted by maxWeight ascending
 * @param {number} weight the weight to look the box for
 * @return the matching boxModel or null if none could be found
 */
function findBestFittingBox(boxes, weight) {
  // sanity check
  if (!weight || weight <= 0) return null;
  // loop for all the available box sizes
  for (const box of boxes) {
    if (box.maxWeight > weight) {
      return box;
    }
  }
  return null;
}

/**
 * Extract the items from the given order,
 * populate all the required shipping details from the matching products
 * and explode it to the actual number of individual items to be shipped.
 * @param {orderModel} order
 * @returns list of individually shippable items
 */
async function getIndividualItems(order) {
  // order items do have a quantity (1 to a lot)
  // but the algorithm will refer to items invidually
  // so we need to split the orderItems into individual items
  const items = [];
  for (const orderItem of order.items) {
    // There are some properties needed in the matching product
    const product = await getValidProduct(orderItem, order.partnerId, true);
    if (!product) continue; // this items will not be shipped
    // Import product data into order item (shopify doesnt specify item weight in line items)
    orderItem.weight = product.weight;
    // Import product details in order item for future reference
    console.log(`setting product properties in item ${orderItem.name}: weight ${product.weight}`);
    for (let i = 0; i < orderItem.quantity; ++i) {
      // there is no memory allocation here, just filling in an array with references
      items.push(orderItem);
    }
  }
  return items;
}

/**
 * Applies a cool algorithm to estimate the optimal set of boxes to be used for the given items
 * The result is stored in parcel, where items are associated with a box type.
 * @param {[orderItemModel]} items individual items to be shipped
 * @param {[boxModel]} boxes a list of boxes
 * @return a list of parcels for the items
 */
function estimateParcels(items, boxes) {
  // ensure boxes are ordered by weight ascending to simplify box allocation
  sortBoxes(boxes);
  // order the items by weight descending (First Fit algorithm)
  sortItems(items);
  // prepare the parcel list to be returned
  const parcels = [];
  // prepare a parcel object to store data as we go
  let parcel = new parcelModel();
  // now for each item, see if it fits in the parcel, if not, take a bigger box and move on...
  for (const item of items) {
    // AG1263 - setting item size to 10cm if shippable but not provided
    if (item.requiresShipping) {
      item.length = item.length || 100;
      item.width = item.width || 100;
      item.height = item.height || 100;
    }
    // see if this item can fit in a previous parcel
    // if it does fit in, move on to the next
    if (fitInExistingParcels(parcels, item)) continue;
    // find the smallest box for the total items weight
    const box = findBestFittingBox(boxes, parcel.weight + item.weight);
    if (box) {
      // use this box for the parcel
      parcel.addItem(item);
      parcel.setBox(box);
    } else {
      // there is not enough room for this item in the current parcel
      // add the current parcel, as it is full (but only if it had items in it)
      if (parcel.items.length > 0) {
        parcels.push(parcel);
        // ensure the parcel is reset to start fitting the new item
        parcel = new parcelModel();
      }
      // look for a fitting box for this item alone
      const bestBox = findBestFittingBox(boxes, item.weight);
      // assign the item to the parcel
      parcel.addItem(item);
      if (bestBox) {
        // if a box was found, lets use it
        parcel.setBox(bestBox);
      } else {
        console.log(`warning: computing parcels, no box big enough for item ${item.name} (${item.weight}g)!`);
        // if there is no fitting box for this item, let it go alone, no box associated free like the wind
        // prepare a custom box for the item (the item itself will be shipped as is)
        parcel.setBox(prepareBox(item.name, item.height, item.length, item.width, item.weight > 0 ? item.weight : 0, 0));
        // force this parcel in
        parcels.push(parcel);
        // reset the parcel again, and loop with the next item hoping to find a suitable box
        parcel = new parcelModel();
      }
    }
  }
  // eventually, if there are items in the parcel, add it to the list
  if (parcel.items.length > 0) {
    parcels.push(parcel);
  }
  return parcels;
}

/**
 * Create a manifest for the given shipment
 *
 * @param {String} shipmentId
 * @returns
 */
async function createManifest(shipmentId) {
  const createManifestQuery = gql`
  mutation createManifest ($shipmentId: String!) {
    createManifest (shipmentId: $shipmentId) {
      manifestUrl,
      manifestNumber
    }
  }
  `;
  const manifestVariables = {
    shipmentId,
  };
  const manifest = await smsClient.request(createManifestQuery, manifestVariables);
  console.log(`Created manifest ${manifest.manifestNumber} for shipment ${shipmentId}`);
  return manifest;
}

/**
 * Request a pickup at the given time
 * for the given shipment
 * to the specified carrier
 *
 * @param {String} carrierId
 * @param {ShipmentBaseInfo} shipmentBaseInfo
 * @param {Date} pickupTime
 */
async function bookPickup(carrierId, shipmentBaseInfo, pickupTime, orderIds) {
  const pickupRequest = new bookPickupModel();
  // Populate the pickup request with the given arguments
  pickupRequest.from = shipmentBaseInfo.from;
  pickupRequest.to = shipmentBaseInfo.to;
  pickupRequest.parcels = shipmentBaseInfo.parcels;
  pickupRequest.carrierId = carrierId;
  pickupRequest.pickupTime = Math.round(pickupTime);
  pickupRequest.pickupNote = 'Pickup automatically generated by INDACO Logistics';
  pickupRequest.pickupMorningMinTime = '07:00';
  pickupRequest.pickupMorningMaxTime = '12:00';
  pickupRequest.pickupAfternoonMinTime = '13:00';
  pickupRequest.pickupAfternoonMaxTime = '18:00';
  // set orderIds (needed from some carriers)
  pickupRequest.orderIds = orderIds;
  // Definining the query and setting the variables
  const bookPickupMutation = gql`
  mutation bookPickup ($pickupData: BookPickupInput!) {
    bookPickup (pickupData: $pickupData) {
      result,
      confirmationId,
      error,
      message
    }
  }`;
  const variables = {
    pickupData: pickupRequest,
  };
  // Performing the request
  console.log(`Issuing pickup request with carrier ${carrierId}`);
  const response = await smsClient.request(bookPickupMutation, variables);
  // Returning the pickup confirmation id
  return response.bookPickup.confirmationId;
}

/**
 * Request the SMS to create a shipment for an order
 *
 * @param {String} orderId the order to be shipped
 * @param {String} carrierId the carrier to be used
 * @param {String} rateId the rate identifier, obtained by getRates function
 * @param {ShipmentBaseInfo} shipmentBaseInfo basic information about the shipment
 * @returns shipment creation data
 */
async function createShipment(orderId, carrierId, rateId, shipmentBaseInfo) {
  console.log(`Creating shipment for ${orderId} using ${carrierId} rate ${rateId}`);
  // Populating the shipment info with the extended parameters
  const shipmentInfo = createShipmentInfo(orderId, carrierId, rateId, shipmentBaseInfo);
  // Definining the query and setting the variables
  const createShipmentQuery = gql`
  mutation createShipment ($input: ShipmentInfoInput!) {
    createShipment (shipmentData: $input) {
      shipmentId,
      labelUrl,
      trackingNumber,
      trackingCarrier,
    }
  }`;
  const variables = {
    input: shipmentInfo,
  };
  // Perform the graphql query to sms
  const response = await smsClient.request(createShipmentQuery, variables);
  console.log(response);
  console.log(`Shipment for order ${orderId} created: ${response.createShipment.shipmentId}`);
  return response.createShipment;
}

/**
 * Request the SMS the available rates for a given set of shipment details.
 *
 * @param {ShipmentBaseInfo} shipmentBaseInfo
 * @returns
 */
async function fetchRates(shipmentBaseInfo) {
  // Preparing the query and the setting the variables
  const fetchRatesQuery = gql`
  query fetchRates($input: ShipmentBaseInfoInput!) {
    fetchRates (shipmentInfo: $input) {
      rates {
        carrier {
          smsid,
          name,
          service,
        }
        rate,
        rateId,
        deliveryDays,
        currency
      }
    }
  }`;
  const variables = {
    input: shipmentBaseInfo,
  };

  // We are going to return a list of rates
  const rates = []; // This will be populated in the loop below when a response is received
  // Perform the graphql query to sms
  const response = await smsClient.request(fetchRatesQuery, variables)
    .then((resp) => {
      for (const rate of resp.fetchRates.rates) {
        rates.push(rate);
      }
    })
    .catch((err) => { console.log(err); });
  return rates;
}

/**
 * Return the best rate among the ones provided
 * @param {[ShipmentRate]} rates
 * @param {[string]} carrierIds if specified filter the rates to only accept rates from the given set of carrier sms ids
 * @return {ShipmentRate} the best rate or null if there are no rates
 */
function getBestRate(rates, carrierIds = null) {
  let bestRate = null;
  // For all the shipment rates
  for (const rate of rates) {
    // if a list was specified and it doesnt include the given sms id, skip this rate
    // this means an empty list will result in no rates
    if (carrierIds
      && !carrierIds.includes(rate.carrier.smsid)
    ) {
      continue;
    }
    // Keep the rate with the lowest "rate", that is, the estimated shipment cost
    if (!bestRate || rate.rate < bestRate.rate) {
      bestRate = rate;
    }
  }
  return bestRate;
}

/**
 * Return the first day available at the warehouse for the pickup of the order.
 * @param {Date} orderDate
 * @returns {Date} day for the pickup
 */
function getPickupDate(orderDate) {
  const pickupDate = orderDate;
  let weekendOrder = false;

  if (orderDate.getDay() === 6 || orderDate.getDay() === 0 || (orderDate.getDay() === 5 && orderDate.getHours() >= 13 && orderDate.getHours() <= 23)) {
    // it's a weekend order, it will ship on tuesday
    weekendOrder = true;
  }

  // give at least 24 hours to the pickup
  pickupDate.setDate(orderDate.getDate() + 1);

  // skip Saturday and Sunday
  while (pickupDate.getDay() === 6 || pickupDate.getDay() === 0) {
    pickupDate.setDate(pickupDate.getDate() + 1);
  }

  // set pickup next working day at 15:30 if the order has been requested in the morning
  if (orderDate.getHours() < 13 && !weekendOrder) {
    pickupDate.setHours(15);
    pickupDate.setMinutes(0);
    // set pickup after 2 working day at 9:30 if the order has been requested in the afternoon
  } else if ((orderDate.getHours() >= 13 && orderDate.getHours() <= 23) || weekendOrder) {
    pickupDate.setDate(pickupDate.getDate() + 1);
    while (pickupDate.getDay() === 6 || pickupDate.getDay() === 0) {
      pickupDate.setDate(pickupDate.getDate() + 1);
    }
    pickupDate.setHours(9);
    pickupDate.setMinutes(30);
  }

  return pickupDate;
}

/**
 * Look for carriers supported by the given partner
 * @param {*} partnerId the partner database id to look for
 * @return a list of sms ids
 */
async function getCarrierIds(partnerId, refrigerated, localOrder) {
  const partnerCarriers = await carrierModel.find({ partnerId });
  // extract carrier sms ids
  const carrierIds = [];
  for (const carrier of partnerCarriers) {
    console.log(`${carrier.refrigerated}-${refrigerated}`);
    if (localOrder) {
      // carrierIds.push('1942'); // DHL
      carrierIds.push('8396'); // BRT
      console.log('aggiunto corriere Trentino per spedizioni locali');
      break;
    }
    if (refrigerated === carrier.refrigerated) {
      carrierIds.push(carrier.smsid);
      console.log(`pushed ${carrier.smsid}. order refrigerated? ${refrigerated}, carrier refrigerated? ${carrier.refrigerated}`);
    }
  }
  return carrierIds;
}

async function saveReferences(order) {
  console.log('picking quantities...');
  for (let i = 0; i < order.items.length; ++i) {
    const item = order.items[i];
    const reference = await productModel.findOne({ 'offers.sku': item.sku });
    console.log(`saving ${reference.title}`);
    await ims.referenceUpdate(reference, reference.sku);
  }
}

/**
 * pick quantities subtracting them from the booked and the inventoryLevel ones
 * @param {orderModel} order
 * @return a list of sms ids
 */
async function pickQuantities(order, bestWarehouseAddress) {
  console.log('picking quantities...');
  if (!order.fullyPicked) {
    for (let i = 0; i < order.items.length; ++i) {
      const item = order.items[i];
      const reference = await productModel.findOne({ 'offers.sku': item.sku });
      // adjust inventory level first
      for (let j = 0; j < reference.inventoryLevels.length; j++) {
        if (reference.inventoryLevels[j].warehouseId.equals(bestWarehouseAddress.warehouse._id)) {
          let quantity = item.quantity;
          console.log(`[PICKING] inventory level: ${reference.inventoryLevels[j]}`);
          console.log(`[PICKING] starting quantity: ${item.quantity}`);
          console.log('looping through booked quantites first');
          for (let k = 0; k < reference.inventoryLevels[j].bookings.length; k++) {
            if (reference.inventoryLevels[j].bookings[k].omsgid === order.omsgid && !reference.inventoryLevels[j].bookings[k].fulfilled) {
              console.log('found a booking, marking it as fulfilled');
              reference.inventoryLevels[j].bookings[k].fulfilled = true;
              quantity -= reference.inventoryLevels[j].bookings[k].amount;
              console.log(`[PICKING] picked quantity: ${reference.inventoryLevels[j].bookings[k].amount}`);
              console.log(`[PICKING] remaining quantity: ${quantity}`);
            }
          }
          // sanity check
          if (quantity < 0) {
            console.log('############### QUANTITY IS LESS THAN ZERO ###################');
            console.log('The booking procedure experienced an issue, since booking amounts are higher than the ordered quantity.');
          }
          if (quantity > 0) {
            console.log('remaining product found, removing it from availability...');
            console.log(`current availability: ${reference.inventoryLevels[j].amount}`);
            reference.inventoryLevels[j].amount -= quantity;
            console.log(`removed ${item.quantity} from inventory level, new total ${reference.inventoryLevels[j].amount}`);
            await ims.addEntryToWarehouseJournal(bestWarehouseAddress.warehouse._id, reference._id, order.partnerId, item.quantity, reference.inventoryLevels[j].amount, 'ORDER', item.refrigerated);
          }
          await reference.save();
        }
      }
      await reference.save();
      order.fullyPicked = true;
      await order.save();
    }
  }
}