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 6f8ed71..33c8f44 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
@@ -257,6 +257,106 @@ public class TestCatalogRetireElements extends TestIntegrationBase {
}
@Test(groups = "slow")
+ public void testRetirePlanWithUncancelAfterChange() 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);
+
+ // Cancel entitlement
+ bpEntitlement = bpEntitlement.cancelEntitlementWithDate(new LocalDate("2016-05-01"), true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(bpEntitlement.getState(), EntitlementState.ACTIVE);
+ assertListenerStatus();
+
+ // 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();
+
+ // Uncancel entitlement
+ busHandler.pushExpectedEvents(NextEvent.UNCANCEL);
+ bpEntitlement.uncancelEntitlement(ImmutableList.<PluginProperty>of(), callContext);
+ 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 3f2602b..9d6145a 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
@@ -167,7 +167,7 @@ public class PlanAligner extends BaseAligner {
pendingOrLastPlanTransition.getNextPhase().getPhaseType(),
// 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
+ // This is not 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);
@@ -179,6 +179,8 @@ public class PlanAligner extends BaseAligner {
pendingOrLastPlanTransition.getPreviousPlan(),
pendingOrLastPlanTransition.getNextPlan(),
effectiveDate,
+ // Same remark as above
+ subscription.getAlignStartDate(),
pendingOrLastPlanTransition.getEffectiveTransitionTime(),
subscription.getAllTransitions().get(0).getNextPhase().getPhaseType(),
null,
@@ -197,7 +199,7 @@ public class PlanAligner extends BaseAligner {
final DateTime bundleStartDate,
final Plan plan,
@Nullable final PhaseType initialPhase,
- final DateTime effectiveDate,
+ final DateTime catalogEffectiveDate,
final InternalTenantContext context)
throws CatalogApiException, SubscriptionBaseApiException {
final Catalog catalog = catalogService.getFullCatalog(true, true, context);
@@ -205,7 +207,7 @@ public class PlanAligner extends BaseAligner {
final PlanSpecifier planSpecifier = new PlanSpecifier(plan.getName());
final DateTime planStartDate;
- final PlanAlignmentCreate alignment = catalog.planCreateAlignment(planSpecifier, effectiveDate);
+ final PlanAlignmentCreate alignment = catalog.planCreateAlignment(planSpecifier, catalogEffectiveDate);
switch (alignment) {
case START_OF_SUBSCRIPTION:
planStartDate = subscriptionStartDate;
@@ -238,6 +240,7 @@ public class PlanAligner extends BaseAligner {
pendingOrLastPlanTransition.getNextPlan(),
nextPlan,
effectiveDate,
+ effectiveDate,
// This method is only called while doing the change, hence we want to pass the change effective date
effectiveDate,
subscription.getAllTransitions().get(0).getNextPhase().getPhaseType(),
@@ -252,6 +255,7 @@ public class PlanAligner extends BaseAligner {
final Plan currentPlan,
final Plan nextPlan,
final DateTime effectiveDate,
+ final DateTime catalogEffectiveDate,
final DateTime lastOrCurrentChangeEffectiveDate,
final PhaseType originalInitialPhase,
@Nullable final PhaseType newPlanInitialPhaseType,
@@ -264,7 +268,7 @@ public class PlanAligner extends BaseAligner {
final PlanSpecifier toPlanSpecifier = new PlanSpecifier(nextPlan.getName());
final PhaseType initialPhase;
final DateTime planStartDate;
- final PlanAlignmentChange alignment = catalog.planChangeAlignment(fromPlanPhaseSpecifier, toPlanSpecifier, effectiveDate);
+ final PlanAlignmentChange alignment = catalog.planChangeAlignment(fromPlanPhaseSpecifier, toPlanSpecifier, catalogEffectiveDate);
switch (alignment) {
case START_OF_SUBSCRIPTION:
planStartDate = subscriptionStartDate;