killbill-aplcache

Merge remote-tracking branch 'origin/master' into work-for-release-0.17.x Signed-off-by:

11/11/2016 7:04:47 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 fada42e..9badfbc 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
@@ -103,7 +103,7 @@ public interface SubscriptionBaseInternalApi {
 
     public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException;
 
-    public int getDefaultBillCycleDayLocal(final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final PlanPhaseSpecifier planPhaseSpecifier, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal, final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException;
+    public int getDefaultBillCycleDayLocal(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final PlanPhaseSpecifier planPhaseSpecifier, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal, final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException;
 
 
 }
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index 5d476a0..7b6b8da 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -936,9 +936,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
         checkODState("OD1");
         checkChangePlanWithOverdueState(baseEntitlement, true, true);
 
-        invoiceChecker.checkInvoice(account.getId(), 4, callContext,
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 8, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkInvoice(account.getId(), 4, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 8, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
 
         // Fully adjust all invoices
         final List<Invoice> invoicesToAdjust = getUnpaidInvoicesOrderFromRecent();
@@ -946,7 +944,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
             if (i == invoicesToAdjust.size() - 1) {
                 fullyAdjustInvoiceAndCheckForCompletion(account, invoicesToAdjust.get(i), NextEvent.BLOCK, NextEvent.INVOICE_ADJUSTMENT);
             } else {
-                fullyAdjustInvoiceAndCheckForCompletion(account, invoicesToAdjust.get(i), NextEvent.INVOICE_ADJUSTMENT, NextEvent.INVOICE_ADJUSTMENT);
+                fullyAdjustInvoiceAndCheckForCompletion(account, invoicesToAdjust.get(i), NextEvent.INVOICE_ADJUSTMENT);
             }
         }
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
index bd6e009..2fe8c9c 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
@@ -178,6 +178,7 @@ public class EventsStreamBuilder {
         }
 
         // Build the EventsStream objects
+        final Map<UUID, Integer> bcdCache = new HashMap<UUID, Integer>();
         final Map<UUID, Collection<EventsStream>> entitlementsPerBundle = new HashMap<UUID, Collection<EventsStream>>();
         for (final UUID bundleId : subscriptions.keySet()) {
             final SubscriptionBaseBundle bundle = bundlesPerId.get(bundleId);
@@ -217,7 +218,7 @@ public class EventsStreamBuilder {
                 blockingStateSet.addAll(subscriptionBlockingStates);
                 final List<BlockingState> blockingStates = ProxyBlockingStateDao.sortedCopy(blockingStateSet);
 
-                final EventsStream eventStream = buildForEntitlement(account, bundle, baseSubscription, subscription, allSubscriptionsForBundle, blockingStates, internalTenantContext);
+                final EventsStream eventStream = buildForEntitlement(account, bundle, baseSubscription, subscription, allSubscriptionsForBundle, blockingStates, bcdCache, internalTenantContext);
                 entitlementsPerBundle.get(bundleId).add(eventStream);
             }
         }
@@ -249,7 +250,8 @@ public class EventsStreamBuilder {
         // Retrieve the blocking states
         final List<BlockingState> blockingStatesForAccount = defaultBlockingStateDao.getBlockingAllForAccountRecordId(internalTenantContext);
 
-        return buildForEntitlement(blockingStatesForAccount, account, bundle, baseSubscription, subscription, allSubscriptionsForBundle, internalTenantContext);
+        final Map<UUID, Integer> bcdCache = new HashMap<UUID, Integer>();
+        return buildForEntitlement(blockingStatesForAccount, account, bundle, baseSubscription, subscription, allSubscriptionsForBundle, bcdCache, internalTenantContext);
     }
 
     // Special signature for OptimizedProxyBlockingStateDao to save some DAO calls
@@ -259,7 +261,8 @@ public class EventsStreamBuilder {
                                             final SubscriptionBase baseSubscription,
                                             final List<SubscriptionBase> allSubscriptionsForBundle,
                                             final InternalTenantContext internalTenantContext) throws EntitlementApiException {
-        return buildForEntitlement(blockingStatesForAccount, account, bundle, baseSubscription, baseSubscription, allSubscriptionsForBundle, internalTenantContext);
+        final Map<UUID, Integer> bcdCache = new HashMap<UUID, Integer>();
+        return buildForEntitlement(blockingStatesForAccount, account, bundle, baseSubscription, baseSubscription, allSubscriptionsForBundle, bcdCache, internalTenantContext);
     }
 
     private EventsStream buildForEntitlement(final List<BlockingState> blockingStatesForAccount,
@@ -268,6 +271,7 @@ public class EventsStreamBuilder {
                                              @Nullable final SubscriptionBase baseSubscription,
                                              final SubscriptionBase subscription,
                                              final List<SubscriptionBase> allSubscriptionsForBundle,
+                                             final Map<UUID, Integer> bcdCache,
                                              final InternalTenantContext internalTenantContext) throws EntitlementApiException {
         // Optimization: build lookup tables for blocking states states
         final Collection<BlockingState> accountBlockingStates = new LinkedList<BlockingState>();
@@ -319,7 +323,7 @@ public class EventsStreamBuilder {
         blockingStateSet.addAll(subscriptionBlockingStates);
         final List<BlockingState> blockingStates = ProxyBlockingStateDao.sortedCopy(blockingStateSet);
 
-        return buildForEntitlement(account, bundle, baseSubscription, subscription, allSubscriptionsForBundle, blockingStates, internalTenantContext);
+        return buildForEntitlement(account, bundle, baseSubscription, subscription, allSubscriptionsForBundle, blockingStates, bcdCache, internalTenantContext);
     }
 
     private EventsStream buildForEntitlement(final ImmutableAccountData account,
@@ -328,12 +332,13 @@ public class EventsStreamBuilder {
                                              final SubscriptionBase subscription,
                                              final List<SubscriptionBase> allSubscriptionsForBundle,
                                              final List<BlockingState> blockingStates,
+                                             final Map<UUID, Integer> bcdCache,
                                              final InternalTenantContext internalTenantContext) throws EntitlementApiException {
 
 
         try {
             int accountBCD = accountInternalApi.getBCD(account.getId(), internalTenantContext);
-            int defaultAlignmentDay = subscriptionInternalApi.getDefaultBillCycleDayLocal(subscription, baseSubscription, createPlanPhaseSpecifier(subscription), account.getTimeZone(), accountBCD, clock.getUTCNow(), internalTenantContext);
+            int defaultAlignmentDay = subscriptionInternalApi.getDefaultBillCycleDayLocal(bcdCache, subscription, baseSubscription, createPlanPhaseSpecifier(subscription), account.getTimeZone(), accountBCD, clock.getUTCNow(), internalTenantContext);
             return new DefaultEventsStream(account,
                                            bundle,
                                            blockingStates,
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
index fbb5ba5..4106f87 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
@@ -224,8 +224,10 @@ public class ItemsNodeInterval extends NodeInterval {
 
     /**
      * Add the adjustment amount on the item specified by the targetId.
+     *
+     * @return linked item if fully adjusted, null otherwise
      */
-    public void addAdjustment(final InvoiceItem item) {
+    public Item addAdjustment(final InvoiceItem item) {
         final UUID targetId = item.getLinkedItemId();
 
         // TODO we should really be using findNode(adjustmentDate, callback) instead but wrong dates in test creates panic.
@@ -246,8 +248,10 @@ public class ItemsNodeInterval extends NodeInterval {
         if (targetItem.getAmount().compareTo(adjustmentAmount) == 0) {
             // Full item adjustment - treat it like a repair
             addExistingItem(new ItemsNodeInterval(this, targetInvoiceId, new Item(item, targetItem.getStartDate(), targetItem.getEndDate(), targetInvoiceId, ItemAction.CANCEL)));
+            return targetItem;
         } else {
             targetItem.incrementAdjustedAmount(adjustmentAmount);
+            return null;
         }
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
index 81ec626..de29b6e 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
@@ -26,7 +26,6 @@ import java.util.UUID;
 import javax.annotation.Nullable;
 
 import org.joda.time.LocalDate;
-
 import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.tree.Item.ItemAction;
 
@@ -48,7 +47,9 @@ public class SubscriptionItemTree {
 
     private ItemsNodeInterval root;
     private boolean isBuilt;
+    private boolean isMerged;
     private List<Item> items;
+    private List<Item> existingFullyAdjustedItems;
     private List<InvoiceItem> existingFixedItems;
     private Map<LocalDate, InvoiceItem> remainingFixedItems;
     private List<InvoiceItem> pendingItemAdj;
@@ -78,6 +79,7 @@ public class SubscriptionItemTree {
         this.targetInvoiceId = targetInvoiceId;
         this.root = new ItemsNodeInterval(targetInvoiceId);
         this.items = new LinkedList<Item>();
+        this.existingFullyAdjustedItems = new LinkedList<Item>();
         this.existingFixedItems = new LinkedList<InvoiceItem>();
         this.remainingFixedItems = new HashMap<LocalDate, InvoiceItem>();
         this.pendingItemAdj = new LinkedList<InvoiceItem>();
@@ -91,7 +93,10 @@ public class SubscriptionItemTree {
         Preconditions.checkState(!isBuilt);
 
         for (InvoiceItem item : pendingItemAdj) {
-            root.addAdjustment(item);
+            final Item fullyAdjustedItem = root.addAdjustment(item);
+            if (fullyAdjustedItem != null) {
+                existingFullyAdjustedItems.add(fullyAdjustedItem);
+            }
         }
         pendingItemAdj.clear();
         root.buildForExistingItems(items);
@@ -122,6 +127,7 @@ public class SubscriptionItemTree {
         Preconditions.checkState(!isBuilt);
         root.mergeExistingAndProposed(items);
         isBuilt = true;
+        isMerged = true;
     }
 
     /**
@@ -203,7 +209,23 @@ public class SubscriptionItemTree {
         tmp.addAll(Collections2.filter(Collections2.transform(items, new Function<Item, InvoiceItem>() {
             @Override
             public InvoiceItem apply(final Item input) {
-                return input.toInvoiceItem();
+                final InvoiceItem resultingCandidate = input.toInvoiceItem();
+
+                // Post merge, the ADD items are the candidates for the resulting RECURRING items (see toInvoiceItem()).
+                // We will ignore any resulting item matching existing items on disk though as these are the result of full item adjustments.
+                // See https://github.com/killbill/killbill/issues/654
+                if (isMerged) {
+                    for (final Item existingAdjustedItem : existingFullyAdjustedItems) {
+                        // Note: we DO keep the item in case of partial matches, e.g. if the new proposed item end date is before
+                        // the existing (adjusted) item. See TestSubscriptionItemTree#testMaxedOutProRation
+                        final InvoiceItem fullyAdjustedInvoiceItem = existingAdjustedItem.toInvoiceItem();
+                        if (resultingCandidate.matches(fullyAdjustedInvoiceItem)) {
+                            return null;
+                        }
+                    }
+                }
+
+                return resultingCandidate;
             }
         }), new Predicate<InvoiceItem>() {
             @Override
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index f28bca2..9aeedfe 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -57,6 +57,7 @@ import org.killbill.billing.invoice.api.InvoiceStatus;
 import org.killbill.billing.invoice.model.DefaultInvoice;
 import org.killbill.billing.invoice.model.DefaultInvoicePayment;
 import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
 import org.killbill.billing.invoice.model.RecurringInvoiceItem;
 import org.killbill.billing.invoice.model.RepairAdjInvoiceItem;
 import org.killbill.billing.junction.BillingEvent;
@@ -1022,7 +1023,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
     // Regression test for #170 (see https://github.com/killbill/killbill/pull/173)
     @Test(groups = "fast")
     public void testRegressionFor170() throws EntityPersistenceException, InvoiceApiException, CatalogApiException {
-        final UUID accountId = UUID.randomUUID();
+        final UUID accountId = account.getId();
         final Currency currency = Currency.USD;
         final SubscriptionBase subscription = createSubscription();
         final MockInternationalPrice recurringPrice = new MockInternationalPrice(new DefaultPrice(new BigDecimal("2.9500"), Currency.USD));
@@ -1205,6 +1206,55 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         assertTrue(invoice3.getBalance().compareTo(FIFTEEN.multiply(TWO).add(TWELVE)) == 0);
     }
 
+    @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/654")
+    public void testCancelEOTWithFullItemAdjustment() throws CatalogApiException, InvoiceApiException {
+        final BigDecimal rate = new BigDecimal("39.95");
+
+        final BillingEventSet events = new MockBillingEventSet();
+
+        final SubscriptionBase sub = createSubscription();
+        final LocalDate startDate = invoiceUtil.buildDate(2016, 10, 9);
+        final LocalDate endDate = invoiceUtil.buildDate(2016, 11, 9);
+
+        final Plan plan = new MockPlan();
+        final PlanPhase phase = createMockMonthlyPlanPhase(rate);
+
+        final BillingEvent event = createBillingEvent(sub.getId(), sub.getBundleId(), startDate, plan, phase, 9);
+        events.add(event);
+
+        final LocalDate targetDate = invoiceUtil.buildDate(2016, 10, 9);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
+
+        assertNotNull(invoice);
+        assertEquals(invoice.getNumberOfItems(), 1);
+        assertEquals(invoice.getBalance(), KillBillMoney.of(rate, invoice.getCurrency()));
+        assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), sub.getId());
+
+        assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+        assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), startDate);
+        assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), endDate);
+
+        // Cancel EOT and Add the item adjustment
+        final BillingEvent event2 = invoiceUtil.createMockBillingEvent(account, sub, endDate.toDateTimeAtStartOfDay(),
+                                                                       null, phase,
+                                                                       ZERO, null, Currency.USD, BillingPeriod.NO_BILLING_PERIOD, 9,
+                                                                       BillingMode.IN_ADVANCE, "Cancel", 2L,
+                                                                       SubscriptionBaseTransitionType.CANCEL);
+        events.add(event2);
+
+        final InvoiceItem itemAdj = new ItemAdjInvoiceItem(invoice.getInvoiceItems().get(0), new LocalDate(2016, 10, 12), rate.negate(), Currency.USD);
+
+        invoice.addInvoiceItem(itemAdj);
+
+        final List<Invoice> existingInvoices = new ArrayList<Invoice>();
+        existingInvoices.add(invoice);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, existingInvoices, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
+
+        assertNull(invoice2);
+    }
+
     private void printDetailInvoice(final Invoice invoice) {
         log.info("--------------------  START DETAIL ----------------------");
         log.info("Invoice " + invoice.getId() + ": BALANCE = " + invoice.getBalance()
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 85691aa..83d67c7 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
@@ -18,8 +18,10 @@
 
 package org.killbill.billing.junction.plumbing.billing;
 
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.UUID;
@@ -180,7 +182,10 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
         // If dryRun is specified, we don't want to to update the account BCD value, so we initialize the flag updatedAccountBCD to true
         boolean updatedAccountBCD = dryRunMode;
 
-        final int currentAccountBCD = accountApi.getBCD(account.getId(), context);
+        final Map<UUID, Integer> bcdCache = new HashMap<UUID, Integer>();
+
+        int currentAccountBCD = accountApi.getBCD(account.getId(), context);
+
         for (final SubscriptionBase subscription : subscriptions) {
 
             // The subscription did not even start, so there is nothing to do yet, we can skip and avoid some NPE down the line when calculating the BCD
@@ -210,7 +215,7 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
                 overridenBCD = transition.getNextBillCycleDayLocal() != null ? transition.getNextBillCycleDayLocal() : overridenBCD;
                 final int bcdLocal = overridenBCD != null ?
                                      overridenBCD :
-                                     calculateBcdForTransition(catalog, baseSubscription, subscription, account, currentAccountBCD, transition);
+                                     calculateBcdForTransition(catalog, bcdCache, baseSubscription, subscription, account, currentAccountBCD, transition);
 
                 if (currentAccountBCD == 0 && !updatedAccountBCD) {
                     log.info("Setting account BCD='{}', accountId='{}'", bcdLocal, account.getId());
@@ -224,10 +229,10 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
         }
     }
 
-    private int calculateBcdForTransition(final Catalog catalog, final SubscriptionBase baseSubscription, final SubscriptionBase subscription, final ImmutableAccountData account, final int accountBillCycleDayLocal, final EffectiveSubscriptionInternalEvent transition)
+    private int calculateBcdForTransition(final Catalog catalog, final Map<UUID, Integer> bcdCache, final SubscriptionBase baseSubscription, final SubscriptionBase subscription, final ImmutableAccountData account, final int accountBillCycleDayLocal, final EffectiveSubscriptionInternalEvent transition)
             throws CatalogApiException, AccountApiException, SubscriptionBaseApiException {
         final BillingAlignment alignment = catalog.billingAlignment(getPlanPhaseSpecifierFromTransition(catalog, transition), transition.getEffectiveTransitionTime());
-        return BillCycleDayCalculator.calculateBcdForAlignment(subscription, baseSubscription, alignment, account.getTimeZone(), accountBillCycleDayLocal);
+        return BillCycleDayCalculator.calculateBcdForAlignment(bcdCache, subscription, baseSubscription, alignment, account.getTimeZone(), accountBillCycleDayLocal);
     }
 
     private PlanPhaseSpecifier getPlanPhaseSpecifierFromTransition(final Catalog catalog, final EffectiveSubscriptionInternalEvent transition) throws CatalogApiException {
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 4b18a23..843e114 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
@@ -730,12 +730,12 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     }
 
     @Override
-    public int getDefaultBillCycleDayLocal(final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final PlanPhaseSpecifier planPhaseSpecifier, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal, final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException {
+    public int getDefaultBillCycleDayLocal(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final PlanPhaseSpecifier planPhaseSpecifier, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal, final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException {
 
         try {
             final Catalog catalog = catalogService.getFullCatalog(true, true, context);
             final BillingAlignment alignment = catalog.billingAlignment(planPhaseSpecifier, effectiveDate);
-            return BillCycleDayCalculator.calculateBcdForAlignment(subscription, baseSubscription, alignment, accountTimeZone, accountBillCycleDayLocal);
+            return BillCycleDayCalculator.calculateBcdForAlignment(bcdCache, subscription, baseSubscription, alignment, accountTimeZone, accountBillCycleDayLocal);
         } catch (final CatalogApiException e) {
             throw new 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 3ab047c..51a8e01 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
@@ -17,6 +17,9 @@
 
 package org.killbill.billing.util.bcd;
 
+import java.util.Map;
+import java.util.UUID;
+
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.killbill.billing.catalog.api.BillingAlignment;
@@ -29,22 +32,31 @@ public abstract class BillCycleDayCalculator {
 
     private static final Logger log = LoggerFactory.getLogger(BillCycleDayCalculator.class);
 
-    public static int calculateBcdForAlignment(final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final BillingAlignment alignment, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal) {
+    public static int calculateBcdForAlignment(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:
-                result = accountBillCycleDayLocal != 0 ? accountBillCycleDayLocal : calculateBcdFromSubscription(subscription, accountTimeZone);
+                result = accountBillCycleDayLocal != 0 ? accountBillCycleDayLocal : calculateOrRetrieveBcdFromSubscription(bcdCache, subscription, accountTimeZone);
                 break;
             case BUNDLE:
-                result = calculateBcdFromSubscription(baseSubscription, accountTimeZone);
+                result = calculateOrRetrieveBcdFromSubscription(bcdCache, baseSubscription, accountTimeZone);
                 break;
             case SUBSCRIPTION:
-                result = calculateBcdFromSubscription(subscription, accountTimeZone);
+                result = calculateOrRetrieveBcdFromSubscription(bcdCache, subscription, accountTimeZone);
                 break;
         }
         return result;
     }
 
+    private static int calculateOrRetrieveBcdFromSubscription(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final DateTimeZone accountTimeZone) {
+        Integer result = bcdCache.get(subscription.getId());
+        if (result == null) {
+            result = calculateBcdFromSubscription(subscription, accountTimeZone);
+            bcdCache.put(subscription.getId(), result);
+        }
+        return result;
+    }
+
     private static int calculateBcdFromSubscription(final SubscriptionBase subscription, final DateTimeZone accountTimeZone) {
         final DateTime date = subscription.getDateOfFirstRecurringNonZeroCharge();
         final int bcdLocal = ClockUtil.toDateTime(date, accountTimeZone).getDayOfMonth();
diff --git a/util/src/test/java/org/killbill/billing/util/bcd/TestBillCycleDayCalculator.java b/util/src/test/java/org/killbill/billing/util/bcd/TestBillCycleDayCalculator.java
index a8d0b09..9fb4aed 100644
--- a/util/src/test/java/org/killbill/billing/util/bcd/TestBillCycleDayCalculator.java
+++ b/util/src/test/java/org/killbill/billing/util/bcd/TestBillCycleDayCalculator.java
@@ -17,6 +17,9 @@
 
 package org.killbill.billing.util.bcd;
 
+import java.util.HashMap;
+import java.util.UUID;
+
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.killbill.billing.account.api.AccountApiException;
@@ -54,7 +57,7 @@ public class TestBillCycleDayCalculator extends UtilTestSuiteNoDB {
 
         final ImmutableAccountData account = Mockito.mock(ImmutableAccountData.class);
         Mockito.when(account.getTimeZone()).thenReturn(accountTimeZone);
-        final Integer billCycleDayLocal = BillCycleDayCalculator.calculateBcdForAlignment(subscription, subscription, BillingAlignment.BUNDLE, account.getTimeZone(), 0);
+        final Integer billCycleDayLocal = BillCycleDayCalculator.calculateBcdForAlignment(new HashMap<UUID, Integer>(), subscription, subscription, BillingAlignment.BUNDLE, account.getTimeZone(), 0);
 
         Assert.assertEquals(billCycleDayLocal, (Integer) expectedBCDUTC);
     }
@@ -122,7 +125,7 @@ public class TestBillCycleDayCalculator extends UtilTestSuiteNoDB {
         final ImmutableAccountData account = Mockito.mock(ImmutableAccountData.class);
         Mockito.when(account.getTimeZone()).thenReturn(accountTimeZone);
 
-        final Integer bcd = BillCycleDayCalculator.calculateBcdForAlignment(subscription, subscription, BillingAlignment.SUBSCRIPTION, account.getTimeZone(), 0);
+        final Integer bcd = BillCycleDayCalculator.calculateBcdForAlignment(new HashMap<UUID, Integer>(), subscription, subscription, BillingAlignment.SUBSCRIPTION, account.getTimeZone(), 0);
         Assert.assertEquals(bcd, (Integer) bcdLocal);
     }
 }