killbill-memoizeit

invoice: Modify invoice code to correctly generate $0 recurring

11/16/2016 1:03:32 AM

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
index c314ac2..582c2c5 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
@@ -33,6 +33,7 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
 import org.killbill.billing.callcontext.DefaultCallContext;
 import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.CatalogUserApi;
@@ -48,6 +49,7 @@ import org.killbill.billing.catalog.api.TimeUnit;
 import org.killbill.billing.catalog.api.user.DefaultSimplePlanDescriptor;
 import org.killbill.billing.entitlement.api.Entitlement;
 import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.entitlement.api.Subscription;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
@@ -63,6 +65,7 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
@@ -274,6 +277,95 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
         }
     }
 
+
+    @Test(groups = "slow")
+    public void testWith$0RecurruingPlan() throws Exception {
+
+        // Create a per-tenant catalog with one plan
+        final SimplePlanDescriptor zeroDesc = new DefaultSimplePlanDescriptor("zeroDesc-monthly", "Zero", ProductCategory.BASE, account.getCurrency(), BigDecimal.ZERO, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
+        catalogUserApi.addSimplePlan(zeroDesc, init, testCallContext);
+        StaticCatalog catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
+        assertEquals(catalog.getCurrentPlans().size(), 1);
+
+        final PlanPhaseSpecifier specZero = new PlanPhaseSpecifier("zeroDesc-monthly", null);
+
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), specZero, UUID.randomUUID().toString(), ImmutableList.<PlanPhasePriceOverride>of(), null, null, false, ImmutableList.<PluginProperty>of(), testCallContext);
+        assertListenerStatus();
+
+        Subscription refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+        assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 7, 1));
+
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+        assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 8, 1));
+
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+        assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 9, 1));
+
+
+        // Add another Plan in the catalog
+        final SimplePlanDescriptor descNonZero = new DefaultSimplePlanDescriptor("superfoo-monthly", "SuperFoo", ProductCategory.BASE, account.getCurrency(), new BigDecimal("20.00"), BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
+        catalogUserApi.addSimplePlan(descNonZero, init, testCallContext);
+        catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
+        assertEquals(catalog.getCurrentPlans().size(), 2);
+
+
+        final PlanPhaseSpecifier specNonZero = new PlanPhaseSpecifier("superfoo-monthly", null);
+
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        final Entitlement baseEntitlement2 = entitlementApi.createBaseEntitlement(account.getId(), specNonZero, UUID.randomUUID().toString(), ImmutableList.<PlanPhasePriceOverride>of(), null, null, false, ImmutableList.<PluginProperty>of(), testCallContext);
+        assertListenerStatus();
+
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+        assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 10, 1));
+
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+        assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 11, 1));
+
+        busHandler.pushExpectedEvents(NextEvent.BLOCK);
+        baseEntitlement.cancelEntitlementWithDateOverrideBillingPolicy(clock.getUTCToday(), BillingActionPolicy.END_OF_TERM, ImmutableList.<PluginProperty>of(), testCallContext);
+        assertListenerStatus();
+
+
+        busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+        assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 11, 1));
+
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+        assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 11, 1));
+
+    }
+
+
+
     private Entitlement createEntitlement(final String planName, final boolean expectPayment) throws EntitlementApiException {
         final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(planName, null);
         return createEntitlement(spec, null, expectPayment);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java
index 3673a4c..3473252 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java
@@ -45,7 +45,7 @@ public class InvoiceWithMetadata {
         this.invoice = originalInvoice;
         this.perSubscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates;
         build();
-        remove$0RecurringAndUsageItems();
+        remove$0UsageItems();
     }
 
     public Invoice getInvoice() {
@@ -79,12 +79,12 @@ public class InvoiceWithMetadata {
         });
     }
 
