killbill-aplcache

charge backs

5/31/2012 3:12:25 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 6e69072..8cf6f59 100644
--- a/api/src/main/java/com/ning/billing/ErrorCode.java
+++ b/api/src/main/java/com/ning/billing/ErrorCode.java
@@ -17,9 +17,6 @@
 package com.ning.billing;
 
 public enum ErrorCode {
-
-
-    
     /*
      * Range 0 : COMMON EXCEPTIONS
      */
@@ -185,7 +182,15 @@ public enum ErrorCode {
     INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID(4003, "No account id was retrieved for subscription id %s"),
     INVOICE_INVALID_DATE_SEQUENCE(4004, "Date sequence was invalid. Start Date: %s; End Date: %s; Target Date: %s"),
     INVOICE_TARGET_DATE_TOO_FAR_IN_THE_FUTURE(4005, "The target date was too far in the future. Target Date: %s"),
-    
+
+    /*
+     *
+     * Range 4900: Invoice payment
+     *
+     */
+    INVOICE_PAYMENT_NOT_FOUND(4900, "No invoice payment could be found for id %s."),
+    CHARGE_BACK_AMOUNT_TOO_HIGH(4901, "Tried to charge back %s of a %s payment."),
+    CHARGE_BACK_AMOUNT_IS_NEGATIVE(4902, "Charge backs for negative amounts are not permitted"),
     /*
      * 
      * Range 5000: Overdue system
diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoicePayment.java b/api/src/main/java/com/ning/billing/invoice/api/InvoicePayment.java
index 29f175b..4ba42c1 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/InvoicePayment.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoicePayment.java
@@ -32,4 +32,11 @@ public interface InvoicePayment extends Entity {
     BigDecimal getAmount();
 
     Currency getCurrency();
+
+    UUID getReversedInvoicePaymentId();
+
+    /*
+     * @param chargeBackAmount BigDecimal pass the amount as a positive number
+     */
+    InvoicePayment asChargeBack(BigDecimal chargeBackAmount, DateTime chargeBackDate) throws InvoiceApiException;
 }
diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java b/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java
index 0a34604..6f6508b 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java
@@ -26,7 +26,6 @@ import org.joda.time.DateTime;
 import com.ning.billing.catalog.api.Currency;
 
 public interface InvoicePaymentApi {
-
     /**
      * @param accountId
      * @return All invoices, including migrated invoices
@@ -45,4 +44,9 @@ public interface InvoicePaymentApi {
     
     public void notifyOfPaymentAttempt(UUID invoiceId, UUID paymentAttemptId, DateTime paymentAttemptDate, CallContext context);
 
+    public void processChargeBack(UUID invoicePaymentId, BigDecimal amount, CallContext context) throws InvoiceApiException;
+
+    public void processChargeBack(UUID invoicePaymentId, CallContext context) throws InvoiceApiException;
+
+    public BigDecimal getRemainingAmountPaid(UUID invoicePaymentId);
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
index 11712d2..c465621 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
@@ -21,6 +21,7 @@ import java.math.BigDecimal;
 import java.util.List;
 import java.util.UUID;
 
+import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.util.callcontext.CallContext;
 import org.joda.time.DateTime;
 
@@ -78,5 +79,21 @@ public class DefaultInvoicePaymentApi implements InvoicePaymentApi {
         dao.notifyOfPaymentAttempt(invoicePayment, context);
     }
 
+    @Override
+    public void processChargeBack(UUID invoicePaymentId, BigDecimal amount, CallContext context) throws InvoiceApiException {
+        dao.postChargeBack(invoicePaymentId, amount, context);
+    }
 
+    @Override
+    public void processChargeBack(UUID invoicePaymentId, CallContext context) throws InvoiceApiException {
+        // use the invoicePaymentId to get the amount remaining on the payment
+        // (preventing charge backs totalling more than the payment)
+        BigDecimal amount = dao.getRemainingAmountPaid(invoicePaymentId);
+        processChargeBack(invoicePaymentId, amount, context);
+    }
+
+    @Override
+    public BigDecimal getRemainingAmountPaid(UUID invoicePaymentId) {
+        return dao.getRemainingAmountPaid(invoicePaymentId);
+    }
 }
\ No newline at end of file
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 3a4ffe2..d765db2 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
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
+import com.ning.billing.ErrorCode;
 import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.invoice.model.CreditInvoiceItem;
 import org.joda.time.DateTime;
@@ -185,7 +186,7 @@ public class DefaultInvoiceDao implements InvoiceDao {
         });
     }
 
-    private List<EntityAudit> createAudits(TableName tableName, List<Long> recordIdList) {
+    private List<EntityAudit> createAudits(final TableName tableName, final List<Long> recordIdList) {
         List<EntityAudit> entityAuditList = new ArrayList<EntityAudit>();
         for (Long recordId : recordIdList) {
             entityAuditList.add(new EntityAudit(tableName, recordId, ChangeType.INSERT));
@@ -245,26 +246,46 @@ public class DefaultInvoiceDao implements InvoiceDao {
     }
 
     @Override
-    public UUID getInvoiceIdByPaymentAttemptId(UUID paymentAttemptId) {
+    public UUID getInvoiceIdByPaymentAttemptId(final UUID paymentAttemptId) {
         return invoiceSqlDao.getInvoiceIdByPaymentAttemptId(paymentAttemptId.toString());
     }
 
     @Override
-    public InvoicePayment getInvoicePayment(UUID paymentAttemptId) {
+    public InvoicePayment getInvoicePayment(final UUID paymentAttemptId) {
         return invoicePaymentSqlDao.getInvoicePayment(paymentAttemptId);
     }
 
     @Override
-    public void setWrittenOff(UUID invoiceId, CallContext context) {
+    public void setWrittenOff(final UUID invoiceId, final CallContext context) {
         tagDao.insertTag(invoiceId, ObjectType.INVOICE, ControlTagType.WRITTEN_OFF.toTagDefinition(), context);
     }
 
     @Override
-    public void removeWrittenOff(UUID invoiceId, CallContext context) throws InvoiceApiException {
+    public void removeWrittenOff(final UUID invoiceId, final CallContext context) throws InvoiceApiException {
         tagDao.deleteTag(invoiceId, ObjectType.INVOICE, ControlTagType.WRITTEN_OFF.toTagDefinition(), context);
     }
 
     @Override
+    public void postChargeBack(final UUID invoicePaymentId, final BigDecimal amount, final CallContext context) throws InvoiceApiException {
+        InvoicePayment payment = invoicePaymentSqlDao.getById(invoicePaymentId.toString());
+        if (payment == null) {
+            throw new InvoiceApiException(ErrorCode.INVOICE_PAYMENT_NOT_FOUND, invoicePaymentId.toString());
+        } else {
+            if (amount.compareTo(BigDecimal.ZERO) < 0) {
+                throw new InvoiceApiException(ErrorCode.CHARGE_BACK_AMOUNT_IS_NEGATIVE);
+            }
+
+            InvoicePayment chargeBack = payment.asChargeBack(amount, context.getCreatedDate());
+            invoicePaymentSqlDao.create(chargeBack, context);
+        }
+    }
+
+    @Override
+    public BigDecimal getRemainingAmountPaid(UUID invoicePaymentId) {
+        return invoicePaymentSqlDao.getRemainingAmountPaid(invoicePaymentId.toString());
+    }
+
+    @Override
     public void test() {
         invoiceSqlDao.test();
     }
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 59de677..f5aabc8 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
@@ -57,4 +57,7 @@ public interface InvoiceDao {
 
     void removeWrittenOff(UUID invoiceId, CallContext context) throws InvoiceApiException;
 
+    void postChargeBack(UUID invoicePaymentId, BigDecimal amount, CallContext context) throws InvoiceApiException;
+
+    BigDecimal getRemainingAmountPaid(UUID invoicePaymentId);
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.java
index a11c8e0..cf51d82 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.java
@@ -28,9 +28,11 @@ import java.util.List;
 import java.util.UUID;
 
 import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.model.DefaultInvoicePayment;
 import com.ning.billing.util.dao.AuditSqlDao;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.CallContextBinder;
+import com.ning.billing.util.dao.BinderBase;
 import com.ning.billing.util.dao.MapperBase;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import org.joda.time.DateTime;
@@ -81,7 +83,8 @@ public interface InvoicePaymentSqlDao extends EntitySqlDao<InvoicePayment>, Tran
     void notifyOfPaymentAttempt(@InvoicePaymentBinder final InvoicePayment invoicePayment,
                                 @CallContextBinder final CallContext context);
 
-
+    @SqlQuery
+    BigDecimal getRemainingAmountPaid(@Bind("invoicePaymentId") final String invoicePaymentId);
 
     public static class InvoicePaymentMapper extends MapperBase implements ResultSetMapper<InvoicePayment> {
         @Override
@@ -93,33 +96,10 @@ public interface InvoicePaymentSqlDao extends EntitySqlDao<InvoicePayment>, Tran
             final BigDecimal amount = result.getBigDecimal("amount");
             final String currencyString = result.getString("currency");
             final Currency currency = (currencyString == null) ? null : Currency.valueOf(currencyString);
+            final UUID reversedInvoicePaymentId = getUUID(result, "reversed_invoice_Payment_id");
 
-            return new InvoicePayment() {
-                @Override
-                public UUID getId() {
-                    return id;
-                }
-                @Override
-                public UUID getPaymentAttemptId() {
-                    return paymentAttemptId;
-                }
-                @Override
-                public UUID getInvoiceId() {
-                    return invoiceId;
-                }
-                @Override
-                public DateTime getPaymentAttemptDate() {
-                    return paymentAttemptDate;
-                }
-                @Override
-                public BigDecimal getAmount() {
-                    return amount;
-                }
-                @Override
-                public Currency getCurrency() {
-                    return currency;
-                }
-            };
+            return new DefaultInvoicePayment(id, paymentAttemptId, invoiceId, paymentAttemptDate,
+                    amount, currency, reversedInvoicePaymentId);
         }
     }
 
@@ -127,7 +107,7 @@ public interface InvoicePaymentSqlDao extends EntitySqlDao<InvoicePayment>, Tran
     @Retention(RetentionPolicy.RUNTIME)
     @Target({ElementType.PARAMETER})
     public @interface InvoicePaymentBinder {
-        public static class InvoicePaymentBinderFactory implements BinderFactory {
+        public static class InvoicePaymentBinderFactory extends BinderBase implements BinderFactory {
             @Override
             public Binder build(Annotation annotation) {
                 return new Binder<InvoicePaymentBinder, InvoicePayment>() {
@@ -135,11 +115,12 @@ public interface InvoicePaymentSqlDao extends EntitySqlDao<InvoicePayment>, Tran
                     public void bind(SQLStatement q, InvoicePaymentBinder bind, InvoicePayment payment) {
                         q.bind("id", payment.getId().toString());
                         q.bind("invoiceId", payment.getInvoiceId().toString());
-                        q.bind("paymentAttemptId", payment.getPaymentAttemptId().toString());
+                        q.bind("paymentAttemptId", uuidToString(payment.getPaymentAttemptId()));
                         q.bind("paymentAttemptDate", payment.getPaymentAttemptDate().toDate());
                         q.bind("amount", payment.getAmount());
                         Currency currency = payment.getCurrency();
                         q.bind("currency", (currency == null) ? null : currency.toString());
+                        q.bind("reversedInvoicePaymentId", uuidToString(payment.getReversedInvoicePaymentId()));
                     }
                 };
             }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoicePayment.java b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoicePayment.java
index ba02039..1be9662 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoicePayment.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoicePayment.java
@@ -16,7 +16,9 @@
 
 package com.ning.billing.invoice.model;
 
+import com.ning.billing.ErrorCode;
 import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.invoice.api.InvoicePayment;
 import com.ning.billing.util.entity.EntityBase;
 import org.joda.time.DateTime;
@@ -31,24 +33,27 @@ public class DefaultInvoicePayment extends EntityBase implements InvoicePayment 
     private final DateTime paymentDate;
     private final BigDecimal amount;
     private final Currency currency;
+    private final UUID reversedInvoicePaymentId;
 
     public DefaultInvoicePayment(final UUID paymentAttemptId, final UUID invoiceId, final DateTime paymentDate) {
-        this(UUID.randomUUID(), paymentAttemptId, invoiceId, paymentDate, null, null);
+        this(UUID.randomUUID(), paymentAttemptId, invoiceId, paymentDate, null, null, null);
     }
 
     public DefaultInvoicePayment(final UUID paymentAttemptId, final UUID invoiceId, final DateTime paymentDate,
                                  final BigDecimal amount, final Currency currency) {
-        this(UUID.randomUUID(), paymentAttemptId, invoiceId, paymentDate, amount, currency);
+        this(UUID.randomUUID(), paymentAttemptId, invoiceId, paymentDate, amount, currency, null);
     }
 
     public DefaultInvoicePayment(final UUID id, final UUID paymentAttemptId, final UUID invoiceId, final DateTime paymentDate,
-                                 @Nullable final BigDecimal amount, @Nullable final Currency currency) {
+                                 @Nullable final BigDecimal amount, @Nullable final Currency currency,
+                                 @Nullable final UUID reversedInvoicePaymentId) {
         super(id);
         this.paymentAttemptId = paymentAttemptId;
         this.amount = amount;
         this.invoiceId = invoiceId;
         this.paymentDate = paymentDate;
         this.currency = currency;
+        this.reversedInvoicePaymentId = reversedInvoicePaymentId;
     }
 
     @Override
@@ -75,4 +80,18 @@ public class DefaultInvoicePayment extends EntityBase implements InvoicePayment 
     public Currency getCurrency() {
         return currency;
     }
+
+    @Override
+    public UUID getReversedInvoicePaymentId() {
+        return reversedInvoicePaymentId;
+    }
+
+    @Override
+    public InvoicePayment asChargeBack(BigDecimal chargeBackAmount, DateTime chargeBackDate) throws InvoiceApiException {
+        if (chargeBackAmount.compareTo(amount) > 0) {
+            throw new InvoiceApiException(ErrorCode.CHARGE_BACK_AMOUNT_TOO_HIGH, chargeBackAmount, amount);
+        }
+
+        return new DefaultInvoicePayment(UUID.randomUUID(), null, invoiceId, chargeBackDate, chargeBackAmount.negate(), currency, id);
+    }
 }
diff --git a/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg b/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
index 5ec38d3..d201bb9 100644
--- a/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
+++ b/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
@@ -7,18 +7,21 @@ invoicePaymentFields(prefix) ::= <<
   <prefix>payment_attempt_date,
   <prefix>amount,
   <prefix>currency,
+  <prefix>reversed_invoice_payment_id,
   <prefix>created_by,
   <prefix>created_date
 >>
 
 create() ::= <<
   INSERT INTO invoice_payments(<invoicePaymentFields()>)
-  VALUES(:id, :invoiceId, :paymentAttemptId, :paymentAttemptDate, :amount, :currency, :userName, :createdDate);
+  VALUES(:id, :invoiceId, :paymentAttemptId, :paymentAttemptDate, :amount, :currency,
+         :reversedInvoicePaymentId, :userName, :createdDate);
 >>
 
 batchCreateFromTransaction() ::= <<
   INSERT INTO invoice_payments(<invoicePaymentFields()>)
-  VALUES(:id, :invoiceId, :paymentAttemptId, :paymentAttemptDate, :amount, :currency, :userName, :createdDate);
+  VALUES(:id, :invoiceId, :paymentAttemptId, :paymentAttemptDate, :amount, :currency,
+        :reversedInvoicePaymentId, :userName, :createdDate);
 >>
 
 getByPaymentAttemptId() ::= <<
@@ -32,6 +35,12 @@ get() ::= <<
   FROM invoice_payments;
 >>
 
+getById() ::= <<
+  SELECT <invoicePaymentFields()>
+  FROM invoice_payments
+  WHERE id = :id;
+>>
+
 getPaymentsForInvoice() ::= <<
   SELECT <invoicePaymentFields()>
   FROM invoice_payments
@@ -40,7 +49,8 @@ getPaymentsForInvoice() ::= <<
 
 notifyOfPaymentAttempt() ::= <<
   INSERT INTO invoice_payments(<invoicePaymentFields()>)
-  VALUES(:id, :invoiceId, :paymentAttemptId, :paymentAttemptDate, :amount, :currency, :userName, :createdDate);
+  VALUES(:id, :invoiceId, :paymentAttemptId, :paymentAttemptDate, :amount, :currency,
+        :reversedInvoicePaymentId, :userName, :createdDate);
 >>
 
 getInvoicePayment() ::= <<
@@ -78,6 +88,13 @@ insertAuditFromTransaction() ::= <<
 >>
 
 test() ::= <<
-  SELECT 1 FROM invoice_payments;
+    SELECT 1 FROM invoice_payments;
+>>
+
+getRemainingAmountPaid() ::= <<
+    SELECT SUM(amount)
+    FROM invoice_payments
+    WHERE id = :invoicePaymentId
+    OR reversed_invoice_payment_id = :invoicePaymentId;
 >>
 ;
\ No newline at end of file
diff --git a/invoice/src/main/resources/com/ning/billing/invoice/ddl.sql b/invoice/src/main/resources/com/ning/billing/invoice/ddl.sql
index 061d8a0..ee67f37 100644
--- a/invoice/src/main/resources/com/ning/billing/invoice/ddl.sql
+++ b/invoice/src/main/resources/com/ning/billing/invoice/ddl.sql
@@ -84,16 +84,17 @@ CREATE TABLE invoice_payments (
     record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
     id char(36) NOT NULL,
     invoice_id char(36) NOT NULL,
-    payment_attempt_id char(36) COLLATE utf8_bin NOT NULL,
-    payment_attempt_date datetime,
-    amount numeric(10,4),
-    currency char(3),
+    payment_attempt_id char(36) COLLATE utf8_bin,
+    payment_attempt_date datetime NOT NULL,
+    amount numeric(10,4) NOT NULL,
+    currency char(3) NOT NULL,
+    reversed_invoice_payment_id char(36) DEFAULT NULL,
     created_by varchar(50) NOT NULL,
     created_date datetime NOT NULL,
     PRIMARY KEY(record_id)
 ) ENGINE=innodb;
 CREATE UNIQUE INDEX invoice_payments_id ON invoice_payments(id);
-CREATE UNIQUE INDEX invoice_payments_unique ON invoice_payments(invoice_id, payment_attempt_id);
+CREATE INDEX invoice_payments_reversals ON invoice_payments(reversed_invoice_payment_id);
 
 DROP VIEW IF EXISTS invoice_payment_summary;
 CREATE VIEW invoice_payment_summary AS
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java b/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java
index 09b809c..2ec4d63 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java
@@ -27,6 +27,7 @@ import org.joda.time.DateTime;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.invoice.model.DefaultInvoicePayment;
 import com.ning.billing.util.callcontext.CallContext;
+import org.joda.time.DateTimeZone;
 
 public class MockInvoicePaymentApi implements InvoicePaymentApi
 {
@@ -101,4 +102,48 @@ public class MockInvoicePaymentApi implements InvoicePaymentApi
         notifyOfPaymentAttempt(invoicePayment, context);
     }
 
+    @Override
+    public void processChargeBack(UUID invoicePaymentId, BigDecimal amount, CallContext context) throws InvoiceApiException {
+        InvoicePayment existingPayment = null;
+        for (InvoicePayment payment : invoicePayments) {
+            if (payment.getId()  == invoicePaymentId) {
+                existingPayment = payment;
+            }
+        }
+
+        if (existingPayment != null) {
+            invoicePayments.add(existingPayment.asChargeBack(amount, DateTime.now(DateTimeZone.UTC)));
+        }
+    }
+
+    @Override
+    public void processChargeBack(UUID invoicePaymentId, CallContext context) throws InvoiceApiException {
+        InvoicePayment existingPayment = null;
+        for (InvoicePayment payment : invoicePayments) {
+            if (payment.getId()  == invoicePaymentId) {
+                existingPayment = payment;
+            }
+        }
+
+        if (existingPayment != null) {
+            this.processChargeBack(invoicePaymentId, existingPayment.getAmount(), context);
+        }
+    }
+
+    @Override
+    public BigDecimal getRemainingAmountPaid(UUID invoicePaymentId) {
+        BigDecimal amount = BigDecimal.ZERO;
+        for (InvoicePayment payment : invoicePayments) {
+            if (payment.getId().equals(invoicePaymentId)) {
+                amount = amount.add(payment.getAmount());
+            }
+
+            if (payment.getReversedInvoicePaymentId().equals(invoicePaymentId)) {
+                amount = amount.add(payment.getAmount());
+            }
+        }
+
+        return amount;
+    }
+
 }
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 2c65cd8..bb36b40 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,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+import com.ning.billing.invoice.api.InvoiceApiException;
 import org.joda.time.DateTime;
 
 import com.google.inject.Inject;
@@ -210,4 +211,14 @@ public class MockInvoiceDao implements InvoiceDao {
     public void removeWrittenOff(UUID objectId, CallContext context) {
         throw new UnsupportedOperationException();
     }
+
+    @Override
+    public void postChargeBack(UUID invoicePaymentId, BigDecimal amount, CallContext context) throws InvoiceApiException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public BigDecimal getRemainingAmountPaid(UUID invoicePaymentId) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/ChargeBackTests.java b/invoice/src/test/java/com/ning/billing/invoice/tests/ChargeBackTests.java
new file mode 100644
index 0000000..ccd8db6
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/ChargeBackTests.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2010-2011 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.tests;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.dbi.MysqlTestingHelper;
+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.invoice.DefaultInvoicePaymentApi;
+import com.ning.billing.invoice.dao.DefaultInvoiceDao;
+import com.ning.billing.invoice.dao.InvoiceDao;
+import com.ning.billing.invoice.dao.InvoiceSqlDao;
+import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
+import com.ning.billing.invoice.notification.MockNextBillingDatePoster;
+import com.ning.billing.invoice.notification.NextBillingDatePoster;
+import com.ning.billing.mock.BrainDeadProxyFactory;
+import com.ning.billing.mock.BrainDeadProxyFactory.ZombieControl;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.TestCallContext;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+import com.ning.billing.util.tag.dao.MockTagDao;
+import com.ning.billing.util.tag.dao.TagDao;
+import org.skife.jdbi.v2.IDBI;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+@Test(groups = {"slow", "invoicing"})
+public class ChargeBackTests {
+    private final static BigDecimal FIFTEEN = new BigDecimal("15.00");
+    private final static BigDecimal THIRTY = new BigDecimal("30.00");
+    private final static BigDecimal ONE_MILLION = new BigDecimal("1000000.00");
+    private InvoiceSqlDao invoiceSqlDao;
+    private InvoicePaymentApi invoicePaymentApi;
+    private CallContext context;
+    private final Clock clock = new ClockMock();
+    private final static Currency CURRENCY = Currency.EUR;
+
+    @BeforeClass
+    public void setup() {
+        MysqlTestingHelper helper = new MysqlTestingHelper();
+        IDBI dbi = helper.getDBI();
+        invoiceSqlDao = dbi.onDemand(InvoiceSqlDao.class);
+        invoiceSqlDao.test();
+
+        NextBillingDatePoster nextBillingDatePoster = new MockNextBillingDatePoster();
+        TagDao tagDao = new MockTagDao();
+        InvoiceDao invoiceDao = new DefaultInvoiceDao(dbi, nextBillingDatePoster, tagDao);
+        invoicePaymentApi = new DefaultInvoicePaymentApi(invoiceDao);
+
+        context = new TestCallContext("Charge back tests");
+    }
+
+    @Test
+    public void testCompleteChargeBack() throws InvoiceApiException {
+        Invoice invoice = createAndPersistInvoice(THIRTY);
+        InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+
+        // create a full charge back
+        invoicePaymentApi.processChargeBack(payment.getId(), THIRTY, context);
+
+        // check amount owed
+        BigDecimal amount = invoicePaymentApi.getRemainingAmountPaid(payment.getId());
+        assertTrue(amount.compareTo(BigDecimal.ZERO) == 0);
+    }
+
+    @Test
+    public void testPartialChargeBack() throws InvoiceApiException {
+        Invoice invoice = createAndPersistInvoice(THIRTY);
+        InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+
+        // create a partial charge back
+        invoicePaymentApi.processChargeBack(payment.getId(), FIFTEEN, context);
+
+        // check amount owed
+        BigDecimal amount = invoicePaymentApi.getRemainingAmountPaid(payment.getId());
+        assertTrue(amount.compareTo(FIFTEEN) == 0);
+    }
+
+    @Test(expectedExceptions = InvoiceApiException.class)
+    public void testChargeBackLargerThanPaymentAmount() throws InvoiceApiException {
+        Invoice invoice = createAndPersistInvoice(THIRTY);
+        InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+
+        // create a large charge back
+        invoicePaymentApi.processChargeBack(payment.getId(), ONE_MILLION, context);
+    }
+
+    @Test(expectedExceptions = InvoiceApiException.class)
+    public void testNegativeChargeBackAmount() throws InvoiceApiException {
+        Invoice invoice = createAndPersistInvoice(THIRTY);
+        InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+
+        // create a partial charge back
+        invoicePaymentApi.processChargeBack(payment.getId(), BigDecimal.ONE.negate(), context);
+    }
+
+    private Invoice createAndPersistInvoice(BigDecimal amount) {
+        Invoice invoice = BrainDeadProxyFactory.createBrainDeadProxyFor(Invoice.class);
+        UUID invoiceId = UUID.randomUUID();
+        UUID accountId = UUID.randomUUID();
+        ZombieControl zombie = (ZombieControl) invoice;
+        zombie.addResult("getId", invoiceId);
+        zombie.addResult("getAccountId", accountId);
+        zombie.addResult("getInvoiceDate", clock.getUTCNow());
+        zombie.addResult("getTargetDate", clock.getUTCNow());
+        zombie.addResult("getCurrency", CURRENCY);
+        zombie.addResult("isMigrationInvoice", false);
+
+        List<InvoiceItem> items = new ArrayList<InvoiceItem>();
+        items.add(createInvoiceItem(invoiceId, accountId, amount));
+        zombie.addResult("getInvoiceItems", items);
+
+        invoiceSqlDao.create(invoice, context);
+
+        return invoice;
+    }
+
+    private InvoiceItem createInvoiceItem(UUID invoiceId, UUID accountId, BigDecimal amount) {
+        return new FixedPriceInvoiceItem(invoiceId, accountId, UUID.randomUUID(), UUID.randomUUID(),
+                "charge back test", "charge back phase", clock.getUTCNow(), clock.getUTCNow(), amount, CURRENCY);
+    }
+
+    private InvoicePayment createAndPersistPayment(UUID invoiceId, BigDecimal amount) {
+        InvoicePayment payment = BrainDeadProxyFactory.createBrainDeadProxyFor(InvoicePayment.class);
+        ZombieControl zombie = (ZombieControl) payment;
+        zombie.addResult("getId", UUID.randomUUID());
+        zombie.addResult("getInvoiceId", invoiceId);
+        zombie.addResult("getPaymentAttemptId", UUID.randomUUID());
+        zombie.addResult("getPaymentAttemptDate", clock.getUTCNow());
+        zombie.addResult("getAmount", amount);
+        zombie.addResult("getCurrency", CURRENCY);
+        zombie.addResult("getReversedInvoicePaymentId", BrainDeadProxyFactory.ZOMBIE_VOID);
+
+        invoicePaymentApi.notifyOfPaymentAttempt(payment, context);
+
+        return payment;
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/dao/BinderBase.java b/util/src/main/java/com/ning/billing/util/dao/BinderBase.java
index ef20575..df36442 100644
--- a/util/src/main/java/com/ning/billing/util/dao/BinderBase.java
+++ b/util/src/main/java/com/ning/billing/util/dao/BinderBase.java
@@ -19,9 +19,18 @@ package com.ning.billing.util.dao;
 import org.joda.time.DateTime;
 
 import java.util.Date;
+import java.util.UUID;
 
 public abstract class BinderBase {
     protected Date getDate(DateTime dateTime) {
         return dateTime == null ? null : dateTime.toDate();
     }
+
+    protected String uuidToString(UUID uuid) {
+        if (uuid == null) {
+            return null;
+        } else {
+            return uuid.toString();
+        }
+    }
 }