Your IP : 216.73.216.43


Current Path : /home/deltalab/PMS/partner-manager-backend/services/
Upload File :
Current File : //home/deltalab/PMS/partner-manager-backend/services/ims.js

/* eslint-disable max-len */
/* eslint-disable no-continue */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-use-before-define */
/* eslint-disable new-cap */
/**
 * IMS Contacting service
 * Speaks with the IMS connector to create, sync and update
 */

require('dotenv').config();
const axios = require('axios');
const { gql, GraphQLClient } = require('graphql-request');

// CONFIGURATION ===================================
const imsEndpoint = process.env.IMS_CONN_URL;
const marketplaceId = process.env.MARKETPLACE_ID;
// ======================================

// MODELS ==========================================
const { listingModel } = require('../models/mongoose/listing');
const { listingBaseModel } = require('../models/mongoose/listing');
const { productModel } = require('../models/mongoose/product');
const { productBaseModel } = require('../models/mongoose/product');
const { warehouseJournalModel } = require('../models/mongoose/warehouse-journal');
const { channelModel } = require('../models/mongoose/channel');
const { partnerModel } = require('../models/mongoose/partner');
const { warehouseModel } = require('../models/mongoose/warehouse');

// WRAPPERS ========================================

/**
 * Wrapper for the product creation
 * @param {function} resolver The resolver to be wrapped
 * @returns The wrapped resolver
 */
function productCreateWrapper(resolver) {
  return resolver.wrapResolve((next) => async (rp) => {
    // Create the mongo document
    // Create the product model based on the given record
    const response = await next(rp);
    // Sanity check
    const product = response.record;
    // Call IMS to create a product and get a reference
    const imsgid = await createIMSProduct(product);
    // Sanity check
    if (!imsgid) {
      return {
        error: { message: `could not create product ${product.sku}` },
      };
    }
    const savedProduct = new productModel(product);
    // Assign the reference to the product
    savedProduct.imsgid = imsgid;
    savedProduct.isNew = false;
    await savedProduct.save();
    // Save the product
    // Create a suitable response
    return {
      recordId: savedProduct._id,
      record: savedProduct,
    };
  });
}

/**
 * Wrapper for the product update
 * @param {function} resolver The resolver to be wrapped
 * @returns The wrapped resolver
 */
function referenceUpdateWrapper(resolver) {
  return resolver.wrapResolve((next) => async (rp) => {
    // Update the mongo document
    const payload = await next(rp);
    const productId = payload.record.imsgid;
    const listingsId = payload.record.listingIds;

    // If the product is already in the imsgid
    if (productId) {
      if (listingsId && listingsId.length > 0) {
        await addProductToIMSListing(productId, listingsId);
      }
      const product = new productModel(payload.record);
      const response = await updateIMSProduct(payload.record);
      if (!response) {
        console.log(`Could not update product ${productId} in IMS`);
      }
      // check the media for required uploads
      await checkIMSProductMedia(product);
      product.isNew = false; // ensure update only
      await product.save();
    } else {
      // If the product is not in the ims, try and create it there
      const imsgid = await createIMSProduct(payload.record);
      // Sanity check
      if (imsgid) {
        const product = new productModel(payload.record);
        product.imsgid = imsgid;
        // check all the media
        await checkIMSProductMedia(product);
        product.isNew = false; // ensure update only
        await product.save();
      } else {
        console.log(`Cannot retrieve imsgid for ${payload.record._id}`);
      }
    }
    return payload;
  });
}

/**
 * Wrapper for the product deletion
 * @param {function} resolver The resolver to be wrapped
 * @returns The wrapped resolver
 */
function productRemoveByIdWrapper(resolver) {
  return resolver.wrapResolve((next) => async (rp) => {
    const { _id } = rp.args;
    // Look for the product to be removed
    const product = await productModel.findById(_id);
    // Sanity check
    if (!product) {
      return { error: { message: `No such product to remove ${_id}` } };
    }
    // Check the product is mapped in the IMS
    if (product.imsgid) {
      // Contact the IMS connector and register the callbacks
      const imsgid = await deleteIMSProduct(product.imsgid);
      if (!imsgid) {
        console.log(`cannot remove ims product ${product.imsgid}`);
        // return { error: { message: `Cannot remove IMS product ${product.imsgid}` } };
      }
    }
    console.log(`removing product ${product.imsgid}`);
    // If everything is fine, just remove it using the mongoose resolver
    const payload = await next(rp);
    return payload;
  });
}

