killbill-memoizeit

invoice: send InvoiceAdjustmentEvent from InvoiceDao as

8/31/2012 5:45:51 PM

Details

diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoiceAdjustmentEvent.java b/api/src/main/java/com/ning/billing/invoice/api/InvoiceAdjustmentEvent.java
new file mode 100644
index 0000000..c59105c
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoiceAdjustmentEvent.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning 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:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.invoice.api;
+
+import java.util.UUID;
+
+public interface InvoiceAdjustmentEvent extends InvoiceEvent {
+
+    public UUID getInvoiceId();
+}
diff --git a/api/src/main/java/com/ning/billing/util/bus/BusEvent.java b/api/src/main/java/com/ning/billing/util/bus/BusEvent.java
index 919924e..c1636e0 100644
--- a/api/src/main/java/com/ning/billing/util/bus/BusEvent.java
+++ b/api/src/main/java/com/ning/billing/util/bus/BusEvent.java
@@ -26,6 +26,7 @@ public interface BusEvent {
         BUNDLE_REPAIR,
         INVOICE_EMPTY,
         INVOICE_CREATION,
+        INVOICE_ADJUSTMENT,
         PAYMENT_INFO,
         PAYMENT_ERROR,
         CONTROL_TAG_CREATION,
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index 41afabf..380b287 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -37,6 +37,7 @@ import com.ning.billing.beatrix.integration.BeatrixModule;
 import com.ning.billing.beatrix.integration.TestIntegrationBase;
 import com.ning.billing.beatrix.util.InvoiceChecker.ExpectedItemCheck;
 import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.catalog.api.PriceListSet;
 import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
@@ -44,12 +45,15 @@ import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceItemType;
+import com.ning.billing.invoice.api.InvoicePayment;
 import com.ning.billing.invoice.api.InvoiceUserApi;
+import com.ning.billing.junction.api.Blockable;
 import com.ning.billing.junction.api.BlockingApi;
 import com.ning.billing.junction.api.BlockingApiException;
 import com.ning.billing.overdue.OverdueUserApi;
 import com.ning.billing.overdue.config.OverdueConfig;
 import com.ning.billing.overdue.wrapper.OverdueWrapperFactory;
+import com.ning.billing.payment.api.Payment;
 import com.ning.billing.payment.api.PaymentApi;
 import com.ning.billing.payment.api.PaymentMethodPlugin;
 import com.ning.billing.payment.provider.MockPaymentProviderPlugin;
@@ -189,9 +193,9 @@ public class TestOverdueIntegration extends TestIntegrationBase {
     @Test(groups = "slow")
     public void testBasicOverdueState() throws Exception {
         clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
-        paymentPlugin.makeAllInvoicesFailWithError(true);
 
         // Set next invoice to fail and create subscription
+        paymentPlugin.makeAllInvoicesFailWithError(true);
         final Subscription baseSubscription = createSubscriptionAndCheckForCompletion(bundle.getId(), productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
 
         invoiceChecker.checkInvoice(account.getId(), 1, new ExpectedItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
@@ -371,6 +375,267 @@ public class TestOverdueIntegration extends TestIntegrationBase {
         assertEquals(invoiceUserApi.getAccountBalance(account.getId()).compareTo(new BigDecimal("-8.88")), 0);
     }
 
+    @Test(groups = "slow")
+    public void testShouldBeInOverdueAfterExternalCharge() throws Exception {
+        clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+        // Create a subscription without failing payments
+        final Subscription baseSubscription = createSubscriptionAndCheckForCompletion(bundle.getId(), productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+
+        invoiceChecker.checkInvoice(account.getId(), 1, new ExpectedItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 5, 1));
+
+        // Create an external charge on a new invoice
+        addDaysAndCheckForCompletion(5);
+        busHandler.pushExpectedEvents(NextEvent.INVOICE_ADJUSTMENT);
+        invoiceApi.insertExternalChargeForBundle(account.getId(), bundle.getId(), BigDecimal.TEN, "For overdue", new LocalDate(2012, 5, 6), Currency.USD, context);
+        assertTrue(busHandler.isCompleted(DELAY));
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), 2, new ExpectedItemCheck(new LocalDate(2012, 5, 6), null, InvoiceItemType.EXTERNAL_CHARGE, BigDecimal.TEN));
+
+        // DAY 30 have to get out of trial before first payment
+        addDaysAndCheckForCompletion(25, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+
+        invoiceChecker.checkInvoice(account.getId(), 3, new ExpectedItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 6, 30));
+
+        // Should still be in clear state - the invoice for the bundle has been paid, but not the invoice with the external charge
+        // We refresh overdue just to be safe, see below
+        overdueApi.refreshOverdueStateFor(bundle);
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // Past 30 days since the external charge
+        addDaysAndCheckForCompletion(6);
+        // Note! We need to explicitly refresh here because overdue won't get notified to refresh up until the next
+        // payment (when the next invoice is generated)
+        // TODO - we should fix this
+        overdueApi.refreshOverdueStateFor(bundle);
+        // We should now be in OD1
+        checkODState("OD1");
+
+        // Pay the invoice
+        final Invoice externalChargeInvoice = invoiceApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday()).iterator().next();
+        createExternalPaymentAndCheckForCompletion(account, externalChargeInvoice, NextEvent.PAYMENT);
+        // We should be clear now
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+    }
+
+    @Test(groups = "slow")
+    public void testShouldBeInOverdueAfterRefundWithoutAdjustment() throws Exception {
+        clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+        // Create subscription and don't fail payments
+        final Subscription baseSubscription = createSubscriptionAndCheckForCompletion(bundle.getId(), productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+
+        invoiceChecker.checkInvoice(account.getId(), 1, new ExpectedItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 5, 1));
+
+        // DAY 30 have to get out of trial before first payment
+        addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+
+        invoiceChecker.checkInvoice(account.getId(), 2, new ExpectedItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 6, 30));
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 45 - 15 days after invoice
+        addDaysAndCheckForCompletion(15);
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 65 - 35 days after invoice
+        addDaysAndCheckForCompletion(20, NextEvent.INVOICE, NextEvent.PAYMENT);
+
+        invoiceChecker.checkInvoice(account.getId(), 3, new ExpectedItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 7, 31));
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // Now, refund the second (first non-zero dollar) invoice
+        final Payment payment = paymentApi.getPayment(invoiceApi.getInvoicesByAccount(account.getId()).get(1).getPayments().get(0).getPaymentId());
+        refundPaymentAndCheckForCompletion(account, payment, NextEvent.INVOICE_ADJUSTMENT);
+        // We should now be in OD1
+        checkODState("OD1");
+        checkChangePlanWithOverdueState(baseSubscription, true);
+    }
+
+    @Test(groups = "slow")
+    public void testShouldBeInOverdueAfterChargeback() throws Exception {
+        clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+        // Create subscription and don't fail payments
+        final Subscription baseSubscription = createSubscriptionAndCheckForCompletion(bundle.getId(), productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+
+        invoiceChecker.checkInvoice(account.getId(), 1, new ExpectedItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 5, 1));
+
+        // DAY 30 have to get out of trial before first payment
+        addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+
+        invoiceChecker.checkInvoice(account.getId(), 2, new ExpectedItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 6, 30));
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 45 - 15 days after invoice
+        addDaysAndCheckForCompletion(15);
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 65 - 35 days after invoice
+        addDaysAndCheckForCompletion(20, NextEvent.INVOICE, NextEvent.PAYMENT);
+
+        invoiceChecker.checkInvoice(account.getId(), 3, new ExpectedItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 7, 31));
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // Now, create a chargeback for the second (first non-zero dollar) invoice
+        final InvoicePayment payment = invoicePaymentApi.getInvoicePayments(invoiceApi.getInvoicesByAccount(account.getId()).get(1).getPayments().get(0).getPaymentId()).get(0);
+        createChargeBackAndCheckForCompletion(payment, NextEvent.INVOICE_ADJUSTMENT);
+        // We should now be in OD1
+        checkODState("OD1");
+        checkChangePlanWithOverdueState(baseSubscription, true);
+    }
+
+    @Test(groups = "slow")
+    public void testOverdueStateShouldClearAfterExternalPayment() throws Exception {
+        clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+        // Set next invoice to fail and create subscription
+        paymentPlugin.makeAllInvoicesFailWithError(true);
+        final Subscription baseSubscription = createSubscriptionAndCheckForCompletion(bundle.getId(), productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+
+        invoiceChecker.checkInvoice(account.getId(), 1, new ExpectedItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 5, 1));
+
+        // DAY 30 have to get out of trial before first payment
+        addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR);
+
+        invoiceChecker.checkInvoice(account.getId(), 2, new ExpectedItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 6, 30));
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 45 - 15 days after invoice
+        addDaysAndCheckForCompletion(15, NextEvent.PAYMENT_ERROR);
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 65 - 35 days after invoice
+        addDaysAndCheckForCompletion(20, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.PAYMENT_ERROR);
+
+        invoiceChecker.checkInvoice(account.getId(), 3, new ExpectedItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 7, 31));
+
+        // Now we should be in OD1
+        checkODState("OD1");
+        checkChangePlanWithOverdueState(baseSubscription, true);
+
+        // We have two unpaid non-zero dollar invoices at this point
+        // Pay the first one via an external payment - we should then be 5 days apart from the second invoice
+        // (which is the earliest unpaid one) and hence come back to a clear state (see configuration)
+        final Invoice firstNonZeroInvoice = invoiceApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday()).iterator().next();
+        createExternalPaymentAndCheckForCompletion(account, firstNonZeroInvoice, NextEvent.PAYMENT);
+        // We should be clear now
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+    }
+
+    @Test(groups = "slow", enabled = false)
+    public void testOverdueStateAndWRITTEN_OFFTag() throws Exception {
+        // TODO add/remove tag to invoice
+    }
+
+    @Test(groups = "slow")
+    public void testOverdueStateShouldClearAfterCreditOrInvoiceItemAdjustment() throws Exception {
+        clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+        // Set next invoice to fail and create subscription
+        paymentPlugin.makeAllInvoicesFailWithError(true);
+        final Subscription baseSubscription = createSubscriptionAndCheckForCompletion(bundle.getId(), productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+
+        invoiceChecker.checkInvoice(account.getId(), 1, new ExpectedItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 5, 1));
+
+        // DAY 30 have to get out of trial before first payment
+        addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR);
+
+        invoiceChecker.checkInvoice(account.getId(), 2, new ExpectedItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 6, 30));
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 45 - 15 days after invoice
+        addDaysAndCheckForCompletion(15, NextEvent.PAYMENT_ERROR);
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 65 - 35 days after invoice
+        addDaysAndCheckForCompletion(20, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.PAYMENT_ERROR);
+
+        invoiceChecker.checkInvoice(account.getId(), 3, new ExpectedItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 7, 31));
+
+        // Now we should be in OD1
+        checkODState("OD1");
+        checkChangePlanWithOverdueState(baseSubscription, true);
+
+        // We have two unpaid non-zero dollar invoices at this point
+        // Adjust the first (and only) item of the first invoice - we should then be 5 days apart from the second invoice
+        // (which is the earliest unpaid one) and hence come back to a clear state (see configuration)
+        final Invoice firstNonZeroInvoice = invoiceApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday()).iterator().next();
+        fullyAdjustInvoiceItemAndCheckForCompletion(account, firstNonZeroInvoice, 1, NextEvent.INVOICE_ADJUSTMENT);
+        // We should be clear now
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        invoiceChecker.checkRepairedInvoice(account.getId(), 2,
+                                            new ExpectedItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+                                            new ExpectedItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 5, 31), InvoiceItemType.ITEM_ADJ, new BigDecimal("-249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 7, 31));
+
+        // DAY 70 - 10 days after second invoice
+        addDaysAndCheckForCompletion(5);
+
+        // We should still be clear
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 80 - 20 days after second invoice
+        addDaysAndCheckForCompletion(10, NextEvent.PAYMENT_ERROR);
+
+        // We should still be clear
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 95 - 35 days after second invoice
+        addDaysAndCheckForCompletion(15, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR);
+
+        // We should now be in OD1
+        checkODState("OD1");
+        checkChangePlanWithOverdueState(baseSubscription, true);
+
+        invoiceChecker.checkInvoice(account.getId(), 4, new ExpectedItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 8, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+
+        // Fully adjust all invoices
+        final Collection<Invoice> invoices = invoiceApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday());
+        for (final Invoice invoice : invoices) {
+            if (invoice.getBalance().compareTo(BigDecimal.ZERO) > 0) {
+                fullyAdjustInvoiceAndCheckForCompletion(account, invoice, NextEvent.INVOICE_ADJUSTMENT);
+            }
+        }
+
+        // We should be cleared again
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+    }
+
     private void checkChangePlanWithOverdueState(final Subscription subscription, final boolean shouldFail) {
         if (shouldFail) {
             try {
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
index 150a971..e9c42b0 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
@@ -62,7 +62,10 @@ import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionData;
 import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoicePayment;
+import com.ning.billing.invoice.api.InvoicePaymentApi;
 import com.ning.billing.invoice.api.InvoiceService;
 import com.ning.billing.invoice.api.InvoiceUserApi;
 import com.ning.billing.invoice.model.InvoicingConfiguration;
@@ -70,6 +73,7 @@ import com.ning.billing.junction.plumbing.api.BlockingSubscription;
 import com.ning.billing.mock.MockAccountBuilder;
 import com.ning.billing.mock.api.MockBillCycleDay;
 import com.ning.billing.overdue.wrapper.OverdueWrapperFactory;
+import com.ning.billing.payment.api.Payment;
 import com.ning.billing.payment.api.PaymentApi;
 import com.ning.billing.payment.api.PaymentApiException;
 import com.ning.billing.payment.api.PaymentMethodPlugin;
@@ -149,6 +153,9 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
     protected InvoiceUserApi invoiceUserApi;
 
     @Inject
+    protected InvoicePaymentApi invoicePaymentApi;
+
+    @Inject
     protected PaymentApi paymentApi;
 
     @Named(BeatrixModule.PLUGIN_NAME)
@@ -371,6 +378,48 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
         }, events);
     }
 
