const _get = require('lodash.get'); import { EmptyResultError, ValidationError } from '../errors'; import { DIGITAL_AND_BUNDLE_PRODUCT_CODES, DIGITAL_PRODUCT_CODES, EPAPER_CODE, PREMIUM_DIGITAL_AND_BUNDLE_PRODUCT_CODES, PRINT_PRODUCT_CODES, STANDARD_DIGITAL_AND_BUNDLE_PRODUCT_CODES } from '../static-data/product-codes'; import { OfferResponse, Charges, Pricing, FormattedPrice } from '../types/offer'; const DIGITAL = 'DIGITAL'; const TRIAL = 'TRIAL'; const BUNDLE = 'BUNDLE'; const PRINT = 'PRINT'; const BASIS_RECURRING = 'RECURRING'; /** Class representing a offer. */ export class Offer { public rawResponse: OfferResponse; public id: string; public type: string; public productName: string; public productType: string; public productCode: string; public pricing: OfferResponse['pricing']; public productCodes: Array; public printProductCode: string; public digitalProductCode: string; public displayName: string; /** * Create a offer * @param offer Membership graphql offerById data response. */ constructor(offer: OfferResponse) { try { this.rawResponse = offer; this.id = offer.id; this.type = offer.type.toUpperCase(); this.productName = offer.product.name; // when we don't have a displayName let's default // to an empty string so that we can fallback to offer mapping name this.displayName = offer.displayName || ""; this.productType = offer.product.type.toUpperCase(); this.productCode = offer.product.code.toUpperCase(); this.productCodes = offer.product.products || []; this.printProductCode = this.productCodes.find(code => PRINT_PRODUCT_CODES.includes(code.toUpperCase())) || ''; this.digitalProductCode = this.productCodes.find(code => DIGITAL_AND_BUNDLE_PRODUCT_CODES.includes(code.toUpperCase())) || ''; this.pricing = offer.pricing; } catch (error) { throw new ValidationError('Invalid offer response', error); } } /** * Is this offer a digital product. * @returns true|false */ public isDigital(): boolean { return this.productType === DIGITAL; } /** * Is this offer a trial product. * @returns true|false */ public isTrial(): boolean { return this.type === TRIAL; } /** * Is this offer a print product. * @returns true|false */ public isPrint(): boolean { return this.productType === PRINT && this.productCode !== EPAPER_CODE; } /** * Is this offer a bundle product. * @returns true|false */ public isBundle(): boolean { return this.productType === BUNDLE; } /** * Is this offer a standard product. * @returns true|false */ public isStandard(): boolean { return STANDARD_DIGITAL_AND_BUNDLE_PRODUCT_CODES.includes(this.productCode); } /** * Is this offer a premium product. * @returns true|false */ public isPremium(): boolean { return PREMIUM_DIGITAL_AND_BUNDLE_PRODUCT_CODES.includes(this.productCode); } /** * Is this offer an ePaper product. * @returns true|false */ public isEpaper(): boolean { return this.productType === PRINT && this.productCode === EPAPER_CODE; } /** * Returns print product code. * @returns N5D, N6D, NWE */ public getPrintProductCode(): string { const printProductCode = this.productCodes.find(code => PRINT_PRODUCT_CODES.includes(code.toUpperCase())); if (!printProductCode) { throw new EmptyResultError('No print product code for this offer could be found', { offerId: this.id }); } return printProductCode; } /** * Returns digital product code. * @returns P1, P2, P3 */ public getDigitalProductCode(): string { const digitalProductCode = this.productCodes.find(code => DIGITAL_PRODUCT_CODES.includes(code.toUpperCase())); if (!digitalProductCode) { throw new EmptyResultError('No digital product code for this offer could be found', { offerId: this.id }); } return digitalProductCode; } /** * Gets all the available billing countries for the offer. * @returns all the available billing countries (three letter ISO 3166-1 country code). */ public getBillingCountries(): Array { const billingCountries = this.pricing.map((price: Pricing) => price.country); if (!billingCountries.length) { throw new EmptyResultError('No billing countries for this offer could be found', { offerId: this.id }); } return billingCountries; } /** * Return billing countries that offer this currency code * @param currencyCode ISO 4217 3 character currency code * @returns all the available billing countries (three letter ISO 3166-1 country code). */ public getBillingCountriesForCurrency(currencyCode: string): Array { const billingCountries = this.pricing .filter((price: Pricing) => price.currency === currencyCode) .map((price: Pricing) => price.country); if (!billingCountries.length) { throw new EmptyResultError('No billing countries for this offer could be found', { offerId: this.id }); } return billingCountries; } /** * Gets all the prices for the specified country in the offer. * @param countryCode three letter ISO 3166-1 country code. * @param pricing all pricing for the offer. * @returns only the pricing for the country code given. */ public getCountryPricing(countryCode: string = '', pricing: OfferResponse['pricing'] = []): OfferResponse['pricing'] { const code = countryCode.toUpperCase(); const countryPricing = pricing.filter((price: Pricing) => _get(price, 'country', '').toUpperCase() === code); if (!countryPricing.length) { throw new EmptyResultError(`Pricing could not be found for country code: ${countryCode}`); } return countryPricing; } /** * Gets all the prices for the specified fulfilment option in the offer. * @param fulfilmentOption newspaper delivery type. * @param pricing all pricing for the offer. * @returns only the pricing for the fulfilment option given. */ public getFulfilmentPricing(fulfilmentOption: string = '', pricing: OfferResponse['pricing'] = []): OfferResponse['pricing'] { const option = fulfilmentOption.toUpperCase(); const fulfilmentPricing = pricing.filter((price: Pricing) => _get(price, 'option.code', '').toUpperCase() === option); if (!fulfilmentPricing.length) { throw new EmptyResultError(`Pricing could not be found for this fulfilmentOption: ${fulfilmentOption}`); } return fulfilmentPricing; } /** * Gets all the prices for the specified term in the offer. * @param term payment/billing term - how often the user will make a payment. * @param pricing all pricing for the offer. * @returns only the pricing for the term given. */ public getTermPricing(term: string = '', pricing: OfferResponse['pricing'] = []): Pricing { const price = pricing.find(price => this.pricingHasTerm(term, price)); if (!price) { throw new EmptyResultError(`Pricing could not be found for paymentTerm: ${term}`); } return price; } /** * Gets all formatted charges for an offer based on country code and fulfilment option. * @param countryCode three letter ISO 3166-1 country code. * @param fulfilmentOption newspaper delivery type. * @returns all formatted charges for given country code and fulfilment option. */ public getAvailableCharges(countryCode: string, fulfilmentOption?: string): Array { if (countryCode && fulfilmentOption) { return this.getAvailableChargesForCountryAndFulfilmentOption(countryCode, fulfilmentOption); } else if (countryCode) { return this.getAvailableChargesForCountry(countryCode); } else { return this.getAllAvailableCharges(); } } /** * Gets all available charges for the offer. * @returns all available formatted charges. */ public getAllAvailableCharges(): Array { return this.pricing.map(price => this.formatPricing(price)); } /** * Gets available chargers for specified country. * @param countryCode three letter ISO 3166-1 country code. * @returns formatted charges for specified country. */ public getAvailableChargesForCountry(countryCode: string): Array { const pricing = this.getCountryPricing(countryCode, this.pricing); return pricing.map(price => this.formatPricing(price)); } /** * Gets available chargers for specified country and fulfilment option. * @param countryCode three letter ISO 3166-1 country code. * @param fulfilmentOption newspaper delivery type. * @returns formatted charges for specified country and fulfilment option. */ public getAvailableChargesForCountryAndFulfilmentOption(countryCode: string, fulfilmentOption: string): Array { const pricing = this.pricing.filter(price => price.country.toUpperCase() === countryCode.toUpperCase() && _get(price, 'option.code', '').toUpperCase() === fulfilmentOption.toUpperCase() ); if (!pricing.length) { throw new EmptyResultError(`Pricing could not be found for the countryCode: ${countryCode} and fulfilmentOption: ${fulfilmentOption}`); } return pricing.map(price => this.formatPricing(price)); } /** * Gets currency for given specified country code * @param countryCode three letter ISO 3166-1 country code. * @returns currency ISO 4217 code. */ public getCurrency(countryCode: string): string { const countryPricing = this.getCountryPricing(countryCode, this.pricing); return _get(countryPricing, '[0].currency'); // Each price in the countryPricing array will have the same currency. } /** * Gets subscription term. * @param price selected price. * @returns displayName and iso8601Duration. */ public getSubscriptionTerm(price: Pricing): Charges['subscriptionTerm'] { const charge = price.charges.find(charge => charge.basis.toUpperCase() === BASIS_RECURRING); if (!charge) { throw new EmptyResultError(`No charges were found for with the property of basis: ${BASIS_RECURRING}`); } // For trials set the information about when the main payment will start if (this.isTrial()) { charge.subscriptionTerm.paymentDueAfter = _get(charge, 'paymentDue.after'); } return charge.subscriptionTerm; } /** * Formats price by creating a new price object. * @param price selected price. * @returns formatted price. */ private formatPricing(price: Pricing): FormattedPrice { const subscriptionTerm = this.getSubscriptionTerm(price); const value = _get(price, 'total.value'); // For bundle charges, digital and print are split into two separate charges and therefore we need the total amount to get the correct price. const currencySymbol = _get(price, 'total.symbol'); const trialValue = _get(price, 'trialTotal.value', null); const formattedPrice = Object.assign({}, { subscriptionName: subscriptionTerm.displayName, subscriptionCode: subscriptionTerm.iso8601Duration, subscriptionPaymentDue: subscriptionTerm.paymentDueAfter, subscriptionAutoRenewTerm: subscriptionTerm.autoRenewTerm, subscriptionTermType: subscriptionTerm.termType, currency: price.currency, symbol: currencySymbol, option: price.option, value, trialValue, }); return formattedPrice; } /** * Determines if charge exists for a specified term. * @param term payment/billing term - how often the user will make a payment. * @param charge selected charge. * @returns true|false */ private chargeHasTerm(term: string, charge: Pricing['charges']): boolean { const displayName = _get(charge, 'subscriptionTerm.displayName', ''); // trial basis ONE_TIME offer charges do not have a subscriptionTerm return displayName.toLowerCase() === term.toLowerCase(); } /** * Determines if pricing has a specified term. * @param term payment/billing term - how often the user will make a payment. * @param price selected price. * @returns true|false */ private pricingHasTerm(term: string, price: Pricing): boolean { return !!_get(price, 'charges').find((charge: Pricing['charges']) => this.chargeHasTerm(term, charge)); } }