/**
 * Filter resolvers for authenticated users
 */
function listingCreateOneWrapper(resolver) {
  return resolver.wrapResolve(() => async ({ args }) => {
    // Create the mongo document using the standard wrapper
    const { record } = args;
    // Create the local model based on the created record
    const listing = new listingModel();
    listing.name = record.name;
    listing.handle = record.handle;
    listing.partnerId = record.partnerId;
    listing.isNew = true; // if isNew is false, the record will only be updated
    // Contact the IMS connector and retrieve the imsgid
    // const imsgid = await createIMSListing(listing); not needed in magento environment
    // Sanity check
    // if (!imsgid) {
    //   return {
    //     error: { message: `could not create listing ${listing.name}` },
    //   };
    // }
    // Prepare the listing for update
    // listing.imsgid = imsgid;
    // Save the record (the id is the same so it will update the existing entry)
    const savedListing = await listing.save();
    // Compose the mongoose response object
    return {
      recordId: savedListing._id,
      record: savedListing,
    };
  });
}

function listingUpdateByIdWrapper(resolver) {
  return resolver.wrapResolve((next) => async (rp) => {
    const payload = await next(rp);
    // const response = await updateIMSListing(payload);
    return payload;
  });
}

/**
 * Remove the entry both locally
 */
function listingRemoveByIdWrapper(resolver) {
  return resolver.wrapResolve((next) => async (rp) => {
    // Retrieve the listingModel
    const { _id } = rp.args;
    const listing = await listingModel.findById(_id);
    // Sanity check
    if (!listing) {
      return {
        error: { message: `Could not find listing to remove for ${_id}` },
      };
    }
    // Remove the ims listing if one was paired
    if (listing.imsgid) {
      // Contact the IMS connector and remove the listing
      const removeResult = await removeIMSListing(listing.imsgid);
      // Verify the listing was removed for consistency
      if (!removeResult) {
        // Return a readable error to explain the situation
        return {
          error: {
            message: `Could not remove listing ${listing.imsgid} from IMS`,
          },
        };
      }
    }
    // Remove the mongo document using the standard resolver
    const payload = await next(rp);
    return payload;
  });
}

// SERVICE FUNCTIONS ===============================

/**
 * Take the provided product (as a MongoDB document) and send it to the IMSConnector to create it on the IMS
 * @param {productModel} product The product to be created in ims
 * @returns return the IMS Global Id (imsgid) for the product or null if it was not created
 * @deprecated
 */
async function createIMSProduct(product) {
  // Prepare a base product to be sent over
  const productBase = new productBaseModel(product);
  // GraphQL
  const graphClient = new GraphQLClient(imsEndpoint);
  const createProdQuery = gql`
  mutation ($input: ProductBaseInput!){
    productCreateOne(input: $input){
      imsgid
    }
  }`;
  const args = {
    input: productBase,
  };
  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(createProdQuery, args);
  // Sanity check
  if (!response.productCreateOne) return null;
  // Return the imsgid of the IMS created product
  return response.productCreateOne.imsgid;
}

/**
 * Take the provided product (as a MongoDB document) and send it to the IMSConnector to update it on the IMS
 * @param {Product} product The product to update to the ims
 * @returns the imsgid of the updated product
 * @deprecated
 */
async function updateIMSProduct(product) {
  const productBase = new productBaseModel(product);
  // GraphQL related stuff
  const graphClient = new GraphQLClient(imsEndpoint);
  const updateProdQuery = gql`
  mutation ($product: ProductBaseInput!){
    productUpdateOne(input: $product){
      imsgid
    }
  }`;
  const args = {
    product: productBase,
  };
  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(updateProdQuery, args);

  if (!response.productUpdateOne) return null;
  return response.productUpdateOne.imsgid;
}

