import { ExposedSubscriptionHistory } from './types/subscription'; import { UserStates } from './types/states'; import { User } from './models/user'; import { findFutureEvent, ensureDiscount, isEventDateAfter, findEvent } from './helpers/state'; const defaultState = { state: UserStates.DEFAULT, description: 'The fallback state, meaning the user has not been categorised into any of the previously listed states.', requirement: () => true }; export const PossibleUserStates = [ { state: UserStates.STEPUP, description: 'The user has a future stepup scheduled, meaning they will move to a more expensive subscription.', technicalDescription: 'The user subscription history has a future stepup event that is more recent than any undostepup events', requirement: ({ subscriptionHistory }: StateManager) => { return subscriptionHistory.isInStepUp; } }, { state: UserStates.CANCELLATION, description: 'The user has a future cancellation, meaning their subscription will stop on the given date.', requirement: ({ futureCancellation, undoCancellation }: StateManager) => { // If we both have futureCancellation and undoCancellation then need to be sure // that futureCancellation eventDate is after undoCancellation return (futureCancellation && undoCancellation) ? isEventDateAfter(futureCancellation, undoCancellation) : !!futureCancellation; } }, { state: UserStates.FREEZE, description: 'The user has chosen to undo their future stepup, either through contacting Customer Care or the online cancellation journey (as a stepup user).', technicalDescription: 'There is a future UndoStepup event in the user subscription history that is more recent than the Stepup event, indicating the user has frozen their subscription.', requirement: ({ subscriptionHistory, futureTransition }: StateManager) => { // If there is also a future transition, the user should be either DISCOUNT or TRANSITION return subscriptionHistory.hasFrozenPrice && !futureTransition; } }, { state: UserStates.DISCOUNT, description: 'The user has chosen to transition to a cheaper offer for the same product.', requirement: ({ futureTransition, undoTransition }: StateManager) => { // Has End of Term transition // which has not been undone // and which is cheaper AND has a subset of the products const transitionUndone = undoTransition && isEventDateAfter(undoTransition, futureTransition); return futureTransition && !transitionUndone && ensureDiscount(futureTransition); } }, { state: UserStates.TRANSITION, description: 'The user has chosen to transition to a different product.', requirement: ({ futureTransition, undoTransition }: StateManager) => { // If we both have futureTransition and undoTransition then need to be sure // that futureTransition eventDate is after undoTransition return (futureTransition && undoTransition) ? isEventDateAfter(futureTransition, undoTransition) : !!futureTransition; } }, { state: UserStates.TRIALIST, description: 'User is in their trial period.', requirement: ({ userData }: StateManager) => { return userData.isTrialist; } }, defaultState ]; export class StateManager { userData: User; subscriptionHistory: any; futureCancellation: any; futureTransition: any; undoTransition: any; undoCancellation: any; constructor(userData: any) { this.userData = userData; } public setSubscriptionHistory(subscriptionHistory: ExposedSubscriptionHistory): void { this.subscriptionHistory = subscriptionHistory; this.futureTransition = findFutureEvent(subscriptionHistory.history, 'Transition'); this.futureCancellation = findFutureEvent(subscriptionHistory.history, 'Cancellation'); this.undoTransition = findEvent(subscriptionHistory.history, 'UndoTransition'); this.undoCancellation = findEvent(subscriptionHistory.history, 'UndoCancellation'); } /** * Identify the first / highest priority current state of a user given their user data and subscription status */ public identify(): string { // Even though PossibleUserStates will always return something due to defaultState having no requirement // The compiler complains because the return type of .find can possibly be undefined return (PossibleUserStates.find(state => state.requirement(this)) || defaultState).state; } }