+    protected void createExternalPaymentAndCheckForCompletion(final Account account, final Invoice invoice, final NextEvent... events) {
+        doCallAndCheckForCompletion(new Function<Void, Void>() {
+            @Override
+            public Void apply(@Nullable final Void input) {
+                try {
+                    paymentApi.createExternalPayment(account, invoice.getId(), invoice.getBalance(), new DefaultCallContext("test", null, null, clock));
+                } catch (PaymentApiException e) {
+                    fail(e.toString());
+                }
+                return null;
+            }
+        }, events);
+    }
+
+    protected void refundPaymentAndCheckForCompletion(final Account account, final Payment payment, final NextEvent... events) {
+        doCallAndCheckForCompletion(new Function<Void, Void>() {
+            @Override
+            public Void apply(@Nullable final Void input) {
+                try {
+                    paymentApi.createRefund(account, payment.getId(), payment.getPaidAmount(), new DefaultCallContext("test", null, null, clock));
+                } catch (PaymentApiException e) {
+                    fail(e.toString());
+                }
+                return null;
+            }
+        }, events);
+    }
+
+    protected void createChargeBackAndCheckForCompletion(final InvoicePayment payment, final NextEvent... events) {
+        doCallAndCheckForCompletion(new Function<Void, Void>() {
+            @Override
+            public Void apply(@Nullable final Void input) {
+                try {
+                    invoicePaymentApi.createChargeback(payment.getId(), payment.getAmount(), new DefaultCallContext("test", null, null, clock));
+                } catch (InvoiceApiException e) {
+                    fail(e.toString());
+                }
+                return null;
+            }
+        }, events);
+    }
+
     protected Subscription createSubscriptionAndCheckForCompletion(final UUID bundleId,
             final String productName,
             final ProductCategory productCategory,
@@ -433,6 +482,36 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
         }, events);
     }
 