/**
 * Check all the product media are uploaded.
 * If media is correcly uploaded, the imsgid is set and it is not uploaded again
 * @param {*} product the reference to the product, save afterwards to preserve the imsgid!
 */
async function checkIMSProductMedia(product) {
  // check media content
  const mediaLength = product.media.length;
  for (let i = 0; i < mediaLength; ++i) {
    const productMedia = product.media[i];
    // only upload if imsgid is not set
    if (!productMedia.imsgid) {
      console.log(`going to upload ${productMedia.name} to ${product.title}`);
      const imsgid = await uploadIMSProductMedia(product.imsgid, productMedia);
      // set the imsgid and save
      if (!imsgid) {
        console.log(`could not upload ${productMedia.name} to ${product.title}`);
      } else {
        product.media[i].imsgid = imsgid;
      }
    }
  }
  // Look for media in ims that are not in product
  const imsMedia = await getIMSProductMedia(product.imsgid);
  const toDeleteIds = [];
  for (const media of imsMedia) {
    let found = false;
    for (const productMedia of product.media) {
      // Compare the media by imsgid
      if (productMedia.imsgid === media.imsgid) {
        found = true;
        break;
      }
    }
    // If a ims media is not in the product,
    // add it to the list to be removed
    if (!found) {
      console.log(`media ${media.imsgid} is not used and will be removed`);
      toDeleteIds.push(media.imsgid);
    }
  }
  // if there are media to be removed
  if (toDeleteIds.length > 0) {
    console.log(`removing ${toDeleteIds.length} media from ${product.title}`);
    await deleteIMSProductMedia(product.imsgid, toDeleteIds);
  }
}

/**
 * Send the query to upload a media to the IMS
 * and associate it to the product
 * @param {*} productId
 * @param {*} productMedia
 * @returns the media ims gid or null if it failed
 * @deprecated
 */
async function uploadIMSProductMedia(productId, productMedia) {
  // productCreateMedia(
  //   productId: "123123",
  //   media:{
  //     name: "123",
  //     type: "image/jpeg",
  //     data: "123123123",
  //   })
  // {
  //   imsgid
  // }
  // GraphQL related stuff
  const graphClient = new GraphQLClient(imsEndpoint);
  const uploadMediaQuery = gql`
  mutation ($productId: ID!, $media: MediaInput!){
    productCreateMedia (productId: $productId, media: $media){
      imsgid
    }
  }`;
  const args = {
    productId,
    media: productMedia,
  };
  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(uploadMediaQuery, args);

  if (!response.productCreateMedia) return null;
  return response.productCreateMedia.imsgid;
}

/**
 * Remove the given image from the product
 * @param {string} productId
 * @param {[string]} imsgids
 * @deprecated
 */
async function deleteIMSProductMedia(productId, imsgids) {
  // mutation deletemedia {
  //   productDeleteMedia(productId:"gid://shopify/Product/6734676787376",
  //     mediaIds: []
  //   ) {
  //     imsgid
  //   }
  // }
  // GraphQL related stuff
  const graphClient = new GraphQLClient(imsEndpoint);
  const deleteMediaQuery = gql`
  mutation ($productId: ID!, $mediaIds: [ID!]!) {
    productDeleteMedia (
      productId: $productId,
      mediaIds: $mediaIds )
    {
      imsgid
    }
  }`;
  const args = {
    productId,
    mediaIds: imsgids,
  };
  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(deleteMediaQuery, args);
  if (!response.productDeleteMedia) return null;
  return response.productDeleteMedia;
}

/**
 * Retrieve the ims product media ids
 * @param {string} imsgid the product imsgid
 * @deprecated
 */
async function getIMSProductMedia(imsgid) {
  // GraphQL related stuff
  const graphClient = new GraphQLClient(imsEndpoint);
  const productQuery = gql`
  query ($productId: ID!) {
    productMedia (productId: $productId) {
      imsgid
    }
  }`;
  const args = {
    productId: imsgid,
  };
  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(productQuery, args);
  return response.productMedia;
}

