killbill-memoizeit

subscription: Implement START_OF_TERM policy when forcing

12/23/2016 8:09:56 PM

Changes

Details

diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
index 5cda4c4..5d2e050 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
@@ -21,6 +21,7 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 
+import org.joda.time.DateTimeZone;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Plan;
@@ -46,7 +47,7 @@ public interface SubscriptionBase extends Entity, Blockable {
     public boolean cancelWithDate(final DateTime requestedDate, final CallContext context)
             throws SubscriptionBaseApiException;
 
-    public boolean cancelWithPolicy(final BillingActionPolicy policy, final CallContext context)
+    public boolean cancelWithPolicy(final BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, final CallContext context)
             throws SubscriptionBaseApiException;
 
     public boolean uncancel(final CallContext context)
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 617049b..9530310 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
@@ -50,7 +50,7 @@ public interface SubscriptionBaseInternalApi {
     public List<SubscriptionBaseWithAddOns> createBaseSubscriptionsWithAddOns(UUID accountId, Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifier,
                                                                               InternalCallContext contextWithValidAccountRecordId) throws SubscriptionBaseApiException;
 
-    public void cancelBaseSubscriptions(Iterable<SubscriptionBase> subscriptions, BillingActionPolicy policy, InternalCallContext context) throws SubscriptionBaseApiException;
+    public void cancelBaseSubscriptions(Iterable<SubscriptionBase> subscriptions, BillingActionPolicy policy, DateTimeZone accountTimeZone, int accountBillCycleDayLocal, InternalCallContext context) throws SubscriptionBaseApiException;
 
     public SubscriptionBaseBundle createBundleForAccount(UUID accountId, String bundleName, InternalCallContext context)
             throws SubscriptionBaseApiException;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
index 8b5ca40..54788f8 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
@@ -42,6 +42,7 @@ import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import org.killbill.billing.entitlement.api.EntitlementApiException;
 import org.killbill.billing.entitlement.api.EntitlementSpecifier;
+import org.killbill.billing.entitlement.api.Subscription;
 import org.killbill.billing.entitlement.api.SubscriptionEventType;
 import org.killbill.billing.invoice.api.DryRunType;
 import org.killbill.billing.invoice.api.Invoice;
@@ -479,4 +480,74 @@ public class TestSubscription extends TestIntegrationBase {
         clock.addMonths(1);
         assertListenerStatus();
     }
+
+
+
+    @Test(groups = "slow")
+    public void testCancelSubscriptionInTrialWith_START_OF_TERM() throws Exception {
+        final LocalDate initialDate = new LocalDate(2015, 9, 1);
+        clock.setDay(initialDate);
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(initialDate), 0);
+        assertEquals(createdEntitlement.getEffectiveEndDate(), null);
+        assertListenerStatus();
+
+        // Move clock a bit to make sure START_OF_TERM brings us back to initialDate
+        clock.addDays(5);
+
+        busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
+        final Entitlement cancelledEntitlement = createdEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.START_OF_TERM, null, callContext);
+        assertListenerStatus();
+
+        final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(cancelledEntitlement.getId(), callContext);
+
+        assertEquals(subscription.getEffectiveEndDate().compareTo(new LocalDate(2015, 9, 6)), 0);
+        assertEquals(subscription.getBillingEndDate().compareTo(initialDate), 0);
+
+    }
+
+    @Test(groups = "slow")
+    public void testCancelSubscriptionAfterTrialWith_START_OF_TERM() throws Exception {
+        final LocalDate initialDate = new LocalDate(2015, 8, 1);
+        clock.setDay(initialDate);
+
+        Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(initialDate), 0);
+        assertEquals(createdEntitlement.getEffectiveEndDate(), null);
+        assertListenerStatus();
+
+        // Move out of trial : 2015-8-31
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+
+        account = accountUserApi.getAccountById(account.getId(), callContext);
+        Assert.assertEquals(account.getBillCycleDayLocal().intValue(), 31);
+
+
+        // Move clock a bit to make sure START_OF_TERM brings us back to last Phase date : 2015-9-5
+        clock.addDays(5);
+
+        busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.INVOICE);
+        final Entitlement cancelledEntitlement = createdEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.START_OF_TERM, null, callContext);
+        assertListenerStatus();
+
+        final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(cancelledEntitlement.getId(), callContext);
+
+        assertEquals(subscription.getEffectiveEndDate().compareTo(new LocalDate(2015, 9, 5)), 0);
+        assertEquals(subscription.getBillingEndDate().compareTo(new LocalDate(2015, 8, 31)), 0);
+
+    }
+
 }
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 fcc3d52..8ba0800 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
@@ -28,11 +28,15 @@ 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.BillingActionPolicy;
 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.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