+    protected void fullyAdjustInvoiceAndCheckForCompletion(final Account account, final Invoice invoice, final NextEvent... events) {
+        doCallAndCheckForCompletion(new Function<Void, Void>() {
+            @Override
+            public Void apply(@Nullable final Void input) {
+                try {
+                    invoiceUserApi.insertCreditForInvoice(account.getId(), invoice.getId(), invoice.getBalance(), invoice.getInvoiceDate(),
+                                                          account.getCurrency(), new DefaultCallContext("test", null, null, clock));
+                } catch (InvoiceApiException e) {
+                    fail(e.toString());
+                }
+                return null;
+            }
+        }, events);
+    }
+
+    protected void fullyAdjustInvoiceItemAndCheckForCompletion(final Account account, final Invoice invoice, final int itemNb, final NextEvent... events) {
+        doCallAndCheckForCompletion(new Function<Void, Void>() {
+            @Override
+            public Void apply(@Nullable final Void input) {
+                try {
+                    invoiceUserApi.insertInvoiceItemAdjustment(account.getId(), invoice.getId(), invoice.getInvoiceItems().get(itemNb - 1).getId(),
+                                                               invoice.getInvoiceDate(), new DefaultCallContext("test", null, null, clock));
+                } catch (InvoiceApiException e) {
+                    fail(e.toString());
+                }
+                return null;
+            }
+        }, events);
+    }
+
     private <T> T doCallAndCheckForCompletion(Function<Void, T> f, final NextEvent... events) {
 
         Joiner joiner = Joiner.on(", ");
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceAdjustmentEvent.java b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceAdjustmentEvent.java
new file mode 100644
index 0000000..d4e6170
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceAdjustmentEvent.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning 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:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.invoice.api.user;
+
+import java.util.UUID;
+
+import com.ning.billing.invoice.api.InvoiceAdjustmentEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultInvoiceAdjustmentEvent implements InvoiceAdjustmentEvent {
+
+    private final UUID invoiceId;
+    private final UUID accountId;
+    private final UUID userToken;
+
+    @JsonCreator
+    public DefaultInvoiceAdjustmentEvent(@JsonProperty("invoiceId") final UUID invoiceId,
+                                         @JsonProperty("accountId") final UUID accountId,
+                                         @JsonProperty("userToken") final UUID userToken) {
+        this.invoiceId = invoiceId;
+        this.accountId = accountId;
+        this.userToken = userToken;
+    }
+
+    @Override
+    public UUID getInvoiceId() {
+        return invoiceId;
+    }
+
+    @Override
+    public UUID getAccountId() {
+        return accountId;
+    }
+
+    @JsonIgnore
+    @Override
+    public BusEventType getBusEventType() {
+        return BusEventType.INVOICE_ADJUSTMENT;
+    }
+
+    @Override
+    public UUID getUserToken() {
+        return userToken;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("DefaultInvoiceAdjustmentEvent");
+        sb.append("{invoiceId=").append(invoiceId);
+        sb.append(", accountId=").append(accountId);
+        sb.append(", userToken=").append(userToken);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final DefaultInvoiceAdjustmentEvent that = (DefaultInvoiceAdjustmentEvent) o;
+
+        if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+            return false;
+        }
+        if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+            return false;
+        }
+        if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = invoiceId != null ? invoiceId.hashCode() : 0;
+        result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+        result = 31 * result + (userToken != null ? userToken.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java
index 1abfe3d..ba3ec0c 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java
@@ -30,6 +30,9 @@ import org.joda.time.LocalDate;
 import org.skife.jdbi.v2.IDBI;
 import org.skife.jdbi.v2.Transaction;
 import org.skife.jdbi.v2.TransactionStatus;
+import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.ning.billing.ErrorCode;
 import com.ning.billing.catalog.api.Currency;
@@ -39,6 +42,7 @@ import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.invoice.api.InvoicePayment;
 import com.ning.billing.invoice.api.InvoicePayment.InvoicePaymentType;
+import com.ning.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent;
 import com.ning.billing.invoice.generator.InvoiceDateUtils;
 import com.ning.billing.invoice.model.CreditAdjInvoiceItem;
 import com.ning.billing.invoice.model.CreditBalanceAdjInvoiceItem;
@@ -52,6 +56,8 @@ import com.ning.billing.invoice.notification.NextBillingDatePoster;
 import com.ning.billing.util.ChangeType;
 import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.bus.Bus;
+import com.ning.billing.util.bus.Bus.EventBusException;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.dao.EntityAudit;
@@ -68,24 +74,29 @@ import com.google.inject.Inject;
 
 public class AuditedInvoiceDao implements InvoiceDao {
 
+    private static final Logger log = LoggerFactory.getLogger(AuditedInvoiceDao.class);
+
     private final InvoiceSqlDao invoiceSqlDao;
     private final InvoicePaymentSqlDao invoicePaymentSqlDao;
     private final TagUserApi tagUserApi;
     private final NextBillingDatePoster nextBillingDatePoster;
     private final InvoiceItemSqlDao invoiceItemSqlDao;
     private final Clock clock;
+    private final Bus eventBus;
 
     @Inject
     public AuditedInvoiceDao(final IDBI dbi,
                              final NextBillingDatePoster nextBillingDatePoster,
                              final TagUserApi tagUserApi,
-                             final Clock clock) {
+                             final Clock clock,
+                             final Bus eventBus) {
         this.invoiceSqlDao = dbi.onDemand(InvoiceSqlDao.class);
         this.invoicePaymentSqlDao = dbi.onDemand(InvoicePaymentSqlDao.class);
         this.invoiceItemSqlDao = dbi.onDemand(InvoiceItemSqlDao.class);
         this.nextBillingDatePoster = nextBillingDatePoster;
         this.tagUserApi = tagUserApi;
         this.clock = clock;
+        this.eventBus = eventBus;
     }
 
     @Override
@@ -301,12 +312,32 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
     @Override
     public void setWrittenOff(final UUID invoiceId, final CallContext context) throws TagApiException {
-        tagUserApi.addTag(invoiceId, ObjectType.INVOICE, ControlTagType.WRITTEN_OFF.getId(), context);
+        invoiceSqlDao.inTransaction(new Transaction<Void, InvoiceSqlDao>() {
+            @Override
+            public Void inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                tagUserApi.addTag(invoiceId, ObjectType.INVOICE, ControlTagType.WRITTEN_OFF.getId(), context);
+
+                final Invoice invoice = transactional.getById(invoiceId.toString());
+                notifyBusOfInvoiceAdjustment(transactional, invoiceId, invoice.getAccountId(), context.getUserToken());
+
+                return null;
+            }
+        });
     }
 
     @Override
     public void removeWrittenOff(final UUID invoiceId, final CallContext context) throws TagApiException {
-        tagUserApi.removeTag(invoiceId, ObjectType.INVOICE, ControlTagType.WRITTEN_OFF.getId(), context);
+        invoiceSqlDao.inTransaction(new Transaction<Void, InvoiceSqlDao>() {
+            @Override
+            public Void inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                tagUserApi.removeTag(invoiceId, ObjectType.INVOICE, ControlTagType.WRITTEN_OFF.getId(), context);
+
+                final Invoice invoice = transactional.getById(invoiceId.toString());
+                notifyBusOfInvoiceAdjustment(transactional, invoiceId, invoice.getAccountId(), context.getUserToken());
+
+                return null;
+            }
+        });
     }
 
     @Override
@@ -387,6 +418,9 @@ public class AuditedInvoiceDao implements InvoiceDao {
                     }
                 }
 
