killbill-memoizeit

subscription: fix previously uncaught exception in PlanAligner getNextTimedPhase

8/3/2017 8:37:17 AM

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
index f893c7e..6f8ed71 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
@@ -24,11 +24,13 @@ import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
 import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import org.killbill.billing.entitlement.api.EntitlementApiException;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.payment.api.PluginProperty;
@@ -101,6 +103,160 @@ public class TestCatalogRetireElements extends TestIntegrationBase {
     }
 
     @Test(groups = "slow")
+    public void testRetirePlanWithUncancel() throws Exception {
+        // Catalog v1 starts in 2011-01-01
+        // Catalog v2 starts in 2015-12-01
+        final LocalDate today = new LocalDate(2015, 10, 5);
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(today);
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        final String productName = "Pistol";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+
+        Entitlement bpEntitlement =
+                createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName,
+                                                           ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+        assertNotNull(bpEntitlement);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 1);
+        assertEquals(bpEntitlement.getState(), EntitlementState.ACTIVE);
+
+        // Move out a month.
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        // Cancel entitlement
+        bpEntitlement = bpEntitlement.cancelEntitlementWithDate(new LocalDate("2016-05-01"), true, ImmutableList.<PluginProperty>of(), callContext);
+        assertEquals(bpEntitlement.getState(), EntitlementState.ACTIVE);
+        assertListenerStatus();
+
+        // Move out a month.
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        // Catalog v2 should start now.
+
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, term, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        try {
+            entitlementApi.createBaseEntitlement(account.getId(), spec, "externalKey2", null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+            fail(); // force to fail is there is not an exception
+        } catch (final EntitlementApiException e) {
+            assertEquals(e.getCode(), ErrorCode.CAT_PLAN_NOT_FOUND.getCode());
+        }
+
+        // Uncancel entitlement
+        busHandler.pushExpectedEvents(NextEvent.UNCANCEL);
+        bpEntitlement.uncancelEntitlement(ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        // Move out a month and verify 'Pistol' plan continue working as expected.
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+        assertEquals(invoices.size(), 4);
+        for (final Invoice invoice : invoices) {
+            assertEquals(invoice.getInvoiceItems().get(0).getPlanName(), "pistol-monthly");
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testRetirePlanAfterChange() throws Exception {
+        // Catalog v1 starts in 2011-01-01
+        // Catalog v3 starts in 2016-01-01
+        final LocalDate today = new LocalDate(2015, 7, 5);
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(today);
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
+
+        final String productName = "Pistol";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+        final PlanPhaseSpecifier spec1 = new PlanPhaseSpecifier(productName, term, "DEFAULT", null);
+
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        Entitlement bpEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec1, "externalKey", null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        assertNotNull(bpEntitlement);
+        assertEquals(bpEntitlement.getLastActivePhase().getPhaseType(), PhaseType.TRIAL);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 1);
+
+        // Move out after trial (2015-08-04)
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+
+        assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getPhaseType(), PhaseType.EVERGREEN);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 2);
+
+        // 2015-08-05
+        clock.addDays(1);
+        assertListenerStatus();
+
+        // Change to discount phase in SpecialDiscount pricelist (CBA generated, no payment)
+        // Note that we need to trigger a CHANGE outside a TRIAL phase to generate a CHANGE event (otherwise, a CREATE is generated)
+        final PlanPhaseSpecifier spec2 = new PlanPhaseSpecifier(productName, term, "SpecialDiscount", null);
+        busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
+        bpEntitlement = bpEntitlement.changePlanWithDate(spec2, null, clock.getUTCToday(), ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getPhaseType(), PhaseType.DISCOUNT);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 3);
+
+        // Move out after discount phase (happens on 2015-11-04)
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        // 2015-09-05
+        clock.addMonths(1);
+        assertListenerStatus();
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        // 2015-10-05
+        clock.addMonths(1);
+        assertListenerStatus();
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        // 2015-11-05
+        clock.addMonths(1);
+        // This verifies the PlanAligner.getNextTimedPhase codepath with a CHANGE transition type
+        assertListenerStatus();
+
+        assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getPhaseType(), PhaseType.EVERGREEN);
+
+        // Move out a month (2015-12-05)
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        // Move out a month (2016-01-01)
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        // Catalog v3 should start now.
+
+        try {
+            entitlementApi.createBaseEntitlement(account.getId(), spec2, "externalKey2", null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+            fail(); // force to fail is there is not an exception
+        } catch (final EntitlementApiException e) {
+            assertEquals(e.getCode(), ErrorCode.CAT_NO_SUCH_PRODUCT.getCode());
+        }
+
+        // Move out a month and verify 'Pistol' discounted plan continues working as expected.
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+        assertEquals(invoices.size(), 9);
+    }
+
+    @Test(groups = "slow")
     public void testRetireProduct() throws Exception {
         // Catalog v1 starts in 2011-01-01
         // Catalog v3 starts in 2016-01-01
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java b/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
index 6e66d0c..3f2602b 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
@@ -165,7 +165,11 @@ public class PlanAligner extends BaseAligner {
                                                                                subscription.getBundleStartDate(),
                                                                                pendingOrLastPlanTransition.getNextPlan(),
                                                                                pendingOrLastPlanTransition.getNextPhase().getPhaseType(),
-                                                                               effectiveDate,
+                                                                               // Use the catalog version at subscription creation time: this allows
+                                                                               // for PHASE events and uncancel operations for plans/products/pricelists already retired
+                                                                               // This is no 100% correct in a scenario where the catalog was updated and the alignment rules changed since
+                                                                               // See https://github.com/killbill/killbill/issues/784
+                                                                               subscription.getAlignStartDate(),
                                                                                context);
                     return getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
                 case CHANGE: