killbill-memoizeit

jaxrs, entitlement: Add ability to specify an effectiveFromDate

6/2/2016 7:03:07 PM

Details

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 a389d59..c194b4c 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
@@ -25,6 +25,7 @@ import java.util.UUID;
 import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
@@ -98,5 +99,5 @@ public interface SubscriptionBaseInternalApi {
     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;
+    public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException;
 }
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
index ff42a6e..fcc3d52 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
@@ -68,7 +68,7 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         clock.addDays(3);
 
         // Set next BCD to be the 15
-        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, internalCallContext);
+        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, null, internalCallContext);
         Thread.sleep(1000);
         assertListenerStatus();
 
@@ -122,7 +122,7 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         assertListenerStatus();
 
         // Set next BCD to be the 15
-        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, internalCallContext);
+        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15,  null, internalCallContext);
         Thread.sleep(1000);
         assertListenerStatus();
 
@@ -155,7 +155,7 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         expectedInvoices.clear();
 
         // Set next BCD to be the 10
-        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, internalCallContext);
+        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10,  null, internalCallContext);
         Thread.sleep(1000);
         assertListenerStatus();
 
@@ -191,7 +191,7 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         clock.addDays(30);
         assertListenerStatus();
 
-        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, internalCallContext);
+        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10,  null, internalCallContext);
 
         // 2016-5-5
         clock.addDays(4);
@@ -250,7 +250,7 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
         expectedInvoices.clear();
 
-        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, internalCallContext);
+        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10,  null, internalCallContext);
 
         // 2016-5-10
         busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
@@ -284,7 +284,7 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         clock.addDays(30);
         assertListenerStatus();
 
-        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, internalCallContext);
+        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10,  null, internalCallContext);
 
         busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
         clock.addDays(9);
@@ -355,7 +355,7 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         clock.addDays(3);
 
         busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
-        subscriptionBaseInternalApi.updateBCD(aoEntitlement.getId(), 4, internalCallContext);
+        subscriptionBaseInternalApi.updateBCD(aoEntitlement.getId(), 4,  null, internalCallContext);
         assertListenerStatus();
 
         final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
@@ -446,7 +446,7 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         // 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);
+        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 16,  null, internalCallContext);
         assertListenerStatus();
 
         // Third, unblock starting at the 16, will generate a full period invoice
@@ -460,7 +460,56 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
         invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
         expectedInvoices.clear();
-
     }
 
+
+    @Test(groups = "slow")
+    public void testBCDChangeWithEffectiveDateFromInTheFuture() 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 but only starting from 2016-5-31
+        subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15,  new LocalDate(2016, 5, 31), internalCallContext);
+        Thread.sleep(1000);
+        assertListenerStatus();
+
+        // 2016-5-15 : We don't expect anything yet because of effectiveDateFrom = 2016-6-1
+        clock.addDays(14);
+        Thread.sleep(1000);
+        assertListenerStatus();
+
+        // 2016-6-1 : We expect a pro-ration from 2016-6-1 -> 2016-6-15
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addDays(17);
+        assertListenerStatus();
+
+        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 1), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("116.64")));
+        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+        expectedInvoices.clear();
+
+        //  2016-6-15 : Finally we get the BCD_CHANGE event and start building for full monthly period
+        busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.NULL_INVOICE,  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();
+    }
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index 801299d..5a7eae4 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -699,10 +699,10 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
     }
 
     @Override
-    public void updateBCD(final int newBCD, final CallContext callContext) throws EntitlementApiException {
+    public void updateBCD(final int newBCD, @Nullable final LocalDate effectiveFromDate, final CallContext callContext) throws EntitlementApiException {
         final InternalCallContext context = internalCallContextFactory.createInternalCallContext(getAccountId(), callContext);
         try {
-            subscriptionInternalApi.updateBCD(getId(), newBCD, context);
+            subscriptionInternalApi.updateBCD(getId(), newBCD, effectiveFromDate, context);
         } catch (final SubscriptionBaseApiException e) {
             throw new EntitlementApiException(e);
         }
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 18513b1..1b52ef3 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
@@ -79,6 +79,7 @@ public interface JaxrsResource {
     public static final String QUERY_ENTITLEMENT_POLICY = "entitlementPolicy";
     public static final String QUERY_SEARCH_OFFSET = "offset";
     public static final String QUERY_SEARCH_LIMIT = "limit";
+    public static final String QUERY_ENTITLEMENT_EFFECTIVE_FROM_DT = "effectiveFromDate";
 
     public static final String QUERY_ACCOUNT_WITH_BALANCE = "accountWithBalance";
     public static final String QUERY_ACCOUNT_WITH_BALANCE_AND_CBA = "accountWithBalanceAndCBA";
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 d420a75..e770d05 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
@@ -209,7 +209,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
                                            entitlementApi.addEntitlement(getBundleIdForAddOnCreation(entitlement), spec, overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, pluginProperties, callContext) :
                                            entitlementApi.createBaseEntitlement(account.getId(), spec, entitlement.getExternalKey(), overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, pluginProperties, callContext);
                 if (newBCD != null) {
-                    result.updateBCD(newBCD, callContext);
+                    result.updateBCD(newBCD, null, callContext);
                 }
                 return result;
             }
@@ -556,6 +556,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
     @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid entitlement supplied")})
     public Response updateSubscriptionBCD(final SubscriptionJson json,
                                           @PathParam(ID_PARAM_NAME) final String id,
+                                          @QueryParam(QUERY_ENTITLEMENT_EFFECTIVE_FROM_DT) final String effectiveFromDateStr,
                                           @HeaderParam(HDR_CREATED_BY) final String createdBy,
                                           @HeaderParam(HDR_REASON) final String reason,
                                           @HeaderParam(HDR_COMMENT) final String comment,
@@ -565,11 +566,12 @@ public class SubscriptionResource extends JaxRsResourceBase {
         verifyNonNullOrEmpty(json, "SubscriptionJson body should be specified");
         verifyNonNullOrEmpty(json.getBillCycleDayLocal(), "SubscriptionJson new BCD should be specified");
 
+        final LocalDate effectiveFromDate = toLocalDate(effectiveFromDateStr);
         final CallContext callContext = context.createContext(createdBy, reason, comment, request);
         final UUID subscriptionId = UUID.fromString(id);
 
         final Entitlement entitlement = entitlementApi.getEntitlementForId(subscriptionId, callContext);
-        entitlement.updateBCD(json.getBillCycleDayLocal(), callContext);
+        entitlement.updateBCD(json.getBillCycleDayLocal(), effectiveFromDate, callContext);
         return Response.status(Status.OK).build();
     }
 
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 065afad..175207b 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
@@ -633,21 +633,21 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     }
 
     @Override
-    public void updateBCD(final UUID subscriptionId, final int bcd, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
+    public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
         final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) getSubscriptionFromId(subscriptionId, internalCallContext);
-        final DateTime effectiveDate = getEffectiveDateForNewBCD(bcd, internalCallContext);
+        final DateTime effectiveDate = getEffectiveDateForNewBCD(bcd, effectiveFromDate, internalCallContext);
         final BCDEvent bcdEvent = BCDEventData.createBCDEvent(subscription, effectiveDate, bcd);
         dao.createBCDChangeEvent(subscription, bcdEvent, internalCallContext);
     }
 
 
-    private DateTime getEffectiveDateForNewBCD(final int bcd, final InternalCallContext internalCallContext) {
+    private DateTime getEffectiveDateForNewBCD(final int bcd, @Nullable final LocalDate effectiveFromDate, 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());
+        final LocalDate startDate = effectiveFromDate != null ? effectiveFromDate : 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
         //