killbill-uncached

Fixes #154. The code change it to look for reparee in all cases,

1/31/2014 2:23:27 AM

Details

diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
index 2332a52..153a605 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
@@ -26,9 +26,11 @@ import javax.inject.Inject;
 
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
+import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.Test;
 
+import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.api.TestApiListener.NextEvent;
 import com.ning.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
@@ -39,7 +41,9 @@ import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.catalog.api.PriceListSet;
 import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.DefaultEntitlement;
+import com.ning.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
 import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.invoice.generator.DefaultInvoiceGeneratorWithSwitchRepairLogic;
 import com.ning.billing.invoice.generator.DefaultInvoiceGeneratorWithSwitchRepairLogic.REPAIR_INVOICE_LOGIC;
@@ -700,4 +704,160 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 11), new LocalDate(2012, 6, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
         invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
     }
+
+    @Test(groups = "slow")
+    public void testRepairWithFullItemAdjustment() throws Exception {
+
+        invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
+
+        final LocalDate today = new LocalDate(2013, 7, 19);
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.ANNUAL;
+        final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+        DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+        assertNotNull(bpEntitlement);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
+
+        assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+
+        // Move out of trials for interesting invoices adjustments
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 2);
+        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 8, 18), new LocalDate(2014, 8, 18), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+
+        // Move clock to 2013-09-17
+        clock.addDays(30);
+        busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE_ADJUSTMENT);
+        bpEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.IMMEDIATE, callContext);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 2);
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 8, 18), new LocalDate(2014, 8, 18), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 9, 17), new LocalDate(2014, 8, 18), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2202.67")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 9, 17), new LocalDate(2013, 9, 17), InvoiceItemType.CBA_ADJ, new BigDecimal("2202.67")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+
+
+        //
+        // ITEM ADJUSTMENT PRIOR TO DOING THE REPAIR
+        //
+        final Invoice invoice1 = invoices.get(1);
+        final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), callContext);
+        final ExpectedPaymentCheck expectedPaymentCheck = new ExpectedPaymentCheck(clock.getUTCNow().toLocalDate(), new BigDecimal("2399.95"), PaymentStatus.SUCCESS, invoice1.getId(), Currency.USD);
+        final Payment payment1 = payments.get(0);
+
+        final Map<UUID, BigDecimal> iias = new HashMap<UUID, BigDecimal>();
+        iias.put(invoice1.getInvoiceItems().get(0).getId(), new BigDecimal("197.28"));
+        busHandler.pushExpectedEvents(NextEvent.INVOICE_ADJUSTMENT);
+        paymentApi.createRefundWithItemsAdjustments(account, payment1.getId(), iias, callContext);
+        assertListenerStatus();
+
+        try {
+            invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(clock.getUTCToday()), false, callContext);
+            Assert.fail("Should not gnenerated an new invoice");
+        } catch (InvoiceApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.INVOICE_NOTHING_TO_DO.getCode());
+        }
+    }
+
+    //
+    // This is the exact same test as testRepairWithFullItemAdjustment except we now only do a partial item adjustment.
+    // The invoice code will NOT reinvoice for the remaining part as the item was both repaired and adjusted and so
+    // it does not have enough info to understand what should be re-invoiced.
+    //
+    // Note that there is no real use case for those scenarii in real life
+    //
+    @Test(groups = "slow")
+    public void testRepairWithPartialItemAdjustment() throws Exception {
+
+        invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
+
+        final LocalDate today = new LocalDate(2013, 7, 19);
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.ANNUAL;
+        final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+        DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+        assertNotNull(bpEntitlement);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
+
+        assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+
+        // Move out of trials for interesting invoices adjustments
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 2);
+        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 8, 18), new LocalDate(2014, 8, 18), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+
+        // Move clock to 2013-09-17
+        clock.addDays(30);
+        busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE_ADJUSTMENT);
+        bpEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.IMMEDIATE, callContext);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 2);
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 8, 18), new LocalDate(2014, 8, 18), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 9, 17), new LocalDate(2014, 8, 18), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2202.67")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 9, 17), new LocalDate(2013, 9, 17), InvoiceItemType.CBA_ADJ, new BigDecimal("2202.67")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+
+
+        //
+        // ITEM ADJUSTMENT PRIOR TO DOING THE REPAIR
+        //
+        final Invoice invoice1 = invoices.get(1);
+        final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), callContext);
+        final ExpectedPaymentCheck expectedPaymentCheck = new ExpectedPaymentCheck(clock.getUTCNow().toLocalDate(), new BigDecimal("2399.95"), PaymentStatus.SUCCESS, invoice1.getId(), Currency.USD);
+        final Payment payment1 = payments.get(0);
+
+        final Map<UUID, BigDecimal> iias = new HashMap<UUID, BigDecimal>();
+        iias.put(invoice1.getInvoiceItems().get(0).getId(), new BigDecimal("100.00"));
+        busHandler.pushExpectedEvents(NextEvent.INVOICE_ADJUSTMENT);
+        paymentApi.createRefundWithItemsAdjustments(account, payment1.getId(), iias, callContext);
+        assertListenerStatus();
+
+        try {
+            invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(clock.getUTCToday()), false, callContext);
+            Assert.fail("Should not gnenerated an new invoice");
+        } catch (InvoiceApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.INVOICE_NOTHING_TO_DO.getCode());
+        }
+    }
+
 }
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 4d82d6d..78d5e9a 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
@@ -64,7 +64,7 @@ import com.google.inject.Inject;
  * Terminology for repair scenarii:
  * <p/>
  * - A 'repaired' item is an item that was generated and that needs to be repaired because the plan changed for that subscription on that period of time
