import ZoneRatesRepositoryService from './rates-repository.service'; import ZoneRatesHelperService from './shipping-rates-helper.service'; import { NullEmptyChecker } from '@deliverysolutions/utils'; import { CustomRatesContext } from '@deliverysolutions/rule-engine'; import { logger } from '@deliverysolutions/logger'; import Currency from './currency.service'; import { cloneDeep } from 'lodash'; import { ProviderController } from '@deliverysolutions/dsp-integrator'; import { CUSTOM_RATES_ERRORS, DEFAULT_SHIPPING_RATE_CURRENCY_CONFIG } from '@constants/rates.constant'; import { GenericErrorType } from '@deliverysolutions/response-error-structure'; import { EstimateErrors, Estimates, EstimatesList, EstimatesResponse } from '@deliverysolutions/rule-engine/src/pojos/Estimates'; import { PostalCodes } from '@interfaces/postal-codes.interface'; import { ZoneRatesParameters } from '@interfaces/zone-rates'; import { OrderDTO } from '@dtos/order.dto'; import { CorporateDsp } from '@deliverysolutions/rule-engine/src/pojos/CorporateDsp'; import { CorporateDsp as ExtendedCorporateDsp } from '@deliverysolutions/dsp-integrator/src/pojos/CorporateDsp'; import { ServiceData } from '@interfaces/postal-services.interface'; export default class ZoneRatesService { ratesHelperService = new ZoneRatesHelperService(); customRatesRepository = new ZoneRatesRepositoryService(); currencyInstance = new Currency(); /** * Main method which will be called from order.js. Takes the original estimates and original errors, overrides them with zone rates if applicable * @param {*} providerEstimates * @param {*} corporateDsps * @param {*} order * @returns Object { estimates, errors } */ async calculateRates(providerEstimates: EstimatesResponse, corporateDsps: CorporateDsp[], order: OrderDTO) { try { logger.info(`Shipping custom rate: calculating shipping rates with original estimates`, providerEstimates); const { estimates: originalEstimates, errors: originalErrors } = cloneDeep(providerEstimates); const finalEstimates: any[] = []; const finalErrors: any[] = []; const processedProviderServiceIdMap = new Map(); if (!this.ratesHelperService.isValidPostalCodes(order)) { return providerEstimates; } this.ratesHelperService.updateOriginalEstimatesWithValidErrors(originalEstimates as any, originalErrors, corporateDsps, order, finalErrors); const postalCodes = this.ratesHelperService.getPostalCodes(order); for (const providerLevelEstimate of originalEstimates as any) { if (NullEmptyChecker.isNonEmptyArray(providerLevelEstimate.data)) { await this.processEstimatesForProvider(providerLevelEstimate, corporateDsps, finalEstimates, finalErrors, postalCodes, order, processedProviderServiceIdMap ); } } logger.info(`Shipping custom rate: final estimates after calculating shipping rates`, finalEstimates, finalErrors); return { estimates: finalEstimates, errors: finalErrors } } catch (error) { logger.error('Shipping custom rate: Error occurred in shipping rate calculation: ', error); const zoneEstimates = this.ratesHelperService.updateEstimatesWithInternalServerError(providerEstimates, corporateDsps); return zoneEstimates; } } /** * Process estimates for each provider * @param {*} providerLevelEstimate * @param {*} corporateDsps * @param {*} finalEstimates * @param {*} finalErrors * @param {*} postalCodes * @param {*} order * @param {*} processedProviderServiceIdMap */ async processEstimatesForProvider(providerLevelEstimate: ServiceData, corporateDsps: CorporateDsp[], finalEstimates: EstimatesList[], finalErrors: EstimateErrors[], postalCodes: PostalCodes, order: OrderDTO, processedProviderServiceIdMap: any) { const serviceEstimates = providerLevelEstimate.data; const [{ provider: providerDisplayName }] = serviceEstimates as any; const corporateDsp = corporateDsps.find((dsp: any) => dsp.getDisplayName() === providerDisplayName) as CorporateDsp; const { error: emptyPackagesError } = this.ratesHelperService.validatePackages(order, corporateDsp); const eligible = this.ratesHelperService.shouldCalculateServiceLevelRates(corporateDsp, finalErrors, emptyPackagesError, finalEstimates, providerLevelEstimate as unknown as EstimatesList); if (eligible) { const providerEstimateData = { ...providerLevelEstimate, data: [] }; const [zones, remoteZipCodes] = await Promise.all([ this.customRatesRepository.getZones({ provider: corporateDsp.getDspName(), ...postalCodes }), this.customRatesRepository.getExtendedZipCodes(corporateDsp.getDspName(), postalCodes.sourcePostalCode, postalCodes.destinationPostalCode) ]); const billableWeights = this.calculateBillableWeights(order, corporateDsp); this.ratesHelperService.updateServiceIdNameMap(corporateDsp); logger.info(`Shipping custom rate: ${providerDisplayName} is eligible for zone rates. Available zones and billableWeights `, zones, billableWeights); const ratePromises: Array = []; for (const estimate of serviceEstimates) { !processedProviderServiceIdMap.get(this.ratesHelperService.getProviderServiceIdMapKey(estimate.provider, estimate.serviceId)) && ratePromises.push(this.processEstimate({ estimate, corporateDsp, zones, finalEstimates: providerEstimateData.data, finalErrors, billableWeights, order, processedProviderServiceIdMap, remoteZipCodes } as any)); processedProviderServiceIdMap.set(this.ratesHelperService.getProviderServiceIdMapKey(providerDisplayName, estimate.serviceId), true); } await Promise.all(ratePromises); NullEmptyChecker.isNonEmptyArray(providerEstimateData.data) && finalEstimates.push(providerEstimateData as unknown as EstimatesList); } } /** * Calculate estimate for each service and update finalEstimates * @param {*} Object * @returns */ async processEstimate({ estimate, zones, finalEstimates, finalErrors, billableWeights, order, remoteZipCodes, corporateDsp }: ZoneRatesParameters.ProcessEstimates) { const { serviceId, provider: providerDisplayName } = estimate; const zone = zones?.find(({ id }: any) => serviceId === id)?.zone; if (!zone) { const error = this.ratesHelperService.constructError({ provider: providerDisplayName, service: serviceId, message: CUSTOM_RATES_ERRORS.MISSING_ZONE, type: GenericErrorType.INVALID_DATA }); return finalErrors.push(error); } const totalAmount = await this.calculateRateForService({ order, providerDisplayName, serviceId: serviceId as string, zone, billableWeights, remoteZipCodes, finalErrors, corporateDsp }); if (!NullEmptyChecker.isNull(totalAmount)) { const normalizedAmount = await this.normalizeAmount(order, totalAmount || 0); estimate.amount = normalizedAmount; estimate.currencyCode = order.currencyCode || ''; estimate.currency = this.ratesHelperService.getCurrency(order.currencyCode as string) as string, finalEstimates.push(estimate); } } /** * Iterates over each package and calculate rate for service * @param {*} Object * @returns null | number (rate + surcharge for all packages) */ async calculateRateForService({ order, providerDisplayName, serviceId, zone, billableWeights, remoteZipCodes, corporateDsp, finalErrors }: ZoneRatesParameters.CalculateRateForService) { const totalRates = await this.getRatesForAllPackages({ order, providerDisplayName, serviceId, zone, billableWeights, finalErrors }); logger.info(`Shipping custom rate: rates calculated for all packages for ${serviceId}`, totalRates); if (this.ratesHelperService.isPackageLevelAmountsValid(totalRates)) { const surchargeResponse: any = await this.getSurchargesForAllPackages({ order, billableWeights, zone, serviceId, remoteZipCodes, corporateDsp, finalErrors }); const totalSurcharges = surchargeResponse.map((response: any) => response.surcharge); logger.info(`Shipping custom rate: surcharges calculated for all packages for ${serviceId}`, totalSurcharges); if (this.ratesHelperService.isPackageLevelAmountsValid(totalSurcharges)) { return this.ratesHelperService.sumBaseRateWithSurcharge(totalRates || [], totalSurcharges); } } this.ratesHelperService.updateServiceFailureError(providerDisplayName, serviceId, finalErrors); return null; } async getRatesForAllPackages({ order, providerDisplayName, serviceId, zone, billableWeights, finalErrors }: ZoneRatesParameters.GetRatesForAllPackages) { const ratesPromises: Array = []; if (order.packages) { for (let packageIndex = 0;packageIndex < order.packages.length;packageIndex++) { ratesPromises.push(this.calculateRateForPackage({ order, providerDisplayName, serviceId, zone, weight: billableWeights[packageIndex] as number, })); } } try { return Promise.all(ratesPromises); } catch (error) { logger.error(`Shipping custom rate: error occurred in rate calculation`, error); this.ratesHelperService.updateServiceFailureError(providerDisplayName, serviceId, finalErrors); return null; } } async getSurchargesForAllPackages({ order, billableWeights, zone, serviceId, remoteZipCodes, baseRate, finalErrors, corporateDsp }: ZoneRatesParameters.GetSurchargesForAllPackages) { const customRatesContext = new CustomRatesContext(); customRatesContext.setStrategy('ZONE'); const surchargePromises: Array = []; if (order.packages) { for (let packageIndex = 0;packageIndex < order.packages.length;packageIndex++) { const options = { package: order.packages[packageIndex], billableWeight: billableWeights[packageIndex], zone: isNaN(Number(zone)) ? zone : Number(zone), serviceId, extendedPostalCodes: remoteZipCodes ? remoteZipCodes : { pickupType: null, dropoffType: null }, baseRate } surchargePromises.push(customRatesContext.getSurcharges(order, corporateDsp, options)); } } try { return Promise.all(surchargePromises); } catch (error) { logger.error(`Shipping custom rate: error occurred in surcharge calculation`, error); this.ratesHelperService.updateServiceFailureError(corporateDsp.getDisplayName(), serviceId, finalErrors); return null; } } /** * Calculate rate for each package * @param {*} Object * @returns number */ async calculateRateForPackage({ order, providerDisplayName, serviceId, zone, weight }: ZoneRatesParameters.CalculateRateForPackage) { const activeVersionPayload = { tenantId: order.tenantId, providerDisplayName, serviceId, }; const activeVersion = await this.customRatesRepository.getActiveRateVersion(activeVersionPayload); const ratesPayload = { ...activeVersionPayload, zone, weight, version: activeVersion }; return this.customRatesRepository.getRate(ratesPayload); } /** * * @param {*} order * @returns */ calculateBillableWeights(order: OrderDTO, corporateDsp: CorporateDsp) { const billableWeights: Array = []; if (order.packages) { for (const packageInfo of order.packages) { const billableWeight = ProviderController.getBillableWeight(packageInfo as any, corporateDsp as unknown as ExtendedCorporateDsp); billableWeights.push(billableWeight); } } return billableWeights; } /** * Takes the order object and amount * return currencyCode in request is USD, will return the amount in cents * Else convert the currency to requested format and return that amount * @param {Order} order * @param {number} amount * @returns Promise */ async normalizeAmount(order: OrderDTO, amount: number) { if (order.currencyCode === DEFAULT_SHIPPING_RATE_CURRENCY_CONFIG.CODE) { return this.ratesHelperService.convertToMinorUnits(amount); } const convertedAmount = await this.currencyInstance.convertCurrencies({ to: order.currencyCode, base: DEFAULT_SHIPPING_RATE_CURRENCY_CONFIG.CODE, val: amount, tenantId: order.tenantId, orderId: order._id?.toString() || order.orderId || '', orderExternalId: order.orderExternalId }); return this.ratesHelperService.convertToMinorUnits(convertedAmount); } }