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();
+ }
+ }
}