/**
 * Take the provided product (as a MongoDB document) and send it to the IMSConnector to delete it off the IMS
 * @param {Product} record The product to update
 * @returns the id of the removed product or null if error
 * @deprecated
 */
async function deleteIMSProduct(imsgid) {
  // GraphQL related stuff
  const graphClient = new GraphQLClient(imsEndpoint);
  const deleteProdQuery = gql`
  mutation ($id: ID!){
    productDeleteById(id: $id){
      imsgid
    }
  }`;
  const args = {
    id: imsgid,
  };
  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(deleteProdQuery, args);
  if (!response.productDeleteById) return null;
  return response.productDeleteById.imsgid;
}

/**
 * Create
 * @param {listingModel} listing
 * @returns imsgid
 */
async function createIMSListing(listing) {
  const listingBase = new listingBaseModel(listing);
  // GraphQL related stuff
  const graphClient = new GraphQLClient(imsEndpoint);
  const createListingQuery = gql`
  mutation listingCreateOne($input: ListingBaseInput!) {
    listingCreateOne(input: $input) {
      imsgid
    }
  }`;
  const args = {
    input: listingBase,
  };
  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(createListingQuery, args);
  // Sanity check
  if (!response.listingCreateOne) return null;
  return response.listingCreateOne.imsgid;
}

/**
 * Take the provided listing (as a MongoDB document) and send it to the IMSConnector to update it on the IMS
 * @param {Listing} record The listing to update to the ims
 * @returns the imsgid of the updated listing
 */
async function updateIMSListing(listingInput) {
  const listingData = new listingModel(listingInput.record);
  // GraphQL related stuff
  const graphClient = new GraphQLClient(imsEndpoint);
  const updateListQuery = gql`
  mutation ($listing: ListingInput!){
    listingUpdateOne(input: $listing){
      imsgid
    }
  }`;
  const args = {
    listing: listingData,
  };

  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(updateListQuery, args);
  if (!response.listingUpdateOne) return null;
  return response.listingUpdateOne.imsgid;
}

/**
 * Request the IMS to remove he listing with the given imsgid
 * @param {*} imsgid
 * @returns the imsgid of he removed listing or null
 */
async function removeIMSListing(imsgid) {
  // GraphQL related stuff
  const graphClient = new GraphQLClient(imsEndpoint);
  const createListingQuery = gql`
  mutation ($imsgid: String!) {
    listingDeleteById(imsgid: $imsgid) {
      imsgid
    }
  }`;
  const args = {
    imsgid,
  };
  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(createListingQuery, args);
  // Sanity check
  if (!response.listingDeleteById) return null;
  return response.listingDeleteById.imsgid;
}

/**
 * @param {*} imsgid
 * @param {*} quantity
 * @deprecated
 */
async function adjustProductQuantity(imsgid, quantity) {
  // GraphQL related stuff
  const graphClient = new GraphQLClient(imsEndpoint);
  const adjustQuantityQuery = gql`
  mutation ($imsgid: ID!, $quantity: Float!) {
    productAdjustQuantity(imsgid: $imsgid, quantity: $quantity) {
      imsgid, 
      availability
    }
  }`;
  const args = {
    imsgid,
    quantity,
  };
  // Send request to IMS connector using the ad-hoc client
  const response = await graphClient.request(adjustQuantityQuery, args);
  // Sanity check
  if (!response.productAdjustQuantity) return null;
  return response.productAdjustQuantity;
}

// INTERNAL FUNCTIONS ==============================================
async function addProductToIMSListing(productImsgid, listingIds) {
  let success = true;
  // Add the product to all the listings
  for (const listingId of listingIds) {
    const listing = await listingModel.findById(listingId);
    if (!listing) {
      console.log(`cannot add ${productImsgid} to ${listingId}: no such listing`);
      continue;
    }
    const response = await addProductToListing(listing.imsgid, productImsgid);
    success = success && (response != null);
  }
  return success;
}

async function removeProductFromIMSListing(productImsgid, listingIds) {
  let success = true;
  // Add the product to all the listings
  for (const listingId of listingIds) {
    const listing = await listingModel.findById(listingId);
    const response = await removeProductFromListing(listing.imsgid, productImsgid);
    success = success && (response != null);
  }
  return success;
}