- * - The 'repair' item is the item that cancels the (to be) repaired item; the repair item amount might not match (to be) repaired item because:
+ * - The 'repair' item is the item that cancels the (to be) repaired item; the repair item amount might not match the (to be) repaired item because:
  * * the (to be) repaired item was already adjusted so we will only repair what is left
  * * in case of partial repair we only repair the part that is not used
  * - The 'reparee' item is only present on disk-- in the existing item list -- in case of full repair; in that case it represents the portion of the item that should still
@@ -181,7 +181,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
                 final BigDecimal amountNegated = existingItem.getAmount() == null ? null : existingItem.getAmount().subtract(existingAdjustedPositiveAmount).negate();
                 if (amountNegated != null && amountNegated.compareTo(BigDecimal.ZERO) < 0) {
                     final RepairAdjInvoiceItem candidateRepairItem = new RepairAdjInvoiceItem(existingItem.getInvoiceId(), existingItem.getAccountId(), existingItem.getStartDate(), existingItem.getEndDate(), amountNegated, existingItem.getCurrency(), existingItem.getId());
-                    addRepairItem(existingItem, candidateRepairItem, proposedItems);
+                    addRepairsForItem(existingItem, candidateRepairItem, proposedItems);
                 }
             }
         }
@@ -194,7 +194,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
      * @param candidateRepairItem the repair item we would have if we were to repair the full period
      * @param proposedItems       the list of proposed items
      */
-    void addRepairItem(final InvoiceItem repairedItem, final RepairAdjInvoiceItem candidateRepairItem, final List<InvoiceItem> proposedItems) {
+    void addRepairsForItem(final InvoiceItem repairedItem, final RepairAdjInvoiceItem candidateRepairItem, final List<InvoiceItem> proposedItems) {
 
         int nbTotalRepaireeDays = 0;
 
@@ -224,7 +224,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
             }
         });
 
-        //Build the reparees
+        //Build the repaired
         BigDecimal totalRepairItemAmount = BigDecimal.ZERO;
         List<InvoiceItem> repairedItems = new ArrayList<InvoiceItem>();
         InvoiceItem prevReparee = null;
@@ -391,10 +391,10 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
                 itemsToRemove.add(repairItem.getLinkedItemId());
 
                 final InvoiceItem repairedItem = getRepairedInvoiceItem(repairItem.getLinkedItemId(), existingItems);
-                // if this is a full repair there is no reparee so nothing to remove; if not reparees need to be removed from proposed list
-                if (!isFullRepair(repairedItem, repairItem, existingItems)) {
-                    removeProposedRepareesForPartialrepair(repairedItem, repairItem, proposedItems);
-                }
+                // Always look for reparees; if this is a full repair there may not be any reparee to remove, but
+                // if this is a partial repair with an additional invoice item adjustment, this is seen as a full repair
+                // and yet there is a reparee to remove
+                removeProposedRepareesForPartialrepair(repairedItem, repairItem, proposedItems);
             }
         }
         final Iterator<InvoiceItem> iterator = existingItems.iterator();
