killbill-aplcache
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java 256(+256 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java 69(+64 -5)
catalog/src/test/resources/catalogTest.xml 48(+47 -1)
invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java 17(+17 -0)
NEWS 3(+3 -0)
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..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
@@ -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,260 @@ 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 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/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
index 1f1028b..f215100 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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
@@ -30,6 +30,7 @@ import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+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.PlanPhasePriceOverride;
@@ -38,7 +39,6 @@ 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.EntitlementApiException;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.DryRunType;
@@ -50,14 +50,12 @@ import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
-import org.testng.Assert;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
import static com.tc.util.Assert.fail;
import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
public class TestIntegrationInvoice extends TestIntegrationBase {
@@ -506,4 +504,65 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
}
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/783")
+ public void testIntegrationWithRecurringFreePlan() throws Exception {
+ final DateTime initialCreationDate = new DateTime(2017, 1, 1, 0, 0, 0, 0, testTimeZone);
+ // set clock to the initial start date
+ clock.setTime(initialCreationDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+
+ // Price override of $0
+ final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
+ overrides.add(new DefaultPlanPhasePriceOverride("blowdart-monthly-notrial-evergreen", account.getCurrency(), null, BigDecimal.ZERO));
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleExternalKey", overrides, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 1, 1), new LocalDate(2017, 2, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // 2017-02-01
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 2, 1), new LocalDate(2017, 3, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // Do the change mid-month so the repair triggers the bug in https://github.com/killbill/killbill/issues/783
+ entitlement.changePlanWithDate(spec, ImmutableList.<PlanPhasePriceOverride>of(), new LocalDate("2017-02-15"), ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // 2017-02-15
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addDays(15);
+ assertListenerStatus();
+
+ // Note: no repair
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 2, 1), new LocalDate(2017, 3, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 2, 1), new LocalDate(2017, 2, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO),
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 2, 15), new LocalDate(2017, 3, 1), InvoiceItemType.RECURRING, new BigDecimal("14.98")));
+
+ // 2017-03-01
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addDays(15);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 3, 1), new LocalDate(2017, 4, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // 2017-04-01
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 5, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 4, 1), new LocalDate(2017, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ }
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
index 8ba0800..e25d782 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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
@@ -28,8 +28,12 @@ import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+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.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
@@ -538,4 +542,197 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
expectedInvoices.clear();
}
+
+ @Test(groups = "slow")
+ public void testBCDChangeFromFreePlanToPayingPlanNoTrial() throws Exception {
+ final PlanPhaseSpecifier specNoTrial = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+ testBCDChangeFromFreePlanToPayingPlan(specNoTrial);
+ }
+
+ @Test(groups = "slow")
+ public void testBCDChangeFromFreePlanToPayingPlanWithTrial() throws Exception {
+ // Change to the paying plan (alignment is START_OF_SUBSCRIPTION, but because we are already in an EVERGREEN phase, we will re-align with the EVERGREEN phase of the new 3-phases paying plan)
+ final PlanPhaseSpecifier specWithTrial = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "DEFAULT", null);
+ testBCDChangeFromFreePlanToPayingPlan(specWithTrial);
+ }
+
+ @Test(groups = "slow")
+ public void testBCDChangeFromFreePlanToPayingPlanWithTrialAndCHANGE_OF_PLANPolicy30DaysMonth() throws Exception {
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setTime(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+
+ // Price override of $0
+ final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
+ overrides.add(new DefaultPlanPhasePriceOverride("blowdart-monthly-notrial-evergreen", account.getCurrency(), null, BigDecimal.ZERO));
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ // BP creation : Will set Account BCD to the first (DateOfFirstRecurringNonZeroCharge is the subscription start date in this case)
+ final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleExternalKey", overrides, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 5, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // 2016-4-15
+ clock.addDays(14);
+
+ // Set next BCD to be the 15
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE);
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, null, internalCallContext);
+ assertListenerStatus();
+
+ // Re-alignment invoice
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 4, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO),
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 15), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // Change to the paying plan (alignment is CHANGE_OF_PLAN: we end up in TRIAL)
+ final PlanPhaseSpecifier specWithTrial = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "trial", null);
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
+ baseEntitlement.changePlanOverrideBillingPolicy(specWithTrial, ImmutableList.<PlanPhasePriceOverride>of(), clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Trial invoice
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 15), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+
+ // Verify next month (extra null invoice because of the original notification set on the 1st)
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // First paying invoice
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // Verify next month
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 5, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 15), new LocalDate(2016, 7, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ }
+
+ @Test(groups = "slow")
+ public void testBCDChangeFromFreePlanToPayingPlanWithTrialAndCHANGE_OF_PLANPolicy31DaysMonth() throws Exception {
+ final DateTime initialDate = new DateTime(2016, 5, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setTime(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+
+ // Price override of $0
+ final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
+ overrides.add(new DefaultPlanPhasePriceOverride("blowdart-monthly-notrial-evergreen", account.getCurrency(), null, BigDecimal.ZERO));
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ // BP creation : Will set Account BCD to the first (DateOfFirstRecurringNonZeroCharge is the subscription start date in this case)
+ final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleExternalKey", overrides, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 6, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // 2016-5-15
+ clock.addDays(14);
+
+ // Set next BCD to be the 14
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 14, null, internalCallContext);
+ // No bus event, no invoice expected
+ assertListenerStatus();
+
+ // Change to the paying plan (alignment is CHANGE_OF_PLAN: we end up in TRIAL)
+ // Extra NULL_INVOICE event because invoice computes a future notification effective right away
+ final PlanPhaseSpecifier specWithTrial = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "trial", null);
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.NULL_INVOICE, NextEvent.INVOICE);
+ baseEntitlement.changePlanOverrideBillingPolicy(specWithTrial, ImmutableList.<PlanPhasePriceOverride>of(), clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Trial invoice (with re-alignment invoice item from free plan)
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO),
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+
+ // Verify next month (extra null invoice because of the original notification set on the 1st)
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // First paying invoice
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 14), new LocalDate(2016, 7, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // Verify next month
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 14), new LocalDate(2016, 8, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ }
+
+ private void testBCDChangeFromFreePlanToPayingPlan(final PlanSpecifier toSpec) throws Exception {
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setTime(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+
+ // Price override of $0
+ final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
+ overrides.add(new DefaultPlanPhasePriceOverride("blowdart-monthly-notrial-evergreen", account.getCurrency(), null, BigDecimal.ZERO));
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ // BP creation : Will set Account BCD to the first (DateOfFirstRecurringNonZeroCharge is the subscription start date in this case)
+ final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleExternalKey", overrides, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 5, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // 2016-4-15
+ clock.addDays(14);
+
+ // Set next BCD to be the 15
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE);
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, null, internalCallContext);
+ assertListenerStatus();
+
+ // Re-alignment invoice
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 4, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO),
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 15), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // Change to the paying plan
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ baseEntitlement.changePlanOverrideBillingPolicy(toSpec, ImmutableList.<PlanPhasePriceOverride>of(), clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // First paying invoice
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 15), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // Verify next month (null invoice because of the original notification set on the 1st)
+ busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // Verify next month
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 5, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 15), new LocalDate(2016, 7, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ }
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
index cf7c9a3..5afa415 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -223,7 +223,7 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
for (final DefaultPlanPhase cur : initialPhases) {
cur.validate(catalog, errors);
- if (cur.getPhaseType() == PhaseType.EVERGREEN || cur.getPhaseType() == PhaseType.FIXEDTERM) {
+ if (cur.getPhaseType() == PhaseType.EVERGREEN) {
errors.add(new ValidationError(String.format("Initial Phase %s of plan %s cannot be of type %s",
cur.getName(), name, cur.getPhaseType()),
catalog.getCatalogURI(), DefaultPlan.class, ""));
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
index f0707ed..e66e48d 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
@@ -39,6 +39,7 @@ import org.killbill.billing.catalog.api.TimeUnit;
import org.killbill.billing.catalog.api.user.DefaultSimplePlanDescriptor;
import org.killbill.xmlloader.XMLLoader;
import org.killbill.xmlloader.XMLWriter;
+import org.testng.Assert;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
@@ -258,6 +259,59 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
@Test(groups = "fast")
+ public void testPlanWithNonFinalFixedTermPhase() throws Exception {
+
+
+ final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
+
+ final MutableStaticCatalog mutableCatalog = new DefaultMutableStaticCatalog(catalog);
+
+ final DefaultProduct newProduct = new DefaultProduct();
+ newProduct.setName("Something");
+ newProduct.setCatagory(ProductCategory.BASE);
+ newProduct.initialize((StandaloneCatalog) mutableCatalog, null);
+ mutableCatalog.addProduct(newProduct);
+
+
+ final DefaultPlanPhase trialPhase = new DefaultPlanPhase();
+ trialPhase.setPhaseType(PhaseType.TRIAL);
+ trialPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(14));
+ trialPhase.setFixed(new DefaultFixed().setFixedPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.ZERO)})));
+
+ // Add a Plan with a FIXEDTERM phase
+ final DefaultPlanPhase fixedTermPhase = new DefaultPlanPhase();
+ fixedTermPhase.setPhaseType(PhaseType.FIXEDTERM);
+ fixedTermPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(3));
+ fixedTermPhase.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
+
+
+ final DefaultPlanPhase evergreenPhase = new DefaultPlanPhase();
+ evergreenPhase.setPhaseType(PhaseType.EVERGREEN);
+ evergreenPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(1));
+ evergreenPhase.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
+
+
+ final DefaultPlan newPlan = new DefaultPlan();
+ newPlan.setName("something-with-fixed-term");
+ newPlan.setPriceListName(DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
+ newPlan.setProduct(newProduct);
+ newPlan.setInitialPhases(new DefaultPlanPhase[] {trialPhase, fixedTermPhase});
+ newPlan.setFinalPhase(fixedTermPhase);
+ mutableCatalog.addPlan(newPlan);
+ newPlan.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
+
+ final String newCatalogStr = XMLWriter.writeXML((StandaloneCatalog) mutableCatalog, StandaloneCatalog.class);
+ final StandaloneCatalog newCatalog = XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(newCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
+
+ final DefaultPlan targetPlan = newCatalog.findCurrentPlan("something-with-fixed-term");
+ Assert.assertEquals(targetPlan.getInitialPhases().length, 2);
+ Assert.assertEquals(targetPlan.getInitialPhases()[1].getPhaseType(), PhaseType.FIXEDTERM);
+
+ }
+
+
+
+ @Test(groups = "fast")
public void testVerifyXML() throws Exception {
final StandaloneCatalog originalCatalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
catalog/src/test/resources/catalogTest.xml 48(+47 -1)
diff --git a/catalog/src/test/resources/catalogTest.xml b/catalog/src/test/resources/catalogTest.xml
index d15d55f..f8d9f93 100644
--- a/catalog/src/test/resources/catalogTest.xml
+++ b/catalog/src/test/resources/catalogTest.xml
@@ -143,6 +143,11 @@
<alignment>CHANGE_OF_PRICELIST</alignment>
</changeAlignmentCase>
<changeAlignmentCase>
+ <fromPriceList>notrial</fromPriceList>
+ <toPriceList>trial</toPriceList>
+ <alignment>CHANGE_OF_PLAN</alignment>
+ </changeAlignmentCase>
+ <changeAlignmentCase>
<alignment>START_OF_SUBSCRIPTION</alignment>
</changeAlignmentCase>
</changeAlignment>
@@ -297,6 +302,43 @@
</recurring>
</finalPhase>
</plan>
+ <plan name="blowdart-monthly-trial">
+ <product>Blowdart</product>
+ <initialPhases>
+ <phase type="TRIAL">
+ <duration>
+ <unit>DAYS</unit>
+ <number>30</number>
+ </duration>
+ <fixed>
+ <fixedPrice>
+ </fixedPrice>
+ </fixed>
+ </phase>
+ </initialPhases>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <recurring>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price>
+ <currency>USD</currency>
+ <value>29.95</value>
+ </price>
+ <price>
+ <currency>EUR</currency>
+ <value>29.95</value>
+ </price>
+ <price>
+ <currency>GBP</currency>
+ <value>29.95</value>
+ </price>
+ </recurringPrice>
+ </recurring>
+ </finalPhase>
+ </plan>
<plan name="pistol-weekly">
<product>Pistol</product>
<initialPhases>
@@ -1333,7 +1375,11 @@
<plans>
<plan>blowdart-monthly-notrial</plan>
<plan>pistol-monthly-notrial</plan>
-
+ </plans>
+ </childPriceList>
+ <childPriceList name="trial">
+ <plans>
+ <plan>blowdart-monthly-trial</plan>
</plans>
</childPriceList>
</priceLists>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index e4217f3..0e7fa4d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -306,6 +306,9 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
} else {
if (existingInvoicesForExternalCharges.get(invoiceIdForExternalCharge) == null) {
final Invoice existingInvoiceForExternalCharge = getInvoice(invoiceIdForExternalCharge, context);
+ if (InvoiceStatus.COMMITTED.equals(existingInvoiceForExternalCharge.getStatus())) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_ALREADY_COMMITTED, existingInvoiceForExternalCharge.getId());
+ }
existingInvoicesForExternalCharges.put(invoiceIdForExternalCharge, existingInvoiceForExternalCharge);
}
invoiceForExternalCharge = existingInvoicesForExternalCharges.get(invoiceIdForExternalCharge);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
index b9eda76..0e6ea00 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
@@ -206,6 +206,9 @@ public abstract class InvoiceItemBase extends EntityBase implements InvoiceItem
final InvoiceItemBase that = (InvoiceItemBase) o;
+ if (getInvoiceItemType() != null ? !getInvoiceItemType().equals(that.getInvoiceItemType()) : that.getInvoiceItemType() != null) {
+ return false;
+ }
if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
return false;
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
index df593b8..d194140 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2014 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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:
*
@@ -16,11 +18,10 @@
package org.killbill.billing.invoice.tree;
+import java.math.BigDecimal;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
@@ -44,8 +45,8 @@ public class SubscriptionItemTree {
private final List<Item> items = new LinkedList<Item>();
private final List<Item> existingFullyAdjustedItems = new LinkedList<Item>();
- private final List<InvoiceItem> existingFixedItems = new LinkedList<InvoiceItem>();
- private final Map<LocalDate, InvoiceItem> remainingFixedItems = new HashMap<LocalDate, InvoiceItem>();
+ private final List<InvoiceItem> existingIgnoredItems = new LinkedList<InvoiceItem>();
+ private final List<InvoiceItem> remainingIgnoredItems = new LinkedList<InvoiceItem>();
private final List<InvoiceItem> pendingItemAdj = new LinkedList<InvoiceItem>();
private final UUID targetInvoiceId;
@@ -91,7 +92,12 @@ public class SubscriptionItemTree {
switch (invoiceItem.getInvoiceItemType()) {
case RECURRING:
- root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, targetInvoiceId, ItemAction.ADD)));
+ if (invoiceItem.getAmount().compareTo(BigDecimal.ZERO) == 0) {
+ // Nothing to repair -- https://github.com/killbill/killbill/issues/783
+ existingIgnoredItems.add(invoiceItem);
+ } else {
+ root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, targetInvoiceId, ItemAction.ADD)));
+ }
break;
case REPAIR_ADJ:
@@ -99,7 +105,7 @@ public class SubscriptionItemTree {
break;
case FIXED:
- existingFixedItems.add(invoiceItem);
+ existingIgnoredItems.add(invoiceItem);
break;
case ITEM_ADJ:
@@ -158,6 +164,17 @@ public class SubscriptionItemTree {
public void mergeProposedItem(final InvoiceItem invoiceItem) {
Preconditions.checkState(!isBuilt, "Tree already built, unable to add new invoiceItem=%s", invoiceItem);
+ // Check if it was an existing item ignored for tree purposes (e.g. FIXED or $0 RECURRING, both of which aren't repaired)
+ final InvoiceItem existingItem = Iterables.tryFind(existingIgnoredItems, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return input.matches(invoiceItem);
+ }
+ }).orNull();
+ if (existingItem != null) {
+ return;
+ }
+
switch (invoiceItem.getInvoiceItemType()) {
case RECURRING:
// merged means we've either matched the proposed to an existing, or triggered a repair
@@ -168,15 +185,7 @@ public class SubscriptionItemTree {
break;
case FIXED:
- final InvoiceItem existingItem = Iterables.tryFind(existingFixedItems, new Predicate<InvoiceItem>() {
- @Override
- public boolean apply(final InvoiceItem input) {
- return input.matches(invoiceItem);
- }
- }).orNull();
- if (existingItem == null) {
- remainingFixedItems.put(invoiceItem.getStartDate(), invoiceItem);
- }
+ remainingIgnoredItems.add(invoiceItem);
break;
default:
@@ -204,7 +213,7 @@ public class SubscriptionItemTree {
public List<InvoiceItem> getView() {
final List<InvoiceItem> tmp = new LinkedList<InvoiceItem>();
- tmp.addAll(remainingFixedItems.values());
+ tmp.addAll(remainingIgnoredItems);
tmp.addAll(Collections2.filter(Collections2.transform(items, new Function<Item, InvoiceItem>() {
@Override
public InvoiceItem apply(final Item input) {
@@ -278,8 +287,8 @@ public class SubscriptionItemTree {
sb.append(", isMerged=").append(isMerged);
sb.append(", items=").append(items);
sb.append(", existingFullyAdjustedItems=").append(existingFullyAdjustedItems);
- sb.append(", existingFixedItems=").append(existingFixedItems);
- sb.append(", remainingFixedItems=").append(remainingFixedItems);
+ sb.append(", existingIgnoredItems=").append(existingIgnoredItems);
+ sb.append(", remainingIgnoredItems=").append(remainingIgnoredItems);
sb.append(", pendingItemAdj=").append(pendingItemAdj);
sb.append('}');
return sb.toString();
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
index be8c47a..d3e6ee4 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
@@ -27,6 +27,7 @@ import javax.annotation.Nullable;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.callcontext.DefaultCallContext;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
@@ -35,6 +36,7 @@ import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoiceStatus;
+import org.killbill.billing.invoice.api.InvoiceUserApi;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.util.api.TagApiException;
import org.killbill.billing.util.callcontext.CallContext;
@@ -373,5 +375,20 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
creditInvoice = invoiceUserApi.getInvoice(invoiceId, callContext);
Assert.assertEquals(creditInvoice.getStatus(), InvoiceStatus.COMMITTED);
+ try {
+ final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(invoiceId, accountId, null, "Initial external charge", clock.getUTCToday(), new BigDecimal("12.33"), accountCurrency);
+ invoiceUserApi.insertExternalCharges(accountId, clock.getUTCToday(), ImmutableList.of(externalCharge), true, callContext);
+ Assert.fail("Should fail to add external charge on already committed invoice");
+ } catch (final InvoiceApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.INVOICE_ALREADY_COMMITTED.getCode());
+ }
+
+ try {
+ invoiceUserApi.insertCreditForInvoice(accountId, invoiceId, creditAmount,
+ clock.getUTCToday(), accountCurrency, null, callContext);
+ Assert.fail("Should fail to add credit on already committed invoice");
+ } catch (final InvoiceApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.INVOICE_ALREADY_COMMITTED.getCode());
+ }
}
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
index 26f4170..49f3ce2 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -1493,6 +1493,32 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
Assert.assertEquals(previousExistingSize, 3);
}
+ @Test(groups = "fast")
+ public void testWithFreeRecurring() {
+ final LocalDate startDate = new LocalDate(2012, 8, 1);
+ final LocalDate endDate = new LocalDate(2012, 9, 1);
+
+ final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+ final BigDecimal monthlyRate2 = new BigDecimal("24.00");
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+ final InvoiceItem freeMonthly = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.ZERO, BigDecimal.ZERO, currency);
+ tree.addItem(freeMonthly);
+ final InvoiceItem payingMonthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyRate1, monthlyRate1, currency);
+ tree.addItem(payingMonthly1);
+ tree.flatten(true);
+
+ final InvoiceItem proposedPayingMonthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyRate2, monthlyRate2, currency);
+ tree.mergeProposedItem(proposedPayingMonthly2);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(proposedPayingMonthly2);
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate, monthlyRate1.negate(), currency, payingMonthly1.getId());
+ expectedResult.add(repair);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
private void printTreeJSON(final SubscriptionItemTree tree) throws IOException {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
tree.getRoot().jsonSerializeTree(OBJECT_MAPPER, outputStream);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index 79a3744..1e139a7 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -350,12 +350,11 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
}
protected void validatePaymentMethodForAccount(final UUID accountId, final UUID paymentMethodId, final CallContext callContext) throws PaymentApiException {
- if (paymentMethodId == null) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, accountId);
- }
- final PaymentMethod paymentMethod = paymentApi.getPaymentMethodById(paymentMethodId, false, false, ImmutableList.<PluginProperty>of(), callContext);
- if (!paymentMethod.getAccountId().equals(accountId)) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
+ if (paymentMethodId != null) {
+ final PaymentMethod paymentMethod = paymentApi.getPaymentMethodById(paymentMethodId, false, false, ImmutableList.<PluginProperty>of(), callContext);
+ if (!paymentMethod.getAccountId().equals(accountId)) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
+ }
}
}
NEWS 3(+3 -0)
diff --git a/NEWS b/NEWS
index 94d650c..e3a0cb7 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,6 @@
+0.18.12
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.18.12
+
0.18.11
See https://github.com/killbill/killbill/releases/tag/killbill-0.18.11
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
index 34a3561..e114d2e 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
@@ -116,7 +116,7 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
}
@Override
- public Payment createAuthorizationWithPaymentControl(final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency,
+ public Payment createAuthorizationWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency,
@Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey,
final Iterable<PluginProperty> properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions, callContext);
@@ -325,17 +325,12 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
checkNotNullParameter(properties, "plugin properties");
checkExternalKeyLength(paymentTransactionExternalKey);
- if (paymentMethodId == null && !paymentOptions.isExternalPayment()) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, "paymentMethodId", "should not be null");
- }
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
- // TODO validate if the code is located properly here
- // The code should understand that the external payment method needs to be created
- // if it doesn't exist yet and trigger a CREDIT using the (built-in) external payment plugin.
- final UUID nonNullPaymentMethodId = (paymentMethodId != null) ?
- paymentMethodId :
- paymentMethodProcessor.createOrGetExternalPaymentMethod(UUIDs.randomUUID().toString(), account, properties, callContext, internalCallContext);
+
+ final UUID resolvedPaymentMethodId = (paymentMethodId == null && paymentOptions.isExternalPayment()) ?
+ paymentMethodProcessor.createOrGetExternalPaymentMethod(UUIDs.randomUUID().toString(), account, properties, callContext, internalCallContext) :
+ paymentMethodId;
final String transactionType = TransactionType.PURCHASE.name();
Payment payment = null;
@@ -344,7 +339,7 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
try {
logEnterAPICall(log, transactionType, account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey, null, paymentControlPluginNames);
- payment = pluginControlPaymentProcessor.createPurchase(IS_API_PAYMENT, account, nonNullPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+ payment = pluginControlPaymentProcessor.createPurchase(IS_API_PAYMENT, account, resolvedPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
@@ -607,7 +602,7 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
}
@Override
- public Payment createCreditWithPaymentControl(final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency,
+ public Payment createCreditWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency,
@Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey,
final Iterable<PluginProperty> properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions, callContext);
@@ -632,14 +627,11 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
- // TODO validate if the code is located properly here
- // The code should understand that the external payment method needs to be created
- // if it doesn't exist yet and trigger a CREDIT using the (built-in) external payment plugin.
- final UUID nonNullPaymentMethodId = (paymentMethodId != null) ?
- paymentMethodId :
- paymentMethodProcessor.createOrGetExternalPaymentMethod(UUIDs.randomUUID().toString(), account, properties, callContext, internalCallContext);
+ final UUID resolvedPaymentMethodId = (paymentMethodId == null && paymentOptions.isExternalPayment()) ?
+ paymentMethodProcessor.createOrGetExternalPaymentMethod(UUIDs.randomUUID().toString(), account, properties, callContext, internalCallContext) :
+ paymentMethodId;
- payment = pluginControlPaymentProcessor.createCredit(IS_API_PAYMENT, account, nonNullPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+ payment = pluginControlPaymentProcessor.createCredit(IS_API_PAYMENT, account, resolvedPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
index ff1ee48..c30581d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
@@ -230,7 +230,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
final PaymentPluginApi pluginApi = getPaymentPluginApi(paymentMethodModelDao.getPluginName());
paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), properties, tenantContext);
} catch (final PaymentPluginApiException e) {
- throw new PaymentApiException(ErrorCode.PAYMENT_GET_PAYMENT_METHODS, paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId());
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_GET_PAYMENT_METHODS, paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId());
}
} else {
paymentMethodPlugin = null;
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
index 4205e8a..a0f6ad4 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
@@ -107,7 +107,7 @@ public class TestCatalog extends TestJaxrsBase {
Assert.assertEquals(catalogsJson.get(0).getEffectiveDate(), Date.valueOf("2011-01-01"));
Assert.assertEquals(catalogsJson.get(0).getCurrencies().size(), 3);
Assert.assertEquals(catalogsJson.get(0).getProducts().size(), 11);
- Assert.assertEquals(catalogsJson.get(0).getPriceLists().size(), 6);
+ Assert.assertEquals(catalogsJson.get(0).getPriceLists().size(), 7);
for (final Product productJson : catalogsJson.get(0).getProducts()) {
if (!"BASE".equals(productJson.getType())) {
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 1ce9758..278ea3c 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
@@ -63,6 +63,7 @@ public class PlanAligner extends BaseAligner {
NEXT
}
+
/**
* Returns the current and next phase for the subscription in creation
*
@@ -88,8 +89,8 @@ public class PlanAligner extends BaseAligner {
bundleStartDate,
plan,
initialPhase,
- effectiveDate,
catalog,
+ effectiveDate,
context);
final TimedPhase[] result = new TimedPhase[2];
result[0] = getTimedPhase(timedPhases, effectiveDate, WhichPhase.CURRENT);
@@ -164,8 +165,12 @@ public class PlanAligner extends BaseAligner {
subscription.getBundleStartDate(),
pendingOrLastPlanTransition.getNextPlan(),
pendingOrLastPlanTransition.getNextPhase().getPhaseType(),
- effectiveDate,
catalog,
+ // Use the catalog version at subscription creation time: this allows
+ // for PHASE events and uncancel operations for plans/products/pricelists already retired
+ // 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);
return getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
case CHANGE:
@@ -175,6 +180,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,
@@ -194,14 +201,14 @@ public class PlanAligner extends BaseAligner {
final DateTime bundleStartDate,
final Plan plan,
@Nullable final PhaseType initialPhase,
- final DateTime effectiveDate,
final Catalog catalog,
+ final DateTime catalogEffectiveDate,
final InternalTenantContext context)
throws CatalogApiException, SubscriptionBaseApiException {
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;
@@ -235,6 +242,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(),
@@ -250,6 +258,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,
@@ -262,7 +271,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;