+                // Notify the bus since the balance of the invoice changed
+                notifyBusOfInvoiceAdjustment(transactional, invoice.getId(), invoice.getAccountId(), context.getUserToken());
+
                 return refund;
             }
         });
@@ -483,12 +517,16 @@ public class AuditedInvoiceDao implements InvoiceDao {
                 } else {
                     final InvoicePayment chargeBack = new DefaultInvoicePayment(UUID.randomUUID(), InvoicePaymentType.CHARGED_BACK, payment.getPaymentId(),
                                                                                 payment.getInvoiceId(), context.getCreatedDate(), requestedChargedBackAmout.negate(), payment.getCurrency(), null, payment.getId());
-                    invoicePaymentSqlDao.create(chargeBack, context);
+                    transactional.create(chargeBack, context);
 
                     // Add audit
-                    final Long recordId = invoicePaymentSqlDao.getRecordId(chargeBack.getId().toString());
+                    final Long recordId = transactional.getRecordId(chargeBack.getId().toString());
                     final EntityAudit audit = new EntityAudit(TableName.INVOICE_PAYMENTS, recordId, ChangeType.INSERT);
-                    invoicePaymentSqlDao.insertAuditFromTransaction(audit, context);
+                    transactional.insertAuditFromTransaction(audit, context);
+
+                    // Notify the bus since the balance of the invoice changed
+                    final UUID accountId = transactional.getAccountIdFromInvoicePaymentId(chargeBack.getId().toString());
+                    notifyBusOfInvoiceAdjustment(transactional, payment.getInvoiceId(), accountId, context.getUserToken());
 
                     return chargeBack;
                 }
