killbill-aplcache

invoice: fix errors when cancelling BP+AO the same day * Fix

7/13/2012 2:46:40 PM

Details

diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegration.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegration.java
index 7d74b2d..d43f6d1 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegration.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegration.java
@@ -37,7 +37,8 @@ import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.entitlement.api.user.SubscriptionData;
 import com.ning.billing.invoice.api.Invoice;
-import com.ning.billing.invoice.generator.InvoiceDateUtils;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
 
 import com.google.common.collect.ImmutableList;
 
@@ -48,6 +49,79 @@ import static org.testng.Assert.assertTrue;
 @Guice(modules = {BeatrixModule.class})
 public class TestIntegration extends TestIntegrationBase {
     @Test(groups = "slow")
+    public void testCancelBPWithAOTheSameDay() throws Exception {
+        // We take april as it has 30 days (easier to play with BCD)
+        final DateTime today = new DateTime(2012, 4, 1, 0, 0, 0, 0, testTimeZone);
+        final DateTime trialEndDate = new DateTime(2012, 5, 1, 0, 0, 0, 0, testTimeZone);
+        final Account account = createAccountWithPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date
+        clock.setDeltaFromReality(today.getMillis() - clock.getUTCNow().getMillis());
+        final SubscriptionBundle bundle = entitlementUserApi.createBundleForAccount(account.getId(), "whatever", context);
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+        final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.INVOICE);
+        final PlanPhaseSpecifier bpPlanPhaseSpecifier = new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, planSetName, null);
+        final SubscriptionData bpSubscription = subscriptionDataFromSubscription(entitlementUserApi.createSubscription(bundle.getId(),
+                                                                                                                       bpPlanPhaseSpecifier,
+                                                                                                                       null,
+                                                                                                                       context));
+        assertNotNull(bpSubscription);
+        assertTrue(busHandler.isCompleted(DELAY));
+        assertListenerStatus();
+
+        //
+        // ADD ADD_ON ON THE SAME DAY
+        //
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        final PlanPhaseSpecifier addonPlanPhaseSpecifier = new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final SubscriptionData aoSubscription = subscriptionDataFromSubscription(entitlementUserApi.createSubscription(bundle.getId(), addonPlanPhaseSpecifier, null, context));
+        assertTrue(busHandler.isCompleted(DELAY));
+        assertListenerStatus();
+
+        //
+        // CANCEL BP ON THE SAME DAY (we should have two cancellations, BP and AO)
+        //
+        busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.CANCEL, NextEvent.INVOICE);
+        bpSubscription.cancel(clock.getUTCNow(), false, context);
+        assertTrue(busHandler.isCompleted(DELAY));
+        assertListenerStatus();
+
+        final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId());
+        assertEquals(invoices.size(), 3);
+        // The first invoice is for the trial BP
+        assertEquals(invoices.get(0).getNumberOfItems(), 1);
+        assertEquals(invoices.get(0).getInvoiceItems().get(0).getStartDate().compareTo(today), 0);
+        assertEquals(invoices.get(0).getInvoiceItems().get(0).getEndDate().compareTo(trialEndDate), 0);
+        // The second invoice should be adjusted for the AO (we paid for the full period)
+        assertEquals(invoices.get(1).getNumberOfItems(), 3);
+        for (final InvoiceItem item : invoices.get(1).getInvoiceItems()) {
+            if (InvoiceItemType.RECURRING.equals(item.getInvoiceItemType())) {
+                assertEquals(item.getStartDate().compareTo(today), 0);
+                assertEquals(item.getEndDate().compareTo(trialEndDate), 0);
+                assertEquals(item.getAmount().compareTo(new BigDecimal("399.9500")), 0);
+            } else if (InvoiceItemType.REPAIR_ADJ.equals(item.getInvoiceItemType())) {
+                assertEquals(item.getStartDate().compareTo(today), 0);
+                assertEquals(item.getEndDate().compareTo(trialEndDate), 0);
+                assertEquals(item.getAmount().compareTo(new BigDecimal("-399.9500")), 0);
+            } else {
+                assertEquals(item.getInvoiceItemType(), InvoiceItemType.CBA_ADJ);
+                assertEquals(item.getStartDate().compareTo(today), 0);
+                assertEquals(item.getEndDate().compareTo(today), 0);
+                assertEquals(item.getAmount().compareTo(new BigDecimal("399.9500")), 0);
+            }
+        }
+        // Null invoice
+        assertEquals(invoices.get(2).getNumberOfItems(), 0);
+    }
+
+    @Test(groups = "slow")
     public void testBasePlanCompleteWithBillingDayInPast() throws Exception {
         final DateTime startDate = new DateTime(2012, 2, 1, 0, 3, 42, 0, testTimeZone);
         final LinkedHashMap<DateTime, List<NextEvent>> expectedStates = new LinkedHashMap<DateTime, List<NextEvent>>();
diff --git a/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
index 7d34da2..4264af1 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -146,7 +146,9 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         for (final UUID invoiceId : amountOwedByInvoice.keySet()) {
             final BigDecimal invoiceBalance = amountOwedByInvoice.get(invoiceId);
             if (invoiceBalance.compareTo(BigDecimal.ZERO) < 0) {
-                proposedItems.add(new CreditBalanceAdjInvoiceItem(invoiceId, accountId, clock.getUTCNow(), invoiceBalance.negate(), currency));
+                final DateTime creditDate = InvoiceDateUtils.roundDateTimeToDate(clock.getUTCNow(), DateTimeZone.UTC);
+                final CreditBalanceAdjInvoiceItem creditInvoiceItem = new CreditBalanceAdjInvoiceItem(invoiceId, accountId, creditDate, invoiceBalance.negate(), currency);
+                proposedItems.add(creditInvoiceItem);
             }
         }
     }