async function addProductToListing(listingId, productId) {
  const graphClient = new GraphQLClient(imsEndpoint);
  const addProductToCollection = gql`
  mutation ($imsgid: String, $products:[String!]!) {
    listingAddProducts(imsgid:$imsgid, products:$products) {
      imsgid
    }
  }`;
  const args = {
    imsgid: listingId,
    products: [productId],
  };
  const response = await graphClient.request(addProductToCollection, args);
  if (!response.listingAddProducts) return null;
  return response.listingAddProducts.imsgid;
}

async function removeProductFromListing(listingId, productId) {
  const graphClient = new GraphQLClient(imsEndpoint);
  const removeProductFromCollection = gql`
  mutation ($imsgid: String, $products:[String!]!) {
    listingRemoveProducts(imsgid:$imsgid, products:$products) {
      imsgid
    }
  }`;
  const args = {
    imsgid: listingId,
    products: [productId],
  };
  const response = await graphClient.request(removeProductFromCollection, args);
  if (!response.listingRemoveProducts) return null;
  return response.listingRemoveProducts.imsgid;
}

async function addEntryToWarehouseJournal(warehouseId, productId, partnerId, variation, totalQuantity, reason, refrigerated, confirmed = false, transfer = false, omsgid = '', origin = undefined) {
  const journalEntry = new warehouseJournalModel();
  journalEntry.date = new Date();
  journalEntry.warehouseId = warehouseId;
  if (transfer) {
    journalEntry.warehouseOriginId = origin;
  }
  journalEntry.productId = productId;
  journalEntry.partnerId = partnerId;
  journalEntry.variation = variation;
  journalEntry.totalQuantity = totalQuantity;
  journalEntry.reason = reason;
  journalEntry.refrigerated = refrigerated;
  journalEntry.confirmed = confirmed || !(reason === 'INCREASE' || reason === 'INITIAL'); //if I add new stock to a warehouse, it has to be confirmed before being put on sale
  journalEntry.transfer = transfer;
  journalEntry.omsgid = omsgid;
  await journalEntry.save();
}

async function createReferenceBooking(productId, warehouseId, variation, omsgid) {
  const product = await productModel.findById(productId);
  if (product) {
    let found = false;
    for (let j = 0; j < product.inventoryLevels.length; j++) {
      if (product.inventoryLevels[j].warehouseId.equals(warehouseId)) {
        found = true;
        if (omsgid) {
          if (!product.inventoryLevels[j].bookings) {
            product[j].bookings = [];
          }
          product.inventoryLevels[j].bookings.push({ amount: variation, omsgid, fulfilled: false });
        } else {
          product.inventoryLevels[j].amount += variation;
        }
        await product.save();
        console.log('AGGIORNATO PRODOTTO');
        break;
      }
    }
    if (!found) {
      console.log(`Order from channel without transfer needed, adding it with ${variation} starting quantity`);
      product.inventoryLevels.push({ warehouseId, amount: 0, bookings: [{ amount: variation, omsgid, fulfilled: false }] });
      await product.save();
    }
  }
}

// REST ===========================================

async function createIMSProductRest(reference, channelId) {
  // retrieve channel from product
  const channel = await channelModel.findOne({ _id: channelId });
  const websiteIds = await getWebsiteIdList(reference, channel);
  // get partner to send
  const partner = await partnerModel.findOne({ _id: reference.partnerId });
  // Prepare a base product to be sent over
  const productBase = new productBaseModel(reference);
  const payload = {
    product: productBase,
    channel,
    partner,
    websiteIds,
  };
  // const response = await axios.post(`${imsEndpoint}/rest/products`, payload);
  const response = await axios.post(`${channel.imsAddress}/rest/products`, payload);
  // Sanity check
  if (!response.data.success) return null;
  // Return the imsgid of the IMS created product
  return response.data.data.id;
}

async function productCreate(product, channelId) {
  const imsgid = await createIMSProductRest(product, channelId);
  // Sanity check
  if (!imsgid) {
    throw new Error(`could not create product ${product.sku}`);
  }
  return imsgid;
}