-    protected void remove$0RecurringAndUsageItems() {
+    protected void remove$0UsageItems() {
         if (invoice != null) {
             final Iterator<InvoiceItem> it = invoice.getInvoiceItems().iterator();
             while (it.hasNext()) {
                 final InvoiceItem item = it.next();
-                if ((item.getInvoiceItemType() == InvoiceItemType.RECURRING ||  item.getInvoiceItemType() == InvoiceItemType.USAGE) &&
+                if ((item.getInvoiceItemType() == InvoiceItemType.USAGE) &&
                     item.getAmount().compareTo(BigDecimal.ZERO) == 0) {
                     it.remove();
                 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index c29c28a..01d03b7 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -484,18 +484,9 @@ public class InvoiceDispatcher {
                                                            final Iterable<InvoiceItemModelDao> invoiceItemModelDaos,
                                                            final FutureAccountNotifications futureAccountNotifications,
                                                            final boolean isRealInvoiceWithItems, final InternalCallContext context) throws SubscriptionBaseApiException, InvoiceApiException {
-        // We filter any zero amount for USAGE items prior we generate the invoice, which may leave us with an invoice with no items;
-        // we recompute the isRealInvoiceWithItems flag based on what is left (the call to invoice is still necessary to set the future notifications).
-        final Iterable<InvoiceItemModelDao> filteredInvoiceItemModelDaos = Iterables.filter(invoiceItemModelDaos, new Predicate<InvoiceItemModelDao>() {
-            @Override
-            public boolean apply(@Nullable final InvoiceItemModelDao input) {
-                return (input.getType() != InvoiceItemType.USAGE || input.getAmount().compareTo(BigDecimal.ZERO) != 0);
-            }
-        });
-
-        final boolean isThereAnyItemsLeft = filteredInvoiceItemModelDaos.iterator().hasNext();
+        final boolean isThereAnyItemsLeft = invoiceItemModelDaos.iterator().hasNext();
         if (isThereAnyItemsLeft) {
-            invoiceDao.createInvoice(invoiceModelDao, ImmutableList.copyOf(filteredInvoiceItemModelDaos), isRealInvoiceWithItems, futureAccountNotifications, context);
+            invoiceDao.createInvoice(invoiceModelDao, ImmutableList.copyOf(invoiceItemModelDaos), isRealInvoiceWithItems, futureAccountNotifications, context);
         } else {
             invoiceDao.setFutureAccountNotificationsForEmptyInvoice(account.getId(), futureAccountNotifications, context);
         }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index 9aeedfe..e104024 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -629,7 +629,9 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
         final Invoice invoice = invoiceWithMetadata.getInvoice();
-        assertNull(invoice);
+        assertNotNull(invoice);
+        assertEquals(invoice.getInvoiceItems().size(), 1);
+        assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
     }
 
     @Test(groups = "fast")
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
index 5fbfc79..5d81097 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
@@ -78,6 +78,7 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
         }
     }
 
+
     @Test(groups = "fast")
     public void testIsSameDayAndSameSubscriptionWithNullPrevEvent() {
 
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceWithMetadata.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceWithMetadata.java
new file mode 100644
index 0000000..b77cec7
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceWithMetadata.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.DefaultPrice;
+import org.killbill.billing.catalog.MockInternationalPrice;
+import org.killbill.billing.catalog.MockPlan;
+import org.killbill.billing.catalog.MockPlanPhase;
+import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+public class TestInvoiceWithMetadata extends InvoiceTestSuiteNoDB {
+
+
+    private Account account;
+    private SubscriptionBase subscription;
+
+    @Override
+    @BeforeMethod(groups = "fast")
+    public void beforeMethod() {
+        super.beforeMethod();
+
+        try {
+            account = invoiceUtil.createAccount(callContext);
+            subscription = invoiceUtil.createSubscription();
+        } catch (final Exception e) {
+            fail(e.getMessage());
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testWith$0RecurringItem() {
+
+
+        final LocalDate invoiceDate = new LocalDate(2016, 11, 15);
+
+        final Invoice originalInvoice = new DefaultInvoice(account.getId(), invoiceDate, account.getCurrency());
+
+        final Plan plan = new MockPlan("my-plan");
+        final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(BigDecimal.TEN, account.getCurrency()));
+
+        final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+
+        final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+                                                                      subscription,
+                                                                      invoiceDate.toDateTimeAtStartOfDay(),
+                                                                      plan,
+                                                                      planPhase,
+                                                                      null,
+                                                                      BigDecimal.ZERO,
+                                                                      account.getCurrency(),
+                                                                      planPhase.getRecurring().getBillingPeriod(),
+                                                                      1,
+                                                                      BillingMode.IN_ADVANCE,
+                                                                      "Billing Event Desc",
+                                                                      1L,
+                                                                      SubscriptionBaseTransitionType.CREATE);
+
+
+        final InvoiceItem invoiceItem  = new RecurringInvoiceItem(UUID.randomUUID(),
+                                                                  invoiceDate.toDateTimeAtStartOfDay(),
+                                                                  originalInvoice.getId(),
+                                                                  account.getId(),
+                                                                  subscription.getBundleId(),
+                                                                  subscription.getId(),
+                                                                  event.getPlan().getName(),
+                                                                  event.getPlanPhase().getName(),
+                                                                  invoiceDate,
+                                                                  invoiceDate.plusMonths(1),
+                                                                  BigDecimal.ZERO,
+                                                                  BigDecimal.ZERO,
+                                                                  account.getCurrency());
+
+        originalInvoice.addInvoiceItem(invoiceItem);
+
+        final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates = new HashMap<UUID, SubscriptionFutureNotificationDates>();
+        final SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(BillingMode.IN_ADVANCE);
+        subscriptionFutureNotificationDates.updateNextRecurringDateIfRequired(invoiceDate.plusMonths(1));
+
+        perSubscriptionFutureNotificationDates.put(subscription.getId(), subscriptionFutureNotificationDates);
+
+        final InvoiceWithMetadata invoiceWithMetadata = new InvoiceWithMetadata(originalInvoice, perSubscriptionFutureNotificationDates);
+
+        // We generate an invoice with one item, invoicing for $0
+        final Invoice resultingInvoice = invoiceWithMetadata.getInvoice();
+        Assert.assertNotNull(resultingInvoice);
+        Assert.assertEquals(resultingInvoice.getInvoiceItems().size(), 1);
+        Assert.assertEquals(resultingInvoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+
+        final Map<UUID, InvoiceWithMetadata.SubscriptionFutureNotificationDates> dateMap = invoiceWithMetadata.getPerSubscriptionFutureNotificationDates();
+
+        final InvoiceWithMetadata.SubscriptionFutureNotificationDates futureNotificationDates = dateMap.get(subscription.getId());
+
+        // We verify that we generated the future notification for a month ahead
+        Assert.assertNotNull(futureNotificationDates.getNextRecurringDate());
+        Assert.assertEquals(futureNotificationDates.getNextRecurringDate().compareTo(invoiceDate.plusMonths(1)), 0 );
+    }
+
+}
\ No newline at end of file