Your IP : 216.73.216.220


Current Path : /home/deltalab/PMS/sms-connector/adapters/
Upload File :
Current File : //home/deltalab/PMS/sms-connector/adapters/shippypro-adapter.js

/**
 * SMS adapter for ShippyPro. It exposes all standard methods of the adapter for an SMS system.
 *  - fetching rates from SMS (getRatesAsync)
 *  - create a new shipment in the SMS (createShipmentAsync)
 *  - get the shipment status in the SMS (getShipmentStatusAsync)
 *  - get the list of carriers in the SMS (getCarriersAsync)
 */
class ShippyProAdapter {
  constructor() {
    // http client used to perform requests to ShippyPro is axios
    this._httpClient = require('axios');
  }

  /**
   * Fetch all the rates for this given shipment
   * 
   * @param {shipmentInfoModel} shipmentInfo the shipment information used to calculate the rate as described in the graphql schemas
   * @returns a list of ShipmentRate objects representing supported carriers and their cost for the shipment
   */
  getRatesAsync(shipmentInfo) {
    return this._performRequestAsync(shipmentInfo,
      this._mapRatesRequestInputAsync,
      this._mapRatesRequestOutputAsync);
  }

  /**
   * Create a shipment in the SMS system
   * 
   * @param {object} shipmentData the shipment information as described in the graphql schema
   * @returns A promise that will resolve in the creation-related data as described in the graphql schema
   */
  createShipmentAsync(shipmentData) {
    return this._performRequestAsync(shipmentData,
      this._mapShipmentRequestInputAsync,
      this._mapShipmentRequestOuputAsync);
  }

  /**
   * Fetch a specific shipment information from the SMS system
   * 
   * @param {string} shipmentId the shipment ID to fetch
   * @returns A promise that will resolve in the shipment status information as described in the graphql schema
   */
  getShipmentStatusAsync(shipmentId) {
    return this._performRequestAsync(shipmentId,
      this._mapShipmentStatusInputAsync,
      this._mapShipmentStatusOutputAsync);
  }

  /**
   * Fetch all the carriers configured on the SMS system
   * 
   * @returns A promise that will resolve in the list of carriers configured on the SMS system, mapped as the carrier type described in graphql schemas
   */
  getCarriersAsync() {
    return this._performRequestAsync(null,
      this._mapGetCarriersInputAsync,
      this._mapGetCarriersOutputAsync);
  }

  /**
   * Creates a manifest for the specified shipment and returns the created manifest data
   * 
   * @param {string} shipmentId the shipment id to process
   * @returns A promise that will resolve in the manifest data as defined on the graphql schema
   */
  createManifestAsync(shipmentId) {
    return this._performRequestAsync(shipmentId,
      this._mapCreateManifestInputAsync,
      this._mapGetManifestOutputAsync);
  }

  /**
   * Gets an already created manifest using the specified id
   * 
   * @param {int} manifestId the manifest id to search for
   * @returns A promise that will resolve in the manifest data as defined in the graphql schema
   */
  getManifestAsync(manifestId) {
    return this._performRequestAsync(manifestId,
      this._mapGetManifestInputAsync,
      this._mapGetManifestOutputAsync);
  }

  /**
   * Book a pickup for the specified parcels
   * 
   * @param {object} pickupData the data used to book the pickup as described in the graphql schema
   * @returns A promise that will resolve in the booking confirmation as defined in the graphql schema
   */
  bookPickupAsync(pickupData) {
    return this._performRequestAsync(pickupData,
      this._mapBookPickupInputAsync,
      this._mapBookPickupOutputAsync);
  }

  /**
   * This method is working as a template for executing requests to ShippyPro.
   * 
   * All adapter's methods can be resumed in these operations:
   * 1. map input data to match the required from ShippyPro
   * 2. execute the request and fetch the reply
   * 3. map the result to match the SMS connector output data
   * 
   * All mapping functions are asynchronous and are called with a parameter that contains the data to transform.
   * 
   * @param {object} data the input data to the request 
   * @param {function} inputMappingFunction the function that maps input data to the required from ShippyPro
   * @param {function} outputMappingFunction the mapping function that transforms the data fetched from ShippyPro to the one required from the connector
   * 
   * @returns A promise that will resolve in the fetched data from ShippyPro.
   */
  async _performRequestAsync(data, inputMappingFunction, outputMappingFunction) {
    const requestData = await inputMappingFunction.call(this, data);
    const apiResponse = await this._executeRequestAsync(requestData);
    const result = await outputMappingFunction.call(this, apiResponse);
    return result;
  }

  /**
   * Gets a specific carrier from the SMS system
   * 
   * @param {int} carrierId the ID of the carrier to fetch
   * @returns A promise that will resolve in a specific carrier
   */
  async _getSpecificCarrierAsync(carrierId) {
    const carriers = await this.getCarriersAsync();
    return carriers.find(c => c.smsid == carrierId);
  }