@@ -156,7 +158,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
             if (existingItem.getInvoiceItemType() == InvoiceItemType.RECURRING ||
                     existingItem.getInvoiceItemType() == InvoiceItemType.FIXED) {
                 final BigDecimal amountNegated = existingItem.getAmount() == null ? null : existingItem.getAmount().negate();
-                RepairAdjInvoiceItem repairItem  = new RepairAdjInvoiceItem(existingItem.getInvoiceId(), existingItem.getAccountId(), existingItem.getStartDate(),existingItem.getEndDate(), amountNegated, existingItem.getCurrency(), existingItem.getId());
+                final RepairAdjInvoiceItem repairItem  = new RepairAdjInvoiceItem(existingItem.getInvoiceId(), existingItem.getAccountId(), existingItem.getStartDate(),existingItem.getEndDate(), amountNegated, existingItem.getCurrency(), existingItem.getId());
                 proposedItems.add(repairItem);
             }
         }
@@ -192,7 +194,9 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         }
 
         if (creditAmount.compareTo(BigDecimal.ZERO) < 0) {
-            proposedItems.add(new CreditBalanceAdjInvoiceItem(invoiceId, accountId, clock.getUTCNow(), creditAmount, targetCurrency));
+            final DateTime creditDate = InvoiceDateUtils.roundDateTimeToDate(clock.getUTCNow(), DateTimeZone.UTC);
+            final CreditBalanceAdjInvoiceItem creditInvoiceItem = new CreditBalanceAdjInvoiceItem(invoiceId, accountId, creditDate, creditAmount, targetCurrency);
+            proposedItems.add(creditInvoiceItem);
         }
     }
 
diff --git a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
index 4bcb2e0..d7e7b13 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
@@ -100,8 +100,7 @@ public class InvoiceDispatcher {
                                     final CallContext context) throws InvoiceApiException {
         final UUID subscriptionId = transition.getSubscriptionId();
         final DateTime targetDate = transition.getEffectiveTransitionTime();
-        log.info("Got subscription transition from InvoiceListener. id: " + subscriptionId.toString() + "; targetDate: " + targetDate.toString());
-        log.info("Transition type: " + transition.getTransitionType().toString());
+        log.info("Got subscription transition: type: " + transition.getTransitionType().toString() + "; id: " + subscriptionId.toString() + "; targetDate: " + targetDate.toString());
         processSubscription(subscriptionId, targetDate, context);
     }
 
diff --git a/junction/src/main/java/com/ning/billing/junction/plumbing/billing/BillCycleDayCalculator.java b/junction/src/main/java/com/ning/billing/junction/plumbing/billing/BillCycleDayCalculator.java
index ce98f24..2538869 100644
--- a/junction/src/main/java/com/ning/billing/junction/plumbing/billing/BillCycleDayCalculator.java
+++ b/junction/src/main/java/com/ning/billing/junction/plumbing/billing/BillCycleDayCalculator.java
@@ -86,30 +86,35 @@ public class BillCycleDayCalculator {
                 break;
             case BUNDLE:
                 final Subscription baseSub = entitlementApi.getBaseSubscription(bundle.getId());
-                final Plan basePlan = baseSub.getCurrentPlan();
+                Plan basePlan = baseSub.getCurrentPlan();
+                if (basePlan == null) {
+                    // The BP has been cancelled
+                    final EffectiveSubscriptionEvent previousTransition = baseSub.getPreviousTransition();
+                    basePlan = catalog.findPlan(previousTransition.getPreviousPlan(), previousTransition.getEffectiveTransitionTime(), previousTransition.getSubscriptionStartDate());
+                }
                 result = calculateBcdFromSubscription(baseSub, basePlan, account);
                 break;
             case SUBSCRIPTION:
                 result = calculateBcdFromSubscription(subscription, plan, account);
                 break;
         }
+
         if (result == -1) {
             throw new CatalogApiException(ErrorCode.CAT_INVALID_BILLING_ALIGNMENT, alignment.toString());
         }
-        return result;
 
+        return result;
     }
 
     private int calculateBcdFromSubscription(final Subscription subscription, final Plan plan, final Account account) throws AccountApiException {
         final DateTime date = plan.dateOfFirstRecurringNonZeroCharge(subscription.getStartDate());
         // There are really two kind of billCycleDay:
         // - a System billingCycleDay which should be computed from UTC time (in order to get the correct notification time at
-        //   the end of each service period
+        //   the end of each service period)
         // - a User billingCycleDay which should align with the account timezone
         //
-        // TODO At this point we only compute the system one; should we need two filds in the account table
+        // TODO At this point we only compute the system one; should we need two fields in the account table
         //return date.toDateTime(account.getTimeZone()).getDayOfMonth();
         return date.getDayOfMonth();
     }
-
 }