Details
diff --git a/invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceDateUtils.java b/invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceDateUtils.java
index 8a439b1..ff1591c 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceDateUtils.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceDateUtils.java
@@ -60,6 +60,7 @@ public class InvoiceDateUtils {
return days.divide(daysInPeriod, 2 * NUMBER_OF_DECIMALS, ROUNDING_METHOD);
}
+
public static BigDecimal calculateProRationBeforeFirstBillingPeriod(final LocalDate startDate, final LocalDate nextBillingCycleDate,
final BillingPeriod billingPeriod) {
final LocalDate previousBillingCycleDate = nextBillingCycleDate.plusMonths(-billingPeriod.getNumberOfMonths());
@@ -73,10 +74,129 @@ public class InvoiceDateUtils {
return numberOfMonths / numberOfMonthsInPeriod;
}
+ public static LocalDate calculateLastBillingCycleDateBefore(final LocalDate date, final LocalDate previousBillCycleDate,
+ final int billingCycleDay, final BillingPeriod billingPeriod) {
+ LocalDate proposedDate = previousBillCycleDate;
+
+ int numberOfPeriods = 0;
+ while (!proposedDate.isAfter(date)) {
+ proposedDate = previousBillCycleDate.plusMonths(numberOfPeriods * billingPeriod.getNumberOfMonths());
+ numberOfPeriods += 1;
+ }
+
+ proposedDate = proposedDate.plusMonths(-billingPeriod.getNumberOfMonths());
+
+ if (proposedDate.dayOfMonth().get() < billingCycleDay) {
+ final int lastDayOfTheMonth = proposedDate.dayOfMonth().getMaximumValue();
+ if (lastDayOfTheMonth < billingCycleDay) {
+ proposedDate = new LocalDate(proposedDate.getYear(), proposedDate.getMonthOfYear(), lastDayOfTheMonth);
+ } else {
+ proposedDate = new LocalDate(proposedDate.getYear(), proposedDate.getMonthOfYear(), billingCycleDay);
+ }
+ }
+
+ if (proposedDate.isBefore(previousBillCycleDate)) {
+ // Make sure not to go too far in the past
+ return previousBillCycleDate;
+ } else {
+ return proposedDate;
+ }
+ }
+
+ public static LocalDate calculateEffectiveEndDate(final LocalDate billCycleDate, final LocalDate targetDate,
+ final BillingPeriod billingPeriod) {
+ if (targetDate.isBefore(billCycleDate)) {
+ return billCycleDate;
+ }
+
+ final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
+ int numberOfPeriods = 0;
+ LocalDate proposedDate = billCycleDate;
+
+ while (!proposedDate.isAfter(targetDate)) {
+ proposedDate = billCycleDate.plusMonths(numberOfPeriods * numberOfMonthsInPeriod);
+ numberOfPeriods += 1;
+ }
+
+ return proposedDate;
+ }
+
+ public static LocalDate calculateEffectiveEndDate(final LocalDate billCycleDate, final LocalDate targetDate,
+ final LocalDate endDate, final BillingPeriod billingPeriod) {
+ if (targetDate.isBefore(endDate)) {
+ if (targetDate.isBefore(billCycleDate)) {
+ return billCycleDate;
+ }
+
+ final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
+ int numberOfPeriods = 0;
+ LocalDate proposedDate = billCycleDate;
+
+ while (!proposedDate.isAfter(targetDate)) {
+ proposedDate = billCycleDate.plusMonths(numberOfPeriods * numberOfMonthsInPeriod);
+ numberOfPeriods += 1;
+ }
+
+ // the current period includes the target date
+ // check to see whether the end date truncates the period
+ if (endDate.isBefore(proposedDate)) {
+ return endDate;
+ } else {
+ return proposedDate;
+ }
+ } else {
+ return endDate;
+ }
+ }
+
+
public static BigDecimal calculateProRationAfterLastBillingCycleDate(final LocalDate endDate, final LocalDate previousBillThroughDate,
final BillingPeriod billingPeriod) {
// Note: assumption is that previousBillThroughDate is correctly aligned with the billing cycle day
final LocalDate nextBillThroughDate = previousBillThroughDate.plusMonths(billingPeriod.getNumberOfMonths());
return calculateProrationBetweenDates(previousBillThroughDate, endDate, previousBillThroughDate, nextBillThroughDate);
}
+
+ /*
+ public static LocalDate calculateBillingCycleDateOnOrAfter(final LocalDate date, final DateTimeZone accountTimeZone,
+ final int billingCycleDayLocal) {
+ final DateTime tmp = date.toDateTimeAtStartOfDay(accountTimeZone);
+ final DateTime proposedDateTime = calculateBillingCycleDateOnOrAfter(tmp, billingCycleDayLocal);
+
+ return new LocalDate(proposedDateTime, accountTimeZone);
+ }
+
+ public static LocalDate calculateBillingCycleDateAfter(final LocalDate date, final DateTimeZone accountTimeZone,
+ final int billingCycleDayLocal) {
+ final DateTime tmp = date.toDateTimeAtStartOfDay(accountTimeZone);
+ final DateTime proposedDateTime = calculateBillingCycleDateAfter(tmp, billingCycleDayLocal);
+
+ return new LocalDate(proposedDateTime, accountTimeZone);
+ }
+ */
+
+ public static LocalDate calculateBillingCycleDateOnOrAfter(final LocalDate date, final int billingCycleDayLocal) {
+ final int lastDayOfMonth = date.dayOfMonth().getMaximumValue();
+
+ final LocalDate fixedDate;
+ if (billingCycleDayLocal > lastDayOfMonth) {
+ fixedDate = new LocalDate(date.getYear(), date.getMonthOfYear(), lastDayOfMonth, date.getChronology());
+ } else {
+ fixedDate = new LocalDate(date.getYear(), date.getMonthOfYear(), billingCycleDayLocal, date.getChronology());
+ }
+
+ LocalDate proposedDate = fixedDate;
+ while (proposedDate.isBefore(date)) {
+ proposedDate = proposedDate.plusMonths(1);
+ }
+ return proposedDate;
+ }
+
+ public static LocalDate calculateBillingCycleDateAfter(final LocalDate date, final int billingCycleDayLocal) {
+ LocalDate proposedDate = calculateBillingCycleDateOnOrAfter(date, billingCycleDayLocal);
+ if (date.compareTo(proposedDate) == 0) {
+ proposedDate = proposedDate.plusMonths(1);
+ }
+ return proposedDate;
+ }
}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
index 4491632..5ad6d44 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
@@ -27,8 +27,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ning.billing.catalog.api.BillingPeriod;
-import com.ning.billing.invoice.generator.BillingIntervalDetail;
+import static com.ning.billing.invoice.generator.InvoiceDateUtils.calculateBillingCycleDateOnOrAfter;
+import static com.ning.billing.invoice.generator.InvoiceDateUtils.calculateEffectiveEndDate;
+import static com.ning.billing.invoice.generator.InvoiceDateUtils.calculateLastBillingCycleDateBefore;
import static com.ning.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods;
import static com.ning.billing.invoice.generator.InvoiceDateUtils.calculateProRationAfterLastBillingCycleDate;
import static com.ning.billing.invoice.generator.InvoiceDateUtils.calculateProRationBeforeFirstBillingPeriod;
@@ -50,8 +52,7 @@ public class InAdvanceBillingMode implements BillingMode {
final List<RecurringInvoiceItemData> results = new ArrayList<RecurringInvoiceItemData>();
- final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod);
-
+ final LocalDate firstBillingCycleDate = calculateBillingCycleDateOnOrAfter(startDate, billingCycleDayLocal);
// We are not billing for less than a day (we could...)
if (endDate != null && endDate.equals(startDate)) {
@@ -61,7 +62,7 @@ public class InAdvanceBillingMode implements BillingMode {
// If there is an endDate and that endDate is before our first coming firstBillingCycleDate, all we have to do
// is to charge for that period
//
- if (endDate != null && !endDate.isAfter(billingIntervalDetail.getFirstBillingCycleDate())) {
+ if (endDate != null && !endDate.isAfter(firstBillingCycleDate)) {
final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, endDate, billingPeriod);
final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, endDate, leadingProRationPeriods);
results.add(itemData);
@@ -73,11 +74,11 @@ public class InAdvanceBillingMode implements BillingMode {
// i) The first firstBillingCycleDate is strictly after our start date AND
// ii) The endDate is is not null and is strictly after our firstBillingCycleDate (previous check)
//
- if (billingIntervalDetail.getFirstBillingCycleDate().isAfter(startDate)) {
- final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, billingIntervalDetail.getFirstBillingCycleDate(), billingPeriod);
+ if (firstBillingCycleDate.isAfter(startDate)) {
+ final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, firstBillingCycleDate, billingPeriod);
if (leadingProRationPeriods != null && leadingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
// Not common - add info in the logs for debugging purposes
- final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, billingIntervalDetail.getFirstBillingCycleDate(), leadingProRationPeriods);
+ final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, firstBillingCycleDate, leadingProRationPeriods);
log.info("Adding pro-ration: {}", itemData);
results.add(itemData);
}
@@ -88,13 +89,18 @@ public class InAdvanceBillingMode implements BillingMode {
// - If endDate != null and targetDate is after endDate => this is the endDate and will lead to a trailing pro-ration
// - If not, this is the last billingCycleDate calculation right after the targetDate
//
- final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate();
+ final LocalDate effectiveEndDate;
+ if (endDate != null) {
+ effectiveEndDate = calculateEffectiveEndDate(firstBillingCycleDate, targetDate, endDate, billingPeriod);
+ } else {
+ effectiveEndDate = calculateEffectiveEndDate(firstBillingCycleDate, targetDate, billingPeriod);
+ }
//
// Based on what we calculated previously, code recompute one more time the numberOfWholeBillingPeriods
//
- final LocalDate lastBillingCycleDate = billingIntervalDetail.getLastBillingCycleDate();
- final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(billingIntervalDetail.getFirstBillingCycleDate(), lastBillingCycleDate, billingPeriod);
+ final LocalDate lastBillingCycleDate = calculateLastBillingCycleDateBefore(effectiveEndDate, firstBillingCycleDate, billingCycleDayLocal, billingPeriod);
+ final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(firstBillingCycleDate, lastBillingCycleDate, billingPeriod);
final int numberOfMonthsPerBillingPeriod = billingPeriod.getNumberOfMonths();
for (int i = 0; i < numberOfWholeBillingPeriods; i++) {
@@ -110,7 +116,7 @@ public class InAdvanceBillingMode implements BillingMode {
}
// Make sure to align the end date with the BCD
- final LocalDate servicePeriodEndDate = billingIntervalDetail.getFirstBillingCycleDate().plusMonths((i + 1) * numberOfMonthsPerBillingPeriod);
+ final LocalDate servicePeriodEndDate = firstBillingCycleDate.plusMonths((i + 1) * numberOfMonthsPerBillingPeriod);
results.add(new RecurringInvoiceItemData(servicePeriodStartDate, servicePeriodEndDate, BigDecimal.ONE));
}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/generator/TestInvoiceDateUtils.java b/invoice/src/test/java/com/ning/billing/invoice/generator/TestInvoiceDateUtils.java
index 5728847..2ee9dff 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/generator/TestInvoiceDateUtils.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/generator/TestInvoiceDateUtils.java
@@ -18,6 +18,7 @@ package com.ning.billing.invoice.generator;
import java.math.BigDecimal;
+import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -28,6 +29,22 @@ import com.ning.billing.invoice.InvoiceTestSuiteNoDB;
public class TestInvoiceDateUtils extends InvoiceTestSuiteNoDB {
@Test(groups = "fast")
+ public void testLastBCDShouldNotBeBeforePreviousBCD() throws Exception {
+ final LocalDate from = new LocalDate("2012-07-16");
+ final LocalDate previousBCD = new LocalDate("2012-08-15");
+ final int bcdLocal = 15;
+ final LocalDate lastBCD = InvoiceDateUtils.calculateLastBillingCycleDateBefore(from, previousBCD, bcdLocal, BillingPeriod.MONTHLY);
+ Assert.assertEquals(lastBCD, new LocalDate("2012-08-15"));
+ }
+
+ @Test(groups = "fast")
+ public void testNextBCDShouldNotBeInThePast() throws Exception {
+ final LocalDate from = new LocalDate("2012-07-16");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(from, 15);
+ Assert.assertEquals(to, new LocalDate("2012-08-15"));
+ }
+
+ @Test(groups = "fast")
public void testProRationAfterLastBillingCycleDate() throws Exception {
final LocalDate endDate = new LocalDate("2012-06-02");
final LocalDate previousBillThroughDate = new LocalDate("2012-03-02");
@@ -36,6 +53,67 @@ public class TestInvoiceDateUtils extends InvoiceTestSuiteNoDB {
}
@Test(groups = "fast")
+ public void testBeforeBCDWithAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-02");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-03-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testEqualBCDWithAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-03");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-04-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testAfterBCDWithAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-04");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-04-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testBeforeBCDWithOnOrAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-02");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-03-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testEqualBCDWithOnOrAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-03");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-03-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testAfterBCDWithOnOrAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-04");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-04-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testEffectiveEndDate() throws Exception {
+ final LocalDate firstBCD = new LocalDate(2012, 7, 16);
+ final LocalDate targetDate = new LocalDate(2012, 8, 16);
+ final BillingPeriod billingPeriod = BillingPeriod.MONTHLY;
+ final LocalDate effectiveEndDate = InvoiceDateUtils.calculateEffectiveEndDate(firstBCD, targetDate, billingPeriod);
+ // TODO should that be 2012-09-15?
+ Assert.assertEquals(effectiveEndDate, new LocalDate(2012, 9, 16));
+ }
+
+ @Test(groups = "fast")
+ public void testLastBCD() throws Exception {
+ final LocalDate firstBCD = new LocalDate(2012, 7, 16);
+ final LocalDate effectiveEndDate = new LocalDate(2012, 9, 15);
+ final BillingPeriod billingPeriod = BillingPeriod.MONTHLY;
+ final LocalDate lastBCD = InvoiceDateUtils.calculateLastBillingCycleDateBefore(effectiveEndDate, firstBCD, 16, billingPeriod);
+ Assert.assertEquals(lastBCD, new LocalDate(2012, 8, 16));
+ }
+
+ @Test(groups = "fast")
public void testCalculateNbOfBillingPeriods() throws Exception {
final LocalDate firstBCD = new LocalDate(2012, 7, 16);
final LocalDate lastBCD = new LocalDate(2012, 9, 16);