killbill-aplcache
Merge remote-tracking branch 'origin/master' into work-for-release-0.17.x Signed-off-by: …
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java 6(+2 -4)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java 15(+10 -5)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java 52(+51 -1)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java 13(+9 -4)
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);
}
}