/// <summary> /// Returns how many plan periods are due. /// </summary> /// <param name="plan">The payment plan to calculate.</param> /// <param name="currentDate">The current date, ensuring all calculations are done using the exact same date.</param> /// <returns>The amount of periods to pay.</returns> private decimal CalcPeriodQty(ApiPaymentPlan plan, DateTimeOffset currentDate) { var loopDate = new[] { plan.StartDate, plan.PaidUntilDate, plan.DateAdded }.Max() ?? MovePeriodDate(plan, plan.NextChargeDate !.Value, -1); var qtyTotal = 0m; var qtyTrial = 0m; var count = 0; while (GetPlanPeriod(plan, loopDate, currentDate, out var periodStart, out var periodEnd) && (plan.TransactionsLeft == null || count < plan.TransactionsLeft)) { count++; var fullPeriod = periodEnd - periodStart; var billPeriod = periodEnd - loopDate; if (plan.EndDate.HasValue) { billPeriod -= (periodEnd - plan.EndDate.Value); } // Calculate period by period. var qty = (decimal)billPeriod.Ticks / fullPeriod.Ticks; // Trial period will only be billed once we've completed a billing cycle afterwards. if (plan.PeriodUnit == PaymentPeriodUnit.MonthPostpaidTrial && billPeriod.TotalDays <= (plan.PeriodQty ?? _config.Value.DefaultTrialDays)) { qtyTrial += qty; } else { qtyTotal += qty + qtyTrial; qtyTrial = 0; } loopDate = periodEnd; } return(qtyTotal); }
/// <summary> /// Moves the date by specified number of full billing periods. /// </summary> /// <param name="plan">The plan containing billing period info.</param> /// <param name="datePlan">The date to move.</param> /// <param name="qty">The amount of full periods to add or remove to the date.</param> /// <returns>The date for a different billing period.</returns> private static DateTimeOffset MovePeriodDate(ApiPaymentPlan plan, DateTimeOffset datePlan, int qty) { return(plan.PeriodUnit switch { PaymentPeriodUnit.MonthPostpaid => datePlan.AddMonths(1 * qty), PaymentPeriodUnit.MonthPostpaidTrial => datePlan.AddMonths(1 * qty), PaymentPeriodUnit.Day => datePlan.AddDays((plan.PeriodQty ?? 1) * qty), PaymentPeriodUnit.Week => datePlan.AddDays(7 * (plan.PeriodQty ?? 1) * qty), PaymentPeriodUnit.Month => datePlan.AddMonths((plan.PeriodQty ?? 1) * qty), PaymentPeriodUnit.Year => datePlan.AddYears((plan.PeriodQty ?? 1) * qty), _ => throw new InvalidOperationException(Res.GetPlanPeriodUnrecognizedUnit) });
/// <summary> /// Calculates the start and end dates of a period, and returns whether the period end is past and due to be paid. /// </summary> /// <param name="plan">The payment plan to calculate.</param> /// <param name="planDate">The date to calculate period start and end dates for.</param> /// <param name="currentDate">The current date, ensuring all calculations are done using the exact same date.</param> /// <param name="periodStart">Returns the period start date.</param> /// <param name="periodEnd">Returns the period end date.</param> /// <returns>Whether endDate is past and due to be paid.</returns> private bool GetPlanPeriod(ApiPaymentPlan plan, DateTimeOffset planDate, DateTimeOffset currentDate, out DateTimeOffset periodStart, out DateTimeOffset periodEnd) { if (plan.PeriodUnit == PaymentPeriodUnit.MonthPostpaid || plan.PeriodUnit == PaymentPeriodUnit.MonthPostpaidTrial) { periodStart = new DateTimeOffset(planDate.Year, planDate.Month, 1, 0, 0, 0, TimeSpan.Zero); } else { periodStart = planDate; } periodEnd = MovePeriodDate(plan, periodStart, 1); return(periodEnd <= currentDate.AddHours(_config.Value.PayHoursAhead)); }