diff --git a/invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java b/invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java
index 34c48c7..4c43c7c 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java
@@ -48,9 +48,9 @@ public class DefaultInvoiceGeneratorWithSwitchRepairLogic extends DefaultInvoice
     }
 
     @Override
-    void addRepairItem(final InvoiceItem repairedItem, final RepairAdjInvoiceItem candidateRepairItem, final List<InvoiceItem> proposedItems) {
+    void addRepairsForItem(final InvoiceItem repairedItem, final RepairAdjInvoiceItem candidateRepairItem, final List<InvoiceItem> proposedItems) {
         if (repairtLogic == REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR) {
-            super.addRepairItem(repairedItem, candidateRepairItem, proposedItems);
+            super.addRepairsForItem(repairedItem, candidateRepairItem, proposedItems);
         } else {
             proposedItems.add(candidateRepairItem);
             return;
diff --git a/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java b/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java
index c363267..569812f 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java
@@ -54,7 +54,7 @@ public class TestDefaultInvoiceGeneratorRepairUnit extends InvoiceTestSuiteNoDB 
         this.defaultInvoiceGenerator = (DefaultInvoiceGenerator) generator;
     }
 
-    /*********************************************  addRepairItem logic ********************************/
+    /*********************************************  addRepairsForItem logic ********************************/
 
     //                 repairedItem
     // |-----------------------------------------------|
@@ -82,7 +82,7 @@ public class TestDefaultInvoiceGeneratorRepairUnit extends InvoiceTestSuiteNoDB 
         final InvoiceItem proposed2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startProposed2, endDate, BigDecimal.TEN, rate, currency);
         proposed.add(proposed2);
 
-        defaultInvoiceGenerator.addRepairItem(repairedItem, candidateRepairItem, proposed);
+        defaultInvoiceGenerator.addRepairsForItem(repairedItem, candidateRepairItem, proposed);
 
         assertEquals(proposed.size(), 1);
         assertEquals(proposed.get(0).getStartDate(), endDateProposed1);
@@ -115,7 +115,7 @@ public class TestDefaultInvoiceGeneratorRepairUnit extends InvoiceTestSuiteNoDB 
         final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDateProposed1, BigDecimal.TEN, rate, currency);
         proposed.add(proposed1);
 
-        defaultInvoiceGenerator.addRepairItem(repairedItem, candidateRepairItem, proposed);
+        defaultInvoiceGenerator.addRepairsForItem(repairedItem, candidateRepairItem, proposed);
 
         assertEquals(proposed.size(), 1);
         assertEquals(proposed.get(0).getStartDate(), endDateProposed1);
@@ -149,7 +149,7 @@ public class TestDefaultInvoiceGeneratorRepairUnit extends InvoiceTestSuiteNoDB 
         final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDateProposed1, endDate, BigDecimal.TEN, rate, currency);
         proposed.add(proposed1);
 
-        defaultInvoiceGenerator.addRepairItem(repairedItem, candidateRepairItem, proposed);
+        defaultInvoiceGenerator.addRepairsForItem(repairedItem, candidateRepairItem, proposed);
 
         assertEquals(proposed.size(), 1);
         assertEquals(proposed.get(0).getStartDate(), startDate);
@@ -189,7 +189,7 @@ public class TestDefaultInvoiceGeneratorRepairUnit extends InvoiceTestSuiteNoDB 
         final InvoiceItem proposed3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDateProposed3, endDate, BigDecimal.TEN, rate, currency);
         proposed.add(proposed3);
 
-        defaultInvoiceGenerator.addRepairItem(repairedItem, candidateRepairItem, proposed);
+        defaultInvoiceGenerator.addRepairsForItem(repairedItem, candidateRepairItem, proposed);
 
         assertEquals(proposed.size(), 2);
         assertEquals(proposed.get(0).getStartDate(), endDateProposed1);
@@ -206,7 +206,7 @@ public class TestDefaultInvoiceGeneratorRepairUnit extends InvoiceTestSuiteNoDB 
     }
 
 
-    /*********************************************  addRepairItems logic ********************************/
+    /*********************************************  addRepairsForItems logic ********************************/
 
     @Test(groups = "fast")
     public void testAddRepairedItemsItemsRecurringPrice() {