@@ -557,6 +595,9 @@ public class AuditedInvoiceDao implements InvoiceDao {
                 final InvoiceItemSqlDao transInvoiceItemDao = transactional.become(InvoiceItemSqlDao.class);
                 transInvoiceItemDao.create(externalCharge, context);
 
+                // Notify the bus since the balance of the invoice changed
+                notifyBusOfInvoiceAdjustment(transactional, invoiceId, accountId, context.getUserToken());
+
                 return externalCharge;
             }
         });
@@ -589,6 +630,9 @@ public class AuditedInvoiceDao implements InvoiceDao {
                 final EntityAudit audit = new EntityAudit(TableName.INVOICE_ITEMS, recordId, ChangeType.INSERT);
                 transactional.insertAuditFromTransaction(audit, context);
 
+                // Notify the bus since the balance of the invoice changed
+                notifyBusOfInvoiceAdjustment(transactional, invoiceId, accountId, context.getUserToken());
+
                 return credit;
             }
         });
@@ -604,6 +648,9 @@ public class AuditedInvoiceDao implements InvoiceDao {
                 final InvoiceItem invoiceItemAdjustment = createAdjustmentItem(transactional, invoiceId, invoiceItemId, positiveAdjAmount,
                                                                                currency, effectiveDate);
                 insertItemAndAddCBAIfNeeded(transactional, invoiceItemAdjustment, context);
+
+                notifyBusOfInvoiceAdjustment(transactional, invoiceId, accountId, context.getUserToken());
+
                 return invoiceItemAdjustment;
             }
         });