async function getStoreFromName(storeName) {
  const response = await axios.get(`${imsEndpoint}/rest/stores/${storeName}`);
  if (!response.data.success) {
    throw new Error(`could not delete product ${storeName}`);
  }
  return response.data;
}

async function uploadImageToProduct(sku, filename, extension, base64Data) {
  // Prepare a base product to be sent over
  const address = await retrieveImsEndpointFromSKU(sku);
  const url = `${address}/rest/products/uploadImage`;
  const dataObj = {
    sku,
    filename,
    extension,
    data: base64Data,
  };
  const response = await axios.post(url, dataObj);
  // Sanity check
  if (!response.data) return null;
  return response.data;
}

async function deleteMediafromProduct(sku, imsgid) {
  const address = await retrieveImsEndpointFromSKU(sku);
  // Prepare a base product to be sent over
  const url = `${address}/rest/products/deleteImage/${sku}/${imsgid}`;
  const response = await axios.delete(url);
  // Sanity check
  if (!response.data.success) return null;
  return response.data.success;
}

async function updateIMSReferenceRest(reference, sku) {
  // Prepare a base product to be sent over
  const productBase = new productBaseModel(reference);
  const partner = await partnerModel.findOne({ _id: reference.partnerId });
  for (let i = 0; i < reference.offers.length; i++) {
    const offer = reference.offers[i];
    const channel = await channelModel.findById(offer.channelId);
    const websiteIds = await getWebsiteIdList(reference, channel);
    const linkedProducts = await getLinkedProducts(reference, channel);
    productBase.price = offer.price;
    productBase.imsCategories = offer.imsCategories;
    productBase.deleted = offer.deleted;
    productBase.title = offer.title;
    productBase.customDescription = offer.description;
    productBase.attributes = offer.attributes;

    // validity
    productBase.imsEnabled = productBase.imsEnabled ? offer.imsEnabled : false;

    // VALID FROM

    if (productBase.isValidFrom && offer.isValidFrom) {
      productBase.isValidFrom = true;
      productBase.validFrom = productBase.validFrom > offer.validFrom ? offer.validFrom : productBase.validFrom;
    }

    if (!productBase.isValidFrom && offer.isValidFrom) {
      productBase.isValidFrom = true;
      productBase.validFrom = offer.validFrom;
    }

    // VALID UNTIL

    if (productBase.isValidUntil && offer.isValidUntil) {
      productBase.isValidUntil = true;
      productBase.validUntil = productBase.validUntil < offer.validUntil ? productBase.validUntil : offer.validUntil;
    }

    if (!productBase.isValidUntil && offer.isValidUntil) {
      productBase.isValidUntil = true;
      productBase.validUntil = offer.validUntil;
    }

    const quantity = await getQuantityByChannel(reference, channel);
    const dataObj = {
      referenceUpdate: productBase,
      partner,
      linkedProducts,
      channel,
      quantity,
      websiteIds,
    };
    // const url = product.deleted ? `${imsEndpoint}/rest/products/delete/${sku}` : `${imsEndpoint}/rest/products/${sku}`;
    const url = reference.deleted || offer.deleted ? `${channel.imsAddress}/rest/products/delete/${offer.sku}` : `${channel.imsAddress}/rest/products/${offer.sku}`;
    const response = await axios.put(url, dataObj);
    // Sanity check
    if (!response.data.success) return null;
    // Return the imsgid of the IMS created product
    // return response.data.data.id;
  }
  return true;
}

