import { Base } from './base'; import { ValidationError, EmptyResultError, InvalidResponseError } from './errors'; import { TransitionType, SubscriptionTransition, SubscriptionCancellationResponse, UndoCancelSubscriptionResponse, UndoStepUpResponse, DowngradeOptionsResponse, } from './types/transition'; import { TransitionInfo } from './models/transition-info'; import { StepUpEntry, StepUpResponse } from './types/step-up'; export class Transition extends Base { private membershipApi = this.config.get('membershipApi'); private transitionApiKey = this.config.get('transitionApiKey'); private additionalStepupHeaders = { 'X-Origin-System-Id': this.config.get('originSystemId'), 'X-Origin-User': 'CS' }; public static TYPE = TransitionType; /** * This function is the public method for retrieving product transition options available for a particular user and offer ID. * @param userId User's ID. * @param offerId Offer ID to check transition options for. * @param countryCode To retrieve pricing for provided offer. * @param type When to transition to provided offer. * @param isInStepUp Whether the user is in step up. * @returns Contains transition information and available options. */ public async getUserTransitionInfo(userId: string, offerId: string, countryCode: string, type: TransitionType, isInStepUp: boolean): Promise { const url = `${this.membershipApi}/subs/transitions/subscriptions/transition-info?userId=${userId}&billingInfo=true&selfServe=true&offerId=${offerId}&countryCode=${countryCode}&type=${type}&checkForStepUpOptions=${!!isInStepUp}`; const apiResult = await this.requestGet({ url, key: this.transitionApiKey }); return new TransitionInfo(apiResult); } /** * Transition an eligible subscription to a new offer * * @param subscriptionNumber Subscription to update * @param offerId Offer to transition to * @param term Payment term * @param type Type of transition (immediate, endOfTerm, etc) * @param countryCode Three letter ISO 3166-1 country code i.e. GBR * @param isSingleTerm Optional boolean parameter to indicate whether the subscription is single term or auto renew. * @param fulfilmentOption Two letter optional string to indicate delivery option * @param hidePaymentMethod Boolean parameter for single term users to indicate whether new payment method should be deleted * @param cancelFutureAmendments Boolean parameter to apply transitions for subscriptions with scheduled cancellations * * * @returns Contains transition and subscription information. */ public async applyTransition(subscriptionNumber: string, offerId: string, term: string, type: TransitionType, countryCode: string, isSingleTerm: boolean=false, fulfilmentOption?: string, hidePaymentMethod?: boolean, cancelFutureAmendments: boolean=false): Promise { const url = `${this.membershipApi}/subs/transitions/subscriptions/transition/v2/${subscriptionNumber}?cancelFutureAmendments=${cancelFutureAmendments}`; const needsToBeTermed = term === 'P13M' || term === 'P26M';//effectively annual terms (1 year and 2 years) //This logic relates to the membership API implementation. //Single term subscriptions always need a termType equal to TERMED. //Annual terms always need a termType equal to TERMED, even if they are auto renew. All other terms (non annual and non single term) have EVERGREEN as the termType. const subscriptionTerm = { iso8601Duration: term, termType: (term.includes('Y') || needsToBeTermed) ? 'TERMED' : 'EVERGREEN', autoRenewTerm: isSingleTerm ? false : true, }; const body: any = { offerId, subscriptionTerm, type, countryCode, email: true, // sends confirmation email }; //single term renewals to other single terms always have termType set to TERMED if (isSingleTerm) { body.subscriptionTerm.termType = 'TERMED'; body.hidePaymentMethod = hidePaymentMethod; } if (fulfilmentOption) { body.option = fulfilmentOption; } return this.requestPost({ url, key: this.transitionApiKey, additionalHeaders: this.additionalStepupHeaders, body }); } /** * This function is the public method for cancelling a subscription. * @param userId The user's ID. * @param subscriptionNumber Subscription ID. * @param cancellationReason The user's reason for cancelling (freetext for information purposes). * @param isCustomerServiceAgent Whether this is a customer service agent doing the cancellation. Can be based off of req.cookies.FT_SessionHelpdesk. * @param cancellationEffectiveDate The date when the cancellation should take effect, in the format 'YYYY-MM-DD'. * @param sendEmail Whether to send the customer care email that is sent after a user cancels their subscription * @param cancellationType Type of cancellation (endOfTerm|ImmediateNoRefund|ImmediatePartialRefund|ImmediateFullRefund) * @param refundType Refund type ('ELECTRONIC'=via zuora | 'EXTERNAL': bank transfer) * @return success, cancellationEffectiveDate, subscriptionKey * @throws {EmptyResultError} When the result comes back empty. * @throws {InvalidResponseError} When the result comes back with a non-200 status code. */ public async cancelSubscription( userId: string, subscriptionNumber: string, cancellationReason: string, isCustomerServiceAgent: boolean, cancellationEffectiveDate?: string, sendEmail:boolean = true, cancellationType: string = 'endOfTerm', refundType: string = 'ELECTRONIC' ): Promise { if (!userId) { throw new ValidationError('User ID is not defined'); } if (!subscriptionNumber) { throw new ValidationError('Subscription Number is not defined'); } const url = `${this.membershipApi}/subs/transitions/subscriptions/cancel/v2/${subscriptionNumber}?cancelFutureAmendments=true`; const overrideXOriginUser = isCustomerServiceAgent ? { 'X-Origin-User': 'CS', } : {}; const additionalHeaders = { 'Cache-Control': 'no-cache', ...overrideXOriginUser }; const requestBody = { cancellationReason, cancellationType, userId, cancellationEffectiveDate, email: sendEmail, refundType, }; const response: SubscriptionCancellationResponse = await this.requestPost({ url, key: this.transitionApiKey, additionalHeaders: Object.assign({}, this.additionalStepupHeaders, additionalHeaders), body: requestBody }); if (!response) { throw new EmptyResultError('Empty subscription cancellation response.', { userId, additionalHeaders, subscriptionNumber, cancellationReason, isCustomerServiceAgent, response }); } else if (response.status !== 200) { throw new InvalidResponseError('Response returned with an unsuccessful result.', { userId, additionalHeaders, subscriptionNumber, cancellationReason, isCustomerServiceAgent, response }); } response.success = response.status === 200; return response; } /** * This function is the public method for undoing a subscription cancellation. * @param userId The user's ID. * @param subscriptionNumber Subscription ID. * @param isCustomerServiceAgent Whether this is a customer service agent doing the cancellation. Can be based off of req.cookies.FT_SessionHelpdesk. * @return response from FT Core service * @throws {ValidationError} When validation fails. * @throws {EmptyResultError} When the result comes back empty. * @throws {InvalidResponseError} When the result comes back with `success: false`. */ public async undoCancelSubscription( userId: string, subscriptionNumber: string, isCustomerServiceAgent: boolean ): Promise { if (!userId) { throw new ValidationError('User ID is not defined'); } if (!subscriptionNumber) { throw new ValidationError('Subscription Number is not defined'); } const url = `${this.membershipApi}/subs/transitions/subscriptions/cancel/${subscriptionNumber}`; const overrideXOriginUser = isCustomerServiceAgent ? { 'X-Origin-User': 'CS', } : {}; const additionalHeaders = { 'Cache-Control': 'no-cache', ...overrideXOriginUser }; const response: UndoCancelSubscriptionResponse = await this.requestDelete({ url, key: this.transitionApiKey, additionalHeaders: Object.assign({}, this.additionalStepupHeaders, additionalHeaders), }); if (!response) { throw new EmptyResultError('Empty UNDO subscription cancellation response.', { userId, additionalHeaders, subscriptionNumber, isCustomerServiceAgent, response }); } else if (response.status !== 200) { throw new InvalidResponseError('Response returned with an unsuccessful result.', { userId, additionalHeaders, subscriptionNumber, isCustomerServiceAgent, response }); } response.success = response.status === 200; return response; } /** * This function is the public method for preventing a price from being increased at the end of a subscription period, and freezing it at the current price. * @param subscriptionNumber Subscription ID. * @returns { success, cancellationEffectiveDate, subscriptionKey } * @throws {InvalidResponseError} When the result comes back with a non-200 status code. */ public async undoStepUp(subscriptionNumber: string): Promise { if (!subscriptionNumber) { throw new ValidationError('Subscription Number is not defined'); } const url = `${this.membershipApi}/subs/transitions/subscriptions/stepup/${subscriptionNumber}`; const response = await this.requestDelete({ url, key: this.transitionApiKey, additionalHeaders: this.additionalStepupHeaders }); if (response.status !== 200) { throw new InvalidResponseError('Response returned with an unsuccessful result.', { subscriptionNumber, response }); } response.success = response.status === 200; return response; } /** * This function is the public method for stepping up a user to a new price point. * @param stepUpData Relevant data. * @returns { success, cancellationEffectiveDate, subscriptionKey } * @throws {InvalidResponseError} When the result comes back with invalid JSON. */ public async scheduleStepUp(stepUpData: StepUpEntry): Promise { const url = `${this.membershipApi}/subs/transitions/subscriptions/stepup`; const headers = { 'X-Origin-User': 'customer', }; return this.requestPost({ url: `${url}/${stepUpData.subscriptionNumber}`, key: this.transitionApiKey, additionalHeaders: headers, body: stepUpData }); } /** * This function is the public method for retrieving a relevant alternative downgrade offer. * @param subscriptionNumber - The user's subscription reference ID. * @param checkForStepUpOptions - Whether to check for stepup options. * @returns Array of downgrade offers option. * @throws {InvalidResponseError} When the result comes back with invalid JSON. */ public async getDowngradeOptions(subscriptionNumber: string, checkForStepUpOptions: boolean = false): Promise { const url = `${this.membershipApi}/subs/transitions/subscriptions/downgrade/options/${subscriptionNumber}?checkForStepUpOptions=${checkForStepUpOptions}`; return this.requestGet({ url, key: this.transitionApiKey }); } }