  /**
   * Execute a request against ShippyPro, using the specified data
   * 
   * @param {object} data the data to use as body of the request (in ShippyPro it contains all request params)
   * @returns a promise that will resolve in the data returned from ShippyPro
   */
  async _executeRequestAsync(data) {
    const requestUrl = process.env.SMS_URL;
    const request = this._httpClient.create();

    const headers = {
      'Authorization': `Basic ${Buffer.from(`${process.env.SMS_API_USERNAME}:`).toString('base64')}`,
      'Content-Type': 'application/json;charset=utf-8',
      'Accept': 'application/json;charset=utf-8',
    };

    try {
      const httpResult = await request.post(requestUrl, data, { headers });
      return httpResult.data;
    } catch (e) {
      this._manageHttpException(e);
    }
  }

  /**
   * Normalize an axios exception to a custom http one and throw it
   * 
   * @param {Error} e the exception to manage as http exception
   */
  _manageHttpException(e) {
    const HttpException = require('../exceptions/http-exception');
    const httpResult = e.response ? e.response : { status: 404, data: 'Not found' };
    throw new HttpException(httpResult.status, httpResult.data);
  }

  // MAPPING FUNCTIONS - RATES

  _mapRatesRequestInputAsync(data) {
    const parcels = this._adaptParcels(data);
    return new Promise((resolve) => {
      resolve({
        Method: 'GetRates',
        Params: {
          to_address: data.to,
          from_address: data.from,
          parcels: parcels,
          Insurance: data.shipmentValueInfo.insurance,
          InsuranceCurrency: data.shipmentValueInfo.insuranceCurrency,
          CashOnDelivery: data.shipmentValueInfo.cashOnDelivery,
          CashOnDeliveryCurrency: data.shipmentValueInfo.cashOnDeliveryCurrency,
          ContentDescription: data.shipmentValueInfo.contentDescription,
          TotalValue: `${data.shipmentValueInfo.totalValue} ${data.shipmentValueInfo.totalValueCurrency}`,
          ShippingService: data.shipmentValueInfo.shippingService
        }
      });
    });
  }

  _mapRatesRequestOutputAsync(data) {
    return new Promise((resolve) => {
      if (!data.Rates) resolve([]);
      resolve(data.Rates.map(r => {
        return {
          carrier: {
            smsid: +r.carrier_id,
            name: r.carrier,
            service: r.service,
          },
          rate: r.rate,
          rateId: r.rate_id,
          deliveryDays: r.delivery_days,
          currency: r.currency
        };
      }));
    });
  }


  // MAPPING FUNCTIONS - SHIPMENT CREATION

  async _mapShipmentRequestInputAsync(data) {
    const carrier = await this._getSpecificCarrierAsync(data.carrierId);
    const parcels = this._adaptParcels(data);
    const to_address = this._adaptAddress(data.to);
    const from_address = this._adaptAddress(data.from);
    return {
      Method: 'Ship',
      Params: {
        to_address: to_address,
        from_address: from_address,
        parcels: parcels,
        DeliveryFreightTypeCode: 'DAP',
        Insurance: data.shipmentValueInfo.insurance,
        InsuranceCurrency: data.shipmentValueInfo.insuranceCurrency,
        CashOnDelivery: data.shipmentValueInfo.cashOnDelivery,
        CashOnDeliveryCurrency: data.shipmentValueInfo.cashOnDeliveryCurrency,
        ContentDescription: data.shipmentValueInfo.contentDescription,
        TotalValue: `${data.shipmentValueInfo.totalValue} ${data.shipmentValueInfo.totalValueCurrency}`,
        ShippingService: data.shipmentValueInfo.shippingService,
        CarrierID: data.carrierId,
        CarrierName: carrier.name,
        CarrierService: carrier.service,
        TransactionID: data.orderId,
        OrderID: '',
        RateID: data.rateId,
        Incoterm: data.incoterm,
        PaymentMethod: data.paymentMethod,
        BillAccountNumber: '',
        Note: data.note ? data.note : '',
        Async: false
      }
    };
  }

  _mapShipmentRequestOuputAsync(data) {
    return new Promise((resolve) => {
      resolve({
        error: data.Error,
        shipmentId: data.NewOrderID,
        labelUrl: Array.isArray(data.LabelURL) ? data.LabelURL : [data.LabelURL],
        trackingCarrier: data.TrackingCarrier,
        trackingNumber: data.TrackingNumber
      });
    });
  }

  // MAPPING FUNCTIONS - SHIPMENT STATUS

  _mapShipmentStatusInputAsync(data) {
    return new Promise((resolve) => {
      resolve({
        Method: 'GetOrder',
        Params: {
          OrderID: data
        }
      });
    });
  }

