diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
index a0d22f9..6931288 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
@@ -315,9 +315,9 @@ public class TestIntegrationParentInvoice extends TestIntegrationBase {
}
- // Scenario 1: Follow up Invoice Item Adjustment on unpaid invoice
+ // Scenario 1.a: Follow up Invoice Item Adjustment on unpaid DRAFT invoice
@Test(groups = "slow")
- public void testParentInvoiceItemAdjustmentUnpaidInvoice() throws Exception {
+ public void testParentInvoiceItemAdjustmentUnpaidDraftInvoice() throws Exception {
final int billingDay = 14;
final DateTime initialCreationDate = new DateTime(2014, 5, 15, 0, 0, 0, 0, testTimeZone);
@@ -385,6 +385,140 @@ public class TestIntegrationParentInvoice extends TestIntegrationBase {
}
+ // Scenario 1.b: Follow up Invoice Item Adjustment on unpaid COMMITTED invoice
+ @Test(groups = "slow")
+ public void testParentInvoiceItemAdjustmentUnpaidCommittedInvoice() throws Exception {
+
+ final int billingDay = 14;
+ final DateTime initialCreationDate = new DateTime(2014, 5, 15, 0, 0, 0, 0, testTimeZone);
+ // set clock to the initial start date
+ clock.setTime(initialCreationDate);
+
+ final Account parentAccount = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+ final Account childAccount = createAccountWithNonOsgiPaymentMethod(getChildAccountData(billingDay, parentAccount.getId(), true));
+
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ createBaseEntitlementAndCheckForCompletion(childAccount.getId(), "bundleKey1", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // ---- trial period ----
+ // Moving a day the NotificationQ calls the commitInvoice. No payment is expected because balance is 0
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addDays(1);
+ assertListenerStatus();
+
+ // ---- recurring period ----
+ // Move through time and verify new parent Invoice. No payments are expected.
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE);
+ clock.addDays(29);
+ assertListenerStatus();
+
+ paymentPlugin.makeNextPaymentFailWithError();
+
+ // move one day to have parent invoice paid
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ clock.addDays(1);
+ assertListenerStatus();
+
+ List<Invoice> parentInvoices = invoiceUserApi.getInvoicesByAccount(parentAccount.getId(), false, callContext);
+ List<Invoice> childInvoices = invoiceUserApi.getInvoicesByAccount(childAccount.getId(), false, callContext);
+ // get last child invoice
+ Invoice childInvoice = childInvoices.get(1);
+ assertEquals(childInvoice.getNumberOfItems(), 1);
+
+ // Second Parent invoice over Recurring period
+ assertEquals(parentInvoices.size(), 2);
+
+ Invoice parentInvoice = parentInvoices.get(1);
+ assertEquals(parentInvoice.getNumberOfItems(), 1);
+ assertEquals(parentInvoice.getStatus(), InvoiceStatus.COMMITTED);
+ assertTrue(parentInvoice.isParentInvoice());
+ assertEquals(parentInvoice.getBalance().compareTo(BigDecimal.valueOf(249.95)), 0);
+
+ // issue a $10 adj when invoice is unpaid
+ insertInvoiceItemAdjustmentToChildInvoice(childAccount, childInvoice, BigDecimal.TEN);
+ // make sure there is time difference between item adjustments.
+ // Otherwise they are created with same id and createdDate and it's used to sort them.
+ clock.addDeltaFromReality(1000);
+
+ // issue a $5 adj when invoice is unpaid
+ insertInvoiceItemAdjustmentToChildInvoice(childAccount, childInvoice, BigDecimal.valueOf(5));
+ clock.addDeltaFromReality(1000);
+
+ // issue a $10 adj when invoice is unpaid
+ insertInvoiceItemAdjustmentToChildInvoice(childAccount, childInvoice, BigDecimal.TEN);
+
+ // move one day
+ busHandler.pushExpectedEvents();
+ clock.addDays(1);
+ assertListenerStatus();
+
+ // issue a $5 adj when invoice is unpaid
+ insertInvoiceItemAdjustmentToChildInvoice(childAccount, childInvoice, BigDecimal.valueOf(5));
+ clock.addDeltaFromReality(1000);
+
+ // issue a $10 adj when invoice is unpaid
+ insertInvoiceItemAdjustmentToChildInvoice(childAccount, childInvoice, BigDecimal.TEN);
+
+ // expected child invoice
+ // RECURRING : $ 249.95
+ // ITEM_ADJ : $ -10
+ // ITEM_ADJ : $ -5
+ // ITEM_ADJ : $ -10
+ // ITEM_ADJ : $ -5
+ // ITEM_ADJ : $ -10
+
+ // expected parent invoice
+ // PARENT_SUMMARY : $ 249.95
+ // ITEM_ADJ : $ -10
+ // ITEM_ADJ : $ -5
+ // ITEM_ADJ : $ -10
+ // ITEM_ADJ : $ -5
+ // ITEM_ADJ : $ -10
+
+ childInvoice = invoiceUserApi.getInvoice(childInvoice.getId(), callContext);
+ assertEquals(childInvoice.getNumberOfItems(), 6);
+ assertEquals(childInvoice.getBalance().compareTo(BigDecimal.valueOf(209.95)), 0);
+ assertEquals(childInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(childInvoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(childInvoice.getInvoiceItems().get(1).getAmount().compareTo(BigDecimal.valueOf(-10)), 0);
+ assertEquals(childInvoice.getInvoiceItems().get(2).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(childInvoice.getInvoiceItems().get(2).getAmount().compareTo(BigDecimal.valueOf(-5)), 0);
+ assertEquals(childInvoice.getInvoiceItems().get(3).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(childInvoice.getInvoiceItems().get(3).getAmount().compareTo(BigDecimal.valueOf(-10)), 0);
+ assertEquals(childInvoice.getInvoiceItems().get(4).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(childInvoice.getInvoiceItems().get(4).getAmount().compareTo(BigDecimal.valueOf(-5)), 0);
+ assertEquals(childInvoice.getInvoiceItems().get(5).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(childInvoice.getInvoiceItems().get(5).getAmount().compareTo(BigDecimal.valueOf(-10)), 0);
+
+ // reload parent invoice
+ parentInvoice = invoiceUserApi.getInvoice(parentInvoice.getId(), callContext);
+ assertEquals(parentInvoice.getNumberOfItems(), 6);
+ assertEquals(parentInvoice.getStatus(), InvoiceStatus.COMMITTED);
+ assertTrue(parentInvoice.isParentInvoice());
+ assertEquals(parentInvoice.getBalance().compareTo(BigDecimal.valueOf(209.95)), 0);
+ assertEquals(parentInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.PARENT_SUMMARY);
+ assertEquals(parentInvoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(parentInvoice.getInvoiceItems().get(1).getAmount().compareTo(BigDecimal.valueOf(-10)), 0);
+ assertEquals(parentInvoice.getInvoiceItems().get(2).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(parentInvoice.getInvoiceItems().get(2).getAmount().compareTo(BigDecimal.valueOf(-5)), 0);
+ assertEquals(parentInvoice.getInvoiceItems().get(3).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(parentInvoice.getInvoiceItems().get(3).getAmount().compareTo(BigDecimal.valueOf(-10)), 0);
+ assertEquals(parentInvoice.getInvoiceItems().get(4).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(parentInvoice.getInvoiceItems().get(4).getAmount().compareTo(BigDecimal.valueOf(-5)), 0);
+ assertEquals(parentInvoice.getInvoiceItems().get(5).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ assertEquals(parentInvoice.getInvoiceItems().get(5).getAmount().compareTo(BigDecimal.valueOf(-10)), 0);
+
+ }
+
+ private void insertInvoiceItemAdjustmentToChildInvoice(final Account childAccount, final Invoice childInvoice, BigDecimal amount) throws InvoiceApiException {
+ busHandler.pushExpectedEvents(NextEvent.INVOICE_ADJUSTMENT, NextEvent.INVOICE_ADJUSTMENT);
+ invoiceUserApi.insertInvoiceItemAdjustment(childAccount.getId(), childInvoice.getId(),
+ childInvoice.getInvoiceItems().get(0).getId(),
+ clock.getToday(childAccount.getTimeZone()), amount,
+ childAccount.getCurrency(), "test adjustment", callContext);
+ assertListenerStatus();
+ }
+
// Scenario 2: Follow up Invoice Item Adjustment on PAID invoice
@Test(groups = "slow")
public void testParentInvoiceItemAdjustmentPaidInvoice() throws Exception {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index dff9142..a4b7d54 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -21,6 +21,8 @@ package org.killbill.billing.invoice;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -70,6 +72,7 @@ import org.killbill.billing.invoice.calculator.InvoiceCalculatorUtils;
import org.killbill.billing.invoice.dao.InvoiceDao;
import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
import org.killbill.billing.invoice.dao.InvoiceModelDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDaoHelper;
import org.killbill.billing.invoice.dao.InvoiceParentChildModelDao;
import org.killbill.billing.invoice.generator.InvoiceGenerator;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata;
@@ -78,6 +81,7 @@ import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFu
import org.killbill.billing.invoice.model.DefaultInvoice;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.InvoiceItemFactory;
+import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
import org.killbill.billing.invoice.model.ParentInvoiceItem;
import org.killbill.billing.invoice.model.RecurringInvoiceItem;
import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier;
@@ -88,6 +92,7 @@ import org.killbill.billing.junction.BillingInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
@@ -777,41 +782,63 @@ public class InvoiceDispatcher {
public void processParentInvoiceForAdjustments(final ImmutableAccountData account, final UUID childInvoiceId, final InternalCallContext context) throws InvoiceApiException {
final InvoiceModelDao childInvoiceModelDao = invoiceDao.getById(childInvoiceId, context);
- final Invoice childInvoice = new DefaultInvoice(childInvoiceModelDao);
- final InvoiceModelDao parentInvoice = childInvoiceModelDao.getParentInvoice();
+ final InvoiceModelDao parentInvoiceModelDao = childInvoiceModelDao.getParentInvoice();
- if (parentInvoice == null) {
+ if (parentInvoiceModelDao == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_MISSING_PARENT_INVOICE, childInvoiceModelDao.getId());
- } else if (parentInvoice.getStatus().equals(InvoiceStatus.COMMITTED)) {
- // ignore parent invoice adjustment if it's in COMMITTED status.
+ } else if (InvoiceModelDaoHelper.getBalance(parentInvoiceModelDao).compareTo(BigDecimal.ZERO) == 0) {
+ // ignore item adjustments for paid invoices.
return;
}
- final InvoiceItemModelDao childInvoiceItemAdjustment = Iterables.find(childInvoiceModelDao.getInvoiceItems(), new Predicate<InvoiceItemModelDao>() {
+ final Long parentAccountRecordId = internalCallContextFactory.getRecordIdFromObject(account.getParentAccountId(), ObjectType.ACCOUNT, buildTenantContext(context));
+ final InternalCallContext parentContext = internalCallContextFactory.createInternalCallContext(parentAccountRecordId, context);
+ final String description = "Adjustment for account ".concat(account.getExternalKey());
+
+ // find PARENT_SUMMARY invoice item for this child account
+ final InvoiceItemModelDao parentSummaryInvoiceItem = Iterables.find(parentInvoiceModelDao.getInvoiceItems(), new Predicate<InvoiceItemModelDao>() {
@Override
public boolean apply(@Nullable final InvoiceItemModelDao input) {
- return input.getType().equals(InvoiceItemType.ITEM_ADJ) || input.getType().equals(InvoiceItemType.REPAIR_ADJ);
+ return input.getType().equals(InvoiceItemType.PARENT_SUMMARY)
+ && input.getChildAccountId().equals(childInvoiceModelDao.getAccountId());
}
});
- if (childInvoiceItemAdjustment == null) return;
-
- final BigDecimal childInvoiceAdjustmentAmount = childInvoiceItemAdjustment.getAmount();
-
- final Long parentAccountRecordId = internalCallContextFactory.getRecordIdFromObject(account.getParentAccountId(), ObjectType.ACCOUNT, buildTenantContext(context));
- final InternalCallContext parentContext = internalCallContextFactory.createInternalCallContext(parentAccountRecordId, context);
- final DateTime today = clock.getNow(account.getTimeZone());
- final String description = "Adjustment for account ".concat(account.getExternalKey());
- final InvoiceItemModelDao parentInvoiceItemForChild = Iterables.find(parentInvoice.getInvoiceItems(), new Predicate<InvoiceItemModelDao>() {
+ final Iterable<InvoiceItemModelDao> childAdjustments = Iterables.filter(childInvoiceModelDao.getInvoiceItems(), new Predicate<InvoiceItemModelDao>() {
@Override
public boolean apply(@Nullable final InvoiceItemModelDao input) {
- return childInvoice.getAccountId().equals(input.getChildAccountId());
+ return input.getType().equals(InvoiceItemType.ITEM_ADJ);
}
});
+ // find last ITEM_ADJ invoice added in child invoice
+ final InvoiceItemModelDao lastChildInvoiceItemAdjustment = Collections.max(Lists.newArrayList(childAdjustments), new Comparator<InvoiceItemModelDao>() {
+ @Override
+ public int compare(InvoiceItemModelDao o1, InvoiceItemModelDao o2) {
+ return o1.getCreatedDate().compareTo(o2.getCreatedDate());
+ }
+ });
+
+ final BigDecimal childInvoiceAdjustmentAmount = lastChildInvoiceItemAdjustment.getAmount();
+
+ if (parentInvoiceModelDao.getStatus().equals(InvoiceStatus.COMMITTED)) {
+ ItemAdjInvoiceItem adj = new ItemAdjInvoiceItem(UUIDs.randomUUID(),
+ lastChildInvoiceItemAdjustment.getCreatedDate(),
+ parentSummaryInvoiceItem.getInvoiceId(),
+ parentSummaryInvoiceItem.getAccountId(),
+ lastChildInvoiceItemAdjustment.getStartDate(),
+ description,
+ childInvoiceAdjustmentAmount,
+ parentInvoiceModelDao.getCurrency(),
+ parentSummaryInvoiceItem.getId());
+ parentInvoiceModelDao.addInvoiceItem(new InvoiceItemModelDao(adj));
+ invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(parentInvoiceModelDao), parentContext);
+ return;
+ }
+
// update item amount
- BigDecimal newParentInvoiceItemAmount = childInvoiceAdjustmentAmount.add(parentInvoiceItemForChild.getAmount());
- invoiceDao.updateInvoiceItemAmount(parentInvoiceItemForChild.getId(), newParentInvoiceItemAmount, parentContext);
+ final BigDecimal newParentInvoiceItemAmount = childInvoiceAdjustmentAmount.add(parentSummaryInvoiceItem.getAmount());
+ invoiceDao.updateInvoiceItemAmount(parentSummaryInvoiceItem.getId(), newParentInvoiceItemAmount, parentContext);
}
}