Details
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
index fd03318..fa7cc6c 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
@@ -32,6 +32,7 @@ import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
import org.killbill.billing.beatrix.util.PaymentChecker.ExpectedPaymentCheck;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.PriceListSet;
@@ -609,6 +610,106 @@ public class TestIntegration extends TestIntegrationBase {
checkNoMoreInvoiceToGenerate(account);
}
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/715")
+ public void testWithPauseResumeAfterENT_CANCELLEDBlockingState() throws Exception {
+ final DateTime initialDate = new DateTime(2012, 2, 1, 0, 3, 42, 0, testTimeZone);
+ final int billingDay = 2;
+ // set clock to the initial start date
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+ final UUID accountId = account.getId();
+ assertNotNull(account);
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE, NextEvent.BLOCK NextEvent.INVOICE
+ //
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ assertNotNull(baseEntitlement);
+
+ //
+ // VERIFY CTD HAS BEEN SET
+ //
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) baseEntitlement.getSubscriptionBase();
+ final DateTime startDate = subscription.getCurrentPhaseStart();
+ final BigDecimal rate = subscription.getCurrentPhase().getFixed().getPrice().getPrice(Currency.USD);
+ verifyTestResult(accountId, subscription.getId(), startDate, null, rate, clock.getUTCNow(), 1);
+
+ //
+ // MOVE TIME TO AFTER TRIAL (2012-03-04)
+ //
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addDeltaFromReality(AT_LEAST_ONE_MONTH_MS);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(accountId,
+ 1,
+ callContext,
+ ImmutableList.<ExpectedInvoiceItemCheck>of(new ExpectedInvoiceItemCheck(new LocalDate(2012, 2, 1), null, InvoiceItemType.FIXED, BigDecimal.ZERO)));
+
+ invoiceChecker.checkInvoice(accountId,
+ 2,
+ callContext,
+ ImmutableList.<ExpectedInvoiceItemCheck>of(new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 2), new LocalDate(2012, 4, 2), InvoiceItemType.RECURRING, new BigDecimal("249.95"))));
+
+ // Pause the entitlement between 2012-03-05 and 2012-03-15
+ DefaultEntitlement entitlement = (DefaultEntitlement) entitlementApi.getEntitlementForId(baseEntitlement.getId(), callContext);
+ entitlementApi.pause(entitlement.getBundleId(), new LocalDate(2012, 3, 5), ImmutableList.<PluginProperty>of(), callContext);
+ entitlementApi.resume(entitlement.getBundleId(), new LocalDate(2012, 3, 15), ImmutableList.<PluginProperty>of(), callContext);
+
+ // Advance clock to 2012-03-07
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.INVOICE);
+ clock.addDays(3);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(accountId,
+ 2,
+ callContext,
+ ImmutableList.<ExpectedInvoiceItemCheck>of(new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 2), new LocalDate(2012, 4, 2), InvoiceItemType.RECURRING, new BigDecimal("249.95"))));
+
+ invoiceChecker.checkInvoice(accountId,
+ 3,
+ callContext,
+ ImmutableList.<ExpectedInvoiceItemCheck>of(new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 5), new LocalDate(2012, 4, 2), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-225.76")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 7), new LocalDate(2012, 3, 7), InvoiceItemType.CBA_ADJ, new BigDecimal("225.76"))));
+
+ // Entitlement should be blocked
+ entitlement = (DefaultEntitlement) entitlementApi.getEntitlementForId(baseEntitlement.getId(), callContext);
+ Assert.assertEquals(entitlement.getState(), EntitlementState.BLOCKED);
+
+ // Advance clock to 2012-03-12, nothing should happen
+ clock.addDays(5);
+ assertListenerStatus();
+
+ // Entitlement is still blocked
+ entitlement = (DefaultEntitlement) entitlementApi.getEntitlementForId(baseEntitlement.getId(), callContext);
+ Assert.assertEquals(entitlement.getState(), EntitlementState.BLOCKED);
+
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(accountId, false, callContext);
+ assertEquals(invoices.size(), 3);
+
+ // Cancel entitlement start of term but with billing policy immediate (ENT_BLOCKED must be after ENT_CANCELLED to trigger the bug)
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.NULL_INVOICE);
+ baseEntitlement.cancelEntitlementWithDateOverrideBillingPolicy(new LocalDate(2012, 3, 2), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // 2012-03-16
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.NULL_INVOICE);
+ clock.addDays(4);
+ assertListenerStatus();
+
+ busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE);
+ clock.addMonths(3);
+ assertListenerStatus();
+
+ // No new invoices
+ invoices = invoiceUserApi.getInvoicesByAccount(accountId, false, callContext);
+ assertEquals(invoices.size(), 3);
+ }
+
@Test(groups = "slow")
public void testForMultipleRecurringPhases() throws Exception {
final DateTime initialCreationDate = new DateTime(2012, 2, 1, 0, 3, 42, 0, testTimeZone);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
index 230fdd1..d2ce6d3 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
@@ -24,13 +26,12 @@ import java.util.UUID;
import javax.annotation.Nullable;
import javax.inject.Inject;
-import org.killbill.billing.invoice.api.InvoiceApiException;
-import org.killbill.billing.invoice.api.InvoiceItem;
-import org.killbill.billing.invoice.api.InvoiceItemType;
-import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.entity.EntityPersistenceException;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceStatus;
+import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
import com.google.common.base.Predicate;
@@ -46,41 +47,35 @@ public class CBADao {
this.invoiceDaoHelper = invoiceDaoHelper;
}
-
- public BigDecimal getAccountCBAFromTransaction(final UUID accountId,
- final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
- final InternalTenantContext context) {
- final List<InvoiceModelDao> invoices = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
- return getAccountCBAFromTransaction(invoices);
- }
-
- public BigDecimal getAccountCBAFromTransaction(final List<InvoiceModelDao> invoices) {
- BigDecimal cba = BigDecimal.ZERO;
- for (final InvoiceModelDao cur : invoices) {
- cba = cba.add(InvoiceModelDaoHelper.getCBAAmount(cur));
- }
- return cba;
+ // PERF: Compute the CBA directly in the database (faster than re-constructing all invoices)
+ public BigDecimal getAccountCBAFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
+ final InvoiceItemSqlDao invoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
+ return invoiceItemSqlDao.getAccountCBA(context);
}
// We expect a clean up to date invoice, with all the items except the cba, that we will compute in that method
- public InvoiceItemModelDao computeCBAComplexity(final InvoiceModelDao invoice, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
-
+ public InvoiceItemModelDao computeCBAComplexity(final InvoiceModelDao invoice,
+ @Nullable final BigDecimal accountCBAOrNull,
+ @Nullable final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
final BigDecimal balance = getInvoiceBalance(invoice);
- // Current balance is negative, we need to generate a credit (positive CBA amount).
if (balance.compareTo(BigDecimal.ZERO) < 0) {
- return new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), balance.negate(), invoice.getCurrency()));
-
- // Current balance is positive, we need to use some of the existing if available (negative CBA amount)
- } else if (balance.compareTo(BigDecimal.ZERO) > 0) {
+ // Current balance is negative, we need to generate a credit (positive CBA amount)
+ return buildCBAItem(invoice, balance, context);
+ } else if (balance.compareTo(BigDecimal.ZERO) > 0 && invoice.getStatus() == InvoiceStatus.COMMITTED) {
+ // Current balance is positive and the invoice is COMMITTED, we need to use some of the existing if available (negative CBA amount)
+ // PERF: in some codepaths, the CBA maybe have already been computed
+ BigDecimal accountCBA = accountCBAOrNull;
+ if (accountCBAOrNull == null) {
+ accountCBA = getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
+ }
- final List<InvoiceModelDao> allInvoices = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
- final BigDecimal accountCBA = getAccountCBAFromTransaction(allInvoices);
if (accountCBA.compareTo(BigDecimal.ZERO) <= 0) {
return null;
}
final BigDecimal positiveCreditAmount = accountCBA.compareTo(balance) > 0 ? balance : accountCBA;
- return new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), positiveCreditAmount.negate(), invoice.getCurrency()));
+ return buildCBAItem(invoice, positiveCreditAmount, context);
} else {
// 0 balance, nothing to do.
return null;
@@ -113,64 +108,49 @@ public class CBADao {
}
// We let the code below rehydrate the invoice before we can add the CBA item
- public void addCBAComplexityFromTransaction(final UUID invoiceId, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
-
+ // PERF: when possible, prefer the method below to avoid re-fetching the invoice
+ public void doCBAComplexityFromTransaction(final UUID invoiceId,
+ final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
final InvoiceSqlDao transInvoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final InvoiceModelDao invoice = transInvoiceDao.getById(invoiceId.toString(), context);
invoiceDaoHelper.populateChildren(invoice, entitySqlDaoWrapperFactory, context);
- addCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
+
+ doCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
}
- // We expect a clean up to date invoice, with all the items except the CBA, that we will compute in that method
- public void addCBAComplexityFromTransaction(final InvoiceModelDao invoice, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
- final InvoiceItemModelDao cbaItem = computeCBAComplexity(invoice, entitySqlDaoWrapperFactory, context);
- if (cbaItem != null) {
- final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
- transInvoiceItemDao.create(cbaItem, context);
- }
- List<InvoiceModelDao> invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
- useExistingCBAFromTransaction(invoiceItemModelDaos, entitySqlDaoWrapperFactory, context);
+ public void doCBAComplexityFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+ doCBAComplexityFromTransaction((InvoiceModelDao) null, entitySqlDaoWrapperFactory, context);
}
- public void addCBAComplexityFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+ // Note! We expect an *up-to-date* invoice, with all the items and payments except the CBA, that we will compute in that method
+ public void doCBAComplexityFromTransaction(@Nullable final InvoiceModelDao invoice,
+ final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+ // PERF: It is expensive to retrieve and construct all invoice objects. To check if there is effectively something to use, compute the CBA by the database first
+ BigDecimal remainingAccountCBA = getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
- List<InvoiceModelDao> invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
- for (InvoiceModelDao cur : invoiceItemModelDaos) {
- addCBAIfNeeded(entitySqlDaoWrapperFactory, cur, context);
+ if (invoice != null) {
+ // Generate or use CBA for that specific invoice
+ remainingAccountCBA = computeCBAComplexityAndCreateCBAItem(remainingAccountCBA, invoice, entitySqlDaoWrapperFactory, context);
}
- invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
- useExistingCBAFromTransaction(invoiceItemModelDaos, entitySqlDaoWrapperFactory, context);
- }
- /**
- * Adjust the invoice with a CBA item if the new invoice balance is negative.
- *
- * @param entitySqlDaoWrapperFactory the EntitySqlDaoWrapperFactory from the current transaction
- * @param invoice the invoice to adjust
- * @param context the call callcontext
- */
- private void addCBAIfNeeded(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
- final InvoiceModelDao invoice,
- final InternalCallContext context) throws EntityPersistenceException {
-
- // If invoice balance becomes negative we add some CBA item
- final BigDecimal balance = InvoiceModelDaoHelper.getBalance(invoice);
- if (balance.compareTo(BigDecimal.ZERO) < 0) {
- final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
- final InvoiceItemModelDao cbaAdjItem = new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), balance.negate(), invoice.getCurrency()));
- transInvoiceItemDao.create(cbaAdjItem, context);
- }
+ useExistingCBAFromTransaction(remainingAccountCBA, entitySqlDaoWrapperFactory, context);
}
-
- private void useExistingCBAFromTransaction(final List<InvoiceModelDao> invoices, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws InvoiceApiException, EntityPersistenceException {
-
- final BigDecimal accountCBA = getAccountCBAFromTransaction(invoices);
+ // Distribute account CBA across all COMMITTED unpaid invoices
+ private void useExistingCBAFromTransaction(final BigDecimal accountCBA,
+ final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final InternalCallContext context) throws InvoiceApiException, EntityPersistenceException {
if (accountCBA.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
- final List<InvoiceModelDao> unpaidInvoices = invoiceDaoHelper.getUnpaidInvoicesByAccountFromTransaction(invoices, null);
+ // PERF: Computing the invoice balance is difficult to do in the DB, so we effectively need to retrieve all invoices on the account and filter the unpaid ones in memory.
+ // This should be infrequent though because of the account CBA check above.
+ final List<InvoiceModelDao> allInvoices = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
+ final List<InvoiceModelDao> unpaidInvoices = invoiceDaoHelper.getUnpaidInvoicesByAccountFromTransaction(allInvoices, null);
// We order the same os BillingStateCalculator-- should really share the comparator
final List<InvoiceModelDao> orderedUnpaidInvoices = Ordering.from(new Comparator<InvoiceModelDao>() {
@Override
@@ -180,20 +160,46 @@ public class CBADao {
}).immutableSortedCopy(unpaidInvoices);
BigDecimal remainingAccountCBA = accountCBA;
- for (InvoiceModelDao cur : orderedUnpaidInvoices) {
- final BigDecimal curInvoiceBalance = InvoiceModelDaoHelper.getBalance(cur);
- final BigDecimal cbaToApplyOnInvoice = remainingAccountCBA.compareTo(curInvoiceBalance) <= 0 ? remainingAccountCBA : curInvoiceBalance;
- remainingAccountCBA = remainingAccountCBA.subtract(cbaToApplyOnInvoice);
-
- final InvoiceItemModelDao cbaAdjItem = new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(cur.getId(), cur.getAccountId(), context.getCreatedDate().toLocalDate(), cbaToApplyOnInvoice.negate(), cur.getCurrency()));
-
- final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
- transInvoiceItemDao.create(cbaAdjItem, context);
-
+ for (final InvoiceModelDao unpaidInvoice : orderedUnpaidInvoices) {
+ remainingAccountCBA = computeCBAComplexityAndCreateCBAItem(remainingAccountCBA, unpaidInvoice, entitySqlDaoWrapperFactory, context);
if (remainingAccountCBA.compareTo(BigDecimal.ZERO) <= 0) {
break;
}
}
}
+ // Return the updated account CBA
+ private BigDecimal computeCBAComplexityAndCreateCBAItem(final BigDecimal accountCBA,
+ final InvoiceModelDao invoice,
+ final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+ final InvoiceItemModelDao cbaItem = computeCBAComplexity(invoice, accountCBA, entitySqlDaoWrapperFactory, context);
+ if (cbaItem != null) {
+ createCBAItem(invoice, cbaItem, entitySqlDaoWrapperFactory, context);
+ return accountCBA.add(cbaItem.getAmount());
+ } else {
+ return accountCBA;
+ }
+ }
+
+ private void createCBAItem(final InvoiceModelDao invoiceModelDao,
+ final InvoiceItemModelDao cbaItem,
+ final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final InternalCallContext context) throws EntityPersistenceException {
+ final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
+ transInvoiceItemDao.create(cbaItem, context);
+
+ // Refresh the in-memory item
+ invoiceModelDao.addInvoiceItem(cbaItem);
+ }
+
+ private InvoiceItemModelDao buildCBAItem(final InvoiceModelDao invoice,
+ final BigDecimal amount,
+ final InternalCallContext context) {
+ return new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(),
+ invoice.getAccountId(),
+ context.getCreatedDate().toLocalDate(),
+ amount.negate(),
+ invoice.getCurrency()));
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index e20bef5..1de777b 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -20,6 +20,7 @@ package org.killbill.billing.invoice.dao;
import java.math.BigDecimal;
import java.util.Collection;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
@@ -290,6 +291,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
final UUID accountId = invoices.iterator().next().getAccountId();
+ final Map<UUID, InvoiceModelDao> invoiceByInvoiceId = new HashMap<UUID, InvoiceModelDao>();
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoiceItemModelDao>>() {
@Override
public List<InvoiceItemModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
@@ -298,6 +300,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
final List<InvoiceItemModelDao> createdInvoiceItems = new LinkedList<InvoiceItemModelDao>();
for (final InvoiceModelDao invoiceModelDao : invoices) {
+ invoiceByInvoiceId.put(invoiceModelDao.getId(), invoiceModelDao);
final boolean isRealInvoice = uniqueInvoiceIds.remove(invoiceModelDao.getId());
// Create the invoice if needed
@@ -336,9 +339,17 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
for (final UUID adjustedInvoiceId : adjustedInvoiceIds) {
- cbaDao.addCBAComplexityFromTransaction(adjustedInvoiceId, entitySqlDaoWrapperFactory, context);
+ final boolean newInvoice = createdInvoiceIds.contains(adjustedInvoiceId);
+ if (newInvoice) {
+ // New invoice, so no associated payment yet: no need to refresh the invoice state
+ cbaDao.doCBAComplexityFromTransaction(invoiceByInvoiceId.get(adjustedInvoiceId), entitySqlDaoWrapperFactory, context);
+ } else {
+ // Existing invoice (e.g. we're processing an adjustment): refresh the invoice state to get the correct balance
+ // Should we maybe enforce callers (e.g. InvoiceApiHelper) to properly populate these invoices?
+ cbaDao.doCBAComplexityFromTransaction(adjustedInvoiceId, entitySqlDaoWrapperFactory, context);
+ }
- if (committedInvoiceIds.contains(adjustedInvoiceId) && !createdInvoiceIds.contains(adjustedInvoiceId)) {
+ if (committedInvoiceIds.contains(adjustedInvoiceId) && !newInvoice) {
// Notify the bus since the balance of the invoice changed (only if the invoice is COMMITTED)
notifyBusOfInvoiceAdjustment(entitySqlDaoWrapperFactory, adjustedInvoiceId, accountId, context.getUserToken(), context);
}
@@ -420,7 +431,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<BigDecimal>() {
@Override
public BigDecimal inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
- return cbaDao.getAccountCBAFromTransaction(accountId, entitySqlDaoWrapperFactory, context);
+ return cbaDao.getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
}
});
}
@@ -539,7 +550,8 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
}
- cbaDao.addCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
+ // The invoice object has been kept up-to-date
+ cbaDao.doCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
if (isInvoiceAdjusted) {
notifyBusOfInvoiceAdjustment(entitySqlDaoWrapperFactory, invoice.getId(), invoice.getAccountId(), context.getUserToken(), context);
@@ -595,7 +607,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
// Notify the bus since the balance of the invoice changed
final UUID accountId = transactional.getAccountIdFromInvoicePaymentId(chargeBack.getId().toString(), context);
- cbaDao.addCBAComplexityFromTransaction(payment.getInvoiceId(), entitySqlDaoWrapperFactory, context);
+ cbaDao.doCBAComplexityFromTransaction(payment.getInvoiceId(), entitySqlDaoWrapperFactory, context);
notifyBusOfInvoicePayment(entitySqlDaoWrapperFactory, chargeBack, accountId, context.getUserToken(), context);
@@ -631,7 +643,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
// Notify the bus since the balance of the invoice changed
final UUID accountId = transactional.getAccountIdFromInvoicePaymentId(chargebackReversed.getId().toString(), context);
- cbaDao.addCBAComplexityFromTransaction(chargebackReversed.getInvoiceId(), entitySqlDaoWrapperFactory, context);
+ cbaDao.doCBAComplexityFromTransaction(chargebackReversed.getInvoiceId(), entitySqlDaoWrapperFactory, context);
notifyBusOfInvoicePayment(entitySqlDaoWrapperFactory, chargebackReversed, accountId, context.getUserToken(), context);
@@ -645,7 +657,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoiceItemModelDao>() {
@Override
public InvoiceItemModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
- final InvoiceItemModelDao cbaNewItem = cbaDao.computeCBAComplexity(invoice, entitySqlDaoWrapperFactory, context);
+ final InvoiceItemModelDao cbaNewItem = cbaDao.computeCBAComplexity(invoice, null, entitySqlDaoWrapperFactory, context);
return cbaNewItem;
}
});
@@ -845,7 +857,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
// If there is more account credit than CBA we adjusted, we're done.
// Otherwise, we need to find further invoices on which this credit was consumed
- final BigDecimal accountCBA = cbaDao.getAccountCBAFromTransaction(accountId, entitySqlDaoWrapperFactory, context);
+ final BigDecimal accountCBA = cbaDao.getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
if (accountCBA.compareTo(BigDecimal.ZERO) < 0) {
if (accountCBA.compareTo(cbaItem.getAmount().negate()) < 0) {
throw new IllegalStateException("The account balance can't be lower than the amount adjusted");
@@ -904,12 +916,12 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
});
}
+ @Override
public void consumeExstingCBAOnAccountWithUnpaidInvoices(final UUID accountId, final InternalCallContext context) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
- // In theory we should only have to call useExistingCBAFromTransaction but just to be safe we also check for credit generation
- cbaDao.addCBAComplexityFromTransaction(entitySqlDaoWrapperFactory, context);
+ cbaDao.doCBAComplexityFromTransaction(entitySqlDaoWrapperFactory, context);
return null;
}
});
@@ -1026,6 +1038,8 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
transactional.updateStatus(invoiceId.toString(), newStatus.toString(), context);
+ cbaDao.doCBAComplexityFromTransaction(entitySqlDaoWrapperFactory, context);
+
if (InvoiceStatus.COMMITTED.equals(newStatus)) {
// notify invoice creation event
notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, invoice, context);
@@ -1173,19 +1187,25 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
// save invoices and invoice items
- InvoiceModelDao childInvoice = new InvoiceModelDao(invoiceForExternalCharge);
+ final InvoiceModelDao childInvoice = new InvoiceModelDao(invoiceForExternalCharge);
invoiceSqlDao.create(childInvoice, childAccountContext);
- createInvoiceItemFromTransaction(transInvoiceItemSqlDao, new InvoiceItemModelDao(externalChargeItem), childAccountContext);
+ final InvoiceItemModelDao childExternalChargeItem = new InvoiceItemModelDao(externalChargeItem);
+ createInvoiceItemFromTransaction(transInvoiceItemSqlDao, childExternalChargeItem, childAccountContext);
+ // Keep invoice up-to-date for CBA below
+ childInvoice.addInvoiceItem(childExternalChargeItem);
- InvoiceModelDao parentInvoice = new InvoiceModelDao(invoiceForCredit);
+ final InvoiceModelDao parentInvoice = new InvoiceModelDao(invoiceForCredit);
invoiceSqlDao.create(parentInvoice, parentAccountContext);
- createInvoiceItemFromTransaction(transInvoiceItemSqlDao, new InvoiceItemModelDao(creditItem), parentAccountContext);
+ final InvoiceItemModelDao parentCreditItem = new InvoiceItemModelDao(creditItem);
+ createInvoiceItemFromTransaction(transInvoiceItemSqlDao, parentCreditItem, parentAccountContext);
+ // Keep invoice up-to-date for CBA below
+ parentInvoice.addInvoiceItem(parentCreditItem);
// add CBA complexity and notify bus on child invoice creation
- cbaDao.addCBAComplexityFromTransaction(childInvoice.getId(), entitySqlDaoWrapperFactory, childAccountContext);
+ cbaDao.doCBAComplexityFromTransaction(childInvoice, entitySqlDaoWrapperFactory, childAccountContext);
notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, childInvoice, childAccountContext);
- cbaDao.addCBAComplexityFromTransaction(parentInvoice.getId(), entitySqlDaoWrapperFactory, parentAccountContext);
+ cbaDao.doCBAComplexityFromTransaction(parentInvoice, entitySqlDaoWrapperFactory, parentAccountContext);
notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, parentInvoice, parentAccountContext);
return null;
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java
index 8805d52..e814a69 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java
@@ -183,6 +183,10 @@ public class InvoiceItemModelDao extends EntityModelDaoBase implements EntityMod
this.subscriptionId = subscriptionId;
}
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
public void setPlanName(final String planName) {
this.planName = planName;
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java
index e30b5d2..8c7ded7 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
@@ -20,16 +22,15 @@ import java.math.BigDecimal;
import java.util.List;
import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.util.audit.ChangeType;
import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.BindBean;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
-
-import org.killbill.billing.invoice.api.InvoiceItem;
-import org.killbill.billing.callcontext.InternalTenantContext;
-import org.killbill.billing.util.entity.dao.EntitySqlDao;
-import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
@EntitySqlDaoStringTemplate
@@ -57,4 +58,7 @@ public interface InvoiceItemSqlDao extends EntitySqlDao<InvoiceItemModelDao, Inv
@SqlQuery
List<InvoiceItemModelDao> getInvoiceItemsByParentInvoice(@Bind("parentInvoiceId") final String parentInvoiceId,
@BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ BigDecimal getAccountCBA(@BindBean final InternalTenantContext context);
}
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
index ae19a63..c99d95c 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
@@ -85,4 +85,17 @@ getInvoiceItemsByParentInvoice() ::= <<
<AND_CHECK_TENANT("items.")>
<defaultOrderBy()>
;
->>
\ No newline at end of file
+>>
+
+getAccountCBA() ::= <<
+select coalesce(sum(ii.amount), 0) cba
+from invoice_items ii
+join invoices i on i.id = ii.invoice_id
+where i.status = 'COMMITTED'
+and ii.type = 'CBA_ADJ'
+and <accountRecordIdField("i.")> = :accountRecordId
+and <accountRecordIdField("ii.")> = :accountRecordId
+<AND_CHECK_TENANT("i.")>
+<AND_CHECK_TENANT("ii.")>
+;
+>>
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
index 44d6178..1cb8bbf 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
@@ -813,7 +813,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final UUID accountId = account.getId();
final UUID bundleId = UUID.randomUUID();
- createCredit(accountId, clock.getUTCToday(), new BigDecimal("20.0"));
+ final InvoiceItemModelDao credit = createCredit(accountId, clock.getUTCToday(), new BigDecimal("20.0"), true);
final String description = UUID.randomUUID().toString();
final InvoiceModelDao invoiceForExternalCharge = new InvoiceModelDao(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD, false);
@@ -821,8 +821,55 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
invoiceForExternalCharge.addInvoiceItem(externalCharge);
final InvoiceItemModelDao charge = invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceForExternalCharge), context).get(0);
- final InvoiceModelDao newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
- final List<InvoiceItemModelDao> items = newInvoice.getInvoiceItems();
+ InvoiceModelDao newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
+ List<InvoiceItemModelDao> items = newInvoice.getInvoiceItems();
+ // No CBA consumed yet since the credit was created on a DRAFT invoice
+ assertEquals(items.size(), 1);
+ assertEquals(items.get(0).getType(), InvoiceItemType.EXTERNAL_CHARGE);
+ assertEquals(items.get(0).getDescription(), description);
+
+ invoiceDao.changeInvoiceStatus(credit.getInvoiceId(), InvoiceStatus.COMMITTED, context);
+
+ // CBA should have been consumed
+ newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
+ items = newInvoice.getInvoiceItems();
+ assertEquals(items.size(), 2);
+ for (final InvoiceItemModelDao cur : items) {
+ if (cur.getId().equals(charge.getId())) {
+ assertEquals(cur.getType(), InvoiceItemType.EXTERNAL_CHARGE);
+ assertEquals(cur.getDescription(), description);
+ } else {
+ assertEquals(cur.getType(), InvoiceItemType.CBA_ADJ);
+ assertTrue(cur.getAmount().compareTo(new BigDecimal("-15.00")) == 0);
+ }
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testExternalChargeOnDRAFTInvoiceWithCBA() throws InvoiceApiException, EntityPersistenceException {
+ final UUID accountId = account.getId();
+ final UUID bundleId = UUID.randomUUID();
+
+ final InvoiceItemModelDao credit = createCredit(accountId, clock.getUTCToday(), new BigDecimal("20.0"), false);
+
+ final String description = UUID.randomUUID().toString();
+ final InvoiceModelDao draftInvoiceForExternalCharge = new InvoiceModelDao(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD, false, InvoiceStatus.DRAFT);
+ final InvoiceItemModelDao externalCharge = new InvoiceItemModelDao(new ExternalChargeInvoiceItem(draftInvoiceForExternalCharge.getId(), accountId, bundleId, description, clock.getUTCToday(), new BigDecimal("15.0"), Currency.USD));
+ draftInvoiceForExternalCharge.addInvoiceItem(externalCharge);
+ final InvoiceItemModelDao charge = invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(draftInvoiceForExternalCharge), context).get(0);
+
+ InvoiceModelDao newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
+ List<InvoiceItemModelDao> items = newInvoice.getInvoiceItems();
+ // No CBA consumed yet since the charge was created on a DRAFT invoice
+ assertEquals(items.size(), 1);
+ assertEquals(items.get(0).getType(), InvoiceItemType.EXTERNAL_CHARGE);
+ assertEquals(items.get(0).getDescription(), description);
+
+ invoiceDao.changeInvoiceStatus(charge.getInvoiceId(), InvoiceStatus.COMMITTED, context);
+
+ // CBA should have been consumed
+ newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
+ items = newInvoice.getInvoiceItems();
assertEquals(items.size(), 2);
for (final InvoiceItemModelDao cur : items) {
if (cur.getId().equals(charge.getId())) {
@@ -924,13 +971,13 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
- public void testAccountCredit() {
+ public void testAccountCredit() throws InvoiceApiException {
final UUID accountId = account.getId();
final LocalDate effectiveDate = new LocalDate(2011, 3, 1);
final BigDecimal creditAmount = new BigDecimal("5.0");
- createCredit(accountId, effectiveDate, creditAmount);
+ createCredit(accountId, effectiveDate, creditAmount, true);
final List<InvoiceModelDao> invoices = invoiceDao.getAllInvoicesByAccount(context);
assertEquals(invoices.size(), 1);
@@ -952,6 +999,12 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
}
assertTrue(foundCredit);
assertTrue(foundCBA);
+
+ // No account CBA yet since the invoice is in DRAFT mode
+ assertEquals(invoiceDao.getAccountCBA(accountId, context).compareTo(BigDecimal.ZERO), 0);
+
+ invoiceDao.changeInvoiceStatus(invoice.getId(), InvoiceStatus.COMMITTED, context);
+ assertEquals(invoiceDao.getAccountCBA(accountId, context).compareTo(creditAmount), 0);
}
@Test(groups = "slow")
@@ -999,7 +1052,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
// Create the credit item
final LocalDate effectiveDate = new LocalDate(2011, 3, 1);
- createCredit(accountId, invoice1.getId(), effectiveDate, creditAmount);
+ createCredit(accountId, invoice1.getId(), effectiveDate, creditAmount, false);
final List<InvoiceModelDao> invoices = invoiceDao.getAllInvoicesByAccount(context);
assertEquals(invoices.size(), 1);
@@ -1114,7 +1167,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
assertEquals(allInvoicesByAccount.size(), 1);
// insert DRAFT invoice
- createCredit(accountId, new LocalDate(2011, 12, 31), BigDecimal.TEN);
+ createCredit(accountId, new LocalDate(2011, 12, 31), BigDecimal.TEN, true);
allInvoicesByAccount = invoiceDao.getInvoicesByAccount(new LocalDate(2011, 1, 1), context);
assertEquals(allInvoicesByAccount.size(), 2);
@@ -1694,14 +1747,14 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
}
- private void createCredit(final UUID accountId, final LocalDate effectiveDate, final BigDecimal creditAmount) {
- createCredit(accountId, null, effectiveDate, creditAmount);
+ private InvoiceItemModelDao createCredit(final UUID accountId, final LocalDate effectiveDate, final BigDecimal creditAmount, final boolean draft) {
+ return createCredit(accountId, null, effectiveDate, creditAmount, draft);
}
- private void createCredit(final UUID accountId, @Nullable final UUID invoiceId, final LocalDate effectiveDate, final BigDecimal creditAmount) {
+ private InvoiceItemModelDao createCredit(final UUID accountId, @Nullable final UUID invoiceId, final LocalDate effectiveDate, final BigDecimal creditAmount, final boolean draft) {
final InvoiceModelDao invoiceModelDao;
if (invoiceId == null) {
- invoiceModelDao = new InvoiceModelDao(accountId, effectiveDate, effectiveDate, Currency.USD, false, InvoiceStatus.DRAFT);
+ invoiceModelDao = new InvoiceModelDao(accountId, effectiveDate, effectiveDate, Currency.USD, false, draft ? InvoiceStatus.DRAFT : InvoiceStatus.COMMITTED);
} else {
invoiceModelDao = invoiceDao.getById(invoiceId, context);
}
@@ -1715,7 +1768,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
creditAmount.negate(),
invoiceModelDao.getCurrency());
invoiceModelDao.addInvoiceItem(new InvoiceItemModelDao(invoiceItem));
- invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceModelDao), context);
+ return invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceModelDao), context).get(0);
}
@Test(groups = "slow")
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java
index a03af67..cf7507e 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java
@@ -131,7 +131,7 @@ public class BlockingCalculator {
}
final List<BlockingState> subscriptionBlockingEvents = perSubscriptionBlockingEvents.get(subscription.getId()) != null ? perSubscriptionBlockingEvents.get(subscription.getId()) : ImmutableList.<BlockingState>of();
- final List<BlockingState> aggregateSubscriptionBlockingEvents = getAggregateBlockingEventsPerSubscription(subscriptionBlockingEvents, bundleBlockingEvents, accountBlockingEvents);
+ final List<BlockingState> aggregateSubscriptionBlockingEvents = getAggregateBlockingEventsPerSubscription(subscription.getEndDate(), subscriptionBlockingEvents, bundleBlockingEvents, accountBlockingEvents);
final List<DisabledDuration> accountBlockingDurations = createBlockingDurations(aggregateSubscriptionBlockingEvents);
billingEventsToAdd.addAll(createNewEvents(accountBlockingDurations, billingEvents, subscription, context));
@@ -150,9 +150,16 @@ public class BlockingCalculator {
return !(billingEventsToAdd.isEmpty() && billingEventsToRemove.isEmpty());
}
- final List<BlockingState> getAggregateBlockingEventsPerSubscription(final Iterable<BlockingState> subscriptionBlockingEvents, final Iterable<BlockingState> bundleBlockingEvents, final Iterable<BlockingState> accountBlockingEvents) {
+ final List<BlockingState> getAggregateBlockingEventsPerSubscription(@Nullable final DateTime subscriptionEndDate, final Iterable<BlockingState> subscriptionBlockingEvents, final Iterable<BlockingState> bundleBlockingEvents, final Iterable<BlockingState> accountBlockingEvents) {
final Iterable<BlockingState> tmp = Iterables.concat(subscriptionBlockingEvents, bundleBlockingEvents, accountBlockingEvents);
- final List<BlockingState> result = Lists.newArrayList(tmp);
+ final Iterable<BlockingState> allEventsPriorToCancelDate = Iterables.filter(tmp,
+ new Predicate<BlockingState>() {
+ @Override
+ public boolean apply(final BlockingState input) {
+ return subscriptionEndDate == null || input.getEffectiveDate().compareTo(subscriptionEndDate) <= 0;
+ }
+ });
+ final List<BlockingState> result = Lists.newArrayList(allEventsPriorToCancelDate);
Collections.sort(result);
return result;
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
index 0fcfb7f..f6cd52a 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -496,7 +496,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
(BigDecimal) row.get("amount"),
Currency.valueOf((String) row.get("currency")),
(String) row.get("gateway_error_code"),
- (String) row.get("gateway_error_msg"));
+ String.valueOf(row.get("gateway_error_msg")));
result.add(transactionModelDao);
}
return result;
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
index d70c150..9ee931b 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
@@ -776,7 +776,7 @@ public class TestInvoice extends TestJaxrsBase {
credit.setCreditAmount(creditAmount);
// insert credit to child account
- final Credit creditJson = killBillClient.createCredit(credit, false, createdBy, reason, comment);
+ final Credit creditJson = killBillClient.createCredit(credit, true, requestOptions);
Invoices childInvoices = killBillClient.getInvoicesForAccount(childAccount.getAccountId(), true, false);
Assert.assertEquals(childInvoices.size(), 1);