/// <summary> /// This method "rolls forward" subscriptions, which means if the user unsubscribed we /// make sure the subscription is billed until the end of their billing period by extending /// the subscription length here. /// </summary> public IReadOnlyList <MergedSnapshot> Execute( DateTime endTimeExclusive, IReadOnlyList <MergedSnapshot> inputSnapshots) { var snapshots = inputSnapshots.ToList(); var activeSubscriptions = new Dictionary <ChannelId, ActiveSubscription>(); for (int i = 0; i < snapshots.Count; i++) { var snapshot = snapshots[i]; foreach (var subscription in snapshot.SubscriberChannels.SubscribedChannels) { var billingWeekEndDateExclusive = BillingWeekUtilities.CalculateBillingWeekEndDateExclusive( subscription.SubscriptionStartDate, snapshot.Timestamp); ActiveSubscription activeSubscription; if (activeSubscriptions.TryGetValue(subscription.ChannelId, out activeSubscription)) { // The following condition is a relaxation to allow people to briefly re-subscribe without // trigging a whole new billing week, and therefore to reduce anticipated complaints. // If they restarted their subscription before the minumum end date of their // previous subscription, then we do not reset the minimum end date yet. // However if they exceed that billing week then the new subscription's billing week will be used. if (snapshot.Timestamp < activeSubscription.BillingWeekEndDateExclusive) { billingWeekEndDateExclusive = activeSubscription.BillingWeekEndDateExclusive; } activeSubscription = new ActiveSubscription(billingWeekEndDateExclusive, subscription); activeSubscriptions[subscription.ChannelId] = activeSubscription; } else { activeSubscription = new ActiveSubscription(billingWeekEndDateExclusive, subscription); activeSubscriptions.Add(subscription.ChannelId, activeSubscription); } } // Copy the list so we can modify activeSubscriptions within the loop. foreach (var activeSubscription in activeSubscriptions.Values.ToList()) { var activeChannelId = activeSubscription.Subscription.ChannelId; if (snapshot.SubscriberChannels.SubscribedChannels.All(v => !v.ChannelId.Equals(activeChannelId))) { // The subscriber has unsubscribed from this channel. var billingWeekFinalSnapshotTime = this.GetBillingWeekFinalSnapshotTime(activeSubscription.BillingWeekEndDateExclusive); var subscriptionDuration = snapshot.Timestamp - activeSubscription.Subscription.SubscriptionStartDate; if (subscriptionDuration.TotalDays > DaysInWeek) { // We have exceeded the minimum subscripion period, so there is nothing to adjust. activeSubscriptions.Remove(activeSubscription.Subscription.ChannelId); } else if (snapshot.Timestamp == billingWeekFinalSnapshotTime) { // We are at the end of the billing week, so there is nothing to adjust. activeSubscriptions.Remove(activeSubscription.Subscription.ChannelId); } else if (snapshot.Timestamp < billingWeekFinalSnapshotTime) { if (snapshot.CreatorChannels.CreatorChannels.All(v => !v.ChannelId.Equals(activeChannelId))) { // We are within the billing week, but the channel is no longer published. // So we stop billing at this point. activeSubscriptions.Remove(activeSubscription.Subscription.ChannelId); } else { // We are still within their billing week and the creator still publishes the channel. // We must extend their subscription to this snapshot. var newSnapshot = new MergedSnapshot( snapshot.Timestamp, snapshot.CreatorChannels, snapshot.CreatorFreeAccessUsers, new SubscriberChannelsSnapshot( snapshot.SubscriberChannels.Timestamp, snapshot.SubscriberChannels.SubscriberId, snapshot.SubscriberChannels.SubscribedChannels.Concat(new[] { activeSubscription.Subscription }).ToList()), snapshot.Subscriber, snapshot.CalculatedAccountBalance); snapshots[i] = newSnapshot; if ((i == snapshots.Count - 1 && activeSubscription.BillingWeekEndDateExclusive < endTimeExclusive) || (i < snapshots.Count - 1 && snapshots[i + 1].Timestamp > billingWeekFinalSnapshotTime)) { // Either this is the last snapshot and the billing week ends before the end date of // our search range, or the next snapshot will be in the next billing week. // Either way, we need to insert a snapshot to end the biling at the correct time. var endSnapshot = new MergedSnapshot( billingWeekFinalSnapshotTime, snapshot.CreatorChannels, snapshot.CreatorFreeAccessUsers, snapshot.SubscriberChannels, snapshot.Subscriber, snapshot.CalculatedAccountBalance); snapshots.Insert(i + 1, endSnapshot); } snapshot = snapshots[i]; } } } } } return(snapshots); }
/// <summary> /// Ensures there is a snapshot on each billing week end date. /// This makes it easier to discard periods of time due to the creator /// not posting in a channel, as we know the period won't span billing weeks. /// </summary> public IReadOnlyList <MergedSnapshot> Execute(IReadOnlyList <MergedSnapshot> snapshots) { if (snapshots.Count == 0) { return(snapshots); } var uniqueStartDates = (from snapshot in snapshots from subscribedChannel in snapshot.SubscriberChannels.SubscribedChannels select subscribedChannel.SubscriptionStartDate).Distinct().ToList(); var maximumDate = snapshots.Max(v => v.Timestamp); var minimumDate = snapshots.Min(v => v.Timestamp); var billingEndDates = new List <DateTime>(); foreach (var startDate in uniqueStartDates) { var billingEndDate = BillingWeekUtilities.CalculateBillingWeekEndDateExclusive(startDate, minimumDate); while (billingEndDate < maximumDate) { billingEndDates.Add(billingEndDate); billingEndDate = billingEndDate.AddDays(7); } } billingEndDates = billingEndDates.Distinct().OrderBy(v => v).ToList(); var outputSnapshots = snapshots.ToList(); int index = 1; foreach (var billingEndDate in billingEndDates) { for (; index < outputSnapshots.Count; index++) { var snapshot = outputSnapshots[index]; if (snapshot.Timestamp == billingEndDate) { break; } if (snapshot.Timestamp > billingEndDate) { var source = outputSnapshots[index - 1]; outputSnapshots.Insert( index, new MergedSnapshot( billingEndDate, source.CreatorChannels, source.CreatorFreeAccessUsers, source.SubscriberChannels, source.Subscriber, source.CalculatedAccountBalance)); ++index; break; } } } return(outputSnapshots); }