killbill-memoizeit

invoice: add support to adjust invoice items We were already

8/1/2012 2:14:53 PM

Details

diff --git a/api/src/main/java/com/ning/billing/ErrorCode.java b/api/src/main/java/com/ning/billing/ErrorCode.java
index 97f8824..8e000da 100644
--- a/api/src/main/java/com/ning/billing/ErrorCode.java
+++ b/api/src/main/java/com/ning/billing/ErrorCode.java
@@ -186,6 +186,10 @@ public enum ErrorCode {
     INVOICE_NOT_FOUND(4006, "No invoice could be found for id %s."),
     INVOICE_NOTHING_TO_DO(4007, "No invoice to generate for account %s and date %s"),
     INVOICE_NO_SUCH_CREDIT(4008, "Credit Item for id %s does not exist"),
+    CREDIT_AMOUNT_INVALID(4009, "Credit amount %s should be strictly positive"),
+    INVOICE_ITEM_ADJUSTMENT_AMOUNT_INVALID(4010, "Invoice adjustment amount %s should be strictly positive"),
+    INVOICE_ITEM_NOT_FOUND(4011, "No invoice item could be found for id %s."),
+    INVOICE_INVALID_FOR_INVOICE_ITEM_ADJUSTMENT(4012, "Invoice item %s doesn't belong to invoice %s."),
 
     /*
      *
@@ -199,7 +203,6 @@ public enum ErrorCode {
     CHARGE_BACK_DOES_NOT_EXIST(4004, "Could not find chargeback for id %s."),
     INVOICE_PAYMENT_BY_ATTEMPT_NOT_FOUND(4905, "No invoice payment could be found for paymentAttempt id %s."),
     REFUND_AMOUNT_TOO_HIGH(4906, "Tried to refund %s of a %s payment."),
-    CREDIT_AMOUNT_INVALID(4907, "Credit amount %s should be striclty positive"),
 
     /*
      *
diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoiceItemType.java b/api/src/main/java/com/ning/billing/invoice/api/InvoiceItemType.java
index 93afad8..548bfd5 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/InvoiceItemType.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoiceItemType.java
@@ -17,10 +17,19 @@
 package com.ning.billing.invoice.api;
 
 public enum InvoiceItemType {
+    // Fixed (one-time) charge
     FIXED,
+    // Recurring charge
     RECURRING,
+    // Internal adjustment, used for repair
     REPAIR_ADJ,
+    // Internal adjustment, used as rollover credits
     CBA_ADJ,
+    // Credit adjustment, either at the account level (on its own invoice) or against an existing invoice
+    // (invoice level adjustment)
     CREDIT_ADJ,
+    // Invoice item adjustment
+    ITEM_ADJ,
+    // Refund adjustment (against a posted payment)
     REFUND_ADJ
 }
diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoiceUserApi.java b/api/src/main/java/com/ning/billing/invoice/api/InvoiceUserApi.java
index 4a3b43c..38543f2 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/InvoiceUserApi.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoiceUserApi.java
@@ -22,6 +22,8 @@ import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.LocalDate;
 
 import com.ning.billing.account.api.AccountApiException;
@@ -143,7 +145,7 @@ public interface InvoiceUserApi {
                                     Currency currency, CallContext context) throws InvoiceApiException;
 
     /**
-     * Add a credit to an invoice.
+     * Add a credit to an invoice. This can be used to adjust invoices.
      *
      * @param accountId     account id
      * @param invoiceId     invoice id
@@ -158,6 +160,22 @@ public interface InvoiceUserApi {
                                               Currency currency, CallContext context) throws InvoiceApiException;
 
     /**
+     * Adjust a given invoice item.
+     *
+     * @param accountId     account id
+     * @param invoiceId     invoice id
+     * @param invoiceItemId invoice item id
+     * @param effectiveDate the effective date for this adjustment invoice item
+     * @param amount        the adjustment amount. Pass null to adjust for the full amount of the original item
+     * @param currency      adjustment currency. Pass null to use the original currency
+     * @param context       the call context
+     * @return the adjustment invoice item
+     * @throws InvoiceApiException
+     */
+    public InvoiceItem insertInvoiceItemAdjustment(UUID accountId, UUID invoiceId, UUID invoiceItemId, LocalDate effectiveDate,
+                                                   @Nullable BigDecimal amount, @Nullable Currency currency, CallContext context) throws InvoiceApiException;
+
+    /**
      * Retrieve the invoice formatted in HTML.
      *
      * @param invoiceId invoice id
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
index ea8e17a..2fc3623 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -22,6 +22,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 
@@ -156,6 +158,16 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
     }
 
     @Override
+    public InvoiceItem insertInvoiceItemAdjustment(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId,
+                                                   final LocalDate effectiveDate, @Nullable final BigDecimal amount,
+                                                   @Nullable final Currency currency, final CallContext context) throws InvoiceApiException {
+        if (amount != null && amount.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_ADJUSTMENT_AMOUNT_INVALID, amount);
+        }
+        return dao.insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItemId, effectiveDate, amount, currency, context);
+    }
+
+    @Override
     public String getInvoiceAsHTML(final UUID invoiceId) throws AccountApiException, IOException, InvoiceApiException {
         final Invoice invoice = getInvoice(invoiceId);
         if (invoice == null) {
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
index 68cf45c..0de61e7 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
@@ -43,6 +43,7 @@ import com.ning.billing.invoice.model.CreditAdjInvoiceItem;
 import com.ning.billing.invoice.model.CreditBalanceAdjInvoiceItem;
 import com.ning.billing.invoice.model.DefaultInvoice;
 import com.ning.billing.invoice.model.DefaultInvoicePayment;
+import com.ning.billing.invoice.model.ItemAdjInvoiceItem;
 import com.ning.billing.invoice.model.RecurringInvoiceItem;
 import com.ning.billing.invoice.model.RefundAdjInvoiceItem;
 import com.ning.billing.invoice.notification.NextBillingDatePoster;
@@ -56,6 +57,7 @@ import com.ning.billing.util.dao.ObjectType;
 import com.ning.billing.util.dao.TableName;
 import com.ning.billing.util.tag.ControlTagType;
 
+import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.inject.Inject;
@@ -441,31 +443,52 @@ public class DefaultInvoiceDao implements InvoiceDao {
         return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
             @Override
             public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
-                UUID invoiceIdForRefund = invoiceId;
-                if (invoiceIdForRefund == null) {
-                    final Invoice invoiceForRefund = new DefaultInvoice(accountId, effectiveDate, effectiveDate, currency);
-                    transactional.create(invoiceForRefund, context);
-                    invoiceIdForRefund = invoiceForRefund.getId();
+                UUID invoiceIdForCredit = invoiceId;
+                // Create an invoice for that credit if it doesn't exist
+                if (invoiceIdForCredit == null) {
+                    final Invoice invoiceForCredit = new DefaultInvoice(accountId, effectiveDate, effectiveDate, currency);
+                    transactional.create(invoiceForCredit, context);
+                    invoiceIdForCredit = invoiceForCredit.getId();
                 }
 
-                final InvoiceItem credit = new CreditAdjInvoiceItem(invoiceIdForRefund, accountId, effectiveDate, positiveCreditAmount.negate(), currency);
-                final InvoiceItemSqlDao transInvoiceItemDao = transactional.become(InvoiceItemSqlDao.class);
-                transInvoiceItemDao.create(credit, context);
+                // Note! The amount is negated here!
+                final InvoiceItem credit = new CreditAdjInvoiceItem(invoiceIdForCredit, accountId, effectiveDate, positiveCreditAmount.negate(), currency);
+                createItemAndAddCBAIfNeeded(transactional, credit, context);
+                return credit;
+            }
+        });
+    }
 
-                final Invoice invoice = transactional.getById(invoiceIdForRefund.toString());
-                if (invoice != null) {
-                    populateChildren(invoice, transactional);
-                } else {
-                    throw new IllegalStateException("Invoice shouldn't be null for credit at this stage " + invoiceIdForRefund);
+    @Override
+    public InvoiceItem insertInvoiceItemAdjustment(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId,
+                                                   final LocalDate effectiveDate, @Nullable final BigDecimal positiveAdjAmount,
+                                                   @Nullable final Currency currency, final CallContext context) {
+        return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
+            @Override
+            public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                // First, retrieve the invoice item in question
+                final InvoiceItemSqlDao invoiceItemSqlDao = transactional.become(InvoiceItemSqlDao.class);
+                final InvoiceItem invoiceItemToBeAdjusted = invoiceItemSqlDao.getById(invoiceItemId.toString());
+                if (invoiceItemToBeAdjusted == null) {
+                    throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, invoiceItemId);
                 }
-                // If invoice balance becomes negative we add some CBA item
-                if (invoice.getBalance().compareTo(BigDecimal.ZERO) < 0) {
-                    final InvoiceItem cbaAdjItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
-                                                                                   invoice.getBalance().negate(), invoice.getCurrency());
-                    transInvoiceItemDao.create(cbaAdjItem, context);
 
+                // Validate the invoice it belongs to
+                if (!invoiceItemToBeAdjusted.getInvoiceId().equals(invoiceId)) {
+                    throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_FOR_INVOICE_ITEM_ADJUSTMENT, invoiceItemId, invoiceId);
                 }
-                return credit;
+
+                // Retrieve the amount and currency if needed
+                final BigDecimal amountToRefund = Objects.firstNonNull(positiveAdjAmount, invoiceItemToBeAdjusted.getAmount());
+                // TODO - should we enforce the currency (and respect the original one) here if the amount passed was null?
+                final Currency currencyForAdjustment = Objects.firstNonNull(currency, invoiceItemToBeAdjusted.getCurrency());
+
+                // Finally, create the adjustment
+                // Note! The amount is negated here!
+                final InvoiceItem invoiceItemAdjustment = new ItemAdjInvoiceItem(invoiceItemToBeAdjusted, effectiveDate,
+                                                                                 amountToRefund.negate(), currencyForAdjustment);
+                createItemAndAddCBAIfNeeded(transactional, invoiceItemAdjustment, context);
+                return invoiceItemAdjustment;
             }
         });
     }
@@ -475,6 +498,33 @@ public class DefaultInvoiceDao implements InvoiceDao {
         invoiceSqlDao.test();
     }
 
+    /**
+     * Create an invoice item and adjust the invoice with a CBA item if the new invoice balance is negative.
+     *
+     * @param transactional the InvoiceSqlDao
+     * @param item          the invoice item to create
+     * @param context       the call context
+     */
+    private void createItemAndAddCBAIfNeeded(final InvoiceSqlDao transactional, final InvoiceItem item, final CallContext context) {
+        final InvoiceItemSqlDao transInvoiceItemDao = transactional.become(InvoiceItemSqlDao.class);
+        transInvoiceItemDao.create(item, context);
+
+        final Invoice invoice = transactional.getById(item.getInvoiceId().toString());
+        if (invoice != null) {
+            populateChildren(invoice, transactional);
+        } else {
+            throw new IllegalStateException("Invoice shouldn't be null for this item at this stage " + item.getInvoiceId());
+        }
+
+        // If invoice balance becomes negative we add some CBA item
+        if (invoice.getBalance().compareTo(BigDecimal.ZERO) < 0) {
+            final InvoiceItem cbaAdjItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
+                                                                           invoice.getBalance().negate(), invoice.getCurrency());
+            transInvoiceItemDao.create(cbaAdjItem, context);
+
+        }
+    }
+
     private BigDecimal getAccountCBAFromTransaction(final UUID accountId, final InvoiceSqlDao transactional) {
         BigDecimal cba = BigDecimal.ZERO;
         final List<Invoice> invoices = getAllInvoicesByAccountFromTransaction(accountId, transactional);
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
index 9b8c893..8a352a3 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
@@ -24,7 +24,6 @@ import javax.annotation.Nullable;
 
 import org.joda.time.LocalDate;
 
-import com.ning.billing.account.api.BillCycleDay;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceApiException;
@@ -88,4 +87,18 @@ public interface InvoiceDao {
     InvoiceItem insertCredit(final UUID accountId, final UUID invoiceId, final BigDecimal amount,
                              final LocalDate effectiveDate, final Currency currency, final CallContext context);
 
+    /**
+     * Adjust an invoice item.
+     *
+     * @param accountId     the account id
+     * @param invoiceId     the invoice id
+     * @param invoiceItemId the invoice item id to adjust
+     * @param effectiveDate adjustment effective date, in the account timezone
+     * @param amount        the amount to adjust. Pass null to adjust the full amount of the original item
+     * @param currency      the currency of the amount. Pass null to default to the original currency used
+     * @param context       the call context
+     * @return the newly created adjustment item
+     */
+    InvoiceItem insertInvoiceItemAdjustment(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId, final LocalDate effectiveDate,
+                                            @Nullable final BigDecimal amount, @Nullable final Currency currency, final CallContext context);
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceItemSqlDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceItemSqlDao.java
index e7ddfb8..8156f30 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceItemSqlDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceItemSqlDao.java
@@ -47,6 +47,7 @@ import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.invoice.model.CreditAdjInvoiceItem;
 import com.ning.billing.invoice.model.CreditBalanceAdjInvoiceItem;
 import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
+import com.ning.billing.invoice.model.ItemAdjInvoiceItem;
 import com.ning.billing.invoice.model.RecurringInvoiceItem;
 import com.ning.billing.invoice.model.RefundAdjInvoiceItem;
 import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
@@ -149,8 +150,11 @@ public interface InvoiceItemSqlDao extends EntitySqlDao<InvoiceItem> {
                 case REPAIR_ADJ:
                     item = new RepairAdjInvoiceItem(id, invoiceId, accountId, startDate, endDate, amount, currency, linkedItemId);
                     break;
+                case ITEM_ADJ:
+                    item = new ItemAdjInvoiceItem(id, invoiceId, accountId, startDate, amount, currency, linkedItemId);
+                    break;
                 default:
-                    throw new RuntimeException("Unexpected type of event item " + item);
+                    throw new RuntimeException("Unexpected type of event item " + type);
             }
             return item;
         }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemList.java b/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemList.java
index 3b495a4..616e935 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemList.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemList.java
@@ -41,7 +41,7 @@ public class InvoiceItemList extends ArrayList<InvoiceItem> {
     }
 
     public BigDecimal getTotalAdjAmount() {
-        return getAmoutForItems(InvoiceItemType.CREDIT_ADJ, InvoiceItemType.REFUND_ADJ);
+        return getAmoutForItems(InvoiceItemType.CREDIT_ADJ, InvoiceItemType.REFUND_ADJ, InvoiceItemType.ITEM_ADJ);
     }
 
     public BigDecimal getCreditAdjAmount() {
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/ItemAdjInvoiceItem.java b/invoice/src/main/java/com/ning/billing/invoice/model/ItemAdjInvoiceItem.java
new file mode 100644
index 0000000..cbfe229
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/ItemAdjInvoiceItem.java
@@ -0,0 +1,50 @@
+/*
+ * 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.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
+
+public class ItemAdjInvoiceItem extends AdjInvoiceItem {
+
+    public ItemAdjInvoiceItem(final InvoiceItem invoiceItem, final LocalDate effectiveDate,
+                              final BigDecimal amount, final Currency currency) {
+        super(invoiceItem.getInvoiceId(), invoiceItem.getAccountId(), effectiveDate, effectiveDate,
+              amount, currency, invoiceItem.getId());
+    }
+
+    public ItemAdjInvoiceItem(final UUID id, final UUID invoiceId, final UUID accountId, final LocalDate startDate,
+                              final BigDecimal amount, final Currency currency, final UUID linkedItemId) {
+        super(id, invoiceId, accountId, startDate, startDate, amount, currency, linkedItemId);
+    }
+
+    @Override
+    public InvoiceItemType getInvoiceItemType() {
+        return InvoiceItemType.ITEM_ADJ;
+    }
+
+    @Override
+    public String getDescription() {
+        return "item-adj";
+    }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/migration/InvoiceApiTestBase.java b/invoice/src/test/java/com/ning/billing/invoice/api/migration/InvoiceApiTestBase.java
new file mode 100644
index 0000000..4d243c1
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/migration/InvoiceApiTestBase.java
@@ -0,0 +1,163 @@
+/*
+ * 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.migration;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.AfterSuite;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.Guice;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.AccountApiException;
+import com.ning.billing.account.api.AccountUserApi;
+import com.ning.billing.catalog.MockPlan;
+import com.ning.billing.catalog.MockPlanPhase;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.entitlement.api.SubscriptionTransitionType;
+import com.ning.billing.entitlement.api.billing.BillingModeType;
+import com.ning.billing.entitlement.api.user.Subscription;
+import com.ning.billing.invoice.InvoiceDispatcher;
+import com.ning.billing.invoice.MockBillingEventSet;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceMigrationApi;
+import com.ning.billing.invoice.api.InvoiceNotifier;
+import com.ning.billing.invoice.api.InvoicePaymentApi;
+import com.ning.billing.invoice.api.InvoiceUserApi;
+import com.ning.billing.invoice.dao.InvoiceDao;
+import com.ning.billing.invoice.generator.InvoiceGenerator;
+import com.ning.billing.invoice.notification.NullInvoiceNotifier;
+import com.ning.billing.invoice.tests.InvoicingTestBase;
+import com.ning.billing.junction.api.BillingApi;
+import com.ning.billing.junction.api.BillingEventSet;
+import com.ning.billing.mock.api.MockBillCycleDay;
+import com.ning.billing.util.bus.BusService;
+import com.ning.billing.util.bus.DefaultBusService;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.DefaultCallContextFactory;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.globallocker.GlobalLocker;
+
+import com.google.inject.Inject;
+
+@Guice(modules = {MockModuleNoEntitlement.class})
+public abstract class InvoiceApiTestBase extends InvoicingTestBase {
+
+    protected static final Currency accountCurrency = Currency.USD;
+
+    @Inject
+    protected InvoiceUserApi invoiceUserApi;
+
+    @Inject
+    protected InvoicePaymentApi invoicePaymentApi;
+
+    @Inject
+    protected InvoiceMigrationApi migrationApi;
+
+    @Inject
+    protected InvoiceGenerator generator;
+
+    @Inject
+    protected BillingApi billingApi;
+
+    @Inject
+    protected AccountUserApi accountUserApi;
+
+    @Inject
+    protected BusService busService;
+
+    @Inject
+    protected InvoiceDao invoiceDao;
+
+    @Inject
+    protected GlobalLocker locker;
+
+    @Inject
+    protected Clock clock;
+
+    @BeforeSuite(groups = "slow")
+    public void setup() throws Exception {
+        busService.getBus().start();
+    }
+
+    @AfterSuite(groups = "slow")
+    public void tearDown() {
+        try {
+            ((DefaultBusService) busService).stopBus();
+        } catch (Exception e) {
+            log.warn("Failed to tearDown test properly ", e);
+        }
+    }
+
+    protected UUID generateRegularInvoice(final Account account, final DateTime targetDate) throws Exception {
+        final Subscription subscription = Mockito.mock(Subscription.class);
+        Mockito.when(subscription.getId()).thenReturn(UUID.randomUUID());
+        Mockito.when(subscription.getBundleId()).thenReturn(new UUID(0L, 0L));
+        final BillingEventSet events = new MockBillingEventSet();
+        final Plan plan = MockPlan.createBicycleNoTrialEvergreen1USD();
+        final PlanPhase planPhase = MockPlanPhase.create1USDMonthlyEvergreen();
+        final DateTime effectiveDate = new DateTime().minusDays(1);
+        final Currency currency = Currency.USD;
+        final BigDecimal fixedPrice = null;
+        events.add(createMockBillingEvent(account, subscription, effectiveDate, plan, planPhase,
+                                          fixedPrice, BigDecimal.ONE, currency, BillingPeriod.MONTHLY, 1,
+                                          BillingModeType.IN_ADVANCE, "", 1L, SubscriptionTransitionType.CREATE));
+
+        Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId())).thenReturn(events);
+
+        final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
+        final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountUserApi, billingApi,
+                                                                   invoiceDao, invoiceNotifier, locker, busService.getBus(), clock);
+
+        final CallContext context = new DefaultCallContextFactory(clock).createCallContext("Unit test", CallOrigin.TEST, UserType.TEST);
+        Invoice invoice = dispatcher.processAccount(account.getId(), targetDate, true, context);
+        Assert.assertNotNull(invoice);
+
+        List<Invoice> invoices = invoiceDao.getInvoicesByAccount(account.getId());
+        Assert.assertEquals(invoices.size(), 0);
+
+        invoice = dispatcher.processAccount(account.getId(), targetDate, false, context);
+        Assert.assertNotNull(invoice);
+
+        invoices = invoiceDao.getInvoicesByAccount(account.getId());
+        Assert.assertEquals(invoices.size(), 1);
+
+        return invoice.getId();
+    }
+
+    protected Account createAccount() throws AccountApiException {
+        final UUID accountId = UUID.randomUUID();
+        final Account account = Mockito.mock(Account.class);
+        Mockito.when(accountUserApi.getAccountById(accountId)).thenReturn(account);
+        Mockito.when(account.getCurrency()).thenReturn(accountCurrency);
+        Mockito.when(account.getId()).thenReturn(accountId);
+        Mockito.when(account.isNotifiedForInvoices()).thenReturn(true);
+        Mockito.when(account.getBillCycleDay()).thenReturn(new MockBillCycleDay(31));
+
+        return account;
+    }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java b/invoice/src/test/java/com/ning/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java
index c8c3e71..007f2f6 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java
@@ -23,130 +23,47 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
-import org.mockito.Mockito;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.Assert;
-import org.testng.annotations.AfterSuite;
 import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.BeforeSuite;
-import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
 
-import com.google.inject.Inject;
 import com.ning.billing.account.api.Account;
-import com.ning.billing.account.api.AccountUserApi;
-import com.ning.billing.catalog.MockPlan;
-import com.ning.billing.catalog.MockPlanPhase;
-import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Currency;
-import com.ning.billing.catalog.api.Plan;
-import com.ning.billing.catalog.api.PlanPhase;
-import com.ning.billing.entitlement.api.SubscriptionTransitionType;
-import com.ning.billing.entitlement.api.billing.BillingModeType;
-import com.ning.billing.entitlement.api.user.Subscription;
-import com.ning.billing.invoice.InvoiceDispatcher;
-import com.ning.billing.invoice.MockBillingEventSet;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceMigrationApi;
-import com.ning.billing.invoice.api.InvoiceNotifier;
 import com.ning.billing.invoice.api.InvoicePaymentApi;
 import com.ning.billing.invoice.api.InvoiceUserApi;
-import com.ning.billing.invoice.dao.InvoiceDao;
-import com.ning.billing.invoice.generator.InvoiceGenerator;
-import com.ning.billing.invoice.notification.NullInvoiceNotifier;
-import com.ning.billing.invoice.tests.InvoicingTestBase;
-import com.ning.billing.junction.api.BillingApi;
-import com.ning.billing.junction.api.BillingEventSet;
-import com.ning.billing.mock.api.MockBillCycleDay;
-import com.ning.billing.util.bus.BusService;
-import com.ning.billing.util.bus.DefaultBusService;
-import com.ning.billing.util.callcontext.CallContext;
-import com.ning.billing.util.callcontext.CallOrigin;
-import com.ning.billing.util.callcontext.DefaultCallContextFactory;
-import com.ning.billing.util.callcontext.UserType;
-import com.ning.billing.util.clock.Clock;
-import com.ning.billing.util.clock.ClockMock;
-import com.ning.billing.util.globallocker.GlobalLocker;
-
-@Guice(modules = {MockModuleNoEntitlement.class})
-public class TestDefaultInvoiceMigrationApi extends InvoicingTestBase {
-    private final Logger log = LoggerFactory.getLogger(TestDefaultInvoiceMigrationApi.class);
-
-    @Inject
-    InvoiceUserApi invoiceUserApi;
-
-    @Inject
-    InvoicePaymentApi invoicePaymentApi;
-
-    @Inject
-    private InvoiceGenerator generator;
-
-    @Inject
-    private InvoiceDao invoiceDao;
-
-    @Inject
-    private GlobalLocker locker;
 
-    @Inject
-    private BusService busService;
-
-    @Inject
-    private InvoiceMigrationApi migrationApi;
+import com.google.inject.Inject;
 
-    @Inject
-    private BillingApi billingApi;
+public class TestDefaultInvoiceMigrationApi extends InvoiceApiTestBase {
 
-    @Inject
-    private AccountUserApi accountUserApi;
+    private final Logger log = LoggerFactory.getLogger(TestDefaultInvoiceMigrationApi.class);
 
-    private Account account;
-    private UUID accountId;
-    private UUID subscriptionId;
     private LocalDate date_migrated;
     private DateTime date_regular;
 
+    private UUID accountId;
     private UUID migrationInvoiceId;
     private UUID regularInvoiceId;
 
     private static final BigDecimal MIGRATION_INVOICE_AMOUNT = new BigDecimal("100.00");
     private static final Currency MIGRATION_INVOICE_CURRENCY = Currency.USD;
 
-    private final Clock clock = new ClockMock();
-
-    @BeforeSuite(groups = "slow")
-    public void setup() throws Exception {
-        busService.getBus().start();
-    }
-
     @BeforeMethod(groups = "slow")
     public void setupMethod() throws Exception {
-        accountId = UUID.randomUUID();
-        subscriptionId = UUID.randomUUID();
         date_migrated = clock.getUTCToday().minusYears(1);
         date_regular = clock.getUTCNow();
 
-        account = Mockito.mock(Account.class);
-        Mockito.when(accountUserApi.getAccountById(accountId)).thenReturn(account);
-        Mockito.when(account.getCurrency()).thenReturn(Currency.USD);
-        Mockito.when(account.getId()).thenReturn(accountId);
-        Mockito.when(account.isNotifiedForInvoices()).thenReturn(true);
-        Mockito.when(account.getBillCycleDay()).thenReturn(new MockBillCycleDay(31));
-
-        migrationInvoiceId = createAndCheckMigrationInvoice();
-        regularInvoiceId = generateRegularInvoice();
+        final Account account = createAccount();
+        accountId = account.getId();
+        migrationInvoiceId = createAndCheckMigrationInvoice(accountId);
+        regularInvoiceId = generateRegularInvoice(account, date_regular);
     }
 
-    @AfterSuite(groups = "slow")
-    public void tearDown() {
-        try {
-            ((DefaultBusService) busService).stopBus();
-        } catch (Exception e) {
-            log.warn("Failed to tearDown test properly ", e);
-        }
-    }
-
-    private UUID createAndCheckMigrationInvoice() {
+    private UUID createAndCheckMigrationInvoice(final UUID accountId) {
         final UUID migrationInvoiceId = migrationApi.createMigrationInvoice(accountId, date_migrated, MIGRATION_INVOICE_AMOUNT, MIGRATION_INVOICE_CURRENCY);
         Assert.assertNotNull(migrationInvoiceId);
         //Double check it was created and values are correct
@@ -166,42 +83,6 @@ public class TestDefaultInvoiceMigrationApi extends InvoicingTestBase {
         return migrationInvoiceId;
     }
 
-    private UUID generateRegularInvoice() throws Exception {
-        final Subscription subscription = Mockito.mock(Subscription.class);
-        Mockito.when(subscription.getId()).thenReturn(subscriptionId);
-        Mockito.when(subscription.getBundleId()).thenReturn(new UUID(0L, 0L));
-        final BillingEventSet events = new MockBillingEventSet();
-        final Plan plan = MockPlan.createBicycleNoTrialEvergreen1USD();
-        final PlanPhase planPhase = MockPlanPhase.create1USDMonthlyEvergreen();
-        final DateTime effectiveDate = new DateTime().minusDays(1);
-        final Currency currency = Currency.USD;
-        final BigDecimal fixedPrice = null;
-        events.add(createMockBillingEvent(account, subscription, effectiveDate, plan, planPhase,
-                                          fixedPrice, BigDecimal.ONE, currency, BillingPeriod.MONTHLY, 1,
-                                          BillingModeType.IN_ADVANCE, "", 1L, SubscriptionTransitionType.CREATE));
-
-        Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId)).thenReturn(events);
-
-        final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
-        final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountUserApi, billingApi,
-                                                                   invoiceDao, invoiceNotifier, locker, busService.getBus(), clock);
-
-        final CallContext context = new DefaultCallContextFactory(clock).createCallContext("Migration test", CallOrigin.TEST, UserType.TEST);
-        Invoice invoice = dispatcher.processAccount(accountId, date_regular, true, context);
-        Assert.assertNotNull(invoice);
-
-        List<Invoice> invoices = invoiceDao.getInvoicesByAccount(accountId);
-        Assert.assertEquals(invoices.size(), 0);
-
-        invoice = dispatcher.processAccount(accountId, date_regular, false, context);
-        Assert.assertNotNull(invoice);
-
-        invoices = invoiceDao.getInvoicesByAccount(accountId);
-        Assert.assertEquals(invoices.size(), 1);
-
-        return invoice.getId();
-    }
-
     @Test(groups = "slow")
     public void testUserApiAccess() {
         final List<Invoice> byAccount = invoiceUserApi.getInvoicesByAccount(accountId);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/user/TestDefaultInvoiceUserApi.java b/invoice/src/test/java/com/ning/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
new file mode 100644
index 0000000..88ce414
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
@@ -0,0 +1,207 @@
+/*
+ * 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.math.BigDecimal;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.ErrorCode;
+import com.ning.billing.account.api.Account;
+import com.ning.billing.invoice.api.InvoiceApiException;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
+import com.ning.billing.invoice.api.migration.InvoiceApiTestBase;
+import com.ning.billing.invoice.model.InvoicingConfiguration;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.DefaultCallContextFactory;
+import com.ning.billing.util.callcontext.UserType;
+
+public class TestDefaultInvoiceUserApi extends InvoiceApiTestBase {
+
+    private UUID accountId;
+    private UUID invoiceId;
+    private CallContext context;
+
+    @BeforeMethod(groups = "slow")
+    public void setupMethod() throws Exception {
+        final Account account = createAccount();
+        accountId = account.getId();
+        invoiceId = generateRegularInvoice(account, clock.getUTCNow());
+        context = new DefaultCallContextFactory(clock).createCallContext("Unit test", CallOrigin.TEST, UserType.TEST);
+    }
+
+    @Test(groups = "slow")
+    public void testAdjustFullInvoice() throws Exception {
+        // Verify the initial invoice balance
+        final BigDecimal invoiceBalance = invoiceUserApi.getInvoice(invoiceId).getBalance();
+        Assert.assertEquals(invoiceBalance.compareTo(BigDecimal.ZERO), 1);
+
+        // Verify the initial account balance
+        final BigDecimal accountBalance = invoiceUserApi.getAccountBalance(accountId);
+        Assert.assertEquals(accountBalance, invoiceBalance);
+
+        // Adjust the invoice for the full amount
+        final InvoiceItem creditInvoiceItem = invoiceUserApi.insertCreditForInvoice(accountId, invoiceId, invoiceBalance,
+                                                                                    clock.getUTCToday(), accountCurrency, context);
+        Assert.assertEquals(creditInvoiceItem.getInvoiceId(), invoiceId);
+        Assert.assertEquals(creditInvoiceItem.getInvoiceItemType(), InvoiceItemType.CREDIT_ADJ);
+        Assert.assertEquals(creditInvoiceItem.getAccountId(), accountId);
+        Assert.assertEquals(creditInvoiceItem.getAmount(), invoiceBalance.negate());
+        Assert.assertEquals(creditInvoiceItem.getCurrency(), accountCurrency);
+        Assert.assertNull(creditInvoiceItem.getLinkedItemId());
+
+        // Verify the adjusted invoice balance
+        final BigDecimal adjustedInvoiceBalance = invoiceUserApi.getInvoice(invoiceId).getBalance();
+        Assert.assertEquals(adjustedInvoiceBalance.compareTo(BigDecimal.ZERO), 0);
+
+        // Verify the adjusted account balance
+        final BigDecimal adjustedAccountBalance = invoiceUserApi.getAccountBalance(accountId);
+        Assert.assertEquals(adjustedAccountBalance, adjustedInvoiceBalance);
+    }
+
+    @Test(groups = "slow")
+    public void testAdjustPartialInvoice() throws Exception {
+        // Verify the initial invoice balance
+        final BigDecimal invoiceBalance = invoiceUserApi.getInvoice(invoiceId).getBalance();
+        Assert.assertEquals(invoiceBalance.compareTo(BigDecimal.ZERO), 1);
+
+        // Verify the initial account balance
+        final BigDecimal accountBalance = invoiceUserApi.getAccountBalance(accountId);
+        Assert.assertEquals(accountBalance, invoiceBalance);
+
+        // Adjust the invoice for a fraction of the balance
+        final BigDecimal creditAmount = invoiceBalance.divide(BigDecimal.TEN);
+        final InvoiceItem creditInvoiceItem = invoiceUserApi.insertCreditForInvoice(accountId, invoiceId, creditAmount,
+                                                                                    clock.getUTCToday(), accountCurrency, context);
+        Assert.assertEquals(creditInvoiceItem.getInvoiceId(), invoiceId);
+        Assert.assertEquals(creditInvoiceItem.getInvoiceItemType(), InvoiceItemType.CREDIT_ADJ);
+        Assert.assertEquals(creditInvoiceItem.getAccountId(), accountId);
+        Assert.assertEquals(creditInvoiceItem.getAmount(), creditAmount.negate());
+        Assert.assertEquals(creditInvoiceItem.getCurrency(), accountCurrency);
+        Assert.assertNull(creditInvoiceItem.getLinkedItemId());
+
+        // Verify the adjusted invoice balance
+        final BigDecimal adjustedInvoiceBalance = invoiceUserApi.getInvoice(invoiceId).getBalance();
+        // Note! The invoice code will round (see InvoiceItemList)
+        Assert.assertEquals(adjustedInvoiceBalance, invoiceBalance.add(creditAmount.negate()).setScale(InvoicingConfiguration.getNumberOfDecimals(),
+                                                                                                       InvoicingConfiguration.getRoundingMode()));
+
+        // Verify the adjusted account balance
+        final BigDecimal adjustedAccountBalance = invoiceUserApi.getAccountBalance(accountId);
+        Assert.assertEquals(adjustedAccountBalance, adjustedInvoiceBalance);
+    }
+
+    @Test(groups = "slow")
+    public void testCantAdjustInvoiceWithNegativeAmount() throws Exception {
+        try {
+            invoiceUserApi.insertCreditForInvoice(accountId, invoiceId, BigDecimal.TEN.negate(), clock.getUTCToday(), accountCurrency, context);
+            Assert.fail("Should not have been able to adjust an invoice with a negative amount");
+        } catch (InvoiceApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.CREDIT_AMOUNT_INVALID.getCode());
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testAdjustFullInvoiceItem() throws Exception {
+        final InvoiceItem invoiceItem = invoiceUserApi.getInvoice(invoiceId).getInvoiceItems().get(0);
+        // Verify we picked a non zero item
+        Assert.assertEquals(invoiceItem.getAmount().compareTo(BigDecimal.ZERO), 1);
+
+        // Verify the initial invoice balance
+        final BigDecimal invoiceBalance = invoiceUserApi.getInvoice(invoiceId).getBalance();
+        Assert.assertEquals(invoiceBalance.compareTo(BigDecimal.ZERO), 1);
+
+        // Verify the initial account balance
+        final BigDecimal accountBalance = invoiceUserApi.getAccountBalance(accountId);
+        Assert.assertEquals(accountBalance, invoiceBalance);
+
+        // Adjust the invoice for the full amount
+        final InvoiceItem adjInvoiceItem = invoiceUserApi.insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItem.getId(),
+                                                                                      clock.getUTCToday(), null, null, context);
+        Assert.assertEquals(adjInvoiceItem.getInvoiceId(), invoiceId);
+        Assert.assertEquals(adjInvoiceItem.getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+        Assert.assertEquals(adjInvoiceItem.getAccountId(), accountId);
+        Assert.assertEquals(adjInvoiceItem.getAmount(), invoiceItem.getAmount().negate());
+        Assert.assertEquals(adjInvoiceItem.getCurrency(), accountCurrency);
+        Assert.assertEquals(adjInvoiceItem.getLinkedItemId(), invoiceItem.getId());
+
+        // Verify the adjusted invoice balance
+        final BigDecimal adjustedInvoiceBalance = invoiceUserApi.getInvoice(invoiceId).getBalance();
+        // Note! The invoice code will round (see InvoiceItemList)
+        Assert.assertEquals(adjustedInvoiceBalance, invoiceBalance.add(invoiceItem.getAmount().negate()).setScale(InvoicingConfiguration.getNumberOfDecimals(),
+                                                                                                                  InvoicingConfiguration.getRoundingMode()));
+
+        // Verify the adjusted account balance
+        final BigDecimal adjustedAccountBalance = invoiceUserApi.getAccountBalance(accountId);
+        Assert.assertEquals(adjustedAccountBalance, adjustedInvoiceBalance);
+    }
+
+    @Test(groups = "slow")
+    public void testAdjustPartialInvoiceItem() throws Exception {
+        final InvoiceItem invoiceItem = invoiceUserApi.getInvoice(invoiceId).getInvoiceItems().get(0);
+        // Verify we picked a non zero item
+        Assert.assertEquals(invoiceItem.getAmount().compareTo(BigDecimal.ZERO), 1);
+
+        // Verify the initial invoice balance
+        final BigDecimal invoiceBalance = invoiceUserApi.getInvoice(invoiceId).getBalance();
+        Assert.assertEquals(invoiceBalance.compareTo(BigDecimal.ZERO), 1);
+
+        // Verify the initial account balance
+        final BigDecimal accountBalance = invoiceUserApi.getAccountBalance(accountId);
+        Assert.assertEquals(accountBalance, invoiceBalance);
+
+        // Adjust the invoice for a fraction of the balance
+        final BigDecimal adjAmount = invoiceItem.getAmount().divide(BigDecimal.TEN);
+        final InvoiceItem adjInvoiceItem = invoiceUserApi.insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItem.getId(),
+                                                                                      clock.getUTCToday(), adjAmount, accountCurrency,
+                                                                                      context);
+        Assert.assertEquals(adjInvoiceItem.getInvoiceId(), invoiceId);
+        Assert.assertEquals(adjInvoiceItem.getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+        Assert.assertEquals(adjInvoiceItem.getAccountId(), accountId);
+        Assert.assertEquals(adjInvoiceItem.getAmount(), adjAmount.negate());
+        Assert.assertEquals(adjInvoiceItem.getCurrency(), accountCurrency);
+        Assert.assertEquals(adjInvoiceItem.getLinkedItemId(), invoiceItem.getId());
+
+        // Verify the adjusted invoice balance
+        final BigDecimal adjustedInvoiceBalance = invoiceUserApi.getInvoice(invoiceId).getBalance();
+        // Note! The invoice code will round (see InvoiceItemList)
+        Assert.assertEquals(adjustedInvoiceBalance, invoiceBalance.add(adjAmount.negate()).setScale(InvoicingConfiguration.getNumberOfDecimals(),
+                                                                                                    InvoicingConfiguration.getRoundingMode()));
+
+        // Verify the adjusted account balance
+        final BigDecimal adjustedAccountBalance = invoiceUserApi.getAccountBalance(accountId);
+        Assert.assertEquals(adjustedAccountBalance, adjustedInvoiceBalance);
+    }
+
+    @Test(groups = "slow")
+    public void testCantAdjustInvoiceItemWithNegativeAmount() throws Exception {
+        final InvoiceItem invoiceItem = invoiceUserApi.getInvoice(invoiceId).getInvoiceItems().get(0);
+
+        try {
+            invoiceUserApi.insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItem.getId(), clock.getUTCToday(),
+                                                       BigDecimal.TEN.negate(), accountCurrency, context);
+            Assert.fail("Should not have been able to adjust an item with a negative amount");
+        } catch (InvoiceApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.INVOICE_ITEM_ADJUSTMENT_AMOUNT_INVALID.getCode());
+        }
+    }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
index 09c196b..a483c06 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
@@ -23,6 +23,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 
@@ -269,6 +271,12 @@ public class MockInvoiceDao implements InvoiceDao {
     }
 
     @Override
+    public InvoiceItem insertInvoiceItemAdjustment(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId,
+                                                   final LocalDate effectiveDate, @Nullable final BigDecimal amount, @Nullable final Currency currency, final CallContext context) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public BigDecimal getAccountCBA(UUID accountId) {
         return null;
     }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java
new file mode 100644
index 0000000..5e82f30
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java
@@ -0,0 +1,147 @@
+/*
+ * 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.dao;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.ErrorCode;
+import com.ning.billing.catalog.api.Currency;
+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.InvoiceItemType;
+import com.ning.billing.invoice.model.DefaultInvoice;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
+import com.ning.billing.util.callcontext.CallContext;
+
+public class TestInvoiceDaoForItemAdjustment extends InvoiceDaoTestBase {
+
+    private static final BigDecimal INVOICE_ITEM_AMOUNT = new BigDecimal("21.00");
+
+    @Test(groups = "slow")
+    public void testAddInvoiceItemAdjustmentForNonExistingInvoiceItemId() throws Exception {
+        final UUID accountId = UUID.randomUUID();
+        final UUID invoiceId = UUID.randomUUID();
+        final UUID invoiceItemId = UUID.randomUUID();
+        final LocalDate effectiveDate = new LocalDate();
+        final CallContext context = Mockito.mock(CallContext.class);
+
+        try {
+            invoiceDao.insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItemId, effectiveDate, null, null, context);
+            Assert.fail("Should not have been able to adjust a non existing invoice item");
+        } catch (Exception e) {
+            Assert.assertEquals(((InvoiceApiException) e.getCause()).getCode(), ErrorCode.INVOICE_ITEM_NOT_FOUND.getCode());
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testAddInvoiceItemAdjustmentForWrongInvoice() throws Exception {
+        final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+        final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoice.getId(), invoice.getAccountId(), UUID.randomUUID(),
+                                                                 UUID.randomUUID(), "test plan", "test phase",
+                                                                 new LocalDate(2010, 1, 1), new LocalDate(2010, 4, 1),
+                                                                 INVOICE_ITEM_AMOUNT, new BigDecimal("7.00"), Currency.USD);
+        invoice.addInvoiceItem(invoiceItem);
+        invoiceDao.create(invoice, 1, context);
+
+        try {
+            invoiceDao.insertInvoiceItemAdjustment(invoice.getAccountId(), UUID.randomUUID(), invoiceItem.getId(), new LocalDate(2010, 1, 1), null, null, context);
+            Assert.fail("Should not have been able to adjust an item on a non existing invoice");
+        } catch (Exception e) {
+            Assert.assertEquals(((InvoiceApiException) e.getCause()).getCode(), ErrorCode.INVOICE_INVALID_FOR_INVOICE_ITEM_ADJUSTMENT.getCode());
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testAddInvoiceItemAdjustmentForFullAmount() throws Exception {
+        final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+        final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoice.getId(), invoice.getAccountId(), UUID.randomUUID(),
+                                                                 UUID.randomUUID(), "test plan", "test phase",
+                                                                 new LocalDate(2010, 1, 1), new LocalDate(2010, 4, 1),
+                                                                 INVOICE_ITEM_AMOUNT, new BigDecimal("7.00"), Currency.USD);
+        invoice.addInvoiceItem(invoiceItem);
+        invoiceDao.create(invoice, 1, context);
+
+        final InvoiceItem adjustedInvoiceItem = createAndCheckAdjustment(invoice, invoiceItem, null);
+        Assert.assertEquals(adjustedInvoiceItem.getAmount().compareTo(invoiceItem.getAmount().negate()), 0);
+    }
+
+    @Test(groups = "slow")
+    public void testAddInvoiceItemAdjustmentForPartialAmount() throws Exception {
+        final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+        final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoice.getId(), invoice.getAccountId(), UUID.randomUUID(),
+                                                                 UUID.randomUUID(), "test plan", "test phase",
+                                                                 new LocalDate(2010, 1, 1), new LocalDate(2010, 4, 1),
+                                                                 INVOICE_ITEM_AMOUNT, new BigDecimal("7.00"), Currency.USD);
+        invoice.addInvoiceItem(invoiceItem);
+        invoiceDao.create(invoice, 1, context);
+
+        final InvoiceItem adjustedInvoiceItem = createAndCheckAdjustment(invoice, invoiceItem, BigDecimal.TEN);
+        Assert.assertEquals(adjustedInvoiceItem.getAmount().compareTo(BigDecimal.TEN.negate()), 0);
+    }
+
+    private InvoiceItem createAndCheckAdjustment(final Invoice invoice, final InvoiceItem invoiceItem, final BigDecimal amount) {
+        final LocalDate effectiveDate = new LocalDate(2010, 1, 1);
+        final InvoiceItem adjustedInvoiceItem = invoiceDao.insertInvoiceItemAdjustment(invoice.getAccountId(), invoice.getId(), invoiceItem.getId(),
+                                                                                       effectiveDate, amount, null, context);
+        Assert.assertEquals(adjustedInvoiceItem.getAccountId(), invoiceItem.getAccountId());
+        Assert.assertNull(adjustedInvoiceItem.getBundleId());
+        Assert.assertEquals(adjustedInvoiceItem.getCurrency(), invoiceItem.getCurrency());
+        Assert.assertEquals(adjustedInvoiceItem.getDescription(), "item-adj");
+        Assert.assertEquals(adjustedInvoiceItem.getEndDate(), effectiveDate);
+        Assert.assertEquals(adjustedInvoiceItem.getInvoiceId(), invoiceItem.getInvoiceId());
+        Assert.assertEquals(adjustedInvoiceItem.getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+        Assert.assertEquals(adjustedInvoiceItem.getLinkedItemId(), invoiceItem.getId());
+        Assert.assertNull(adjustedInvoiceItem.getPhaseName());
+        Assert.assertNull(adjustedInvoiceItem.getPlanName());
+        Assert.assertNull(adjustedInvoiceItem.getRate());
+        Assert.assertEquals(adjustedInvoiceItem.getStartDate(), effectiveDate);
+        Assert.assertNull(adjustedInvoiceItem.getSubscriptionId());
+
+        // Retrieve the item by id
+        final InvoiceItem retrievedInvoiceItem = invoiceItemSqlDao.getById(adjustedInvoiceItem.getId().toString());
+        Assert.assertEquals(retrievedInvoiceItem, adjustedInvoiceItem);
+
+        // Retrieve the item by invoice id
+        final Invoice retrievedInvoice = invoiceDao.getById(adjustedInvoiceItem.getInvoiceId());
+        final List<InvoiceItem> invoiceItems = retrievedInvoice.getInvoiceItems();
+        Assert.assertEquals(invoiceItems.size(), 2);
+        final InvoiceItem retrievedByInvoiceInvoiceItem;
+        if (invoiceItems.get(0).getId().equals(adjustedInvoiceItem.getId())) {
+            retrievedByInvoiceInvoiceItem = invoiceItems.get(0);
+        } else {
+            retrievedByInvoiceInvoiceItem = invoiceItems.get(1);
+        }
+        Assert.assertEquals(retrievedByInvoiceInvoiceItem, adjustedInvoiceItem);
+
+        // Verify the invoice balance
+        if (amount == null) {
+            Assert.assertEquals(retrievedInvoice.getBalance().compareTo(BigDecimal.ZERO), 0);
+        } else {
+            Assert.assertEquals(retrievedInvoice.getBalance().compareTo(INVOICE_ITEM_AMOUNT.add(amount.negate())), 0);
+        }
+
+        return adjustedInvoiceItem;
+    }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/model/TestItemAdjInvoiceItem.java b/invoice/src/test/java/com/ning/billing/invoice/model/TestItemAdjInvoiceItem.java
new file mode 100644
index 0000000..316b613
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/model/TestItemAdjInvoiceItem.java
@@ -0,0 +1,39 @@
+/*
+ * 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.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
+
+public class TestItemAdjInvoiceItem {
+
+    @Test(groups = "fast")
+    public void testType() throws Exception {
+        final InvoiceItem invoiceItem = new ItemAdjInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(),
+                                                               new LocalDate(2010, 1, 1), new BigDecimal("7.00"), Currency.USD,
+                                                               UUID.randomUUID());
+        Assert.assertEquals(invoiceItem.getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+    }
+}