async function getQuantityByChannel(reference, channel, omsgid = undefined) {
  let totalQuantity = 0;
  for (let i = 0; i < reference.inventoryLevels.length; i++) {
    const inventoryLevel = reference.inventoryLevels[i];
    const warehouse = await warehouseModel.findById({ _id: inventoryLevel.warehouseId });
    if (warehouse) {
      for (let j = 0; j < warehouse.channelAssignments.length; j++) {
        if (warehouse.channelAssignments[j].channelId.equals(channel._id)) {
          console.log(`[QUANTITA] - Aggiungo ${inventoryLevel.amount} come giacenza iniziale`);
          totalQuantity += inventoryLevel.amount;
          if (inventoryLevel.bookings) {
            for (let k = 0; k < inventoryLevel.bookings.length; k++) {
              if (omsgid && inventoryLevel.bookings[k].omsgid !== omsgid) {
                continue;
              }
              if (!inventoryLevel.bookings[k].fulfilled) {
                console.log(`[QUANTITA] - Trovata prenotazione aperta, aggiungo se è del canale giusto`);
                if (inventoryLevel.bookings[k].channelId) {
                  if (inventoryLevel.bookings[k].channelId === channel._id.toString()) {
                    console.log(`[QUANTITA] - Aggiungo ${inventoryLevel.bookings[k].amount} come prenotazione`);
                    totalQuantity += inventoryLevel.bookings[k].amount;
                    console.log(`[QUANTITA] - Totale parziale: ${totalQuantity}`);
                  }
                }
              }
            }
          }
          //add quantity from pending transactions
          totalQuantity += await readPendingTransactionsQuantity(reference._id, warehouse._id, omsgid);
        }
      }
    }
  }
  console.log(`[QUANTITA] - TOTAL QUANTITY FOR ${reference.title} IS ${totalQuantity}`);
  return totalQuantity;
}

async function readPendingTransactionsQuantity(referenceId, warehouseId, omsgid = undefined) {
  const requestBody = {
    confirmed: false,
    reason: 'INCREASE',
    productId: referenceId,
    warehouseId,
  };

  if (omsgid) {
    requestBody.omsgid = omsgid;
  }

  const warehouseJournalList = await warehouseJournalModel.find(requestBody);
  let quantity = 0;
  for (let i = 0; i < warehouseJournalList.length; i++) {
    quantity += warehouseJournalList[i].variation;
  }
  return quantity;
}

async function getLinkedProducts(product, channel) {
  if (channel._id.toString() !== process.env.MARKETPLACE_ID) {
    console.log('[LINKED PRODUCTS] Il canale non è quello giusto!');
    return [];
  }

  //TODO: ricarico referenza per avere dati puliti 
  const reference = await productModel.findById(product._id);

  const recomms = [];
  if (reference && reference.linkedProducts) {
    console.log(`[LINKED PRODUCTS] Ce ne sono! ${reference.linkedProducts.length}`);
    for (let i = 0; i < reference.linkedProducts.length; i++) {
      const recommendedProduct = await productModel.findById(reference.linkedProducts[i].productId);
      if (recommendedProduct && recommendedProduct.offers) {
        for (let j = 0; j < recommendedProduct.offers.length; j++) {
          if (recommendedProduct.offers[j].channelId.toString() === process.env.MARKETPLACE_ID) {
            console.log('[LINKED PRODUCTS] Canale corretto!');
            recomms.push(recommendedProduct.offers[j].sku);
          }
        }
      }
    }
  }
  console.log(`[LINKED PRODUCTS] ${recomms.length}`);
  return recomms;
}

async function referenceUpdate(reference, sku) {
  const updatedProduct = await updateIMSReferenceRest(reference, sku);
  // Sanity check
  if (!updatedProduct) {
    throw new Error(`could not update product ${reference.sku}`);
  }
  return updatedProduct;
}

async function isOfferInIMS(offer) {
  const channel = await channelModel.findOne({ _id: offer.channelId });
  const response = await axios.get(`${channel.imsAddress}/rest/products/imsProduct/${offer.sku}/${channel.storeName}`);
  if (!response.data.success) {
    throw new Error('could not find product in store by sku');
  }
  return response.data.data;
}

async function productDelete(referenceSku) {
  const address = await retrieveImsEndpointFromSKU(referenceSku);
  const response = await axios.delete(`${address}/rest/products/${referenceSku}`);
  if (!response.data.success) {
    throw new Error(`could not delete product ${referenceSku}`);
  }
}

async function getAttributeSets(storeName, channelId) {
  const channel = await channelModel.findOne({ _id: channelId });
  const response = await axios.get(`${channel.imsAddress}/rest/attribute-sets/${storeName}`);
  if (!response.data.success) {
    throw new Error('could not find attribute sets');
  }
  return response.data.data;
}

