Details
diff --git a/account/src/main/resources/com/ning/billing/account/ddl.sql b/account/src/main/resources/com/ning/billing/account/ddl.sql
index 42cf2ef..acca205 100644
--- a/account/src/main/resources/com/ning/billing/account/ddl.sql
+++ b/account/src/main/resources/com/ning/billing/account/ddl.sql
@@ -2,12 +2,12 @@ DROP TABLE IF EXISTS accounts;
CREATE TABLE accounts (
id char(36) NOT NULL,
external_key varchar(128) NULL,
- email varchar(50) DEFAULT NULL,
+ email varchar(50) NOT NULL,
name varchar(100) NOT NULL,
first_name_length int NOT NULL,
phone varchar(13) DEFAULT NULL,
- currency char(3) NOT NULL,
- billing_cycle_day int NOT NULL,
+ currency char(3) DEFAULT NULL,
+ billing_cycle_day int DEFAULT NULL,
payment_provider_name varchar(20) DEFAULT NULL,
PRIMARY KEY(id)
) ENGINE=innodb;
diff --git a/api/src/main/java/com/ning/billing/invoice/api/Invoice.java b/api/src/main/java/com/ning/billing/invoice/api/Invoice.java
index 4c7d6fd..f69b934 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/Invoice.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/Invoice.java
@@ -48,4 +48,6 @@ public interface Invoice extends Entity {
BigDecimal getTotalAmount();
BigDecimal getAmountOutstanding();
+
+ boolean isDueForPayment(DateTime targetDate, int numberOfDays);
}
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 c09bb25..1186456 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
@@ -51,8 +51,39 @@ public class DefaultInvoiceDao implements InvoiceDao {
}
@Override
+ public List<Invoice> get() {
+ return invoiceDao.inTransaction(new Transaction<List<Invoice>, InvoiceSqlDao>() {
+ @Override
+ public List<Invoice> inTransaction(InvoiceSqlDao invoiceDao, TransactionStatus status) throws Exception {
+ List<Invoice> invoices = invoiceDao.get();
+
+ InvoiceItemSqlDao invoiceItemDao = invoiceDao.become(InvoiceItemSqlDao.class);
+ for (Invoice invoice : invoices) {
+ List<InvoiceItem> invoiceItems = invoiceItemDao.getInvoiceItemsByInvoice(invoice.getId().toString());
+ invoice.add(invoiceItems);
+ }
+
+ return invoices;
+ }
+ });
+ }
+
+ @Override
public Invoice getById(final String invoiceId) {
- return invoiceDao.getById(invoiceId);
+ return invoiceDao.inTransaction(new Transaction<Invoice, InvoiceSqlDao>() {
+ @Override
+ public Invoice inTransaction(InvoiceSqlDao invoiceDao, TransactionStatus status) throws Exception {
+ Invoice invoice = invoiceDao.getById(invoiceId);
+
+ if (invoice != null) {
+ InvoiceItemSqlDao invoiceItemDao = invoiceDao.become(InvoiceItemSqlDao.class);
+ List<InvoiceItem> invoiceItems = invoiceItemDao.getInvoiceItemsByInvoice(invoiceId);
+ invoice.add(invoiceItems);
+ }
+
+ return invoice;
+ }
+ });
}
@Override
@@ -83,8 +114,21 @@ public class DefaultInvoiceDao implements InvoiceDao {
}
@Override
- public List<Invoice> getInvoicesBySubscription(String subscriptionId) {
- return invoiceDao.getInvoicesBySubscription(subscriptionId);
+ public List<Invoice> getInvoicesBySubscription(final String subscriptionId) {
+ return invoiceDao.inTransaction(new Transaction<List<Invoice>, InvoiceSqlDao>() {
+ @Override
+ public List<Invoice> inTransaction(InvoiceSqlDao invoiceDao, TransactionStatus status) throws Exception {
+ List<Invoice> invoices = invoiceDao.getInvoicesBySubscription(subscriptionId);
+
+ InvoiceItemSqlDao invoiceItemDao = invoiceDao.become(InvoiceItemSqlDao.class);
+ for (Invoice invoice : invoices) {
+ List<InvoiceItem> invoiceItems = invoiceItemDao.getInvoiceItemsByInvoice(invoice.getId().toString());
+ invoice.add(invoiceItems);
+ }
+
+ return invoices;
+ }
+ });
}
@Override
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 477be07..b59e4ed 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
@@ -27,6 +27,8 @@ public interface InvoiceDao {
Invoice getById(final String id);
+ List<Invoice> get();
+
List<Invoice> getInvoicesByAccount(final String accountId);
List<Invoice> getInvoicesBySubscription(final String subscriptionId);
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceSqlDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceSqlDao.java
index 67e94bb..aa0051a 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceSqlDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceSqlDao.java
@@ -91,8 +91,8 @@ public interface InvoiceSqlDao extends EntityDao<Invoice>, Transactional<Invoice
q.bind("targetDate", invoice.getTargetDate().toDate());
q.bind("amountPaid", invoice.getAmountPaid());
q.bind("amountOutstanding", invoice.getAmountOutstanding());
- DateTime invoiceDate = invoice.getLastPaymentAttempt();
- q.bind("lastPaymentAttempt", invoiceDate == null ? null : invoiceDate.toDate());
+ DateTime last_payment_date = invoice.getLastPaymentAttempt();
+ q.bind("lastPaymentAttempt", last_payment_date == null ? null : last_payment_date.toDate());
q.bind("currency", invoice.getCurrency().toString());
}
};
@@ -108,6 +108,9 @@ public interface InvoiceSqlDao extends EntityDao<Invoice>, Transactional<Invoice
DateTime invoiceDate = new DateTime(result.getTimestamp("invoice_date"));
DateTime targetDate = new DateTime(result.getTimestamp("target_date"));
BigDecimal amountPaid = result.getBigDecimal("amount_paid");
+ if (amountPaid == null) {
+ amountPaid = BigDecimal.ZERO;
+ }
Timestamp lastPaymentAttemptTimeStamp = result.getTimestamp("last_payment_attempt");
DateTime lastPaymentAttempt = lastPaymentAttemptTimeStamp == null ? null : new DateTime(lastPaymentAttemptTimeStamp);
Currency currency = Currency.valueOf(result.getString("currency"));
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoice.java b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoice.java
index 0c956e4..797f587 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoice.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoice.java
@@ -123,5 +123,14 @@ public class DefaultInvoice implements Invoice {
public BigDecimal getAmountOutstanding() {
return getTotalAmount().subtract(getAmountPaid());
}
+
+ @Override
+ public boolean isDueForPayment(final DateTime targetDate, final int numberOfDays) {
+ if (lastPaymentAttempt == null) {
+ return true;
+ }
+
+ return lastPaymentAttempt.plusDays(numberOfDays).isBefore(targetDate);
+ }
}
diff --git a/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoiceSqlDao.sql.stg b/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoiceSqlDao.sql.stg
index 9414228..0323040 100644
--- a/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoiceSqlDao.sql.stg
+++ b/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoiceSqlDao.sql.stg
@@ -1,20 +1,31 @@
group InvoiceDao;
+get() ::= <<
+ SELECT i.id, i.account_id, i.invoice_date, i.target_date, i.currency, SUM(ii.amount) AS amount,
+ SUM(ip.amount) AS amount_paid, MAX(ip.payment_date) AS last_payment_attempt
+ FROM invoices i
+ LEFT JOIN invoice_payments ip ON ip.invoice_id = i.id
+ LEFT JOIN invoice_items ii ON ii.invoice_id = i.id
+ GROUP BY i.id, i.account_id, i.invoice_date, i.target_date, i.currency
+ ORDER BY i.invoice_date ASC;
+>>
+
getInvoicesByAccount() ::= <<
- SELECT i.id, i.account_id, i.invoice_date, i.target_date, i.currency,
+ SELECT i.id, i.account_id, i.invoice_date, i.target_date, i.currency, SUM(ii.amount) AS amount,
SUM(ip.amount) AS amount_paid, MAX(ip.payment_date) AS last_payment_attempt
FROM invoices i
LEFT JOIN invoice_payments ip ON ip.invoice_id = i.id
+ LEFT JOIN invoice_items ii ON ii.invoice_id = i.id
WHERE i.account_id = :accountId
GROUP BY i.id, i.account_id, i.invoice_date, i.target_date, i.currency
ORDER BY i.invoice_date ASC;
>>
getInvoicesBySubscription() ::= <<
- SELECT i.id, i.account_id, i.invoice_date, i.target_date, i.currency,
+ SELECT i.id, i.account_id, i.invoice_date, i.target_date, i.currency, SUM(ii.amount) AS amount,
SUM(ip.amount) AS amount_paid, MAX(ip.payment_date) AS last_payment_attempt
FROM invoices i
- INNER JOIN invoice_items ii ON i.id = ii.invoice_id
+ LEFT JOIN invoice_items ii ON i.id = ii.invoice_id
LEFT JOIN invoice_payments ip ON ip.invoice_id = i.id
WHERE ii.subscription_id = :subscriptionId
GROUP BY i.id, i.account_id, i.invoice_date, i.target_date, i.currency;
@@ -23,13 +34,20 @@ getInvoicesBySubscription() ::= <<
getInvoicesForPayment() ::= <<
SELECT i.id
FROM invoices i
- GROUP BY i.id;
+ LEFT JOIN invoice_payment_summary ips ON ips.invoice_id = i.id
+ LEFT JOIN invoice_item_summary iis ON iis.invoice_id = i.id
+ WHERE ((ips.last_payment_date IS NULL) OR (DATEDIFF(:targetDate, ips.last_payment_date) >= :numberOfDays))
+ AND ((ips.total_paid IS NULL) OR (iis.total_amount >= ips.total_paid))
+ AND ((iis.total_amount IS NOT NULL) AND (iis.total_amount > 0))
+ GROUP BY i.id, i.account_id, i.invoice_date, i.target_date, i.currency;
>>
getById() ::= <<
- SELECT i.id, i.account_id, i.invoice_date, i.target_date, SUM(ii.amount) AS amount, i.currency
+ SELECT i.id, i.account_id, i.invoice_date, i.target_date, i.currency, SUM(ii.amount) AS amount,
+ SUM(ip.amount) AS amount_paid, MAX(ip.payment_date) AS last_payment_attempt
FROM invoices i
- INNER JOIN invoice_items ii ON i.id = ii.invoice_id
+ LEFT JOIN invoice_items ii ON i.id = ii.invoice_id
+ LEFT JOIN invoice_payments ip ON ip.invoice_id = i.id
WHERE i.id = :id
GROUP BY i.id, i.account_id, i.invoice_date, i.target_date, i.currency;
>>
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 f6f2c41..a3ff37d 100644
--- a/invoice/src/main/resources/com/ning/billing/invoice/ddl.sql
+++ b/invoice/src/main/resources/com/ning/billing/invoice/ddl.sql
@@ -35,4 +35,16 @@ CREATE TABLE invoice_payments (
currency char(3),
PRIMARY KEY(invoice_id, payment_id)
) ENGINE=innodb;
-CREATE UNIQUE INDEX invoice_payments_unique ON invoice_payments(invoice_id, payment_id);
\ No newline at end of file
+CREATE UNIQUE INDEX invoice_payments_unique ON invoice_payments(invoice_id, payment_id);
+
+DROP VIEW IF EXISTS invoice_payment_summary;
+CREATE VIEW invoice_payment_summary AS
+SELECT invoice_id, SUM(amount) AS total_paid, MAX(payment_date) AS last_payment_date
+FROM invoice_payments
+GROUP BY invoice_id;
+
+DROP VIEW IF EXISTS invoice_item_summary;
+CREATE VIEW invoice_item_summary AS
+SELECT invoice_id, SUM(amount) AS total_amount
+FROM invoice_items
+GROUP BY invoice_id;
\ No newline at end of file
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTests.java b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTests.java
index 42c5d47..0666ada 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTests.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTests.java
@@ -17,9 +17,11 @@
package com.ning.billing.invoice.dao;
import java.math.BigDecimal;
+import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
+import org.joda.time.Days;
import org.testng.annotations.Test;
import com.ning.billing.catalog.api.Currency;
import com.ning.billing.invoice.api.Invoice;
@@ -70,8 +72,8 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
Invoice savedInvoice = invoiceDao.getById(invoiceId.toString());
assertNotNull(savedInvoice);
- assertEquals(savedInvoice.getTotalAmount(), new BigDecimal("21.00"));
- assertEquals(savedInvoice.getAmountOutstanding(), new BigDecimal("21.00"));
+ assertEquals(savedInvoice.getTotalAmount().compareTo(new BigDecimal("21.00")), 0);
+ assertEquals(savedInvoice.getAmountOutstanding().compareTo(new BigDecimal("21.00")), 0);
assertEquals(savedInvoice.getAmountPaid(), BigDecimal.ZERO);
assertEquals(savedInvoice.getItems().size(), 1);
@@ -81,10 +83,10 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
Invoice retrievedInvoice = invoiceDao.getById(invoiceId.toString());
assertNotNull(retrievedInvoice);
- assertEquals(retrievedInvoice.getTotalAmount(), new BigDecimal("21.00"));
- assertEquals(retrievedInvoice.getAmountOutstanding(), new BigDecimal("10.00"));
- assertEquals(retrievedInvoice.getAmountPaid(), new BigDecimal("11.00"));
assertEquals(retrievedInvoice.getItems().size(), 1);
+ assertEquals(retrievedInvoice.getTotalAmount().compareTo(new BigDecimal("21.00")), 0);
+ assertEquals(retrievedInvoice.getAmountOutstanding().compareTo(new BigDecimal("10.00")), 0);
+ assertEquals(retrievedInvoice.getAmountPaid().compareTo(new BigDecimal("11.00")), 0);
}
@Test
@@ -148,15 +150,10 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
List<UUID> invoices;
DateTime notionalDate = new DateTime();
- // determine the number of existing invoices available for payment (to avoid side effects from other tests)
- invoices = invoiceDao.getInvoicesForPayment(notionalDate.toDate(), NUMBER_OF_DAY_BETWEEN_RETRIES);
- int existingInvoiceCount = invoices.size();
-
// create a new invoice with one item
UUID accountId = UUID.randomUUID();
DateTime targetDate = new DateTime(2011, 10, 6, 0, 0, 0, 0);
Invoice invoice = new DefaultInvoice(accountId, targetDate, Currency.USD);
- invoiceDao.save(invoice);
UUID invoiceId = invoice.getId();
UUID subscriptionId = UUID.randomUUID();
@@ -165,50 +162,76 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
BigDecimal amount = rate.multiply(new BigDecimal("3.0"));
DefaultInvoiceItem item = new DefaultInvoiceItem(invoiceId, subscriptionId, targetDate, endDate, "test", amount, rate, Currency.USD);
- invoiceItemDao.save(item);
+ invoice.add(item);
+ invoiceDao.save(invoice);
// ensure that the number of invoices for payment has increased by 1
+ int count;
invoices = invoiceDao.getInvoicesForPayment(notionalDate.toDate(), NUMBER_OF_DAY_BETWEEN_RETRIES);
- assertEquals(invoices.size(), existingInvoiceCount + 1);
+ List<Invoice> invoicesDue = getInvoicesDueForPaymentAttempt(invoiceDao.get(), notionalDate);
+ count = invoicesDue.size();
+ assertEquals(invoices.size(), count);
- // attempt a payment; ensure that the number of invoices for payment has decreased by 1 (no retries for eight days)
+ // attempt a payment; ensure that the number of invoices for payment has decreased by 1
+ // (no retries for NUMBER_OF_DAYS_BETWEEN_RETRIES days)
invoiceDao.notifyFailedPayment(invoice.getId().toString(), UUID.randomUUID().toString(), notionalDate.toDate());
invoices = invoiceDao.getInvoicesForPayment(notionalDate.toDate(), NUMBER_OF_DAY_BETWEEN_RETRIES);
- assertEquals(invoices.size(), existingInvoiceCount);
+ count = getInvoicesDueForPaymentAttempt(invoiceDao.get(), notionalDate).size();
+ assertEquals(invoices.size(), count);
- // advance clock by 8 days; ensure that number of invoices for payment has increased by 1 (retry)
+ // advance clock by NUMBER_OF_DAYS_BETWEEN_RETRIES days
+ // ensure that number of invoices for payment has increased by 1 (retry)
notionalDate = notionalDate.plusDays(NUMBER_OF_DAY_BETWEEN_RETRIES);
invoices = invoiceDao.getInvoicesForPayment(notionalDate.toDate(), NUMBER_OF_DAY_BETWEEN_RETRIES);
- assertEquals(invoices.size(), existingInvoiceCount + 1);
+ count = getInvoicesDueForPaymentAttempt(invoiceDao.get(), notionalDate).size();
+ assertEquals(invoices.size(), count);
// post successful partial payment; ensure that number of invoices for payment has decreased by 1
invoiceDao.notifySuccessfulPayment(invoiceId.toString(), new BigDecimal("22.0000"), Currency.USD.toString(), UUID.randomUUID().toString(), notionalDate.toDate());
invoices = invoiceDao.getInvoicesForPayment(notionalDate.toDate(), NUMBER_OF_DAY_BETWEEN_RETRIES);
- assertEquals(invoices.size(), existingInvoiceCount);
+ count = getInvoicesDueForPaymentAttempt(invoiceDao.get(), notionalDate).size();
+ assertEquals(invoices.size(), count);
// get invoice; verify amount paid is correct
invoice = invoiceDao.getById(invoiceId.toString());
assertEquals(invoice.getAmountPaid().compareTo(new BigDecimal("22.0")), 0);
- // advance clock eight days; ensure that number of invoices for payment has increased by 1 (retry)
+ // advance clock NUMBER_OF_DAYS_BETWEEN_RETRIES days
+ // ensure that number of invoices for payment has increased by 1 (retry)
notionalDate = notionalDate.plusDays(NUMBER_OF_DAY_BETWEEN_RETRIES);
invoices = invoiceDao.getInvoicesForPayment(notionalDate.toDate(), NUMBER_OF_DAY_BETWEEN_RETRIES);
- assertEquals(invoices.size(), existingInvoiceCount + 1);
+ count = getInvoicesDueForPaymentAttempt(invoiceDao.get(), notionalDate).size();
+ assertEquals(invoices.size(), count);
// post completed payment; ensure that the number of invoices for payment has decreased by 1
invoiceDao.notifySuccessfulPayment(invoiceId.toString(), new BigDecimal("5.0000"), Currency.USD.toString(), UUID.randomUUID().toString(), notionalDate.toDate());
invoices = invoiceDao.getInvoicesForPayment(notionalDate.toDate(), NUMBER_OF_DAY_BETWEEN_RETRIES);
- assertEquals(invoices.size(), existingInvoiceCount);
+ count = getInvoicesDueForPaymentAttempt(invoiceDao.get(), notionalDate).size();
+ assertEquals(invoices.size(), count);
// get invoice; verify amount paid is correct
invoice = invoiceDao.getById(invoiceId.toString());
+ count = getInvoicesDueForPaymentAttempt(invoiceDao.get(), notionalDate).size();
assertEquals(invoice.getAmountPaid().compareTo(new BigDecimal("27.0")), 0);
- // advance clock by 8 days; ensure that the number of invoices for payment hasn't changed
+ // advance clock by NUMBER_OF_DAYS_BETWEEN_RETRIES days
+ // ensure that the number of invoices for payment hasn't changed
notionalDate = notionalDate.plusDays(NUMBER_OF_DAY_BETWEEN_RETRIES);
invoices = invoiceDao.getInvoicesForPayment(notionalDate.toDate(), NUMBER_OF_DAY_BETWEEN_RETRIES);
- assertEquals(invoices.size(), existingInvoiceCount);
+ count = getInvoicesDueForPaymentAttempt(invoiceDao.get(), notionalDate).size();
+ assertEquals(invoices.size(), count);
+ }
+
+ private List<Invoice> getInvoicesDueForPaymentAttempt(List<Invoice> invoices, DateTime date) {
+ List<Invoice> invoicesDue= new ArrayList<Invoice>();
+
+ for (Invoice invoice : invoices) {
+ if (invoice.isDueForPayment(date, NUMBER_OF_DAY_BETWEEN_RETRIES)) {
+ invoicesDue.add(invoice);
+ }
+ }
+ return invoicesDue;
}
@Test