+import org.killbill.billing.entitlement.api.Subscription;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.junction.DefaultBlockingState;
@@ -42,6 +46,7 @@ import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
 
+import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 
 public class TestWithBCDUpdate extends TestIntegrationBase {
@@ -99,6 +104,17 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
         invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
         expectedInvoices.clear();
+
+        // Add cancellation with START_OF_TERM to verify BCD update is correctly interpreted
+        clock.addDays(3);
+
+        busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.INVOICE);
+        final Entitlement cancelledEntitlement = baseEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.START_OF_TERM, null, callContext);
+        assertListenerStatus();
+
+        final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(cancelledEntitlement.getId(), callContext);
+        assertEquals(subscription.getEffectiveEndDate().compareTo(new LocalDate(2016, 5, 18)), 0);
+        assertEquals(subscription.getBillingEndDate().compareTo(new LocalDate(2016, 5, 15)), 0);
     }
 
 
@@ -169,6 +185,16 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         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();
+
+        clock.addDays(3);
+        busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.INVOICE);
+        final Entitlement cancelledEntitlement = baseEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.START_OF_TERM, null, callContext);
+        assertListenerStatus();
+
+        final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(cancelledEntitlement.getId(), callContext);
+        assertEquals(subscription.getEffectiveEndDate().compareTo(new LocalDate(2016, 7, 13)), 0);
+        assertEquals(subscription.getBillingEndDate().compareTo(new LocalDate(2016, 7, 10)), 0);
+
     }
 
 
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java
index 11a3399..2d21e94 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java
@@ -18,9 +18,12 @@ package org.killbill.billing.catalog.rules;
 
 import javax.xml.bind.annotation.XmlElement;
 
+import org.killbill.billing.catalog.StandaloneCatalog;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.PhaseType;
 import org.killbill.billing.catalog.api.rules.CaseCancelPolicy;
+import org.killbill.xmlloader.ValidationError;
+import org.killbill.xmlloader.ValidationErrors;
 
 public class DefaultCaseCancelPolicy extends DefaultCasePhase<BillingActionPolicy> implements CaseCancelPolicy {
 
@@ -38,6 +41,16 @@ public class DefaultCaseCancelPolicy extends DefaultCasePhase<BillingActionPolic
     }
 
     @Override
+    public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+        if (policy ==  BillingActionPolicy.START_OF_TERM) {
+            errors.add(new ValidationError("Default catalog START_OF_TERM has not been implemented, such policy can be used during cancellation by overriding policy",
+                                           catalog.getCatalogURI(), DefaultCaseCancelPolicy.class, ""));
+        }
+        return errors;
+    }
+
+
+    @Override
     public BillingActionPolicy getBillingActionPolicy() {
         return policy;
     }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java
index bc7f4ae..827aee9 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java
@@ -192,6 +192,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
                 cur.getToPriceList() == null) {
                 foundDefaultCase = true;
             }
+            cur.validate(catalog, errors);
         }
         if (!foundDefaultCase) {
             errors.add(new ValidationError("Missing default rule case for plan change", catalog.getCatalogURI(), DefaultPlanRules.class, ""));
@@ -212,6 +213,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
                 cur.getPriceList() == null) {
                 foundDefaultCase = true;
             }
+            cur.validate(catalog, errors);
         }
         if (!foundDefaultCase) {
             errors.add(new ValidationError("Missing default rule case for plan cancellation", catalog.getCatalogURI(), DefaultPlanRules.class, ""));
@@ -225,6 +227,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
             } else {
                 caseChangePlanAlignmentsSet.add(cur);
             }
+            cur.validate(catalog, errors);
         }
 
         final HashSet<DefaultCaseCreateAlignment> caseCreateAlignmentsSet = new HashSet<DefaultCaseCreateAlignment>();
@@ -234,6 +237,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
             } else {
                 caseCreateAlignmentsSet.add(cur);
             }
+            cur.validate(catalog, errors);
         }
 
         final HashSet<DefaultCaseBillingAlignment> caseBillingAlignmentsSet = new HashSet<DefaultCaseBillingAlignment>();
@@ -243,6 +247,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
             } else {
                 caseBillingAlignmentsSet.add(cur);
             }
+            cur.validate(catalog, errors);
         }
 
         final HashSet<DefaultCasePriceList> casePriceListsSet = new HashSet<DefaultCasePriceList>();