@@ -773,4 +820,12 @@ public class AuditedInvoiceDao implements InvoiceDao {
             nextBillingDatePoster.insertNextBillingNotification(dao, accountId, subscriptionForNextNotification, nextNotificationDateTime);
         }
     }
+
+    private void notifyBusOfInvoiceAdjustment(final Transmogrifier transactional, final UUID invoiceId, final UUID accountId, final UUID userToken) {
+        try {
+            eventBus.postFromTransaction(new DefaultInvoiceAdjustmentEvent(invoiceId, accountId, userToken), transactional);
+        } catch (EventBusException e) {
+            log.warn("Failed to post adjustment event for invoice " + invoiceId, e);
+        }
+    }
 }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java b/invoice/src/test/java/com/ning/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
index 864c4b4..32f52e4 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
@@ -23,6 +23,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 
+import org.mockito.Mockito;
 import org.skife.jdbi.v2.IDBI;
 import org.testng.Assert;
 import org.testng.annotations.BeforeSuite;
@@ -44,6 +45,7 @@ import com.ning.billing.invoice.dao.InvoiceSqlDao;
 import com.ning.billing.invoice.notification.MockNextBillingDatePoster;
 import com.ning.billing.invoice.notification.NextBillingDatePoster;
 import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.bus.Bus;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TestCallContext;
 import com.ning.billing.util.clock.Clock;
