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..14ce36e 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,89 @@ 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
+ busHandler.pushExpectedEvents(NextEvent.INVOICE_ADJUSTMENT, NextEvent.INVOICE_ADJUSTMENT);
+ invoiceUserApi.insertInvoiceItemAdjustment(childAccount.getId(), childInvoice.getId(),
+ childInvoice.getInvoiceItems().get(0).getId(),
+ clock.getToday(childAccount.getTimeZone()), BigDecimal.TEN,
+ childAccount.getCurrency(), "test adjustment", callContext);
+ assertListenerStatus();
+
+ // expected child invoice
+ // RECURRING : $ 249.95
+ // ITEM_ADJ : $ -10
+
+ // expected parent invoice
+ // PARENT_SUMMARY : $ 249.95
+ // ITEM_ADJ : $ -10
+
+ childInvoice = invoiceUserApi.getInvoice(childInvoice.getId(), callContext);
+ assertEquals(childInvoice.getNumberOfItems(), 2);
+ assertEquals(childInvoice.getBalance().compareTo(BigDecimal.valueOf(239.95)), 0);
+ assertEquals(childInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(childInvoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+
+ // reload parent invoice
+ parentInvoice = invoiceUserApi.getInvoice(parentInvoice.getId(), callContext);
+ // check parent invoice is updated and still in DRAFT status
+ assertEquals(parentInvoice.getNumberOfItems(), 2);
+ assertEquals(parentInvoice.getStatus(), InvoiceStatus.COMMITTED);
+ assertTrue(parentInvoice.isParentInvoice());
+ assertEquals(parentInvoice.getBalance().compareTo(BigDecimal.valueOf(239.95)), 0);
+ assertEquals(parentInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.PARENT_SUMMARY);
+ assertEquals(parentInvoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+
+ }
+
// 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..8d2210d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -70,6 +70,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 +79,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 +90,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,16 +780,16 @@ 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.
- return;
}
+ 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());
+
final InvoiceItemModelDao childInvoiceItemAdjustment = Iterables.find(childInvoiceModelDao.getInvoiceItems(), new Predicate<InvoiceItemModelDao>() {
@Override
public boolean apply(@Nullable final InvoiceItemModelDao input) {
@@ -794,24 +797,39 @@ public class InvoiceDispatcher {
}
});
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 InvoiceItemModelDao parentSummaryInvoiceItem = Iterables.find(parentInvoiceModelDao.getInvoiceItems(), new Predicate<InvoiceItemModelDao>() {
@Override
public boolean apply(@Nullable final InvoiceItemModelDao input) {
- return childInvoice.getAccountId().equals(input.getChildAccountId());
+ return input.getType().equals(InvoiceItemType.PARENT_SUMMARY)
+ && input.getChildAccountId().equals(childInvoiceModelDao.getAccountId());
}
});
+ if (parentInvoiceModelDao.getStatus().equals(InvoiceStatus.COMMITTED)) {
+ if (InvoiceModelDaoHelper.getBalance(parentInvoiceModelDao).compareTo(BigDecimal.ZERO) > 0) {
+
+ ItemAdjInvoiceItem adj = new ItemAdjInvoiceItem(UUIDs.randomUUID(),
+ context.getCreatedDate(),
+ parentSummaryInvoiceItem.getInvoiceId(),
+ parentSummaryInvoiceItem.getAccountId(),
+ clock.getUTCToday(),
+ description,
+ childInvoiceAdjustmentAmount,
+ parentInvoiceModelDao.getCurrency(),
+ parentSummaryInvoiceItem.getId());
+ parentInvoiceModelDao.addInvoiceItem(new InvoiceItemModelDao(adj));
+ invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(parentInvoiceModelDao), parentContext);
+ }
+
+ // ignore parent invoice adjustment if it's already paid.
+ return;
+ }
+
// update item amount
- BigDecimal newParentInvoiceItemAmount = childInvoiceAdjustmentAmount.add(parentInvoiceItemForChild.getAmount());
- invoiceDao.updateInvoiceItemAmount(parentInvoiceItemForChild.getId(), newParentInvoiceItemAmount, parentContext);
+ BigDecimal newParentInvoiceItemAmount = childInvoiceAdjustmentAmount.add(parentSummaryInvoiceItem.getAmount());
+ invoiceDao.updateInvoiceItemAmount(parentSummaryInvoiceItem.getId(), newParentInvoiceItemAmount, parentContext);
}
}