@@ -252,6 +257,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
             } else {
                 casePriceListsSet.add(cur);
             }
+            cur.validate(catalog, errors);
         }
         return errors;
     }
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 2cf1acc..9680a76 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
@@ -500,7 +500,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
                 try {
                     // Cancel subscription base first, to correctly compute the add-ons entitlements we need to cancel (see below)
-                    getSubscriptionBase().cancelWithPolicy(billingPolicy, callContext);
+                    getSubscriptionBase().cancelWithPolicy(billingPolicy, eventsStream.getAccountTimeZone(), eventsStream.getDefaultBillCycleDayLocal(), callContext);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e);
                 }
@@ -523,6 +523,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
     }
 
     private LocalDate getLocalDateFromEntitlementPolicy(final EntitlementActionPolicy entitlementPolicy) {
+
         final LocalDate cancellationDate;
         switch (entitlementPolicy) {
             case IMMEDIATE:
@@ -541,6 +542,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
         return (cancellationDate.compareTo(getEffectiveStartDate()) < 0) ? getEffectiveStartDate() : cancellationDate;
     }
 
+
     @Override
     public Entitlement changePlan(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
index 2f7ad68..982b932 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
@@ -32,7 +32,9 @@ import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.account.api.AccountInternalApi;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
@@ -75,6 +77,7 @@ import org.killbill.notificationq.api.NotificationQueueService;
 import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
 
 import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 
 public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase implements EntitlementInternalApi {
@@ -95,6 +98,21 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
 
     @Override
     public void cancel(final Iterable<Entitlement> entitlements, @Nullable final LocalDate effectiveDate, final BillingActionPolicy billingPolicy, final Iterable<PluginProperty> properties, final InternalCallContext internalCallContext) throws EntitlementApiException {
+
+        if (!entitlements.iterator().hasNext()) {
+            return;
+        }
+
+        int bcd = 0;
+        DateTimeZone accountTimeZone = null;
+        try {
+            bcd = accountApi.getBCD(entitlements.iterator().next().getAccountId(), internalCallContext);
+            accountTimeZone = accountApi.getImmutableAccountDataByRecordId(internalCallContext. getAccountRecordId(), internalCallContext).getTimeZone();
+        } catch (final AccountApiException e) {
+            throw new EntitlementApiException(e);
+        }
+        Preconditions.checkState(bcd > 0 && accountTimeZone != null, "Unexpected condition where account info could not be retrieved");
+
         final CallContext callContext = internalCallContextFactory.createCallContext(internalCallContext);
 
         final ImmutableMap.Builder<BlockingState, Optional<UUID>> blockingStates = new ImmutableMap.Builder<BlockingState, Optional<UUID>>();
@@ -141,6 +159,8 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
 
         final Callable<Void> preCallbacksCallback = new BulkSubscriptionBaseCancellation(subscriptions,
                                                                                          billingPolicy,
+                                                                                         accountTimeZone,
+                                                                                         bcd,
                                                                                          internalCallContext);
 
         pluginExecution.executeWithPlugin(preCallbacksCallback, callbacks, pluginContexts);
@@ -182,20 +202,26 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
 
         private final Iterable<SubscriptionBase> subscriptions;
         private final BillingActionPolicy billingPolicy;
+        private final DateTimeZone accountTimeZone;
+        private final int accountBillCycleDayLocal;
         private final InternalCallContext callContext;
 
         public BulkSubscriptionBaseCancellation(final Iterable<SubscriptionBase> subscriptions,
                                                 final BillingActionPolicy billingPolicy,
+                                                final DateTimeZone accountTimeZone,
+                                                final int accountBillCycleDayLocal,
                                                 final InternalCallContext callContext) {
             this.subscriptions = subscriptions;
             this.billingPolicy = billingPolicy;
+            this.accountTimeZone = accountTimeZone;
+            this.accountBillCycleDayLocal = accountBillCycleDayLocal;
             this.callContext = callContext;
         }
 
         @Override
         public Void call() throws Exception {
             try {
-                subscriptionInternalApi.cancelBaseSubscriptions(subscriptions, billingPolicy, callContext);
+                subscriptionInternalApi.cancelBaseSubscriptions(subscriptions, billingPolicy, accountTimeZone, accountBillCycleDayLocal, callContext);
             } catch (final SubscriptionBaseApiException e) {
                 throw new EntitlementApiException(e);
             }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
index 8a75ef0..ea26fcc 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
@@ -21,8 +21,7 @@ package org.killbill.billing.invoice.generator;
 import org.joda.time.LocalDate;
 import org.killbill.billing.catalog.api.BillingMode;
 import org.killbill.billing.catalog.api.BillingPeriod;
-
-import com.google.common.annotations.VisibleForTesting;
+import org.killbill.billing.util.bcd.BillCycleDayCalculator;
 
 public class BillingIntervalDetail {
 
@@ -32,7 +31,6 @@ public class BillingIntervalDetail {
     private final int billingCycleDay;
     private final BillingPeriod billingPeriod;
     private final BillingMode billingMode;
-    private final boolean isMonthBased;
     // First date after the startDate aligned with the BCD
     private LocalDate firstBillingCycleDate;
     // Date up to which we should bill
@@ -56,7 +54,6 @@ public class BillingIntervalDetail {
         }
         this.billingPeriod = billingPeriod;
         this.billingMode = billingMode;
-        this.isMonthBased = (billingPeriod.getPeriod().getMonths() | billingPeriod.getPeriod().getYears()) > 0;
         computeAll();
     }
 
@@ -70,7 +67,7 @@ public class BillingIntervalDetail {
 
     public LocalDate getFutureBillingDateFor(final int nbPeriod) {
         final LocalDate proposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, nbPeriod);
-        return alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+        return BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
     }
 
     public LocalDate getLastBillingCycleDate() {
@@ -79,7 +76,7 @@ public class BillingIntervalDetail {
 
     public LocalDate getNextBillingCycleDate() {
         final LocalDate proposedDate = lastBillingCycleDate != null ? lastBillingCycleDate.plus(billingPeriod.getPeriod()) : firstBillingCycleDate;
-        final LocalDate nextBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+        final LocalDate nextBillingCycleDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
         return nextBillingCycleDate;
     }
 
@@ -109,7 +106,7 @@ public class BillingIntervalDetail {
         while (proposedDate.isBefore(startDate)) {
             proposedDate = proposedDate.plus(billingPeriod.getPeriod());
         }
-        firstBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+        firstBillingCycleDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
     }
 
     private void calculateEffectiveEndDate() {
@@ -141,7 +138,7 @@ public class BillingIntervalDetail {
             nextProposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, numberOfPeriods);
             numberOfPeriods += 1;
         }
-        proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+        proposedDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
 
         // We honor the endDate as long as it does not go beyond our targetDate (by construction this cannot be after the nextProposedDate neither.
         if (endDate != null && !endDate.isAfter(targetDate)) {
@@ -171,7 +168,7 @@ public class BillingIntervalDetail {
             proposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, numberOfPeriods);
             numberOfPeriods += 1;
         }
-        proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+        proposedDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
 
         // The proposedDate is greater to our endDate => return it
         if (endDate != null && endDate.isBefore(proposedDate)) {
@@ -199,7 +196,7 @@ public class BillingIntervalDetail {
 
         // Our proposed date is billingCycleDate prior to the effectiveEndDate
         proposedDate = proposedDate.minus(billingPeriod.getPeriod());
-        proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+        proposedDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
 
         if (proposedDate.isBefore(firstBillingCycleDate)) {
             // Make sure not to go too far in the past
@@ -208,20 +205,4 @@ public class BillingIntervalDetail {
             lastBillingCycleDate = proposedDate;
         }
     }
-
-    //
-    // We start from a billCycleDate
-    //
-    private static LocalDate alignProposedBillCycleDate(final LocalDate proposedDate, final int billingCycleDay, final boolean isMonthBased) {
-        // billingCycleDay alignment only makes sense for month based BillingPeriod (MONTHLY, QUARTERLY, BIANNUAL, ANNUAL)
-        if (!isMonthBased) {
-            return proposedDate;
-        }
-        final int lastDayOfMonth = proposedDate.dayOfMonth().getMaximumValue();
-        int proposedBillCycleDate = proposedDate.getDayOfMonth();
-        if (proposedBillCycleDate < billingCycleDay && billingCycleDay <= lastDayOfMonth) {
-            proposedBillCycleDate = billingCycleDay;
-        }
-        return new LocalDate(proposedDate.getYear(), proposedDate.getMonthOfYear(), proposedBillCycleDate, proposedDate.getChronology());
-    }
 }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
index e3ad715..83c8117 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
@@ -56,10 +57,10 @@ public interface SubscriptionBaseApiService {
     public boolean cancelWithRequestedDate(DefaultSubscriptionBase subscription, DateTime requestedDate, CallContext context)
             throws SubscriptionBaseApiException;
 
-    public boolean cancelWithPolicy(DefaultSubscriptionBase subscription, BillingActionPolicy policy, CallContext context)
+    public boolean cancelWithPolicy(DefaultSubscriptionBase subscription, BillingActionPolicy policy, DateTimeZone accountTimeZone, int accountBillCycleDayLocal, CallContext context)
             throws SubscriptionBaseApiException;
 
-    public boolean cancelWithPolicyNoValidation(Iterable<DefaultSubscriptionBase> subscriptions, BillingActionPolicy policy, InternalCallContext context)
+    public boolean cancelWithPolicyNoValidation(Iterable<DefaultSubscriptionBase> subscriptions, BillingActionPolicy policy, DateTimeZone accountTimeZone, int accountBillCycleDayLocal, InternalCallContext context)
             throws SubscriptionBaseApiException;
 
     public boolean uncancel(DefaultSubscriptionBase subscription, CallContext context)
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 d8bcd5c..798a653 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
@@ -313,7 +313,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     }
 
     @Override
-    public void cancelBaseSubscriptions(final Iterable<SubscriptionBase> subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
+    public void cancelBaseSubscriptions(final Iterable<SubscriptionBase> subscriptions, final BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, final InternalCallContext context) throws SubscriptionBaseApiException {
         apiService.cancelWithPolicyNoValidation(Iterables.<SubscriptionBase, DefaultSubscriptionBase>transform(subscriptions,
                                                                                                                new Function<SubscriptionBase, DefaultSubscriptionBase>() {
                                                                                                                    @Override
@@ -326,6 +326,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                                                                                                                    }
                                                                                                                }),
                                                 policy,
+                                                accountTimeZone,
+                                                accountBillCycleDayLocal,
                                                 context);
     }
 
@@ -656,7 +658,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                             final PlanChangeResult planChangeResult = apiService.getPlanChangeResult(subscriptionForChange, inputSpec, utcNow, tenantContext);
                             policy = planChangeResult.getPolicy();
                         }
-                        changeEffectiveDate = subscriptionForChange.getPlanChangeEffectiveDate(policy);
+                        // We pass null for billingAlignment, accountTimezone, account BCD because this is not available which means that dryRun with START_OF_TERM BillingPolicy will fail
+                        changeEffectiveDate = subscriptionForChange.getPlanChangeEffectiveDate(policy, null, null, -1, context);
                     }
                     dryRunEvents = apiService.getEventsOnChangePlan(subscriptionForChange, plan, plan.getPriceListName(), changeEffectiveDate, utcNow, true, context);
                     break;
@@ -673,7 +676,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                                                                                    subscriptionForCancellation.getCurrentPhase().getPhaseType());
                             policy = catalogService.getFullCatalog(true, true, context).planCancelPolicy(spec, utcNow);
                         }
-                        cancelEffectiveDate = subscriptionForCancellation.getPlanChangeEffectiveDate(policy);
+                        // We pass null for billingAlignment, accountTimezone, account BCD because this is not available which means that dryRun with START_OF_TERM BillingPolicy will fail
+                        cancelEffectiveDate = subscriptionForCancellation.getPlanChangeEffectiveDate(policy, null, null, -1, context);
                     }
                     dryRunEvents = apiService.getEventsOnCancelPlan(subscriptionForCancellation, cancelEffectiveDate, utcNow, true, context);
                     break;
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 90a581e..91acbb2 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
@@ -29,7 +29,12 @@ import java.util.UUID;
 import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingAlignment;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogApiException;
@@ -57,11 +62,13 @@ import org.killbill.billing.subscription.events.phase.PhaseEvent;
 import org.killbill.billing.subscription.events.user.ApiEvent;
 import org.killbill.billing.subscription.events.user.ApiEventType;
 import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.bcd.BillCycleDayCalculator;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.clock.Clock;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 
@@ -235,8 +242,8 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
     }
 
     @Override
-    public boolean cancelWithPolicy(final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
-        return apiService.cancelWithPolicy(this, policy, context);
+    public boolean cancelWithPolicy(final BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, final CallContext context) throws SubscriptionBaseApiException {
+        return apiService.cancelWithPolicy(this, policy, accountTimeZone, accountBillCycleDayLocal, context);
     }
 
     @Override
@@ -526,13 +533,45 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
         return getFutureEndDate() != null;
     }
 
-    public DateTime getPlanChangeEffectiveDate(final BillingActionPolicy policy) {
+    public DateTime getPlanChangeEffectiveDate(final BillingActionPolicy policy, @Nullable final BillingAlignment alignment, @Nullable final DateTimeZone accountTimeZone, @Nullable final Integer accountBillCycleDayLocal, final InternalTenantContext context) {
 
         final DateTime candidateResult;
         switch (policy) {
             case IMMEDIATE:
                 candidateResult = clock.getUTCNow();
                 break;
+            case START_OF_TERM:
+                if (chargedThroughDate == null) {
+                    candidateResult = getStartDate();
+                // Will take care of billing IN_ARREAR or subscriptions that are not invoiced up to date
+                } else if (!chargedThroughDate.isAfter(clock.getUTCNow())) {
+                    candidateResult = chargedThroughDate;
+                } else {
+
+                    // In certain path (dryRun, or default catalog START_OF_TERM policy), the info is not easily available and as a result, such policy is not implemented
+                    Preconditions.checkState(alignment != null && accountTimeZone != null && accountBillCycleDayLocal != null, "START_OF_TERM not implemented in dryRun use case");
+
+                    Preconditions.checkState(alignment != BillingAlignment.BUNDLE || category != ProductCategory.ADD_ON,  "START_OF_TERM not implemented for AO configured with a BUNDLE billing alignment");
+
+                    // If BCD was overriden at the subscription level, we take its latest value (it should also be reflected in the chargedThroughDate) but still required for
+                    // alignment purpose
+                    Integer bcd = getBillCycleDayLocal();
+                    if (bcd == null) {
+                        bcd = BillCycleDayCalculator.calculateBcdForAlignment(null, this, this, alignment, accountTimeZone, accountBillCycleDayLocal);
+                    }
+
+                    final BillingPeriod billingPeriod = getLastActivePlan().getRecurringBillingPeriod();
+                    DateTime proposedDate = chargedThroughDate;
+                    while (proposedDate.isAfter(clock.getUTCNow())) {
+                        proposedDate = proposedDate.minus(billingPeriod.getPeriod());
+                    }
+
+                    final LocalDate resultingLocalDate  = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, bcd, billingPeriod, accountTimeZone);
+                    candidateResult = context.toUTCDateTime(resultingLocalDate);
+                }
+
+                break;
+
             case END_OF_TERM:
                 //
                 // If we have a chargedThroughDate that is 'up to date' we use it, if not default to now
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index 49efce2..19731b9 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -29,12 +29,17 @@ import java.util.UUID;
 import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.joda.time.ReadableInstant;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.ImmutableAccountInternalApi;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingAlignment;
 import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.CatalogEntity;
@@ -71,6 +76,7 @@ import org.killbill.billing.util.callcontext.TenantContext;
 import org.killbill.clock.Clock;
 import org.killbill.clock.DefaultClock;
 
+import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -204,7 +210,10 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         try {
             final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
             final BillingActionPolicy policy = catalogService.getFullCatalog(true, true, internalCallContext).planCancelPolicy(planPhase, now);
-            final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
+
+            Preconditions.checkState(policy != BillingActionPolicy.START_OF_TERM, "A default START_OF_TERM policy is not availaible");
+
+            final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, null, null, -1, null);
 
             return doCancelPlan(ImmutableMap.<DefaultSubscriptionBase, DateTime>of(subscription, effectiveDate), now, internalCallContext);
         } catch (final CatalogApiException e) {
@@ -226,24 +235,30 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
     }
 
     @Override
-    public boolean cancelWithPolicy(final DefaultSubscriptionBase subscription, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+    public boolean cancelWithPolicy(final DefaultSubscriptionBase subscription, final BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, final CallContext context) throws SubscriptionBaseApiException {
         final EntitlementState currentState = subscription.getState();
         if (currentState == EntitlementState.CANCELLED) {
             throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState);
         }
 
         final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
-        return cancelWithPolicyNoValidation(ImmutableList.<DefaultSubscriptionBase>of(subscription), policy, internalCallContext);
+        return cancelWithPolicyNoValidation(ImmutableList.<DefaultSubscriptionBase>of(subscription), policy, accountTimeZone, accountBillCycleDayLocal, internalCallContext);
     }
 
     @Override
-    public boolean cancelWithPolicyNoValidation(final Iterable<DefaultSubscriptionBase> subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
+    public boolean cancelWithPolicyNoValidation(final Iterable<DefaultSubscriptionBase> subscriptions, final BillingActionPolicy policy, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal, final InternalCallContext context) throws SubscriptionBaseApiException {
         final Map<DefaultSubscriptionBase, DateTime> subscriptionsWithEffectiveDate = new HashMap<DefaultSubscriptionBase, DateTime>();
         final DateTime now = clock.getUTCNow();
 
-        for (final DefaultSubscriptionBase subscription : subscriptions) {
-            final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
-            subscriptionsWithEffectiveDate.put(subscription, effectiveDate);
+        try {
+
+            for (final DefaultSubscriptionBase subscription : subscriptions) {
+                final BillingAlignment billingAlignment = (subscription.getState() == EntitlementState.PENDING ? null : catalogService.getFullCatalog(true, true, context).billingAlignment(new PlanPhaseSpecifier(subscription.getLastActivePlan().getName(), subscription.getLastActivePhase().getPhaseType()), clock.getUTCNow()));
+                final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, billingAlignment, accountTimeZone, accountBillCycleDayLocal, context);
+                subscriptionsWithEffectiveDate.put(subscription, effectiveDate);
+            }
+        } catch (final CatalogApiException e) {
+            throw new SubscriptionBaseApiException(e);
         }
 
         return doCancelPlan(subscriptionsWithEffectiveDate, now, context);
@@ -330,7 +345,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         }
 
         if (policyMaybeNull != null) {
-            return subscription.getPlanChangeEffectiveDate(policyMaybeNull);
+            return subscription.getPlanChangeEffectiveDate(policyMaybeNull, null, null, -1, null);
         } else if (requestedDateWithMs != null) {
             return DefaultClock.truncateMs(requestedDateWithMs);
         } else {
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
index 056208c..af4ddbb 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
@@ -20,16 +20,9 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.joda.time.Interval;
-import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
-import org.killbill.billing.entity.EntityPersistenceException;
-import org.killbill.billing.subscription.engine.dao.SubscriptionEventSqlDao;
-import org.killbill.billing.subscription.engine.dao.model.SubscriptionEventModelDao;
-import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
-import org.skife.jdbi.v2.Handle;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
+import org.joda.time.LocalDate;
 import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Duration;
 import org.killbill.billing.catalog.api.PhaseType;
@@ -37,8 +30,17 @@ import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanPhase;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.entity.EntityPersistenceException;
 import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
 import org.killbill.billing.subscription.api.SubscriptionBillingApiException;
+import org.killbill.billing.subscription.engine.dao.SubscriptionEventSqlDao;
+import org.killbill.billing.subscription.engine.dao.model.SubscriptionEventModelDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.skife.jdbi.v2.Handle;
+import org.testng.Assert;
+import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
@@ -344,6 +346,46 @@ public class TestUserApiCancel extends SubscriptionTestSuiteWithEmbeddedDB {
         final SubscriptionBaseTransition previousTransition = subscription.getPreviousTransition();
         Assert.assertEquals(previousTransition.getPreviousState(), EntitlementState.ACTIVE);
         Assert.assertNotNull(previousTransition.getPreviousPlan());
+    }
+
+    @Test(groups = "slow")
+    public void testCancelSubscription_START_OF_TERM() throws SubscriptionBaseApiException {
+
+        // Set date in such a way that Phase align with the first of the month (and so matches our hardcoded accountData account BCD)
+        final DateTime testStartDate = new DateTime(2016, 11, 1, 0, 3, 42, 0);
+        clock.setDeltaFromReality(testStartDate.getMillis() - clock.getUTCNow().getMillis());
+
+        final String prod = "Shotgun";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+        final String planSet = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+        // CREATE
+        DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, prod, term, planSet);
+        PlanPhase currentPhase = subscription.getCurrentPhase();
+        assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+
+        // Move out of TRIAL
+        testListener.pushExpectedEvent(NextEvent.PHASE);
+        clock.addDays(30);
+        assertListenerStatus();
+
+        // Artificially set the CTD
+        final Duration ctd = testUtil.getDurationMonth(1);
+        final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(clock.getUTCNow(), ctd);
+        subscriptionInternalApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate, internalCallContext);
+        subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+
+        // Move ahead a bit abd cancel START_OF_TERM
+        clock.addDays(5);
+        testListener.pushExpectedEvent(NextEvent.CANCEL);
+        subscription.cancelWithPolicy(BillingActionPolicy.START_OF_TERM, accountData.getTimeZone(), accountData.getBillCycleDayLocal(), callContext);
+        assertListenerStatus();
+
+        subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+        Assert.assertEquals(subscription.getAllTransitions().get(subscription.getAllTransitions().size() - 1).getTransitionType(), SubscriptionBaseTransitionType.CANCEL);
+        Assert.assertEquals(new LocalDate(subscription.getAllTransitions().get(subscription.getAllTransitions().size() - 1).getEffectiveTransitionTime(), accountData.getTimeZone()), new LocalDate(2016, 12, 1));
 
     }
+
+
 }
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
index 0c2e6c5..cd663dd 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
@@ -151,7 +151,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
 
         subscription = subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
 
-        subscription.cancelWithPolicy(BillingActionPolicy.END_OF_TERM, callContext);
+        subscription.cancelWithPolicy(BillingActionPolicy.END_OF_TERM, null, -1, callContext);
         try {
             subscription.changePlanWithDate(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCNow(), callContext);
         } catch (final SubscriptionBaseApiException e) {
diff --git a/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java b/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java
index 51a8e01..80fda69 100644
--- a/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java
+++ b/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java
@@ -20,9 +20,13 @@ package org.killbill.billing.util.bcd;
 import java.util.Map;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
 import org.killbill.billing.catalog.api.BillingAlignment;
+import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.subscription.api.SubscriptionBase;
 import org.killbill.clock.ClockUtil;
 import org.slf4j.Logger;
@@ -32,7 +36,7 @@ public abstract class BillCycleDayCalculator {
 
     private static final Logger log = LoggerFactory.getLogger(BillCycleDayCalculator.class);
 
-    public static int calculateBcdForAlignment(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final BillingAlignment alignment, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal) {
+    public static int calculateBcdForAlignment(@Nullable final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final BillingAlignment alignment, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal) {
         int result = 0;
         switch (alignment) {
             case ACCOUNT:
@@ -48,11 +52,33 @@ public abstract class BillCycleDayCalculator {
         return result;
     }
 
-    private static int calculateOrRetrieveBcdFromSubscription(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final DateTimeZone accountTimeZone) {
-        Integer result = bcdCache.get(subscription.getId());
+    public static LocalDate alignProposedBillCycleDate(final LocalDate proposedDate, final int billingCycleDay, final BillingPeriod billingPeriod) {
+        // billingCycleDay alignment only makes sense for month based BillingPeriod (MONTHLY, QUARTERLY, BIANNUAL, ANNUAL)
+        final boolean isMonthBased = (billingPeriod.getPeriod().getMonths() | billingPeriod.getPeriod().getYears()) > 0;
+        if (!isMonthBased) {
+            return proposedDate;
+        }
+        final int lastDayOfMonth = proposedDate.dayOfMonth().getMaximumValue();
+        int proposedBillCycleDate = proposedDate.getDayOfMonth();
+        if (proposedBillCycleDate < billingCycleDay && billingCycleDay <= lastDayOfMonth) {
+            proposedBillCycleDate = billingCycleDay;
+        }
+        return new LocalDate(proposedDate.getYear(), proposedDate.getMonthOfYear(), proposedBillCycleDate, proposedDate.getChronology());
+    }
+
+    public static LocalDate alignProposedBillCycleDate(final DateTime proposedDate, final int billingCycleDay, final BillingPeriod billingPeriod, final DateTimeZone accountTimeZone) {
+        final LocalDate proposedLocalDate = ClockUtil.toLocalDate(proposedDate, accountTimeZone);
+        final LocalDate resultingLocalDate = alignProposedBillCycleDate(proposedLocalDate, billingCycleDay, billingPeriod);
+        return resultingLocalDate;
+    }
+
+    private static int calculateOrRetrieveBcdFromSubscription(@Nullable final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final DateTimeZone accountTimeZone) {
+        Integer result = bcdCache != null ? bcdCache.get(subscription.getId()) : null;
         if (result == null) {
             result = calculateBcdFromSubscription(subscription, accountTimeZone);
-            bcdCache.put(subscription.getId(), result);
+            if (bcdCache != null) {
+                bcdCache.put(subscription.getId(), result);
+            }
         }
         return result;
     }
diff --git a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
index c3287d1..5475050 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.mockito.Mockito;
@@ -71,9 +72,9 @@ public class MockSubscription implements SubscriptionBase {
     }
 
     @Override
-    public boolean cancelWithPolicy(BillingActionPolicy policy, CallContext context)
+    public boolean cancelWithPolicy(BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, CallContext context)
             throws SubscriptionBaseApiException {
-        return sub.cancelWithPolicy(policy, context);
+        return sub.cancelWithPolicy(policy, accountTimeZone, accountBillCycleDayLocal, context);
     }
 
     @Override