@@ -87,7 +89,7 @@ public class TestDefaultInvoicePaymentApi extends InvoiceTestSuiteWithEmbeddedDB
         final TagDefinitionDao tagDefinitionDao = new MockTagDefinitionDao();
         final TagDao tagDao = new MockTagDao();
         final TagUserApi tagUserApi = new DefaultTagUserApi(tagDefinitionDao, tagDao);
-        final InvoiceDao invoiceDao = new AuditedInvoiceDao(dbi, nextBillingDatePoster, tagUserApi, clock);
+        final InvoiceDao invoiceDao = new AuditedInvoiceDao(dbi, nextBillingDatePoster, tagUserApi, clock, Mockito.mock(Bus.class));
         invoicePaymentApi = new DefaultInvoicePaymentApi(invoiceDao);
 
         context = new TestCallContext("Invoice payment tests");
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
index 1b42c4e..902591b 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
@@ -110,7 +110,7 @@ public class InvoiceDaoTestBase extends InvoicingTestBase {
         final TagDefinitionDao tagDefinitionDao = new MockTagDefinitionDao();
         final TagDao tagDao = new AuditedTagDao(dbi, tagEventBuilder, bus);
         final TagUserApi tagUserApi = new DefaultTagUserApi(tagDefinitionDao, tagDao);
-        invoiceDao = new AuditedInvoiceDao(dbi, nextBillingDatePoster, tagUserApi, clock);
+        invoiceDao = new AuditedInvoiceDao(dbi, nextBillingDatePoster, tagUserApi, clock, bus);
         invoiceDao.test();
 
         invoiceItemSqlDao = dbi.onDemand(InvoiceItemSqlDao.class);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java
index 3724b9f..c5510d6 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java
@@ -33,6 +33,7 @@ import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.invoice.api.InvoicePayment;
 import com.ning.billing.invoice.notification.NextBillingDatePoster;
 import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.bus.Bus;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.dao.ObjectType;
@@ -45,6 +46,7 @@ import com.ning.billing.util.tag.dao.TagDao;
 import com.ning.billing.util.tag.dao.TagDefinitionDao;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.eventbus.EventBus;
 
 public class TestDefaultInvoiceDao extends InvoiceTestSuite {
 
@@ -62,7 +64,7 @@ public class TestDefaultInvoiceDao extends InvoiceTestSuite {
         final TagDefinitionDao tagDefinitionDao = new MockTagDefinitionDao();
         final TagDao tagDao = new MockTagDao();
         tagUserApi = new DefaultTagUserApi(tagDefinitionDao, tagDao);
-        dao = new AuditedInvoiceDao(idbi, poster, tagUserApi, Mockito.mock(Clock.class));
+        dao = new AuditedInvoiceDao(idbi, poster, tagUserApi, Mockito.mock(Clock.class), Mockito.mock(Bus.class));
     }
 
     @Test(groups = "fast")
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java b/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java
index e9842eb..928b727 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java
@@ -22,6 +22,7 @@ import java.net.URL;
 import java.util.List;
 import java.util.UUID;
 
+import org.mockito.Mockito;
 import org.skife.jdbi.v2.IDBI;
 import org.skife.jdbi.v2.exceptions.TransactionFailedException;
 import org.testng.annotations.BeforeSuite;
@@ -44,6 +45,7 @@ import com.ning.billing.invoice.glue.InvoiceModuleWithEmbeddedDb;
 import com.ning.billing.invoice.notification.MockNextBillingDatePoster;
 import com.ning.billing.invoice.notification.NextBillingDatePoster;
 import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.bus.Bus;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TestCallContext;
 import com.ning.billing.util.clock.Clock;
@@ -89,7 +91,7 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
         final TagDefinitionDao tagDefinitionDao = new MockTagDefinitionDao();
         final TagDao tagDao = new MockTagDao();
         final TagUserApi tagUserApi = new DefaultTagUserApi(tagDefinitionDao, tagDao);
-        final InvoiceDao invoiceDao = new AuditedInvoiceDao(dbi, nextBillingDatePoster, tagUserApi, clock);
+        final InvoiceDao invoiceDao = new AuditedInvoiceDao(dbi, nextBillingDatePoster, tagUserApi, clock, Mockito.mock(Bus.class));
         invoicePaymentApi = new DefaultInvoicePaymentApi(invoiceDao);
 
         context = new TestCallContext("Charge back tests");
diff --git a/overdue/src/main/java/com/ning/billing/overdue/listener/OverdueListener.java b/overdue/src/main/java/com/ning/billing/overdue/listener/OverdueListener.java
index 1ab250a..b4688b5 100644
--- a/overdue/src/main/java/com/ning/billing/overdue/listener/OverdueListener.java
+++ b/overdue/src/main/java/com/ning/billing/overdue/listener/OverdueListener.java
@@ -21,13 +21,16 @@ import java.util.UUID;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.eventbus.Subscribe;
-import com.google.inject.Inject;
+import com.ning.billing.invoice.api.InvoiceAdjustmentEvent;
 import com.ning.billing.payment.api.PaymentErrorEvent;
 import com.ning.billing.payment.api.PaymentInfoEvent;
 
+import com.google.common.eventbus.Subscribe;
+import com.google.inject.Inject;
+
 public class OverdueListener {
-    OverdueDispatcher dispatcher;
+
+    private final OverdueDispatcher dispatcher;
 
     private static final Logger log = LoggerFactory.getLogger(OverdueListener.class);
 
@@ -49,10 +52,15 @@ public class OverdueListener {
         dispatcher.processOverdueForAccount(accountId);
     }
 
+    @Subscribe
+    public void handleInvoiceAdjustmentEvent(final InvoiceAdjustmentEvent event) {
+        log.info(String.format("Received InvoiceAdjustment event %s", event.toString()));
+        final UUID accountId = event.getAccountId();
+        dispatcher.processOverdueForAccount(accountId);
+    }
+
     public void handleNextOverdueCheck(final UUID overdueableId) {
         log.info(String.format("Received OD checkup notification for %s", overdueableId));
         dispatcher.processOverdue(overdueableId);
     }
-
-
 }
diff --git a/util/src/test/java/com/ning/billing/api/TestApiListener.java b/util/src/test/java/com/ning/billing/api/TestApiListener.java
index 841cffb..b752c19 100644
--- a/util/src/test/java/com/ning/billing/api/TestApiListener.java
+++ b/util/src/test/java/com/ning/billing/api/TestApiListener.java
@@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory;
 
 import com.ning.billing.entitlement.api.timeline.RepairEntitlementEvent;
 import com.ning.billing.entitlement.api.user.EffectiveSubscriptionEvent;
+import com.ning.billing.invoice.api.InvoiceAdjustmentEvent;
 import com.ning.billing.invoice.api.InvoiceCreationEvent;
 import com.ning.billing.payment.api.PaymentErrorEvent;
 import com.ning.billing.payment.api.PaymentInfoEvent;
@@ -65,6 +66,7 @@ public class TestApiListener {
         RESUME,
         PHASE,
         INVOICE,
+        INVOICE_ADJUSTMENT,
         PAYMENT,
         PAYMENT_ERROR,
         REPAIR_BUNDLE
@@ -136,6 +138,13 @@ public class TestApiListener {
     }
 
     @Subscribe
+    public void handleInvoiceAdjustmentEvents(final InvoiceAdjustmentEvent event) {
+        log.info(String.format("Got Invoice adjustment event %s", event.toString()));
+        assertEqualsNicely(NextEvent.INVOICE_ADJUSTMENT);
+        notifyIfStackEmpty();
+    }
+
+    @Subscribe
     public void handlePaymentEvents(final PaymentInfoEvent event) {
         log.info(String.format("Got PaymentInfo event %s", event.toString()));
         assertEqualsNicely(NextEvent.PAYMENT);