async function readAttributeSetByName(storeName, name, channelId) {
  const channel = await channelModel.findOne({ _id: channelId });
  const response = await axios.get(`${channel.imsAddress}/rest/attribute-sets/${storeName}/${name}`);
  if (!response.data.success) {
    throw new Error(`Cannot find attribute set ${name}`);
  }
  return response.data.data;
}

async function getCategories(storeName, channelId) {
  const channel = await channelModel.findOne({ _id: channelId });
  const response = await axios.get(`${channel.imsAddress}/rest/categories/${storeName}`);
  if (!response.data.success) {
    throw new Error('could not find categories');
  }
  return response.data.data;
}

async function getCategoryById(storeName, id, channelId) {
  const channel = await channelModel.findOne({ _id: channelId });
  const response = await axios.get(`${channel.imsAddress}/rest/categories/${storeName}/${id}`);
  if (!response.data.success) {
    throw new Error(`Cannot find category ${id}`);
  }
  return response.data.data;
}

async function getCategoryByName(storeName, id, channelId) {
  const channel = await channelModel.findOne({ _id: channelId });
  const response = await axios.get(`${channel.imsAddress}/rest/categories/${storeName}/filter/${id}`);
  if (!response.data.success) {
    throw new Error(`Cannot find category ${id}`);
  }
  return response.data.data;
}

async function getTaxCodes(storeName, channelId) {
  const channel = await channelModel.findOne({ _id: channelId });
  const response = await axios.get(`${channel.imsAddress}/rest/tax/${storeName}`);
  if (!response.data.success) {
    throw new Error('could not find tax codes');
  }
  return response.data.data;
}

async function getBuyButton(productSKUs, channel) {
  const address = await retrieveImsEndpointFromSKU(productSKUs[0]);
  // Prepare a base product to be sent over
  const url = `${address}/rest/buybutton`;
  const dataObj = {
    productSKUs,
    channel,
  };
  const response = await axios.post(url, dataObj);
  // Sanity check
  if (!response.data) return null;
  return response.data;
}

// UTILITIES

async function retrieveImsEndpointFromSKU(sku) {
  const product = await productModel.find({ sku });
  if (product) {
    const channel = await channelModel.findById(product.channelId);
    if (channel) {
      return channel.imsAddress;
    }
  }

  return '';
}

async function getWebsiteIdList(product, channel) {
  const websiteIds = [];
  for (let i = 0; i < product.offers.length; i++) {
    const offer = product.offers[i];
    const offerChannel = await channelModel.findById(offer.channelId);
    if (offerChannel) {
      if (offerChannel.prefix === channel.prefix) { // load all the websites of the channel where this product is sold
        websiteIds.push(offerChannel.websiteId);
      }
    }
  }
  const ids = [...new Set(websiteIds)];
  return ids;
}

// EXPORTS =========================================
module.exports = {
  // GraphQL wrappers
  productCreateWrapper,
  referenceUpdateWrapper,
  productRemoveByIdWrapper,
  removeProductFromIMSListing,
  listingCreateOneWrapper,
  listingUpdateByIdWrapper,
  listingRemoveByIdWrapper,
  // APIs
  addProductToIMSListing,
  addProductToListing,
  createIMSListing,
  createIMSProduct,
  deleteIMSProduct,
  removeIMSListing,
  removeProductFromListing,
  updateIMSProduct,
  updateIMSListing,
  getIMSProductMedia,
  deleteIMSProductMedia,
  adjustProductQuantity,
  addEntryToWarehouseJournal,
  createReferenceBooking,
  // REST
  productCreate,
  referenceUpdate,
  productDelete,
  uploadImageToProduct,
  deleteMediafromProduct,
  isOfferInIMS,
  getQuantityByChannel,
  // REST ATTRIBUTE SETS
  getAttributeSets,
  readAttributeSetByName,
  // REST CATEGORIES
  getCategories,
  getCategoryById,
  getCategoryByName,
  // REST TAX CODES
  getTaxCodes,
  // REST STORES
  getStoreFromName,
  // BUYBUTTON
  getBuyButton,
};