killbill-memoizeit

jaxrs: add JAX-RS API to adjust items when creating a refund Signed-off-by:

8/2/2012 9:06:34 PM

Details

diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
index 87fcff3..3f49504 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
@@ -16,8 +16,11 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import javax.ws.rs.Consumes;
@@ -37,6 +40,7 @@ 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.jaxrs.json.CustomFieldJson;
+import com.ning.billing.jaxrs.json.InvoiceItemJsonSimple;
 import com.ning.billing.jaxrs.json.RefundJson;
 import com.ning.billing.jaxrs.util.Context;
 import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
@@ -112,8 +116,18 @@ public class PaymentResource extends JaxRsResourceBase {
 
         final Refund result;
         if (json.isAdjusted()) {
-            result = paymentApi.createRefundWithAdjustment(account, paymentUuid, json.getRefundAmount(), context.createContext(createdBy, reason, comment));
+            if (json.getAdjustments() != null && json.getAdjustments().size() > 0) {
+                final Map<UUID, BigDecimal> adjustments = new HashMap<UUID, BigDecimal>();
+                for (final InvoiceItemJsonSimple item : json.getAdjustments()) {
+                    adjustments.put(UUID.fromString(item.getInvoiceItemId()), item.getAmount());
+                }
+                result = paymentApi.createRefundWithItemsAdjustments(account, paymentUuid, adjustments, context.createContext(createdBy, reason, comment));
+            } else {
+                // Invoice adjustment
+                result = paymentApi.createRefundWithAdjustment(account, paymentUuid, json.getRefundAmount(), context.createContext(createdBy, reason, comment));
+            }
         } else {
+            // Refund without adjustment
             result = paymentApi.createRefund(account, paymentUuid, json.getRefundAmount(), context.createContext(createdBy, reason, comment));
         }
 
diff --git a/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java
index ff4ed16..63706ba 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java
@@ -36,6 +36,7 @@ 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.invoice.api.InvoiceApiException;
+import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.invoice.api.InvoicePaymentApi;
 import com.ning.billing.payment.api.DefaultRefund;
 import com.ning.billing.payment.api.PaymentApiException;
@@ -87,12 +88,12 @@ public class RefundProcessor extends ProcessorBase {
     /**
      * Create a refund and adjust the invoice or invoice items as necessary.
      *
-     * @param account      account to refund
-     * @param paymentId    payment associated with that refund
-     * @param specifiedRefundAmount amount to refund. If null, the amount will be the sum of adjusted invoice items
-     * @param isAdjusted whether the refund should trigger an invoice or invoice item adjustment
+     * @param account                   account to refund
+     * @param paymentId                 payment associated with that refund
+     * @param specifiedRefundAmount     amount to refund. If null, the amount will be the sum of adjusted invoice items
+     * @param isAdjusted                whether the refund should trigger an invoice or invoice item adjustment
      * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
-     * @param context the call context
+     * @param context                   the call context
      * @return the created context
      * @throws PaymentApiException
      */
@@ -105,7 +106,7 @@ public class RefundProcessor extends ProcessorBase {
             @Override
             public Refund doOperation() throws PaymentApiException {
                 // First, compute the refund amount, if necessary
-                final BigDecimal refundAmount = computeRefundAmount(specifiedRefundAmount, invoiceItemIdsWithAmounts);
+                final BigDecimal refundAmount = computeRefundAmount(paymentId, specifiedRefundAmount, invoiceItemIdsWithAmounts);
 
                 try {
 
@@ -180,14 +181,18 @@ public class RefundProcessor extends ProcessorBase {
     /**
      * Compute the refund amount (computed from the invoice or invoice items as necessary).
      *
+     * @param paymentId                 payment id associated with this refund
      * @param specifiedRefundAmount     amount to refund. If null, the amount will be the sum of adjusted invoice items
      * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
      * @return the refund amount
      */
-    private BigDecimal computeRefundAmount(@Nullable final BigDecimal specifiedRefundAmount, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts) {
+    private BigDecimal computeRefundAmount(final UUID paymentId, @Nullable final BigDecimal specifiedRefundAmount, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts) {
+        final List<InvoiceItem> items = invoicePaymentApi.getInvoiceForPaymentId(paymentId).getInvoiceItems();
+
         BigDecimal amountFromItems = BigDecimal.ZERO;
-        for (final BigDecimal itemAmount : invoiceItemIdsWithAmounts.values()) {
-            amountFromItems = amountFromItems.add(itemAmount);
+        for (final UUID itemId : invoiceItemIdsWithAmounts.keySet()) {
+            amountFromItems = amountFromItems.add(Objects.firstNonNull(invoiceItemIdsWithAmounts.get(itemId),
+                                                                       getAmountFromItem(items, itemId)));
         }
 
         // Sanity check: if some items were specified, then the sum should be equal to specified refund amount, if specified
@@ -198,6 +203,16 @@ public class RefundProcessor extends ProcessorBase {
         return Objects.firstNonNull(specifiedRefundAmount, amountFromItems);
     }
 
+    private BigDecimal getAmountFromItem(final List<InvoiceItem> items, final UUID itemId) {
+        for (final InvoiceItem item : items) {
+            if (item.getId().equals(itemId)) {
+                return item.getAmount();
+            }
+        }
+
+        throw new IllegalArgumentException("Unable to find invoice item for id " + itemId);
+    }
+
     public Refund getRefund(final UUID refundId)
             throws PaymentApiException {
         RefundModelDao result = paymentDao.getRefund(refundId);
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
index f45c6db..c5334d9 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
@@ -13,10 +13,9 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package com.ning.billing.jaxrs;
 
-import javax.annotation.Nullable;
-import javax.ws.rs.core.Response.Status;
 import java.io.IOException;
 import java.lang.reflect.Method;
 import java.math.BigDecimal;
@@ -31,6 +30,9 @@ import java.util.Map.Entry;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
+import javax.annotation.Nullable;
+import javax.ws.rs.core.Response.Status;
+
 import org.eclipse.jetty.servlet.FilterHolder;
 import org.joda.time.DateTime;
 import org.skife.config.ConfigurationObjectFactory;
@@ -38,27 +40,16 @@ import org.skife.jdbi.v2.IDBI;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.Assert;
-import org.testng.annotations.AfterMethod;
 import org.testng.annotations.AfterSuite;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.BeforeSuite;
 
-import com.fasterxml.jackson.core.JsonGenerationException;
-import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-import com.google.common.collect.ImmutableMap;
-import com.google.inject.Module;
 import com.ning.billing.KillbillTestSuiteWithEmbeddedDB;
-import com.ning.billing.account.api.BillCycleDay;
 import com.ning.billing.account.glue.AccountModule;
 import com.ning.billing.analytics.setup.AnalyticsModule;
 import com.ning.billing.api.TestApiListener;
 import com.ning.billing.beatrix.glue.BeatrixModule;
-import com.ning.billing.beatrix.integration.TestIntegration;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.catalog.api.PriceListSet;
@@ -102,7 +93,6 @@ import com.ning.billing.util.glue.CustomFieldModule;
 import com.ning.billing.util.glue.GlobalLockerModule;
 import com.ning.billing.util.glue.NotificationQueueModule;
 import com.ning.billing.util.glue.TagStoreModule;
-import com.ning.billing.util.io.IOUtils;
 import com.ning.http.client.AsyncCompletionHandler;
 import com.ning.http.client.AsyncHttpClient;
 import com.ning.http.client.AsyncHttpClient.BoundRequestBuilder;
@@ -112,11 +102,19 @@ import com.ning.http.client.Response;
 import com.ning.jetty.core.CoreConfig;
 import com.ning.jetty.core.server.HttpServer;
 
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Module;
+
 import static com.ning.billing.jaxrs.resources.JaxrsResource.QUERY_PAYMENT_METHOD_PLUGIN_INFO;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 
 public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
+
     protected static final String PLUGIN_NAME = "noop";
 
     // STEPH
@@ -156,6 +154,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
     }
 
     public static class TestKillbillGuiceListener extends KillbillGuiceListener {
+
         private final MysqlTestingHelper helper;
         private final Clock clock;
 
@@ -185,6 +184,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
     }
 
     public static class InvoiceModuleWithMockSender extends DefaultInvoiceModule {
+
         @Override
         protected void installInvoiceNotifier() {
             bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
@@ -192,6 +192,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
     }
 
     public static class TestKillbillServerModule extends KillbillServerModule {
+
         private final MysqlTestingHelper helper;
         private final Clock clock;
 
@@ -207,6 +208,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
         }
 
         private static final class PaymentMockModule extends PaymentModule {
+
             @Override
             protected void installPaymentProviderPlugins(final PaymentConfig config) {
                 install(new MockPaymentProviderPluginModule(PLUGIN_NAME));
@@ -327,7 +329,6 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
         return properties;
     }
 
-
     protected List<PaymentMethodProperties> getPaymentMethodPaypalProperties() {
         final List<PaymentMethodProperties> properties = new ArrayList<PaymentMethodJson.PaymentMethodProperties>();
         properties.add(new PaymentMethodProperties("type", "CreditCard", false));
@@ -336,7 +337,6 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
         return properties;
     }
 
-
     protected PaymentMethodJson getPaymentMethodJson(final String accountId, final List<PaymentMethodProperties> properties) {
         final PaymentMethodPluginDetailJson info = new PaymentMethodPluginDetailJson(null, properties);
         return new PaymentMethodJson(null, accountId, true, PLUGIN_NAME, info);
@@ -359,7 +359,6 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
         Response response = doPost(uri, baseJson, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
         Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
 
-
         queryParams = new HashMap<String, String>();
         queryParams.put(JaxrsResource.QUERY_EXTERNAL_KEY, input.getExternalKey());
         response = doGet(JaxrsResource.ACCOUNTS_PATH, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
@@ -390,7 +389,6 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
         return objFromJson;
     }
 
-
     protected BundleJsonNoSubscriptions createBundle(final String accountId, final String key) throws Exception {
         final BundleJsonNoSubscriptions input = new BundleJsonNoSubscriptions(null, accountId, key, null, null);
         String baseJson = mapper.writeValueAsString(input);
@@ -415,7 +413,6 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
         final SubscriptionJsonNoEvents input = new SubscriptionJsonNoEvents(null, bundleId, null, productName, productCategory, billingPeriod, PriceListSet.DEFAULT_PRICELIST_NAME, null, null);
         String baseJson = mapper.writeValueAsString(input);
 
-
         final Map<String, String> queryParams = waitCompletion ? getQueryParamsForCallCompletion("5") : DEFAULT_EMPTY_QUERY;
         Response response = doPost(JaxrsResource.SUBSCRIPTIONS_PATH, baseJson, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
         Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
@@ -472,45 +469,49 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
     }
 
     protected InvoiceJsonSimple getInvoice(final String invoiceId) throws IOException {
+        return doGetInvoice(invoiceId, Boolean.FALSE, InvoiceJsonSimple.class);
+    }
+
+    protected InvoiceJsonWithItems getInvoiceWithItems(final String invoiceId) throws IOException {
+        return doGetInvoice(invoiceId, Boolean.TRUE, InvoiceJsonWithItems.class);
+    }
+
+    private <T> T doGetInvoice(final String invoiceId, final Boolean withItems, final Class<T> clazz) throws IOException {
         final String uri = JaxrsResource.INVOICES_PATH + "/" + invoiceId;
-        final Response response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+
+        final Map<String, String> queryParams = new HashMap<String, String>();
+        queryParams.put(JaxrsResource.QUERY_INVOICE_WITH_ITEMS, withItems.toString());
+
+        final Response response = doGet(uri, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
         Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
         final String baseJson = response.getResponseBody();
 
-        final InvoiceJsonSimple firstInvoiceJson = mapper.readValue(baseJson, InvoiceJsonSimple.class);
+        final T firstInvoiceJson = mapper.readValue(baseJson, clazz);
         assertNotNull(firstInvoiceJson);
 
         return firstInvoiceJson;
     }
 
     protected List<InvoiceJsonSimple> getInvoicesForAccount(final String accountId) throws IOException {
-        final String invoicesURI = JaxrsResource.INVOICES_PATH;
-
-        final Map<String, String> queryParams = new HashMap<String, String>();
-        queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountId);
-
-        final Response invoicesResponse = doGet(invoicesURI, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
-        assertEquals(invoicesResponse.getStatusCode(), Status.OK.getStatusCode());
-
-        final String invoicesBaseJson = invoicesResponse.getResponseBody();
-        final List<InvoiceJsonSimple> invoices = mapper.readValue(invoicesBaseJson, new TypeReference<List<InvoiceJsonSimple>>() {});
-        assertNotNull(invoices);
-
-        return invoices;
+        return doGetInvoicesForAccount(accountId, Boolean.FALSE, InvoiceJsonSimple.class);
     }
 
     protected List<InvoiceJsonWithItems> getInvoicesWithItemsForAccount(final String accountId) throws IOException {
+        return doGetInvoicesForAccount(accountId, Boolean.TRUE, InvoiceJsonWithItems.class);
+    }
+
+    private <T> List<T> doGetInvoicesForAccount(final String accountId, final Boolean withItems, final Class<T> clazz) throws IOException {
         final String invoicesURI = JaxrsResource.INVOICES_PATH;
 
         final Map<String, String> queryParams = new HashMap<String, String>();
         queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountId);
-        queryParams.put(JaxrsResource.QUERY_INVOICE_WITH_ITEMS, "true");
+        queryParams.put(JaxrsResource.QUERY_INVOICE_WITH_ITEMS, withItems.toString());
 
         final Response invoicesResponse = doGet(invoicesURI, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
         assertEquals(invoicesResponse.getStatusCode(), Status.OK.getStatusCode());
 
         final String invoicesBaseJson = invoicesResponse.getResponseBody();
-        final List<InvoiceJsonWithItems> invoices = mapper.readValue(invoicesBaseJson, new TypeReference<List<InvoiceJsonWithItems>>() {});
+        final List<T> invoices = mapper.readValue(invoicesBaseJson, new TypeReference<List<T>>() {});
         assertNotNull(invoices);
 
         return invoices;
@@ -658,9 +659,28 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
     }
 
     protected RefundJson createRefund(final String paymentId, final BigDecimal amount) throws IOException {
+        return doCreateRefund(paymentId, amount, false, ImmutableMap.<String, BigDecimal>of());
+    }
+
+    protected RefundJson createRefundWithInvoiceAdjustment(final String paymentId, final BigDecimal amount) throws IOException {
+        return doCreateRefund(paymentId, amount, true, ImmutableMap.<String, BigDecimal>of());
+    }
+
+    protected RefundJson createRefundWithInvoiceItemAdjustment(final String paymentId, final String invoiceItemId, final BigDecimal amount) throws IOException {
+        final Map<String, BigDecimal> adjustments = new HashMap<String, BigDecimal>();
+        adjustments.put(invoiceItemId, amount);
+        return doCreateRefund(paymentId, amount, true, adjustments);
+    }
+
+    private RefundJson doCreateRefund(final String paymentId, final BigDecimal amount, final boolean adjusted, final Map<String, BigDecimal> itemAdjustments) throws IOException {
         final String uri = JaxrsResource.PAYMENTS_PATH + "/" + paymentId + "/" + JaxrsResource.REFUNDS;
 
-        final RefundJson refundJson = new RefundJson(null, paymentId, amount, false, null, null, null, null);
+        final List<InvoiceItemJsonSimple> adjustments = new ArrayList<InvoiceItemJsonSimple>();
+        for (final String itemId : itemAdjustments.keySet()) {
+            adjustments.add(new InvoiceItemJsonSimple(itemId, null, null, null, null, null, null, null, null, null,
+                                                      itemAdjustments.get(itemId), null, null));
+        }
+        final RefundJson refundJson = new RefundJson(null, paymentId, amount, adjusted, null, null, adjustments, null);
         final String baseJson = mapper.writeValueAsString(refundJson);
         final Response response = doPost(uri, baseJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
         assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java b/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
index 0913fdc..14c4ae9 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
@@ -16,13 +16,18 @@
 
 package com.ning.billing.jaxrs;
 
+import java.io.IOException;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.List;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import com.ning.billing.jaxrs.json.AccountJson;
+import com.ning.billing.jaxrs.json.InvoiceItemJsonSimple;
+import com.ning.billing.jaxrs.json.InvoiceJsonSimple;
+import com.ning.billing.jaxrs.json.InvoiceJsonWithItems;
 import com.ning.billing.jaxrs.json.PaymentJsonSimple;
 import com.ning.billing.jaxrs.json.PaymentMethodJson;
 import com.ning.billing.jaxrs.json.RefundJson;
@@ -30,28 +35,141 @@ import com.ning.billing.jaxrs.json.RefundJson;
 public class TestPayment extends TestJaxrsBase {
 
     @Test(groups = "slow")
-    public void testPaymentWithRefund() throws Exception {
+    public void testFullRefundWithNoAdjustment() throws Exception {
+        final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
+
+        // Issue a refund for the full amount
+        final BigDecimal refundAmount = paymentJsonSimple.getAmount();
+        final BigDecimal expectedInvoiceBalance = refundAmount;
+
+        // Post and verify the refund
+        final RefundJson refundJsonCheck = createRefund(paymentJsonSimple.getPaymentId(), refundAmount);
+        verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+        // Verify the invoice balance
+        verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+    }
+
+    @Test(groups = "slow")
+    public void testPartialRefundWithNoAdjustment() throws Exception {
+        final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
+
+        // Issue a refund for a fraction of the amount
+        final BigDecimal refundAmount = getFractionOfAmount(paymentJsonSimple.getAmount());
+        final BigDecimal expectedInvoiceBalance = refundAmount;
+
+        // Post and verify the refund
+        final RefundJson refundJsonCheck = createRefund(paymentJsonSimple.getPaymentId(), refundAmount);
+        verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+        // Verify the invoice balance
+        verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+    }
+
+    @Test(groups = "slow")
+    public void testFullRefundWithInvoiceAdjustment() throws Exception {
+        final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
+
+        // Issue a refund for the full amount
+        final BigDecimal refundAmount = paymentJsonSimple.getAmount();
+        final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+        // Post and verify the refund
+        final RefundJson refundJsonCheck = createRefundWithInvoiceAdjustment(paymentJsonSimple.getPaymentId(), refundAmount);
+        verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+        // Verify the invoice balance
+        verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+    }
+
+    @Test(groups = "slow")
+    public void testPartialRefundWithInvoiceAdjustment() throws Exception {
+        final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
+
+        // Issue a refund for a fraction of the amount
+        final BigDecimal refundAmount = getFractionOfAmount(paymentJsonSimple.getAmount());
+        final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+        // Post and verify the refund
+        final RefundJson refundJsonCheck = createRefundWithInvoiceAdjustment(paymentJsonSimple.getPaymentId(), refundAmount);
+        verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+        // Verify the invoice balance
+        verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+    }
+
+    @Test(groups = "slow")
+    public void testRefundWithFullInvoiceItemAdjustment() throws Exception {
+        final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
+
+        // Get the individual items for the invoice
+        final InvoiceJsonWithItems invoice = getInvoiceWithItems(paymentJsonSimple.getInvoiceId());
+        final InvoiceItemJsonSimple itemToAdjust = invoice.getItems().get(0);
+
+        // Issue a refund for the full amount
+        final BigDecimal refundAmount = itemToAdjust.getAmount();
+        final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+        // Post and verify the refund
+        final RefundJson refundJsonCheck = createRefundWithInvoiceItemAdjustment(paymentJsonSimple.getPaymentId(),
+                                                                                 itemToAdjust.getInvoiceItemId(),
+                                                                                 null /* null means full adjustment for that item */);
+        verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+        // Verify the invoice balance
+        verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+    }
+
+    @Test(groups = "slow")
+    public void testPartialRefundWithInvoiceItemAdjustment() throws Exception {
+        final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
+
+        // Get the individual items for the invoice
+        final InvoiceJsonWithItems invoice = getInvoiceWithItems(paymentJsonSimple.getInvoiceId());
+        final InvoiceItemJsonSimple itemToAdjust = invoice.getItems().get(0);
+
+        // Issue a refund for a fraction of the amount
+        final BigDecimal refundAmount = getFractionOfAmount(itemToAdjust.getAmount());
+        final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+        // Post and verify the refund
+        final RefundJson refundJsonCheck = createRefundWithInvoiceItemAdjustment(paymentJsonSimple.getPaymentId(),
+                                                                                 itemToAdjust.getInvoiceItemId(),
+                                                                                 refundAmount);
+        verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+        // Verify the invoice balance
+        verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+    }
+
+    private BigDecimal getFractionOfAmount(final BigDecimal amount) {
+        return amount.divide(BigDecimal.TEN).setScale(2, BigDecimal.ROUND_HALF_UP);
+    }
+
+    private PaymentJsonSimple setupScenarioWithPayment() throws Exception {
         final AccountJson accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
 
         final List<PaymentJsonSimple> firstPaymentForAccount = getPaymentsForAccount(accountJson.getAccountId());
         Assert.assertEquals(firstPaymentForAccount.size(), 1);
 
-        final String paymentId = firstPaymentForAccount.get(0).getPaymentId();
-        final BigDecimal paymentAmount = firstPaymentForAccount.get(0).getAmount();
+        final PaymentJsonSimple paymentJsonSimple = firstPaymentForAccount.get(0);
 
         // Check the PaymentMethod from paymentMethodId returned in the Payment object
-        final String paymentMethodId = firstPaymentForAccount.get(0).getPaymentMethodId();
+        final String paymentMethodId = paymentJsonSimple.getPaymentMethodId();
         final PaymentMethodJson paymentMethodJson = getPaymentMethodWithPluginInfo(paymentMethodId);
         Assert.assertEquals(paymentMethodJson.getPaymentMethodId(), paymentMethodId);
         Assert.assertEquals(paymentMethodJson.getAccountId(), accountJson.getAccountId());
         Assert.assertNotNull(paymentMethodJson.getPluginInfo().getExternalPaymentId());
 
         // Verify the refunds
-        final List<RefundJson> objRefundFromJson = getRefundsForPayment(paymentId);
+        final List<RefundJson> objRefundFromJson = getRefundsForPayment(paymentJsonSimple.getPaymentId());
         Assert.assertEquals(objRefundFromJson.size(), 0);
+        return paymentJsonSimple;
+    }
 
-        // Issue a refund for the full amount
-        final RefundJson refundJsonCheck = createRefund(paymentId, paymentAmount);
+    private void verifyRefund(final PaymentJsonSimple paymentJsonSimple, final RefundJson refundJsonCheck, final BigDecimal refundAmount) throws IOException {
+        Assert.assertEquals(refundJsonCheck.getPaymentId(), paymentJsonSimple.getPaymentId());
+        Assert.assertEquals(refundJsonCheck.getRefundAmount().setScale(2, RoundingMode.HALF_UP), refundAmount.setScale(2, RoundingMode.HALF_UP));
         Assert.assertEquals(refundJsonCheck.getEffectiveDate().getYear(), clock.getUTCNow().getYear());
         Assert.assertEquals(refundJsonCheck.getEffectiveDate().getMonthOfYear(), clock.getUTCNow().getMonthOfYear());
         Assert.assertEquals(refundJsonCheck.getEffectiveDate().getDayOfMonth(), clock.getUTCNow().getDayOfMonth());
@@ -60,7 +178,13 @@ public class TestPayment extends TestJaxrsBase {
         Assert.assertEquals(refundJsonCheck.getRequestedDate().getDayOfMonth(), clock.getUTCNow().getDayOfMonth());
 
         // Verify the refunds
-        final List<RefundJson> retrievedRefunds = getRefundsForPayment(paymentId);
+        final List<RefundJson> retrievedRefunds = getRefundsForPayment(paymentJsonSimple.getPaymentId());
         Assert.assertEquals(retrievedRefunds.size(), 1);
     }
+
+    private void verifyInvoice(final PaymentJsonSimple paymentJsonSimple, final BigDecimal expectedInvoiceBalance) throws IOException {
+        final InvoiceJsonSimple invoiceJsonSimple = getInvoice(paymentJsonSimple.getInvoiceId());
+        Assert.assertEquals(invoiceJsonSimple.getBalance().setScale(2, BigDecimal.ROUND_HALF_UP),
+                            expectedInvoiceBalance.setScale(2, BigDecimal.ROUND_HALF_UP));
+    }
 }