import moment, { Moment } from 'moment-timezone'; interface DeliveryZone { id: string; postalCode: string; } interface DeliveryCoverage { id: string; deliveryZones: DeliveryZone[]; } interface Holiday { id: string; startAt: string; endAt: string; } interface DeliveryConfig { id: string; deliveryCoverage: DeliveryCoverage[]; deliveryTiming: { id: string; leadtime: number; cutOffHour: number; sameDays: boolean[]; days: boolean[]; }; holidays: Holiday[]; deliveryPrice: { id: string; sameDayPrice: number; nextDayPrice: number; perItemDeliveryFee: boolean; } } interface Merchant { id: string; holidays: Holiday[]; timezone: string; } interface Config { id: string; deliveryConfigs: DeliveryConfig[]; merchant: Merchant } export default class DeliveryValidator { isPostalCodeWithinCoverage = false; canDeliverSameDay = false; postalCode: string; deliveryConfigs: DeliveryConfig[][]; deliveryOptions: any = []; merchant: Merchant; constructor ( deliveryConfig: Config[], postalCode: string ) { if (!deliveryConfig || !postalCode) { throw new Error('DeliveryValidator requires a valid delivery config and postalCode to initialise'); } this.merchant = deliveryConfig[0].merchant; this.postalCode = postalCode; this.deliveryConfigs = deliveryConfig.map(config => config.deliveryConfigs); } public hasValidDeliveryCoverage = (): boolean => { return this.isPostalCodeWithinCoverage; } public hasSameDaySupport = (): boolean => { return this.deliveryConfigs.every(deliveryConfig => { return deliveryConfig.some(config => { return config.deliveryTiming.sameDays[moment().day() - 1] && this.hasValidDaysAndHoliday(config, moment().tz(this.merchant.timezone)) && this.isBeforeCutoff(config); }) }) } public findSameDayConfig = (momentInstance: Moment): DeliveryConfig => { // @ts-ignore return [].concat.apply([], this.deliveryConfigs.map(configs => this.findSingleSameDayConfig(configs, momentInstance))) .reduce((acc: any, curr: any, currIndex: number): any => { const shouldReturnCurr = !currIndex || !curr.deliveryPrice || curr.deliveryPrice.sameDayPrice > acc.deliveryPrice.sameDayPrice; return shouldReturnCurr ? curr : acc; }, {}) } public findSingleNextDayConfig = (configs: DeliveryConfig[], momentInstance: Moment): DeliveryConfig[] => { return configs.filter((config) => this.hasValidCoverage(config.deliveryCoverage) && this.hasValidDaysAndHoliday(config, momentInstance) ); } public findNextDayConfig = (momentInstance: Moment): DeliveryConfig | null => { const validConfigs = [].concat // @ts-ignore .apply([], this.deliveryConfigs.map((configs) => this.findSingleNextDayConfig(configs, momentInstance))) if (!validConfigs.length) return null; return validConfigs .reduce((acc: any, curr: any, currIndex: any): any => { const shouldReturnCurr = !currIndex || !curr.deliveryPrice || curr.deliveryPrice.sameDayPrice > acc.deliveryPrice.sameDayPrice; return shouldReturnCurr ? curr : acc; }); } public validateDeliveryZones = (): DeliveryValidator => { this.isPostalCodeWithinCoverage = this.deliveryConfigs.some(configs => { return configs.some(config => this.hasValidCoverage(config.deliveryCoverage)) }); return this; } public validateSameDay = (): DeliveryValidator => { this.canDeliverSameDay = this.hasSameDaySupport(); return this; } public buildDeliveryOptions = (): DeliveryValidator => { this.deliveryOptions = []; if (this.canDeliverSameDay) { const config = this.findSameDayConfig(moment().tz(this.merchant.timezone)); if (config.id) { this.deliveryOptions.push( this.setDeliveryOptionConfig( this.findSameDayConfig, moment().tz(this.merchant.timezone), 'sameDayPrice' ) ); } } for (let i = 1, len = 28; i < len; i++) { this.deliveryOptions.push( this.setDeliveryOptionConfig( this.findNextDayConfig, moment().tz(this.merchant.timezone).add(i, 'd'), 'nextDayPrice' ) ) } return this; } public buildConfig = (): any => { return { isPostalCodeWithinCoverage: this.isPostalCodeWithinCoverage, canDeliverSameDay: this.canDeliverSameDay, deliveryOptions: this.deliveryOptions } } private hasValidCoverage = (coverage: DeliveryCoverage[]): boolean => { return coverage.some((coverage: any) => coverage.deliveryZones.length); } private getDayIndex = (momentInstance: Moment): number => { const momentDayIndex = momentInstance.day() - 1; return momentDayIndex === -1 ? 6 : momentDayIndex; } private setDeliveryOptionConfig = ( getConfig: (momentDay: Moment) => any, momentDay: Moment, sameOrNextDay: 'sameDayPrice' | 'nextDayPrice' ) => { const config = getConfig(momentDay); if (config && config.id) { return { config, date: this.setDeliverydate(momentDay, config ? config.deliveryTiming.cutOffHour : 13), price: config ? config.deliveryPrice[sameOrNextDay] : null, canDeliver: !!config, sameDay: sameOrNextDay === 'sameDayPrice' } } return { config: null, date: this.setDeliverydate(momentDay, 13), price: null, canDeliver: false, sameDay: false } } private hasValidDaysAndHoliday = ( config: DeliveryConfig, momentDay: Moment ): boolean => { return !this.isWithinHolidayRange(config, momentDay) && config.deliveryTiming.days[this.getDayIndex(momentDay)]; } private isBeforeCutoff = (config: DeliveryConfig): boolean => moment() .tz(this.merchant.timezone) .hour() < config.deliveryTiming.cutOffHour; private isWithinHolidayRange = ( config: DeliveryConfig, momentDay: Moment ): boolean => { const holidays = [...config.holidays, ...this.merchant.holidays]; return holidays .some(holiday => momentDay.isBetween(holiday.startAt, holiday.endAt)) } private setDeliverydate = (momentDay: Moment, cutOffHour: number) => { return momentDay .utc() .hour(cutOffHour) .minute(0) .second(0) .milliseconds(0) .toISOString() } private findSingleSameDayConfig = ( configs: DeliveryConfig[], momentInstance: Moment ): DeliveryConfig[] => { return configs.filter(config => { return this.hasValidCoverage(config.deliveryCoverage) && this.hasValidDaysAndHoliday(config, momentInstance) && this.isBeforeCutoff(config) }) } }