  async _mapShipmentStatusOutputAsync(data) {

    let shipmentStatus;
    switch (data.Status) {
      case '1':
        shipmentStatus = 'INFO_RECEIVED';
        break;
      case '2':
        shipmentStatus = 'IN_TRANSIT';
        break;
      case '3':
        shipmentStatus = 'OUT_FOR_DELIVERY';
        break;
      case '4':
        shipmentStatus = 'MISSED_DELIVERY';
        break;
      case '5':
        shipmentStatus = 'EXCEPTION';
        break;
      case '6':
        shipmentStatus = 'DELIVERED';
        break;
    }

    const carrier = await this._getSpecificCarrierAsync(data.CarrierID);
    return {
      error: data.Error,
      shipmentId: data.OrderID,
      smsid: data.TransactionID,
      carrier,
      trackingCarrier: data.TrackingCarrier,
      trackingNumber: data.TrackingNumber,
      status: shipmentStatus,
      labelUrl: Array.isArray(data.LabelURL) ? data.LabelURL : [data.LabelURL],
    };
  }

  // MAPPING FUNCTIONS - GET CARRIERS

  _mapGetCarriersInputAsync() {
    return new Promise((resolve) => {
      resolve({
        Method: 'GetCarriers',
        Params: {}
      });
    });
  }

  _mapGetCarriersOutputAsync(data) {
    return new Promise((resolve) => {
      let carriers = [];
      for (const carrierName in data.Carriers) {
        if (Object.hasOwnProperty.call(data.Carriers, carrierName)) {
          const carrierServices = data.Carriers[carrierName];
          carrierServices.forEach(cs => {
            carriers.push({
              smsid: cs.CarrierID,
              name: carrierName,
              service: cs.CarrierService
            });
          });
        }
      }

      resolve(carriers);
    });
  }

  // MAPPING FUNCTIONS - GET/CREATE MANIFEST

  _mapCreateManifestInputAsync(data) {
    return new Promise((resolve) => {
      resolve({
        Method: 'CreateManifest',
        Params: {
          OrderIDS: [parseInt(data)] // it will be a single number instead of a string, otherwise shippypro fails
        }
      });
    });
  }

  _mapGetManifestInputAsync(data) {
    return new Promise((resolve) => {
      resolve({
        Method: 'GetManifest',
        Params: {
          ManifestNumber: data
        }
      });
    });
  }

  _mapGetManifestOutputAsync(data) {
    return new Promise((resolve) => {
      const manifestUrl = Array.isArray(data.ManifestURL)
        ? data.ManifestURL[data.ManifestURL.length - 1]
        : data.ManifestURL;
      resolve({
        error: data.Error,
        manifestUrl: manifestUrl,
        manifestNumber: data.ManifestNumber
      });
    });
  }

  _adaptParcels(data) {
    // adapting parcel weight
    const parcels = [];
    for (const dataParcel of data.parcels) {
      // from grams to Kg
      dataParcel.weight /= 1000;
      dataParcel.weight_unit = "KG";
      dataParcel.height /= 10; // from mm to cm
      dataParcel.length /= 10; // from mm to cm
      dataParcel.width /= 10; // from mm to cm
      parcels.push(dataParcel);
    }
    return parcels;
  }

  _adaptAddress(data) {
    return {
      name: data.name ? data.name.substring(0, 35) : '',
      company: data.company ? data.company.substring(0, 35) : '',
      street1: data.street1 ? data.street1.substring(0, 35) : '',
      street2: data.street2 ? data.street2.substring(0, 35) : '',
      city: data.city ? data.city.substring(0, 35) : '',
      state: data.state ? data.state.substring(0, 35) : '',
      zip: data.zip ? data.zip.substring(0, 12) : '',
      country: data.country,
      phone: data.phone ? data.phone.substring(0, 25) : '+39',
      email: data.email ? data.email.substring(0, 50) : '',
    }
  }

  // MAPPING FUNCTIONS - PICKUP

  async _mapBookPickupInputAsync(data) {
    const carrier = await this._getSpecificCarrierAsync(data.carrierId);
    const parcels = this._adaptParcels(data);
    const to_address = this._adaptAddress(data.to);
    const from_address = this._adaptAddress(data.from);
    return {
      Method: 'BookPickup',
      Params: {
        to_address: to_address,
        from_address: from_address,
        parcels: parcels,
        CarrierName: carrier.name,
        CarrierID: data.carrierId,
        PickupTime: Math.round(data.pickupTime.getTime() / 1000),
        PickupNote: data.pickupNote ? data.pickupNote : '',
        PickupMorningMintime: data.pickupMorningMinTime,
        PickupMorningMaxtime: data.pickupMorningMaxTime,
        PickupAfternoonMintime: data.pickupAfternoonMinTime,
        PickupAfternoonMaxtime: data.pickupAfternoonMaxTime,
        OrderIds: data.orderIds,
      }
    };
  }

  _mapBookPickupOutputAsync(data) {
    return new Promise((resolve) => {
      if (data.Result === 'OK') {
        return resolve({
          result: `${data.Result}`,
          message: `${data.Message}`,
          confirmationId: `${data.ConfirmationID}`,
        });
      } else {
        return resolve({
          error: `${data.Error}`,
          message: `${data.ValidationErrors}`,
        })
      }
    });
  }

}

module.exports = {
  adapter: new ShippyProAdapter(),
};