killbill-memoizeit
Changes
api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java 4(+4 -0)
api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java 6(+6 -0)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java 2(+2 -0)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java 2(+1 -1)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java 11(+10 -1)
subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java 55(+53 -2)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java 13(+13 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java 4(+3 -1)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java 6(+4 -2)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java 16(+14 -2)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java 29(+29 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java 29(+28 -1)
subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java 2(+2 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java 48(+41 -7)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java 59(+44 -15)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java 1(+1 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java 9(+5 -4)
subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventBuilder.java 55(+55 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventData.java 65(+65 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java 3(+2 -1)
subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg 2(+2 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java 6(+5 -1)
Details
diff --git a/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java b/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java
index ea74974..1073d87 100644
--- a/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java
+++ b/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java
@@ -46,6 +46,8 @@ public interface SubscriptionInternalEvent extends BusInternalEvent {
String getPreviousPhase();
+ Integer getPreviousBillCycleDayLocal();
+
String getNextPlan();
String getNextPhase();
@@ -54,6 +56,8 @@ public interface SubscriptionInternalEvent extends BusInternalEvent {
String getNextPriceList();
+ Integer getNextBillCycleDayLocal();
+
Integer getRemainingEventsForUserOperation();
Long getTotalOrdering();
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index a8ccdf6..a389d59 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -96,4 +96,7 @@ public interface SubscriptionBaseInternalApi {
public Iterable<DateTime> getFutureNotificationsForAccount(InternalCallContext context);
public Map<UUID, DateTime> getNextFutureEventForSubscriptions(final SubscriptionBaseTransitionType eventType, final InternalCallContext internalCallContext);
+
+
+ public void updateBCD(final UUID subscriptionId, final int bcd, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException;
}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
index 7a7729e..94606d0 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
@@ -45,6 +45,10 @@ public enum SubscriptionBaseTransitionType {
*/
PHASE,
/**
+ * Update BCD for a specific subscription
+ */
+ BCD_CHANGE,
+ /**
* Generated by the system to mark the start of blocked billing overdue state. This is not on disk but computed by junction to create the billing events.
*/
START_BILLING_DISABLED,
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
index dc56c4e..434b964 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
@@ -73,5 +73,11 @@ public interface SubscriptionBaseTimeline extends Entity {
* @return the name of the phase
*/
public String getPlanPhaseName();
+
+ /**
+ *
+ * @return the new billCycleDayLocal
+ */
+ public Integer getBillCycleDayLocal();
}
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java b/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java
index 77ab028..1703097 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java
@@ -27,7 +27,7 @@ import org.killbill.billing.ObjectType;
import org.killbill.billing.beatrix.BeatrixTestSuiteNoDB;
import org.killbill.billing.util.jackson.ObjectMapper;
-public class TestEventJson extends BeatrixTestSuiteNoDB {
+public class TestEventJson extends BeatrixTestSuiteNoDB {
private final ObjectMapper mapper = new ObjectMapper();
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index bd25206..43d4cff 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -202,6 +202,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
@Inject
protected SubscriptionApi subscriptionApi;
+
@Named(BeatrixIntegrationModule.NON_OSGI_PLUGIN_NAME)
@Inject
protected MockPaymentProviderPlugin paymentPlugin;
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
new file mode 100644
index 0000000..ff42a6e
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
@@ -0,0 +1,466 @@
+/*
+ * 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.beatrix.integration;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+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.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.testng.Assert.assertNotNull;
+
+public class TestWithBCDUpdate extends TestIntegrationBase {
+
+ @Inject
+ protected SubscriptionBaseInternalApi subscriptionBaseInternalApi;
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeInTrial() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-4-4 : (BP still in TRIAL)
+ clock.addDays(3);
+
+ // Set next BCD to be the 15
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, internalCallContext);
+ Thread.sleep(1000);
+ assertListenerStatus();
+
+ // 2016-5-15 : Catch BCD_CHANGE event
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.NULL_INVOICE);
+ clock.addDays(11);
+ assertListenerStatus();
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(16);
+ assertListenerStatus();
+
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, new BigDecimal("116.64")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-5-15 : NEW BCD
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(14);
+ assertListenerStatus();
+
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+ }
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeAfterTrialFollowOtherBCDChange() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ // Set next BCD to be the 15
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, internalCallContext);
+ Thread.sleep(1000);
+ assertListenerStatus();
+
+ // 2016-5-15 : Catch BCD_CHANGE event and repair invoice accordingly
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(14);
+ assertListenerStatus();
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-137.07")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-6-01 : Original notification for 2016-6-01 (prior BCD change)
+ busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE);
+ clock.addDays(17);
+ assertListenerStatus();
+
+
+ // 2016-6-15
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(14);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 15), new LocalDate(2016, 7, 15), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Set next BCD to be the 10
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, internalCallContext);
+ Thread.sleep(1000);
+ assertListenerStatus();
+
+ // 2016-7-10
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(25);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 10), new LocalDate(2016, 8, 10), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 10), new LocalDate(2016, 7, 15), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-41.66")));
+ invoiceChecker.checkInvoice(invoices.get(4).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+ }
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeBeforeChangePlan() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, internalCallContext);
+
+ // 2016-5-5
+ clock.addDays(4);
+ changeEntitlementAndCheckForCompletion(baseEntitlement, "Assault-Rifle", BillingPeriod.MONTHLY, null, NextEvent.CHANGE, NextEvent.INVOICE);
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 5, 10), InvoiceItemType.RECURRING, new BigDecimal("99.99")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-217.70")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 5, 5), InvoiceItemType.CBA_ADJ, new BigDecimal("117.71")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-5-10
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(5);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2016, 6, 10), InvoiceItemType.RECURRING, new BigDecimal("599.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2016, 5, 10), InvoiceItemType.CBA_ADJ, new BigDecimal("-117.71")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ }
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeAfterChangePlan() throws Exception {
+
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ // 2016-5-5
+ clock.addDays(4);
+ changeEntitlementAndCheckForCompletion(baseEntitlement, "Assault-Rifle", BillingPeriod.MONTHLY, null, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("522.54")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-217.70")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, internalCallContext);
+
+ // 2016-5-10
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(5);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2016, 6, 10), InvoiceItemType.RECURRING, new BigDecimal("599.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-425.77")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ }
+
+ @Test(groups = "slow")
+ public void testBCDChangeForAnnualSubscriptionAndCancellation() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.ANNUAL;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, internalCallContext);
+
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(9);
+ assertListenerStatus();
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2017, 5, 10), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2017, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2340.77")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2017, 5, 1 (at 13, 42, 0)
+ busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE);
+ clock.setTime(new DateTime(2017, 5, 1, 0, 13, 42, 0, testTimeZone));
+ assertListenerStatus();
+
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ //clock.setDay(new LocalDate(2017, 5, 10));
+ clock.addDays(9);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 10), new LocalDate(2018, 5, 10), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ }
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeForAO() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-4-4 : (BP still in TRIAL)
+ // Laser-Scope has 1 month DISCOUNT
+ clock.addDays(3);
+ final DefaultEntitlement aoEntitlement = addAOEntitlementAndCheckForCompletion(baseEntitlement.getBundleId(), "Laser-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY,
+ NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ // 2016-5-1 : BP out of TRIAL + AO
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(27);
+ assertListenerStatus();
+
+ // 2016-5-4: Laser-Scope out of DISCOUNT
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(3);
+ assertListenerStatus();
+
+ // 2016-6-1 : BP + AO invoice
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(28);
+ assertListenerStatus();
+
+ // 2016-6-4 : Change BCD for AO and
+ clock.addDays(3);
+
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ subscriptionBaseInternalApi.updateBCD(aoEntitlement.getId(), 4, internalCallContext);
+ assertListenerStatus();
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = null;
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 4), new LocalDate(2016, 7, 4), InvoiceItemType.RECURRING, new BigDecimal("1999.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 4), new LocalDate(2016, 7, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-1799.96")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(5).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-7-1 : BP only
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(27);
+ assertListenerStatus();
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 1), new LocalDate(2016, 8, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(6).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-7-4 : AO only
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(3);
+ assertListenerStatus();
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 4), new LocalDate(2016, 8, 4), InvoiceItemType.RECURRING, new BigDecimal("1999.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(7).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ checkNoMoreInvoiceToGenerate(account);
+ }
+
+ @Test(groups = "slow")
+ public void testBlockPastUnpaidPeriodAndRealignBCD() throws Exception {
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = null;
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+
+ paymentPlugin.makeNextPaymentFailWithError();
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ clock.addDays(30);
+ assertListenerStatus();
+
+
+ //
+ // Let's assume 15 days later, the customer comes back and wants to continue using the service (after he updated his payment method)
+ //
+ // The company 'a.b.c' decides to block both the billing and entitlement for the past 15 days and also move his BCD to
+ // the 16 so he gets to pay right away and for a full period (MONTHLY)
+ //
+ // 2016-5-16
+ busHandler.pushExpectedEvents(NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.PAYMENT_ERROR);
+ paymentPlugin.makeNextPaymentFailWithError();
+ clock.addDays(15);
+ assertListenerStatus();
+
+
+ // First BLOCK subscription starting from the 2016-5-1
+ // This will generate the credit for the full period, bringing by account balance to 0
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.INVOICE);
+ final BlockingState blockingState = new DefaultBlockingState(baseEntitlement.getId(), BlockingStateType.SUBSCRIPTION, "COURTESY_BLOCK", "company.a.b.c", true, true, true, null);
+ subscriptionApi.addBlockingState(blockingState, new LocalDate(2016, 5, 1), ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-249.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 16), new LocalDate(2016, 5, 16), InvoiceItemType.CBA_ADJ, new BigDecimal("249.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Second, move the BCD to the 16
+ // Because we did not unblock yet, we don't have a new invoice but we see the NULL_INVOICE event
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.NULL_INVOICE);
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 16, internalCallContext);
+ assertListenerStatus();
+
+ // Third, unblock starting at the 16, will generate a full period invoice
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ final BlockingState unblockingState = new DefaultBlockingState(baseEntitlement.getId(), BlockingStateType.SUBSCRIPTION, "END_OF_COURTESY_BLOCK", "company.a.b.c", false, false, false, null);
+ subscriptionApi.addBlockingState(unblockingState, new LocalDate(2016, 5, 16), ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 16), new LocalDate(2016, 6, 16), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
index f7898b5..2f66d29 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
@@ -16,9 +16,14 @@
package org.killbill.billing.catalog.api.user;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+
import javax.inject.Inject;
import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogService;
@@ -31,6 +36,7 @@ import org.killbill.billing.tenant.api.TenantUserApi;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.xmlloader.XMLLoader;
public class DefaultCatalogUserApi implements CatalogUserApi {
@@ -65,11 +71,17 @@ public class DefaultCatalogUserApi implements CatalogUserApi {
@Override
public void uploadCatalog(final String catalogXML, final CallContext callContext) throws CatalogApiException {
try {
+ // Validation purpose: Will throw if bad XML or catalog validation fails
+ final InputStream stream = new ByteArrayInputStream(catalogXML.getBytes());
+ XMLLoader.getObjectFromStream(new URI("dummy"), stream, StandaloneCatalog.class);
+
final InternalTenantContext internalTenantContext = createInternalTenantContext(callContext);
catalogCache.clearCatalog(internalTenantContext);
tenantApi.addTenantKeyValue(TenantKey.CATALOG.toString(), catalogXML, callContext);
} catch (TenantApiException e) {
throw new CatalogApiException(e);
+ } catch (final Exception e) {
+ throw new IllegalStateException(e);
}
}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
index c4dcbd3..dc0198b 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
@@ -1571,9 +1571,11 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
null,
null,
null,
+ null,
nextPlan,
nextPhase,
nextPriceList,
+ null,
1L,
createdDate,
UUID.randomUUID(),
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java
index 7537ada..0cb8603 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java
@@ -26,6 +26,7 @@ import java.util.Set;
import java.util.UUID;
import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItem;
@@ -62,11 +63,11 @@ public class AccountTimelineJson {
final List<Payment> payments,
final List<InvoicePayment> invoicePayments,
final List<SubscriptionBundle> bundles,
- final AccountAuditLogs accountAuditLogs) {
+ final AccountAuditLogs accountAuditLogs) throws CatalogApiException {
this.account = new AccountJson(account, null, null, accountAuditLogs);
this.bundles = new LinkedList<BundleJson>();
for (final SubscriptionBundle bundle : bundles) {
- final BundleJson jsonWithSubscriptions = new BundleJson(bundle, accountAuditLogs);
+ final BundleJson jsonWithSubscriptions = new BundleJson(bundle, account.getCurrency(), accountAuditLogs);
this.bundles.add(jsonWithSubscriptions);
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
index 22effcd..0dbd4b5 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
@@ -23,6 +23,8 @@ import java.util.List;
import javax.annotation.Nullable;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entitlement.api.Subscription;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
import org.killbill.billing.util.audit.AccountAuditLogs;
@@ -56,18 +58,20 @@ public class BundleJson extends JsonBase {
this.timeline = timeline;
}
- public BundleJson(final SubscriptionBundle bundle, @Nullable final AccountAuditLogs accountAuditLogs) {
+ public BundleJson(final SubscriptionBundle bundle, @Nullable final Currency currency, @Nullable final AccountAuditLogs accountAuditLogs) throws CatalogApiException {
super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForBundle(bundle.getId())));
this.accountId = bundle.getAccountId().toString();
this.bundleId = bundle.getId().toString();
this.externalKey = bundle.getExternalKey();
this.subscriptions = new LinkedList<SubscriptionJson>();
for (final Subscription subscription : bundle.getSubscriptions()) {
- this.subscriptions.add(new SubscriptionJson(subscription, accountAuditLogs));
+ this.subscriptions.add(new SubscriptionJson(subscription, currency, accountAuditLogs));
}
this.timeline = new BundleTimelineJson(bundle.getTimeline(), accountAuditLogs);
}
+
+
public List<SubscriptionJson> getSubscriptions() {
return subscriptions;
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
index 1ce8cbb..6f8745a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
@@ -90,7 +90,8 @@ public class PhasePriceOverrideJson {
final PhasePriceOverrideJson that = (PhasePriceOverrideJson) o;
- if (fixedPrice != null ? !fixedPrice.equals(that.fixedPrice) : that.fixedPrice != null) {
+
+ if (fixedPrice != null ? fixedPrice.compareTo(that.fixedPrice) != 0 : that.fixedPrice != null) {
return false;
}
if (phaseName != null ? !phaseName.equals(that.phaseName) : that.phaseName != null) {
@@ -99,7 +100,7 @@ public class PhasePriceOverrideJson {
if (phaseType != null ? !phaseType.equals(that.phaseType) : that.phaseType != null) {
return false;
}
- if (recurringPrice != null ? !recurringPrice.equals(that.recurringPrice) : that.recurringPrice != null) {
+ if (recurringPrice != null ? recurringPrice.compareTo(that.recurringPrice) != 0 : that.recurringPrice != null) {
return false;
}
return true;
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
index adb80de..c3eba91 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
@@ -18,6 +18,8 @@
package org.killbill.billing.jaxrs.json;
+import java.math.BigDecimal;
+import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -26,6 +28,9 @@ import javax.annotation.Nullable;
import org.joda.time.LocalDate;
import org.killbill.billing.ObjectType;
import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
@@ -69,7 +74,6 @@ public class SubscriptionJson extends JsonBase {
private final List<EventSubscriptionJson> events;
private final List<PhasePriceOverrideJson> priceOverrides;
-
public static class EventSubscriptionJson extends JsonBase {
private final String eventId;
@@ -314,7 +318,7 @@ public class SubscriptionJson extends JsonBase {
this.priceOverrides = priceOverrides;
}
- public SubscriptionJson(final Subscription subscription, @Nullable final AccountAuditLogs accountAuditLogs) {
+ public SubscriptionJson(final Subscription subscription, @Nullable final Currency currency, @Nullable final AccountAuditLogs accountAuditLogs) throws CatalogApiException {
super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscription(subscription.getId())));
this.startDate = subscription.getEffectiveStartDate();
@@ -360,8 +364,20 @@ public class SubscriptionJson extends JsonBase {
for (final SubscriptionEvent subscriptionEvent : subscription.getSubscriptionEvents()) {
this.events.add(new EventSubscriptionJson(subscriptionEvent, accountAuditLogs));
}
- // It may be nice to recreate the override that were applied on the plans associated with that subscription
- this.priceOverrides = new LinkedList<PhasePriceOverrideJson>();
+
+ // We fill the catalog info every time we get the currency from the account (even if this is not overridden Plan)
+ this.priceOverrides = new ArrayList<PhasePriceOverrideJson>();
+ if (currency != null) {
+ final Plan plan = subscription.getLastActivePlan();
+ if (plan != null) {
+ for (final PlanPhase cur : plan.getAllPhases()) {
+ final BigDecimal fixedPrice = cur.getFixed() != null ? cur.getFixed().getPrice().getPrice(currency) : null;
+ final BigDecimal recurringPrice = cur.getRecurring() != null ? cur.getRecurring().getRecurringPrice().getPrice(currency) : null;
+ final PhasePriceOverrideJson phase = new PhasePriceOverrideJson(cur.getName(), cur.getPhaseType().toString(), fixedPrice, recurringPrice);
+ priceOverrides.add(phase);
+ }
+ }
+ }
}
public String getAccountId() {
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
index 290c0dc..dc1fe50 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
@@ -58,6 +58,7 @@ import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.account.api.AccountEmail;
import org.killbill.billing.account.api.AccountUserApi;
import org.killbill.billing.account.api.MutableAccountData;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
@@ -268,7 +269,7 @@ public class AccountResource extends JaxRsResourceBase {
final TenantContext tenantContext = context.createContext(request);
final UUID uuid = UUID.fromString(accountId);
- accountUserApi.getAccountById(uuid, tenantContext);
+ final Account account = accountUserApi.getAccountById(uuid, tenantContext);
final List<SubscriptionBundle> bundles = (externalKey != null) ?
subscriptionApi.getSubscriptionBundlesForAccountIdAndExternalKey(uuid, externalKey, tenantContext) :
@@ -277,7 +278,12 @@ public class AccountResource extends JaxRsResourceBase {
final Collection<BundleJson> result = Collections2.transform(bundles, new Function<SubscriptionBundle, BundleJson>() {
@Override
public BundleJson apply(final SubscriptionBundle input) {
- return new BundleJson(input, null);
+ try {
+ return new BundleJson(input, account.getCurrency(), null);
+ } catch (final CatalogApiException e) {
+ // Not the cleanest thing, but guava Api don't allow throw..
+ throw new RuntimeException(e);
+ }
}
});
return Response.status(Status.OK).entity(result).build();
@@ -385,7 +391,7 @@ public class AccountResource extends JaxRsResourceBase {
public Response getAccountTimeline(@PathParam("accountId") final String accountIdString,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@QueryParam(QUERY_PARALLEL) @DefaultValue("false") final Boolean parallel,
- @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException, SubscriptionApiException, InvoiceApiException {
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException, SubscriptionApiException, InvoiceApiException, CatalogApiException {
final TenantContext tenantContext = context.createContext(request);
final UUID accountId = UUID.fromString(accountIdString);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java
index a09e5db..f32c0f3 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java
@@ -41,9 +41,11 @@ import javax.ws.rs.core.UriInfo;
import org.joda.time.LocalDate;
import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountUserApi;
import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.EntitlementApi;
import org.killbill.billing.entitlement.api.EntitlementApiException;
@@ -114,10 +116,13 @@ public class BundleResource extends JaxRsResourceBase {
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid bundle id supplied"),
@ApiResponse(code = 404, message = "Bundle not found")})
public Response getBundle(@PathParam("bundleId") final String bundleId,
- @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException, AccountApiException, CatalogApiException {
final UUID id = UUID.fromString(bundleId);
- final SubscriptionBundle bundle = subscriptionApi.getSubscriptionBundle(id, context.createContext(request));
- final BundleJson json = new BundleJson(bundle, null);
+
+ final TenantContext tenantContext = this.context.createContext(request);
+ final SubscriptionBundle bundle = subscriptionApi.getSubscriptionBundle(id, tenantContext);
+ final Account account = accountUserApi.getAccountById(bundle.getAccountId(), tenantContext);
+ final BundleJson json = new BundleJson(bundle, account.getCurrency(), null);
return Response.status(Status.OK).entity(json).build();
}
@@ -127,9 +132,13 @@ public class BundleResource extends JaxRsResourceBase {
@ApiOperation(value = "Retrieve a bundle by external key", response = BundleJson.class)
@ApiResponses(value = {@ApiResponse(code = 404, message = "Bundle not found")})
public Response getBundleByKey(@QueryParam(QUERY_EXTERNAL_KEY) final String externalKey,
- @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
- final SubscriptionBundle bundle = subscriptionApi.getActiveSubscriptionBundleForExternalKey(externalKey, context.createContext(request));
- final BundleJson json = new BundleJson(bundle, null);
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException, AccountApiException, CatalogApiException {
+
+ final TenantContext tenantContext = this.context.createContext(request);
+
+ final SubscriptionBundle bundle = subscriptionApi.getActiveSubscriptionBundleForExternalKey(externalKey, tenantContext);
+ final Account account = accountUserApi.getAccountById(bundle.getAccountId(), tenantContext);
+ final BundleJson json = new BundleJson(bundle, account.getCurrency(), null);
return Response.status(Status.OK).entity(json).build();
}
@@ -155,7 +164,13 @@ public class BundleResource extends JaxRsResourceBase {
if (accountsAuditLogs.get().get(bundle.getAccountId()) == null) {
accountsAuditLogs.get().put(bundle.getAccountId(), auditUserApi.getAccountAuditLogs(bundle.getAccountId(), auditMode.getLevel(), tenantContext));
}
- return new BundleJson(bundle, accountsAuditLogs.get().get(bundle.getAccountId()));
+
+ try {
+ return new BundleJson(bundle, null, accountsAuditLogs.get().get(bundle.getAccountId()));
+ } catch (final CatalogApiException unused) {
+ // Does not happen because we pass a null Currency
+ throw new RuntimeException(unused);
+ }
}
},
nextPageUri);
@@ -185,7 +200,12 @@ public class BundleResource extends JaxRsResourceBase {
if (accountsAuditLogs.get().get(bundle.getAccountId()) == null) {
accountsAuditLogs.get().put(bundle.getAccountId(), auditUserApi.getAccountAuditLogs(bundle.getAccountId(), auditMode.getLevel(), tenantContext));
}
- return new BundleJson(bundle, accountsAuditLogs.get().get(bundle.getAccountId()));
+ try {
+ return new BundleJson(bundle, null, accountsAuditLogs.get().get(bundle.getAccountId()));
+ } catch (final CatalogApiException unused) {
+ // Does not happen because we pass a null Currency
+ throw new RuntimeException(unused);
+ }
}
},
nextPageUri);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
index 04f7f8b..a262d14 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
@@ -16,9 +16,6 @@
package org.killbill.billing.jaxrs.resources;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.net.URI;
import java.util.ArrayList;
import java.util.List;
@@ -38,7 +35,6 @@ import javax.ws.rs.core.UriInfo;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.killbill.billing.account.api.AccountUserApi;
-import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.VersionedCatalog;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
@@ -57,7 +53,6 @@ import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.clock.Clock;
import org.killbill.commons.metrics.TimedResource;
-import org.killbill.xmlloader.XMLLoader;
import org.killbill.xmlloader.XMLWriter;
import com.google.inject.Inject;
@@ -76,9 +71,6 @@ public class CatalogResource extends JaxRsResourceBase {
private final CatalogUserApi catalogUserApi;
- // Catalog API don't quite support multiple catalogs per tenant
- private static final String catalogName = "unused";
-
@Inject
public CatalogResource(final JaxrsUriBuilder uriBuilder,
final TagUserApi tagUserApi,
@@ -114,10 +106,6 @@ public class CatalogResource extends JaxRsResourceBase {
@HeaderParam(HDR_COMMENT) final String comment,
@javax.ws.rs.core.Context final HttpServletRequest request,
@javax.ws.rs.core.Context final UriInfo uriInfo) throws Exception {
- // Validation purpose: Will throw if bad XML or catalog validation fails
- final InputStream stream = new ByteArrayInputStream(catalogXML.getBytes());
- XMLLoader.getObjectFromStream(new URI(JaxrsResource.CATALOG_PATH), stream, StandaloneCatalog.class);
-
final CallContext callContext = context.createContext(createdBy, reason, comment, request);
catalogUserApi.uploadCatalog(catalogXML, callContext);
return uriBuilder.buildResponse(uriInfo, CatalogResource.class, null, null);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
index 1ca26ae..18513b1 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -128,6 +128,8 @@ public interface JaxrsResource {
public static final String QUERY_AUDIT = "audit";
+ public static final String QUERY_BCD = "bcd";
+
public static final String QUERY_PARALLEL = "parallel";
public static final String QUERY_NOTIFICATION_CALLBACK = "cb";
@@ -252,4 +254,6 @@ public interface JaxrsResource {
public static final String COMBO = "combo";
public static final String MIGRATION = "migration";
+ public static final String BCD = "bcd";
+
}
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 0f4de38..ee5cf1a 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
@@ -117,6 +117,10 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
static final Logger log = LoggerFactory.getLogger(JaxRsResourceBase.class);
+ // Catalog API don't quite support multiple catalogs per tenant
+ protected static final String catalogName = "unused";
+
+
protected static final ObjectMapper mapper = new ObjectMapper();
protected final JaxrsUriBuilder uriBuilder;
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index a74d09b..5d7140a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -47,6 +47,7 @@ import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountUserApi;
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.PhaseType;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
@@ -141,10 +142,12 @@ public class SubscriptionResource extends JaxRsResourceBase {
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid subscription id supplied"),
@ApiResponse(code = 404, message = "Subscription not found")})
public Response getEntitlement(@PathParam("subscriptionId") final String subscriptionId,
- @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException, AccountApiException, CatalogApiException {
final UUID uuid = UUID.fromString(subscriptionId);
- final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(uuid, context.createContext(request));
- final SubscriptionJson json = new SubscriptionJson(subscription, null);
+ final TenantContext context = this.context.createContext(request);
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(uuid, context);
+ final Account account = accountUserApi.getAccountById(subscription.getAccountId(), context);
+ final SubscriptionJson json = new SubscriptionJson(subscription, account.getCurrency(), null);
return Response.status(Status.OK).entity(json).build();
}
@@ -436,7 +439,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
}
@Override
- public Response doResponseOk(final Response operationResponse) throws SubscriptionApiException {
+ public Response doResponseOk(final Response operationResponse) throws SubscriptionApiException, AccountApiException, CatalogApiException {
if (operationResponse.getStatus() != Status.OK.getStatusCode()) {
return operationResponse;
}
@@ -600,7 +603,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
public boolean isImmOperation();
- public Response doResponseOk(final T operationResponse) throws SubscriptionApiException;
+ public Response doResponseOk(final T operationResponse) throws SubscriptionApiException, AccountApiException, CatalogApiException;
}
private class EntitlementCallCompletion<T> {
@@ -621,6 +624,8 @@ public class SubscriptionResource extends JaxRsResourceBase {
return callback.doResponseOk(operationValue);
} catch (final InterruptedException e) {
return Response.status(Status.INTERNAL_SERVER_ERROR).build();
+ } catch (final CatalogApiException e) {
+ throw new EntitlementApiException(e);
} catch (final TimeoutException e) {
return Response.status(408).build();
} finally {
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
index b387188..7183e3c 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
@@ -81,7 +81,7 @@ public class DefaultBillingEvent implements BillingEvent {
final String prevPhaseName = transition.getPreviousPhase();
final PlanPhase prevPlanPhase = (prevPhaseName != null) ? catalog.findPhase(prevPhaseName, transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
- this.fixedPrice = getFixedPrice(nextPlanPhase, currency);
+ this.fixedPrice = transition.getTransitionType() != SubscriptionBaseTransitionType.BCD_CHANGE ? getFixedPrice(nextPlanPhase, currency) : null;
this.currency = currency;
this.description = transition.getTransitionType().toString();
this.billingPeriod = getRecurringBillingPeriod(isActive ? nextPlanPhase : prevPlanPhase);
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
index 7ec0b85..cb91c31 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
@@ -195,9 +195,18 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
}
+ Integer overridenBCD = null;
for (final EffectiveSubscriptionInternalEvent transition : billingTransitions) {
try {
- final int bcdLocal = bcdCalculator.calculateBcd(account, currentAccountBCD, bundleId, subscription, transition, context);
+ //
+ // A BCD_CHANGE transition defines a new billCycleDayLocal for the subscription and this overrides whatever computation
+ // occurs below (which is based on billing alignment policy). Also multiple of those BCD_CHANGE transitions could occur,
+ // to define different intervals with different billing cycle days.
+ //
+ overridenBCD = transition.getNextBillCycleDayLocal() != null ? transition.getNextBillCycleDayLocal() : overridenBCD;
+ final int bcdLocal = overridenBCD != null ?
+ overridenBCD :
+ bcdCalculator.calculateBcd(account, currentAccountBCD, bundleId, subscription, transition, context);
if (currentAccountBCD == 0 && !updatedAccountBCD) {
accountApi.updateBCD(account.getExternalKey(), bcdLocal, context);
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
index df07cbc..2a23e7a 100644
--- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
@@ -281,9 +281,9 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
final PriceList nextPriceList = catalog.findPriceList(PriceListSet.DEFAULT_PRICELIST_NAME, now);
final EffectiveSubscriptionInternalEvent t = new MockEffectiveSubscriptionEvent(
- eventId, subId, bunId, then, now, null, null, null, null, EntitlementState.ACTIVE,
+ eventId, subId, bunId, then, now, null, null, null, null, null, EntitlementState.ACTIVE,
nextPlan.getName(), nextPhase.getName(),
- nextPriceList.getName(), 1L,
+ nextPriceList.getName(), null, 1L,
SubscriptionBaseTransitionType.CREATE, 1, null, 1L, 2L, null);
effectiveSubscriptionTransitions.add(t);
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
index 080d812..be975a1 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
@@ -31,7 +31,6 @@ import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
-import org.killbill.billing.client.KillBillClientException;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.Bundle;
import org.killbill.billing.client.model.Invoice;
@@ -66,6 +65,16 @@ public class TestEntitlement extends TestJaxrsBase {
// Retrieves with GET
Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ Assert.assertEquals(objFromJson.getPriceOverrides().size(), 2);
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getFixedPrice(), BigDecimal.ZERO);
+ Assert.assertNull(objFromJson.getPriceOverrides().get(0).getRecurringPrice());
+
+ Assert.assertNull(objFromJson.getPriceOverrides().get(1).getFixedPrice());
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));
+
+ // Equality in java client is not correctly implemented so manually check PriceOverrides section and then reset before equality
+ objFromJson.setPriceOverrides(null);
+ entitlementJson.setPriceOverrides(null);
Assert.assertTrue(objFromJson.equals(entitlementJson));
// Change plan IMM
@@ -111,6 +120,10 @@ public class TestEntitlement extends TestJaxrsBase {
// Retrieves with GET
Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ // Equality in java client is not correctly implemented so manually check PriceOverrides section and then reset before equality
+ objFromJson.setPriceOverrides(null);
+ entitlementJson.setPriceOverrides(null);
+
Assert.assertTrue(objFromJson.equals(entitlementJson));
// MOVE AFTER TRIAL
@@ -167,6 +180,10 @@ public class TestEntitlement extends TestJaxrsBase {
// Retrieves with GET
Subscription objFromJson = killBillClient.getSubscription(subscriptionJson.getSubscriptionId());
+ // Equality in java client is not correctly implemented so manually check PriceOverrides section and then reset before equality
+ objFromJson.setPriceOverrides(null);
+ subscriptionJson.setPriceOverrides(null);
+
Assert.assertTrue(objFromJson.equals(subscriptionJson));
assertEquals(objFromJson.getBillingPeriod(), BillingPeriod.ANNUAL);
@@ -279,4 +296,5 @@ public class TestEntitlement extends TestJaxrsBase {
final Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
Assert.assertTrue(objFromJson.equals(entitlementJson));
}
+
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index 5f4ef51..065afad 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -28,6 +28,7 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
@@ -70,7 +71,8 @@ import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseServ
import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
-import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
+import org.killbill.billing.subscription.events.bcd.BCDEventData;
import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.callcontext.CallContext;
@@ -609,7 +611,15 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
final Iterable<SubscriptionBaseEvent> filteredEvents = Iterables.filter(events, new Predicate<SubscriptionBaseEvent>() {
@Override
public boolean apply(final SubscriptionBaseEvent input) {
- return (eventType == SubscriptionBaseTransitionType.PHASE && input.getType() == EventType.PHASE) || input.getType() != EventType.PHASE;
+ switch (input.getType()) {
+ case PHASE:
+ return eventType == SubscriptionBaseTransitionType.PHASE;
+ case BCD_UPDATE:
+ return eventType == SubscriptionBaseTransitionType.BCD_CHANGE;
+ case API_USER:
+ default:
+ return true;
+ }
}
});
final Map<UUID, DateTime> result = filteredEvents.iterator().hasNext() ? new HashMap<UUID, DateTime>() : ImmutableMap.<UUID, DateTime>of();
@@ -622,6 +632,47 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
return result;
}
+ @Override
+ public void updateBCD(final UUID subscriptionId, final int bcd, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) getSubscriptionFromId(subscriptionId, internalCallContext);
+ final DateTime effectiveDate = getEffectiveDateForNewBCD(bcd, internalCallContext);
+ final BCDEvent bcdEvent = BCDEventData.createBCDEvent(subscription, effectiveDate, bcd);
+ dao.createBCDChangeEvent(subscription, bcdEvent, internalCallContext);
+ }
+
+
+ private DateTime getEffectiveDateForNewBCD(final int bcd, final InternalCallContext internalCallContext) {
+ if (internalCallContext.getAccountRecordId() == null) {
+ throw new IllegalStateException("Need to have a valid context with accountRecordId");
+ }
+
+ // Today as seen by this account
+ final LocalDate startDate = internalCallContext.toLocalDate(clock.getUTCNow());
+
+ // We want to compute a LocalDate in account TZ which maps to the provided 'bcd' and then compute an effectiveDate for when that BCD_CHANGE event needs to be triggered
+ //
+ // There is a bit of complexity to make sure the date we chose exists (e.g: a BCD of 31 in a february month would not make sense).
+ final int currentDay = startDate.getDayOfMonth();
+ final int lastDayOfMonth = startDate.dayOfMonth().getMaximumValue();
+
+ final LocalDate requestedDate;
+ if (bcd < currentDay) {
+ final LocalDate startDatePlusOneMonth = startDate.plusMonths(1);
+ final int lastDayOfNextMonth = startDatePlusOneMonth.dayOfMonth().getMaximumValue();
+ final int originalBCDORLastDayOfMonth = bcd <= lastDayOfNextMonth ? bcd : lastDayOfNextMonth;
+ requestedDate = new LocalDate(startDatePlusOneMonth.getYear(), startDatePlusOneMonth.getMonthOfYear(), originalBCDORLastDayOfMonth);
+ } else if (bcd == currentDay) {
+ // will default to immediate event
+ requestedDate = null;
+ } else if (bcd <= lastDayOfMonth) {
+ requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), bcd);
+ } else /* bcd > lastDayOfMonth && bcd > currentDay */{
+ requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), lastDayOfMonth);
+ }
+ return requestedDate == null ? clock.getUTCNow() : internalCallContext.toUTCDateTime(requestedDate);
+ }
+
+
private DateTime getBundleStartDateWithSanity(final UUID bundleId, @Nullable final DefaultSubscriptionBase baseSubscription, final Plan plan,
final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException {
switch (plan.getProduct().getCategory()) {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java
index 436393f..f554eeb 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java
@@ -37,6 +37,7 @@ import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.user.ApiEvent;
import org.killbill.billing.subscription.events.user.ApiEventType;
@@ -85,6 +86,7 @@ public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline
PhaseType phaseType = null;
String planName = null;
String planPhaseName = null;
+ Integer billCycleDayLocal = null;
ApiEventType apiType = null;
switch (cur.getType()) {
@@ -99,6 +101,11 @@ public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline
priceListName = prevPriceListName;
break;
+ case BCD_UPDATE:
+ final BCDEvent bcdEvent = (BCDEvent) cur;
+ billCycleDayLocal = bcdEvent.getBillCycleDayLocal();
+ break;
+
case API_USER:
final ApiEvent userEV = (ApiEvent) cur;
apiType = userEV.getApiEventType();
@@ -116,6 +123,7 @@ public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline
final String planNameWithClosure = planName;
final String planPhaseNameWithClosure = planPhaseName;
+ final Integer billCycleDayLocalWithClosure = billCycleDayLocal;
final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, category, billingPeriod, priceListName, phaseType);
result.add(new ExistingEvent() {
@Override
@@ -147,6 +155,11 @@ public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline
public String getPlanPhaseName() {
return planPhaseNameWithClosure;
}
+
+ @Override
+ public Integer getBillCycleDayLocal() {
+ return billCycleDayLocalWithClosure;
+ }
});
prevPlanName = planName;
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java
index aca165c..4e4d7b7 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java
@@ -42,10 +42,12 @@ public class DefaultEffectiveSubscriptionEvent extends DefaultSubscriptionEvent
@JsonProperty("previousPlan") final String previousPlan,
@JsonProperty("previousPhase") final String previousPhase,
@JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("previousBillCycleDayLocal") final Integer previousBillCycleDayLocal,
@JsonProperty("nextState") final EntitlementState nextState,
@JsonProperty("nextPlan") final String nextPlan,
@JsonProperty("nextPhase") final String nextPhase,
@JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("nextBillCycleDayLocal") final Integer nextBillCycleDayLocal,
@JsonProperty("totalOrdering") final Long totalOrdering,
@JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
@JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
@@ -54,7 +56,7 @@ public class DefaultEffectiveSubscriptionEvent extends DefaultSubscriptionEvent
@JsonProperty("searchKey2") final Long searchKey2,
@JsonProperty("userToken") final UUID userToken) {
super(eventId, subscriptionId, bundleId, effectiveTransitionTime, effectiveTransitionTime, previousState, previousPlan,
- previousPhase, previousPriceList, nextState, nextPlan, nextPhase, nextPriceList, totalOrdering,
+ previousPhase, previousPriceList, previousBillCycleDayLocal, nextState, nextPlan, nextPhase, nextPriceList, nextBillCycleDayLocal, totalOrdering,
transitionType, remainingEventsForUserOperation, startDate, searchKey1, searchKey2, userToken);
}
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java
index 2c78684..1e918ef 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java
@@ -40,10 +40,12 @@ public class DefaultRequestedSubscriptionEvent extends DefaultSubscriptionEvent
@JsonProperty("previousPlan") final String previousPlan,
@JsonProperty("previousPhase") final String previousPhase,
@JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("previousBillCycleDayLocal") final Integer previousBillCycleDayLocal,
@JsonProperty("nextState") final EntitlementState nextState,
@JsonProperty("nextPlan") final String nextPlan,
@JsonProperty("nextPhase") final String nextPhase,
@JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("nextBillCycleDayLocal") final Integer nextBillCycleDayLocal,
@JsonProperty("totalOrdering") final Long totalOrdering,
@JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
@JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
@@ -52,7 +54,7 @@ public class DefaultRequestedSubscriptionEvent extends DefaultSubscriptionEvent
@JsonProperty("searchKey2") final Long searchKey2,
@JsonProperty("userToken") final UUID userToken) {
super(eventId, subscriptionId, bundleId, requestedTransitionTime, effectiveTransitionTime, previousState, previousPlan,
- previousPhase, previousPriceList, nextState, nextPlan, nextPhase, nextPriceList, totalOrdering,
+ previousPhase, previousPriceList, previousBillCycleDayLocal, nextState, nextPlan, nextPhase, nextPriceList, nextBillCycleDayLocal, totalOrdering,
transitionType, remainingEventsForUserOperation, startDate, searchKey1, searchKey2, userToken);
}
@@ -63,6 +65,6 @@ public class DefaultRequestedSubscriptionEvent extends DefaultSubscriptionEvent
final Long searchKey2,
final UUID userToken) {
this(nextEvent.getId(), nextEvent.getSubscriptionId(), subscription.getBundleId(), nextEvent.getEffectiveDate(), nextEvent.getEffectiveDate(),
- null, null, null, null, null, null, null, null, nextEvent.getTotalOrdering(), transitionType, 0, null, searchKey1, searchKey2, userToken);
+ null, null, null, null, null, null, null, null, null, null, nextEvent.getTotalOrdering(), transitionType, 0, null, searchKey1, searchKey2, userToken);
}
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
index 9594dc5..48d01e9 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
@@ -50,6 +50,7 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionDataIterator.Visibility;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.user.ApiEvent;
import org.killbill.billing.subscription.events.user.ApiEventType;
@@ -558,6 +559,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
EntitlementState nextState = null;
String nextPlanName = null;
String nextPhaseName = null;
+ Integer nextBillingCycleDayLocal = null;
UUID prevEventId = null;
DateTime prevCreatedDate = null;
@@ -565,6 +567,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
PriceList previousPriceList = null;
Plan previousPlan = null;
PlanPhase previousPhase = null;
+ Integer previousBillingCycleDayLocal = null;
transitions = new LinkedList<SubscriptionBaseTransition>();
@@ -588,6 +591,11 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
nextPhaseName = phaseEV.getPhase();
break;
+ case BCD_UPDATE:
+ final BCDEvent bcdEvent = (BCDEvent) cur;
+ nextBillingCycleDayLocal = bcdEvent.getBillCycleDayLocal();
+ break;
+
case API_USER:
final ApiEvent userEV = (ApiEvent) cur;
apiEventType = userEV.getApiEventType();
@@ -635,15 +643,18 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
nextPhase = (nextPhaseName != null) ? catalog.findPhase(nextPhaseName, cur.getEffectiveDate(), getAlignStartDate()) : null;
nextPriceList = (nextPlan != null) ? nextPlan.getPriceList() : null;
- final SubscriptionBaseTransitionData transition = new SubscriptionBaseTransitionData(
+ final SubscriptionBaseTransitionData transition = new SubscriptionBaseTransitionData (
cur.getId(), id, bundleId, cur.getType(), apiEventType,
cur.getEffectiveDate(),
prevEventId, prevCreatedDate,
previousState, previousPlan, previousPhase,
previousPriceList,
+ previousBillingCycleDayLocal,
nextEventId, nextCreatedDate,
nextState, nextPlan, nextPhase,
- nextPriceList, cur.getTotalOrdering(),
+ nextPriceList,
+ nextBillingCycleDayLocal,
+ cur.getTotalOrdering(),
cur.getCreatedDate(),
nextUserToken,
isFromDisk);
@@ -656,6 +667,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
previousPriceList = nextPriceList;
prevEventId = nextEventId;
prevCreatedDate = nextCreatedDate;
+ previousBillingCycleDayLocal = nextBillingCycleDayLocal;
}
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java
index 04601ac..89f8aa7 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java
@@ -41,14 +41,17 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
private final String previousPriceList;
private final String previousPlan;
private final String previousPhase;
+ private final Integer previousBillCycleDayLocal;
private final EntitlementState nextState;
private final String nextPriceList;
private final String nextPlan;
private final String nextPhase;
+ private final Integer nextBillCycleDayLocal;
private final Integer remainingEventsForUserOperation;
private final SubscriptionBaseTransitionType transitionType;
private final DateTime startDate;
+
public DefaultSubscriptionEvent(final SubscriptionBaseTransitionData in, final DateTime startDate,
final Long searchKey1,
final Long searchKey2,
@@ -62,10 +65,12 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
(in.getPreviousPlan() != null) ? in.getPreviousPlan().getName() : null,
(in.getPreviousPhase() != null) ? in.getPreviousPhase().getName() : null,
(in.getPreviousPriceList() != null) ? in.getPreviousPriceList().getName() : null,
+ in.getPreviousBillingCycleDayLocal(),
in.getNextState(),
(in.getNextPlan() != null) ? in.getNextPlan().getName() : null,
(in.getNextPhase() != null) ? in.getNextPhase().getName() : null,
(in.getNextPriceList() != null) ? in.getNextPriceList().getName() : null,
+ in.getNextBillingCycleDayLocal(),
in.getTotalOrdering(),
in.getTransitionType(),
in.getRemainingEventsForUserOperation(),
@@ -85,10 +90,12 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
@JsonProperty("previousPlan") final String previousPlan,
@JsonProperty("previousPhase") final String previousPhase,
@JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("previousBillCycleDayLocal") final Integer previousBillCycleDayLocal,
@JsonProperty("nextState") final EntitlementState nextState,
@JsonProperty("nextPlan") final String nextPlan,
@JsonProperty("nextPhase") final String nextPhase,
@JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("nextBillCycleDayLocal") final Integer nextBillCycleDayLocal,
@JsonProperty("totalOrdering") final Long totalOrdering,
@JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
@JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
@@ -104,11 +111,13 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
this.effectiveTransitionTime = effectiveTransitionTime;
this.previousState = previousState;
this.previousPriceList = previousPriceList;
+ this.previousBillCycleDayLocal = previousBillCycleDayLocal;
this.previousPlan = previousPlan;
this.previousPhase = previousPhase;
this.nextState = nextState;
this.nextPlan = nextPlan;
this.nextPriceList = nextPriceList;
+ this.nextBillCycleDayLocal = nextBillCycleDayLocal;
this.nextPhase = nextPhase;
this.totalOrdering = totalOrdering;
this.transitionType = transitionType;
@@ -154,6 +163,11 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
}
@Override
+ public Integer getPreviousBillCycleDayLocal() {
+ return previousBillCycleDayLocal;
+ }
+
+ @Override
public String getNextPlan() {
return nextPlan;
}
@@ -179,6 +193,11 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
}
@Override
+ public Integer getNextBillCycleDayLocal() {
+ return nextBillCycleDayLocal;
+ }
+
+ @Override
public Integer getRemainingEventsForUserOperation() {
return remainingEventsForUserOperation;
}
@@ -221,10 +240,12 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
sb.append(", effectiveTransitionTime=").append(effectiveTransitionTime);
sb.append(", previousState=").append(previousState);
sb.append(", previousPriceList='").append(previousPriceList).append('\'');
+ sb.append(", previousBillCycleDayLocal='").append(previousBillCycleDayLocal).append('\'');
sb.append(", previousPlan='").append(previousPlan).append('\'');
sb.append(", previousPhase='").append(previousPhase).append('\'');
sb.append(", nextState=").append(nextState);
sb.append(", nextPriceList='").append(nextPriceList).append('\'');
+ sb.append(", nextBillCycleDayLocal='").append(nextBillCycleDayLocal).append('\'');
sb.append(", nextPlan='").append(nextPlan).append('\'');
sb.append(", nextPhase='").append(nextPhase).append('\'');
sb.append(", remainingEventsForUserOperation=").append(remainingEventsForUserOperation);
@@ -263,6 +284,9 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
return false;
}
+ if (nextBillCycleDayLocal != null ? !nextBillCycleDayLocal.equals(that.nextBillCycleDayLocal) : that.nextBillCycleDayLocal != null) {
+ return false;
+ }
if (nextState != that.nextState) {
return false;
}
@@ -275,6 +299,9 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
if (previousPriceList != null ? !previousPriceList.equals(that.previousPriceList) : that.previousPriceList != null) {
return false;
}
+ if (previousBillCycleDayLocal != null ? !previousBillCycleDayLocal.equals(that.previousBillCycleDayLocal) : that.previousBillCycleDayLocal != null) {
+ return false;
+ }
if (previousState != that.previousState) {
return false;
}
@@ -309,10 +336,12 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
result = 31 * result + (effectiveTransitionTime != null ? effectiveTransitionTime.hashCode() : 0);
result = 31 * result + (previousState != null ? previousState.hashCode() : 0);
result = 31 * result + (previousPriceList != null ? previousPriceList.hashCode() : 0);
+ result = 31 * result + (previousBillCycleDayLocal != null ? previousBillCycleDayLocal.hashCode() : 0);
result = 31 * result + (previousPlan != null ? previousPlan.hashCode() : 0);
result = 31 * result + (previousPhase != null ? previousPhase.hashCode() : 0);
result = 31 * result + (nextState != null ? nextState.hashCode() : 0);
result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
+ result = 31 * result + (nextBillCycleDayLocal != null ? nextBillCycleDayLocal.hashCode() : 0);
result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
result = 31 * result + (nextPhase != null ? nextPhase.hashCode() : 0);
result = 31 * result + (remainingEventsForUserOperation != null ? remainingEventsForUserOperation.hashCode() : 0);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java
index df2ccad..34de332 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java
@@ -39,6 +39,7 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
private final DateTime effectiveTransitionTime;
private final EntitlementState previousState;
private final PriceList previousPriceList;
+ private final Integer previousBillingCycleDayLocal;
private final UUID previousEventId;
private final DateTime previousEventCreatedDate;
private final Plan previousPlan;
@@ -47,6 +48,7 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
private final DateTime nextEventCreatedDate;
private final EntitlementState nextState;
private final PriceList nextPriceList;
+ private final Integer nextBillingCycleDayLocal;
private final Plan nextPlan;
private final PlanPhase nextPhase;
private final Boolean isFromDisk;
@@ -66,12 +68,14 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
final Plan previousPlan,
final PlanPhase previousPhase,
final PriceList previousPriceList,
+ final Integer previousBillingCycleDayLocal,
final UUID nextEventId,
final DateTime nextEventCreatedDate,
final EntitlementState nextState,
final Plan nextPlan,
final PlanPhase nextPhase,
final PriceList nextPriceList,
+ final Integer nextBillingCycleDayLocal,
final Long totalOrdering,
final DateTime createdDate,
final UUID userToken,
@@ -84,11 +88,13 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
this.effectiveTransitionTime = effectiveTransitionTime;
this.previousState = previousState;
this.previousPriceList = previousPriceList;
+ this.previousBillingCycleDayLocal = previousBillingCycleDayLocal;
this.previousPlan = previousPlan;
this.previousPhase = previousPhase;
this.nextState = nextState;
this.nextPlan = nextPlan;
this.nextPriceList = nextPriceList;
+ this.nextBillingCycleDayLocal = nextBillingCycleDayLocal;
this.nextPhase = nextPhase;
this.totalOrdering = totalOrdering;
this.previousEventId = previousEventId;
@@ -118,6 +124,7 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
this.previousEventCreatedDate = input.getPreviousEventCreatedDate();
this.previousState = input.getPreviousState();
this.previousPriceList = input.getPreviousPriceList();
+ this.previousBillingCycleDayLocal = input.getPreviousBillingCycleDayLocal();
this.previousPlan = input.getPreviousPlan();
this.previousPhase = input.getPreviousPhase();
this.nextEventId = input.getNextEventId();
@@ -125,6 +132,7 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
this.nextState = input.getNextState();
this.nextPlan = input.getNextPlan();
this.nextPriceList = input.getNextPriceList();
+ this.nextBillingCycleDayLocal = input.getNextBillingCycleDayLocal();
this.nextPhase = input.getNextPhase();
this.totalOrdering = input.getTotalOrdering();
this.isFromDisk = input.isFromDisk();
@@ -208,6 +216,14 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
return nextPriceList;
}
+ public Integer getPreviousBillingCycleDayLocal() {
+ return previousBillingCycleDayLocal;
+ }
+
+ public Integer getNextBillingCycleDayLocal() {
+ return nextBillingCycleDayLocal;
+ }
+
public UUID getUserToken() {
return userToken;
}
@@ -227,6 +243,8 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
return apiEventType.getSubscriptionTransitionType();
case PHASE:
return SubscriptionBaseTransitionType.PHASE;
+ case BCD_UPDATE:
+ return SubscriptionBaseTransitionType.BCD_CHANGE;
default:
throw new SubscriptionBaseError("Unexpected event type " + eventType);
}
@@ -270,10 +288,12 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
sb.append(", effectiveTransitionTime=").append(effectiveTransitionTime);
sb.append(", previousState=").append(previousState);
sb.append(", previousPriceList=").append(previousPriceList);
+ sb.append(", previousBillingCycleDayLocal=").append(previousBillingCycleDayLocal);
sb.append(", previousPlan=").append(previousPlan);
sb.append(", previousPhase=").append(previousPhase);
sb.append(", nextState=").append(nextState);
sb.append(", nextPriceList=").append(nextPriceList);
+ sb.append(", nextBillingCycleDayLocal=").append(nextBillingCycleDayLocal);
sb.append(", nextPlan=").append(nextPlan);
sb.append(", nextPhase=").append(nextPhase);
sb.append(", isFromDisk=").append(isFromDisk);
@@ -321,6 +341,9 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
return false;
}
+ if (nextBillingCycleDayLocal != null ? !nextBillingCycleDayLocal.equals(that.nextBillingCycleDayLocal) : that.nextBillingCycleDayLocal != null) {
+ return false;
+ }
if (nextState != that.nextState) {
return false;
}
@@ -333,6 +356,9 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
if (previousPriceList != null ? !previousPriceList.equals(that.previousPriceList) : that.previousPriceList != null) {
return false;
}
+ if (previousBillingCycleDayLocal != null ? !previousBillingCycleDayLocal.equals(that.previousBillingCycleDayLocal) : that.previousBillingCycleDayLocal != null) {
+ return false;
+ }
if (previousState != that.previousState) {
return false;
}
@@ -348,7 +374,6 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
return false;
}
-
return true;
}
@@ -363,10 +388,12 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
result = 31 * result + (effectiveTransitionTime != null ? effectiveTransitionTime.hashCode() : 0);
result = 31 * result + (previousState != null ? previousState.hashCode() : 0);
result = 31 * result + (previousPriceList != null ? previousPriceList.hashCode() : 0);
+ result = 31 * result + (previousBillingCycleDayLocal != null ? previousBillingCycleDayLocal.hashCode() : 0);
result = 31 * result + (previousPlan != null ? previousPlan.hashCode() : 0);
result = 31 * result + (previousPhase != null ? previousPhase.hashCode() : 0);
result = 31 * result + (nextState != null ? nextState.hashCode() : 0);
result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
+ result = 31 * result + (nextBillingCycleDayLocal != null ? nextBillingCycleDayLocal.hashCode() : 0);
result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
result = 31 * result + (nextPhase != null ? nextPhase.hashCode() : 0);
result = 31 * result + (isFromDisk != null ? isFromDisk.hashCode() : 0);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
index bf05324..6b157c2 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
@@ -159,6 +159,8 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
} else if (event.getType() == EventType.API_USER && subscription.getCategory() == ProductCategory.BASE) {
final CallContext callContext = internalCallContextFactory.createCallContext(context);
eventSent = onBasePlanEvent(subscription, event, callContext);
+ } else if (event.getType() == EventType.BCD_UPDATE) {
+ eventSent = false;
}
if (!eventSent) {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index cead31c..cb42eb6 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -66,6 +66,8 @@ import org.killbill.billing.subscription.engine.dao.model.SubscriptionModelDao;
import org.killbill.billing.subscription.events.EventBaseBuilder;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
+import org.killbill.billing.subscription.events.bcd.BCDEventBuilder;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.phase.PhaseEventBuilder;
import org.killbill.billing.subscription.events.user.ApiEvent;
@@ -622,7 +624,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
final SubscriptionEventSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
final UUID subscriptionId = subscription.getId();
- cancelFutureEventsFromTransaction(subscriptionId, changeEvents.get(0).getEffectiveDate(), entitySqlDaoWrapperFactory, context);
+ cancelFutureEventsFromTransaction(subscriptionId, changeEvents.get(0).getEffectiveDate(), entitySqlDaoWrapperFactory, false, context);
for (final SubscriptionBaseEvent cur : changeEvents) {
transactional.create(new SubscriptionEventModelDao(cur), context);
@@ -673,7 +675,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
private void cancelSubscriptionFromTransaction(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent cancelEvent, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context, final int seqId)
throws EntityPersistenceException {
final UUID subscriptionId = subscription.getId();
- cancelFutureEventsFromTransaction(subscriptionId, cancelEvent.getEffectiveDate(), entitySqlDaoWrapperFactory, context);
+ cancelFutureEventsFromTransaction(subscriptionId, cancelEvent.getEffectiveDate(), entitySqlDaoWrapperFactory, true, context);
entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class).create(new SubscriptionEventModelDao(cancelEvent), context);
final boolean isBusEvent = cancelEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0;
@@ -687,10 +689,12 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
cancelFutureEventFromTransaction(subscriptionId, entitySqlDaoWrapperFactory, EventType.PHASE, null, context);
}
- private void cancelFutureEventsFromTransaction(final UUID subscriptionId, final DateTime effectiveDate, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) {
+ private void cancelFutureEventsFromTransaction(final UUID subscriptionId, final DateTime effectiveDate, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final boolean includingBCDChange, final InternalCallContext context) {
final List<SubscriptionEventModelDao> eventModels = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class).getFutureActiveEventForSubscription(subscriptionId.toString(), effectiveDate.toDate(), context);
for (final SubscriptionEventModelDao cur : eventModels) {
- unactivateEventFromTransaction(cur, entitySqlDaoWrapperFactory, context);
+ if (includingBCDChange || cur.getEventType() != EventType.BCD_UPDATE) {
+ unactivateEventFromTransaction(cur, entitySqlDaoWrapperFactory, context);
+ }
}
}
@@ -868,9 +872,20 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
// Set total ordering value of the fake dryRun event to make sure billing events are correctly ordered
final SubscriptionBaseEvent curAdjustedDryRun;
if (!events.isEmpty()) {
- final EventBaseBuilder eventBuilder = (curDryRun.getType() == EventType.API_USER) ?
- new ApiEventBuilder((ApiEvent) curDryRun) :
- new PhaseEventBuilder((PhaseEvent) curDryRun);
+
+ final EventBaseBuilder eventBuilder;
+ switch(curDryRun.getType()) {
+ case PHASE:
+ eventBuilder = new PhaseEventBuilder((PhaseEvent) curDryRun);
+ break;
+ case BCD_UPDATE:
+ eventBuilder = new BCDEventBuilder((BCDEvent) curDryRun);
+ break;
+ case API_USER:
+ default:
+ eventBuilder = new ApiEventBuilder((ApiEvent) curDryRun);
+ break;
+ }
eventBuilder.setTotalOrdering(events.get(events.size() - 1).getTotalOrdering() + 1);
curAdjustedDryRun = eventBuilder.build();
@@ -915,6 +930,25 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
});
}
+ @Override
+ public void createBCDChangeEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent bcdEvent, final InternalCallContext context) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+ final SubscriptionEventSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
+ transactional.create(new SubscriptionEventModelDao(bcdEvent), context);
+
+ // Notify the Bus
+ notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, bcdEvent, SubscriptionBaseTransitionType.BCD_CHANGE, context);
+ final boolean isBusEvent = bcdEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0;
+ recordBusOrFutureNotificationFromTransaction(subscription, bcdEvent, entitySqlDaoWrapperFactory, isBusEvent, 0, context);
+
+ return null;
+ }
+ });
+
+ }
+
private DefaultSubscriptionBase createSubscriptionForInternalUse(final SubscriptionBase shellSubscription, final List<SubscriptionBaseEvent> events, final InternalTenantContext context) throws CatalogApiException {
final DefaultSubscriptionBase result = new DefaultSubscriptionBase(new SubscriptionBuilder(((DefaultSubscriptionBase) shellSubscription)), null, clock);
if (events.size() > 0) {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
index 9bcdd69..9063bba 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
@@ -19,10 +19,11 @@ package org.killbill.billing.subscription.engine.dao.model;
import java.util.UUID;
import org.joda.time.DateTime;
-
+import org.killbill.billing.subscription.events.EventBaseBuilder;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
-import org.killbill.billing.subscription.events.EventBaseBuilder;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
+import org.killbill.billing.subscription.events.bcd.BCDEventBuilder;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.phase.PhaseEventBuilder;
import org.killbill.billing.subscription.events.user.ApiEvent;
@@ -43,6 +44,7 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
private String planName;
private String phaseName;
private String priceListName;
+ private int billingCycleDayLocal;
private boolean isActive;
public SubscriptionEventModelDao() {
@@ -51,7 +53,7 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
public SubscriptionEventModelDao(final UUID id, final long totalOrdering, final EventType eventType, final ApiEventType userType,
final DateTime effectiveDate, final UUID subscriptionId,
- final String planName, final String phaseName, final String priceListName,
+ final String planName, final String phaseName, final String priceListName, final int billingCycleDayLocal,
final boolean active, final DateTime createDate, final DateTime updateDate) {
super(id, createDate, updateDate);
this.totalOrdering = totalOrdering;
@@ -62,6 +64,7 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
this.planName = planName;
this.phaseName = phaseName;
this.priceListName = priceListName;
+ this.billingCycleDayLocal = billingCycleDayLocal;
this.isActive = active;
}
@@ -73,8 +76,15 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
this.effectiveDate = src.getEffectiveDate();
this.subscriptionId = src.getSubscriptionId();
this.planName = eventType == EventType.API_USER ? ((ApiEvent) src).getEventPlan() : null;
- this.phaseName = eventType == EventType.API_USER ? ((ApiEvent) src).getEventPlanPhase() : ((PhaseEvent) src).getPhase();
+ if (eventType == EventType.API_USER) {
+ this.phaseName = ((ApiEvent) src).getEventPlanPhase();
+ } else if (eventType == EventType.PHASE) {
+ this.phaseName = ((PhaseEvent) src).getPhase();
+ } else {
+ this.phaseName = null;
+ }
this.priceListName = eventType == EventType.API_USER ? ((ApiEvent) src).getPriceList() : null;
+ this.billingCycleDayLocal = eventType == EventType.BCD_UPDATE ? ((BCDEvent) src).getBillCycleDayLocal() : 0;
this.isActive = src.isActive();
}
@@ -110,6 +120,10 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
return priceListName;
}
+ public int getBillingCycleDayLocal() {
+ return billingCycleDayLocal;
+ }
+
// TODO required for jdbi binder
public boolean getIsActive() {
return isActive;
@@ -151,6 +165,11 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
this.priceListName = priceListName;
}
+
+ public void setBillingCycleDayLocal(final int billingCycleDayLocal) {
+ this.billingCycleDayLocal = billingCycleDayLocal;
+ }
+
public void setIsActive(final boolean isActive) {
this.isActive = isActive;
}
@@ -161,16 +180,21 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
return null;
}
- final EventBaseBuilder<?> base = ((src.getEventType() == EventType.PHASE) ?
- new PhaseEventBuilder() :
- new ApiEventBuilder())
- .setTotalOrdering(src.getTotalOrdering())
- .setUuid(src.getId())
- .setSubscriptionId(src.getSubscriptionId())
- .setCreatedDate(src.getCreatedDate())
- .setUpdatedDate(src.getUpdatedDate())
- .setEffectiveDate(src.getEffectiveDate())
- .setActive(src.isActive());
+ final EventBaseBuilder<?> base;
+ if (src.getEventType() == EventType.PHASE) {
+ base = new PhaseEventBuilder();
+ } else if (src.getEventType() == EventType.BCD_UPDATE) {
+ base = new BCDEventBuilder();
+ } else {
+ base = new ApiEventBuilder();
+ }
+ base.setTotalOrdering(src.getTotalOrdering())
+ .setUuid(src.getId())
+ .setSubscriptionId(src.getSubscriptionId())
+ .setCreatedDate(src.getCreatedDate())
+ .setUpdatedDate(src.getUpdatedDate())
+ .setEffectiveDate(src.getEffectiveDate())
+ .setActive(src.isActive());
SubscriptionBaseEvent result;
if (src.getEventType() == EventType.PHASE) {
@@ -184,6 +208,8 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
.setApiEventType(src.getUserType())
.setFromDisk(true);
result = builder.build();
+ } else if (src.getEventType() == EventType.BCD_UPDATE) {
+ result = (new BCDEventBuilder(base).setBillCycleDayLocal(src.getBillingCycleDayLocal())).build();
} else {
throw new SubscriptionBaseError(String.format("Can't figure out event %s", src.getEventType()));
}
@@ -202,6 +228,7 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
sb.append(", planName='").append(planName).append('\'');
sb.append(", phaseName='").append(phaseName).append('\'');
sb.append(", priceListName='").append(priceListName).append('\'');
+ sb.append(", billingCycleDayLocal=").append(billingCycleDayLocal);
sb.append(", isActive=").append(isActive);
sb.append('}');
return sb.toString();
@@ -248,7 +275,9 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
if (userType != that.userType) {
return false;
}
-
+ if (billingCycleDayLocal != that.billingCycleDayLocal) {
+ return false;
+ }
return true;
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
index 8003503..846be18 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -95,5 +95,6 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
public void updateBundleExternalKey(UUID bundleId, String externalKey, InternalCallContext context);
+ public void createBCDChangeEvent(DefaultSubscriptionBase subscription, SubscriptionBaseEvent bcdEvent, InternalCallContext context);
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
index 33358f6..4ead49c 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
@@ -18,7 +18,10 @@ package org.killbill.billing.subscription.engine.dao;
import java.util.Date;
import java.util.List;
+import java.util.UUID;
+import org.joda.time.DateTime;
+import org.killbill.billing.entity.EntityPersistenceException;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.BindBean;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
@@ -43,15 +46,13 @@ public interface SubscriptionEventSqlDao extends EntitySqlDao<SubscriptionEventM
@SqlQuery
public List<SubscriptionEventModelDao> getFutureActiveEventForSubscription(@Bind("subscriptionId") String subscriptionId,
- @Bind("now") Date now,
- @BindBean final InternalTenantContext context);
+ @Bind("now") Date now,
+ @BindBean final InternalTenantContext context);
@SqlQuery
public List<SubscriptionEventModelDao> getEventsForSubscription(@Bind("subscriptionId") String subscriptionId,
@BindBean final InternalTenantContext context);
-
@SqlQuery
public List<SubscriptionEventModelDao> getFutureActiveEventsForAccount(@Bind("now") Date now, @BindBean final InternalTenantContext context);
-
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEvent.java
new file mode 100644
index 0000000..011f54c
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEvent.java
@@ -0,0 +1,25 @@
+/*
+ * 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.subscription.events.bcd;
+
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+
+public interface BCDEvent extends SubscriptionBaseEvent {
+
+ public Integer getBillCycleDayLocal();
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventBuilder.java
new file mode 100644
index 0000000..7c0926d
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventBuilder.java
@@ -0,0 +1,55 @@
+/*
+ * 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.subscription.events.bcd;
+
+import org.killbill.billing.subscription.events.EventBaseBuilder;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+
+public class BCDEventBuilder extends EventBaseBuilder<BCDEventBuilder> {
+
+ private Integer billCycleDayLocal;
+
+ public BCDEventBuilder() {
+ super();
+ }
+
+
+ public BCDEventBuilder(final BCDEvent event) {
+ super(event);
+ this.billCycleDayLocal = event.getBillCycleDayLocal();
+ }
+
+ public BCDEventBuilder(final EventBaseBuilder<?> base) {
+ super(base);
+ }
+
+
+ @Override
+ public SubscriptionBaseEvent build() {
+ return new BCDEventData(this);
+ }
+
+ public Integer getBillCycleDayLocal() {
+ return billCycleDayLocal;
+ }
+
+ public BCDEventBuilder setBillCycleDayLocal(final Integer billCycleDayLocal) {
+ this.billCycleDayLocal = billCycleDayLocal;
+ return this;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventData.java b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventData.java
new file mode 100644
index 0000000..c935d6c
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventData.java
@@ -0,0 +1,65 @@
+/*
+ * 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.subscription.events.bcd;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.events.EventBase;
+
+public class BCDEventData extends EventBase implements BCDEvent {
+
+ private final Integer billCycleDayLocal;
+
+ public BCDEventData(final BCDEventBuilder builder) {
+ super(builder);
+ this.billCycleDayLocal = builder.getBillCycleDayLocal();
+ }
+
+ @Override
+ public EventType getType() {
+ return EventType.BCD_UPDATE;
+ }
+
+ @Override
+ public String toString() {
+ return "BCDEventData {" +
+ "uuid=" + getId() +
+ ", subscriptionId=" + getSubscriptionId() +
+ ", createdDate=" + getCreatedDate() +
+ ", updatedDate=" + getUpdatedDate() +
+ ", effectiveDate=" + getEffectiveDate() +
+ ", totalOrdering=" + getTotalOrdering() +
+ ", isActive=" + isActive() +
+ '}';
+ }
+
+ // Hack until we introduce a proper field for that
+ @Override
+ public Integer getBillCycleDayLocal() {
+ return billCycleDayLocal;
+ }
+
+ public static BCDEvent createBCDEvent(final DefaultSubscriptionBase subscription, final DateTime effectiveDate, final int billCycleDayLocal) {
+ return new BCDEventData(new BCDEventBuilder()
+ .setSubscriptionId(subscription.getId())
+ .setEffectiveDate(effectiveDate)
+ .setActive(true)
+ .setBillCycleDayLocal(billCycleDayLocal));
+ }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java
index f710d44..36e7f24 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java
@@ -27,7 +27,8 @@ public interface SubscriptionBaseEvent extends Comparable<SubscriptionBaseEvent>
public enum EventType {
API_USER,
- PHASE
+ PHASE,
+ BCD_UPDATE
}
public EventType getType();
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql b/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
index d10805e..9e5e0c6 100644
--- a/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
@@ -4,13 +4,14 @@ DROP TABLE IF EXISTS subscription_events;
CREATE TABLE subscription_events (
record_id serial unique,
id varchar(36) NOT NULL,
- event_type varchar(9) NOT NULL,
+ event_type varchar(15) NOT NULL,
user_type varchar(25) DEFAULT NULL,
effective_date datetime NOT NULL,
subscription_id varchar(36) NOT NULL,
plan_name varchar(255) DEFAULT NULL,
phase_name varchar(255) DEFAULT NULL,
price_list_name varchar(64) DEFAULT NULL,
+ billing_cycle_day_local int DEFAULT NULL,
is_active boolean default true,
created_by varchar(50) NOT NULL,
created_date datetime NOT NULL,
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
index a1f8af9..49cca79 100644
--- a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
@@ -21,6 +21,7 @@ tableFields(prefix) ::= <<
, <prefix> plan_name
, <prefix> phase_name
, <prefix> price_list_name
+, <prefix> billing_cycle_day_local
, <prefix> is_active
, <prefix> created_by
, <prefix> created_date
@@ -36,6 +37,7 @@ tableValues() ::= <<
, :planName
, :phaseName
, :priceListName
+, :billingCycleDayLocal
, :isActive
, :createdBy
, :createdDate
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java b/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java
index cee8006..772055c 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java
@@ -37,7 +37,7 @@ public class TestEventJson extends SubscriptionTestSuiteNoDB {
public void testSubscriptionEvent() throws Exception {
final EffectiveSubscriptionInternalEvent e = new DefaultEffectiveSubscriptionEvent(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), new DateTime(),
- EntitlementState.ACTIVE, "pro", "TRIAL", "DEFAULT", EntitlementState.CANCELLED, null, null, null, 3L,
+ EntitlementState.ACTIVE, "pro", "TRIAL", "DEFAULT", null, EntitlementState.CANCELLED, null, null, null, null, 3L,
SubscriptionBaseTransitionType.CANCEL, 0, new DateTime(), 1L, 2L, null);
final String json = mapper.writeValueAsString(e);
@@ -46,4 +46,17 @@ public class TestEventJson extends SubscriptionTestSuiteNoDB {
final Object obj = mapper.readValue(json, claz);
Assert.assertTrue(obj.equals(e));
}
+
+ // Verify deserialization will work when we miss fields (previousBillCycleDayLocal, nextBillCycleDayLocal)
+ @Test(groups = "fast")
+ public void testSubscriptionEventWithNoBillCycleDayLocal() throws Exception {
+
+ final String json = "{\"eventId\":\"9e901bbc-bbcb-4f0a-8511-e58029bbea91\",\"subscriptionId\":\"c373056c-bb0c-4562-ab06-f595176aa4ae\",\"bundleId\":\"f61536b1-fc76-4337-b1e8-e38383894352\",\"effectiveTransitionTime\":\"2016-05-26T23:02:20.322Z\",\"previousState\":\"ACTIVE\",\"previousPlan\":\"pro\",\"previousPhase\":\"TRIAL\",\"previousPriceList\":\"DEFAULT\",\"nextState\":\"CANCELLED\",\"nextPlan\":null,\"nextPhase\":null,\"nextPriceList\":null,\"totalOrdering\":3,\"transitionType\":\"CANCEL\",\"remainingEventsForUserOperation\":0,\"startDate\":\"2016-05-26T23:02:20.322Z\",\"searchKey1\":1,\"searchKey2\":2,\"userToken\":null,\"requestedTransitionTime\":\"2016-05-26T23:02:20.322Z\"}";
+ final Class<?> claz = Class.forName(DefaultEffectiveSubscriptionEvent.class.getName());
+ final DefaultEffectiveSubscriptionEvent obj = (DefaultEffectiveSubscriptionEvent) mapper.readValue(json, claz);
+
+ Assert.assertEquals(obj.getId(), UUID.fromString("9e901bbc-bbcb-4f0a-8511-e58029bbea91"));
+ Assert.assertNull(obj.getPreviousBillCycleDayLocal());
+ Assert.assertNull(obj.getNextBillCycleDayLocal());
+ }
}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
index 2a377af..c6704e1 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
@@ -122,6 +122,11 @@ public class TestDefaultSubscriptionTransferApi extends SubscriptionTestSuiteNoD
}
@Override
+ public Integer getBillCycleDayLocal() {
+ return null;
+ }
+
+ @Override
public UUID getEventId() {
return UUID.randomUUID();
}
@@ -139,5 +144,4 @@ public class TestDefaultSubscriptionTransferApi extends SubscriptionTestSuiteNoD
}
};
}
-
}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index 0dfe039..38500c3 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -501,4 +501,10 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
@Override
public void updateBundleExternalKey(final UUID bundleId, final String externalKey, final InternalCallContext context) {
}
+
+ @Override
+ public void createBCDChangeEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent bcdEvent, final InternalCallContext context) {
+
+ }
+
}
diff --git a/util/src/test/java/org/killbill/billing/api/TestApiListener.java b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
index 61c1252..be41d8d 100644
--- a/util/src/test/java/org/killbill/billing/api/TestApiListener.java
+++ b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
@@ -124,6 +124,7 @@ public class TestApiListener {
TAG,
TAG_DEFINITION,
CUSTOM_FIELD,
+ BCD_CHANGE
}
@Subscribe
@@ -168,6 +169,10 @@ public class TestApiListener {
assertEqualsNicely(NextEvent.PHASE);
notifyIfStackEmpty();
break;
+ case BCD_CHANGE:
+ assertEqualsNicely(NextEvent.BCD_CHANGE);
+ notifyIfStackEmpty();
+ break;
default:
throw new RuntimeException("Unexpected event type " + eventEffective.getRequestedTransitionTime());
}
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java
index 67915ce..e4a5c36 100644
--- a/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * 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
@@ -19,6 +19,7 @@
package org.killbill.billing;
import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.killbill.billing.platform.test.PlatformDBTestingHelper;
import org.killbill.billing.platform.test.config.TestKillbillConfigSource;
import org.killbill.billing.platform.test.glue.TestPlatformModuleWithEmbeddedDB;
@@ -52,8 +53,7 @@ public class GuicyKillbillTestWithEmbeddedDBModule extends GuicyKillbillTestModu
final DBTestingHelper dbTestingHelper = DBTestingHelper.get();
configureEmbeddedDB(dbTestingHelper);
}
- protected void configureKillbillNodesApi() {
- }
+ protected void configureKillbillNodesApi() {}
}
}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java b/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java
index 793f2f0..60f76fa 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java
@@ -39,10 +39,12 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
private final DateTime effectiveTransitionTime;
private final EntitlementState previousState;
private final String previousPriceList;
+ private final Integer previousBillCycleDayLocal;
private final String previousPlan;
private final String previousPhase;
private final EntitlementState nextState;
private final String nextPriceList;
+ private final Integer nextBillCycleDayLocal;
private final String nextPlan;
private final String nextPhase;
private final Integer remainingEventsForUserOperation;
@@ -61,10 +63,12 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
@JsonProperty("previousPlan") final String previousPlan,
@JsonProperty("previousPhase") final String previousPhase,
@JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("previousBillCycleDayLocal") final Integer previousBillCycleDayLocal,
@JsonProperty("nextState") final EntitlementState nextState,
@JsonProperty("nextPlan") final String nextPlan,
@JsonProperty("nextPhase") final String nextPhase,
@JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("nextBillCycleDayLocal") final Integer nextBillCycleDayLocal,
@JsonProperty("totalOrdering") final Long totalOrdering,
@JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
@JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
@@ -80,11 +84,13 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
this.effectiveTransitionTime = effectiveTransitionTime;
this.previousState = previousState;
this.previousPriceList = previousPriceList;
+ this.previousBillCycleDayLocal = previousBillCycleDayLocal;
this.previousPlan = previousPlan;
this.previousPhase = previousPhase;
this.nextState = nextState;
this.nextPlan = nextPlan;
this.nextPriceList = nextPriceList;
+ this.nextBillCycleDayLocal = nextBillCycleDayLocal;
this.nextPhase = nextPhase;
this.totalOrdering = totalOrdering;
this.userToken = userToken;
@@ -132,6 +138,11 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
}
@Override
+ public Integer getPreviousBillCycleDayLocal() {
+ return previousBillCycleDayLocal;
+ }
+
+ @Override
public String getNextPlan() {
return nextPlan;
}
@@ -158,6 +169,11 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
}
@Override
+ public Integer getNextBillCycleDayLocal() {
+ return nextBillCycleDayLocal;
+ }
+
+ @Override
public Integer getRemainingEventsForUserOperation() {
return remainingEventsForUserOperation;
}