diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
index 4f93cad..048a3c5 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
@@ -17,11 +17,13 @@
package org.killbill.billing.jaxrs.json;
-import org.killbill.billing.catalog.api.TimeUnit;
+import java.math.BigDecimal;
+
import org.killbill.billing.jaxrs.json.CatalogJson.DurationJson;
import org.killbill.billing.overdue.api.OverdueCondition;
import org.killbill.billing.overdue.config.DefaultDuration;
import org.killbill.billing.overdue.config.DefaultOverdueCondition;
+import org.killbill.billing.payment.api.PaymentResponse;
import org.killbill.billing.util.tag.ControlTagType;
import com.fasterxml.jackson.annotation.JsonCreator;
@@ -32,20 +34,33 @@ public class OverdueConditionJson {
private final DurationJson timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
private final ControlTagType controlTagInclusion;
private final ControlTagType controlTagExclusion;
+ private final Integer numberOfUnpaidInvoicesEqualsOrExceeds;
+ private final PaymentResponse[] responseForLastFailedPayment;
+ private final BigDecimal totalUnpaidInvoiceBalanceEqualsOrExceeds;
+
@JsonCreator
public OverdueConditionJson(@JsonProperty("timeSinceEarliestUnpaidInvoiceEqualsOrExceeds") final DurationJson timeSinceEarliestUnpaidInvoiceEqualsOrExceeds,
@JsonProperty("controlTagInclusion") final ControlTagType controlTagInclusion,
- @JsonProperty("controlTagExclusion") final ControlTagType controlTagExclusion) {
+ @JsonProperty("controlTagExclusion") final ControlTagType controlTagExclusion,
+ @JsonProperty("numberOfUnpaidInvoicesEqualsOrExceeds") final Integer numberOfUnpaidInvoicesEqualsOrExceeds,
+ @JsonProperty("responseForLastFailedPayment") final PaymentResponse[] responseForLastFailedPayment,
+ @JsonProperty("totalUnpaidInvoiceBalanceEqualsOrExceeds") final BigDecimal totalUnpaidInvoiceBalanceEqualsOrExceeds) {
this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
this.controlTagInclusion = controlTagInclusion;
this.controlTagExclusion = controlTagExclusion;
+ this.numberOfUnpaidInvoicesEqualsOrExceeds = numberOfUnpaidInvoicesEqualsOrExceeds;
+ this.responseForLastFailedPayment = responseForLastFailedPayment;
+ this.totalUnpaidInvoiceBalanceEqualsOrExceeds = totalUnpaidInvoiceBalanceEqualsOrExceeds;
}
public OverdueConditionJson(final OverdueCondition overdueCondition) {
this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = new DurationJson(overdueCondition.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds());
this.controlTagInclusion = overdueCondition.getInclusionControlTagType();
this.controlTagExclusion = overdueCondition.getExclusionControlTagType();
+ this.numberOfUnpaidInvoicesEqualsOrExceeds = overdueCondition.getNumberOfUnpaidInvoicesEqualsOrExceeds();
+ this.responseForLastFailedPayment = overdueCondition.getResponseForLastFailedPaymentIn();
+ this.totalUnpaidInvoiceBalanceEqualsOrExceeds = overdueCondition.getTotalUnpaidInvoiceBalanceEqualsOrExceeds();
}
public DurationJson getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds() {
@@ -60,12 +75,27 @@ public class OverdueConditionJson {
return controlTagExclusion;
}
+ public Integer getNumberOfUnpaidInvoicesEqualsOrExceeds() {
+ return numberOfUnpaidInvoicesEqualsOrExceeds;
+ }
+
+ public PaymentResponse[] getResponseForLastFailedPayment() {
+ return responseForLastFailedPayment;
+ }
+
+ public BigDecimal getTotalUnpaidInvoiceBalanceEqualsOrExceeds() {
+ return totalUnpaidInvoiceBalanceEqualsOrExceeds;
+ }
+
@Override
public String toString() {
return "OverdueConditionJson{" +
"timeSinceEarliestUnpaidInvoiceEqualsOrExceeds=" + timeSinceEarliestUnpaidInvoiceEqualsOrExceeds +
", controlTagInclusion=" + controlTagInclusion +
", controlTagExclusion=" + controlTagExclusion +
+ ", numberOfUnpaidInvoicesEqualsOrExceeds=" + numberOfUnpaidInvoicesEqualsOrExceeds +
+ ", responseForLastFailedPayment=" + responseForLastFailedPayment +
+ ", totalUnpaidInvoiceBalanceEqualsOrExceeds=" + totalUnpaidInvoiceBalanceEqualsOrExceeds +
'}';
}
@@ -86,7 +116,16 @@ public class OverdueConditionJson {
if (controlTagInclusion != that.controlTagInclusion) {
return false;
}
- return controlTagExclusion == that.controlTagExclusion;
+ if (controlTagExclusion != that.controlTagExclusion) {
+ return false;
+ }
+ if (numberOfUnpaidInvoicesEqualsOrExceeds != that.numberOfUnpaidInvoicesEqualsOrExceeds) {
+ return false;
+ }
+ if (responseForLastFailedPayment != that.responseForLastFailedPayment) {
+ return false;
+ }
+ return totalUnpaidInvoiceBalanceEqualsOrExceeds == that.totalUnpaidInvoiceBalanceEqualsOrExceeds;
}
@@ -95,6 +134,9 @@ public class OverdueConditionJson {
int result = timeSinceEarliestUnpaidInvoiceEqualsOrExceeds != null ? timeSinceEarliestUnpaidInvoiceEqualsOrExceeds.hashCode() : 0;
result = 31 * result + (controlTagInclusion != null ? controlTagInclusion.hashCode() : 0);
result = 31 * result + (controlTagExclusion != null ? controlTagExclusion.hashCode() : 0);
+ result = 31 * result + (numberOfUnpaidInvoicesEqualsOrExceeds != null ? numberOfUnpaidInvoicesEqualsOrExceeds.hashCode() : 0);
+ result = 31 * result + (responseForLastFailedPayment != null ? responseForLastFailedPayment.hashCode() : 0);
+ result = 31 * result + (totalUnpaidInvoiceBalanceEqualsOrExceeds != null ? totalUnpaidInvoiceBalanceEqualsOrExceeds.hashCode() : 0);
return result;
}
@@ -105,6 +147,10 @@ public class OverdueConditionJson {
}
result.setControlTagInclusion(input.getControlTagInclusion());
result.setControlTagExclusion(input.getControlTagExclusion());
+ result.setNumberOfUnpaidInvoicesEqualsOrExceeds(input.getNumberOfUnpaidInvoicesEqualsOrExceeds());
+ result.setResponseForLastFailedPayment(input.getResponseForLastFailedPayment());
+ result.setTotalUnpaidInvoiceBalanceEqualsOrExceeds(input.getTotalUnpaidInvoiceBalanceEqualsOrExceeds());
+
return result;
}
}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
index f2f8f2c..dccf9c2 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
@@ -22,11 +22,11 @@ import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
+import org.killbill.billing.client.KillBillClientException;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.Invoice;
import org.killbill.billing.client.model.InvoicePayment;
import org.killbill.billing.client.model.Invoices;
-import org.killbill.billing.client.model.Payment;
import org.killbill.billing.client.model.Tags;
import org.killbill.billing.util.tag.ControlTagType;
import org.testng.Assert;
@@ -42,9 +42,9 @@ public class TestOverdue extends TestJaxrsBase {
@Test(groups = "slow", description = "Upload and retrieve a per tenant overdue config")
public void testMultiTenantOverdueConfig() throws Exception {
final String overdueConfigPath = Resources.getResource("overdue.xml").getPath();
- killBillClient.uploadXMLOverdueConfig(overdueConfigPath, createdBy, reason, comment);
+ killBillClient.uploadXMLOverdueConfig(overdueConfigPath, requestOptions);
- final String overdueConfig = killBillClient.getXMLOverdueConfig();
+ final String overdueConfig = killBillClient.getXMLOverdueConfig(requestOptions);
Assert.assertNotNull(overdueConfig);
}
@@ -54,51 +54,35 @@ public class TestOverdue extends TestJaxrsBase {
final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
// Get the invoices
- final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
// 2 invoices but look for the non zero dollar one
assertEquals(invoices.size(), 2);
// We're still clear - see the configuration
- Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getIsClearState());
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
clock.addDays(30);
crappyWaitForLackOfProperSynchonization();
- Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD1");
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD1");
clock.addDays(10);
crappyWaitForLackOfProperSynchonization();
- Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD2");
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD2");
clock.addDays(10);
crappyWaitForLackOfProperSynchonization();
- Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD3");
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD3");
// Post external payments, paying the most recent invoice first: this is to avoid a race condition where
// a refresh overdue notification kicks in after the first payment, which makes the account goes CLEAR and
// triggers an AUTO_INVOICE_OFF tag removal (hence adjustment of the other invoices balance).
- final Invoices invoicesForAccount = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
- final List<Invoice> mostRecentInvoiceFirst = Ordering.<Invoice>from(new Comparator<Invoice>() {
- @Override
- public int compare(final Invoice invoice1, final Invoice invoice2) {
- return invoice1.getInvoiceDate().compareTo(invoice2.getInvoiceDate());
- }
- }).reverse().sortedCopy(invoicesForAccount);
- for (final Invoice invoice : mostRecentInvoiceFirst) {
- if (invoice.getBalance().compareTo(BigDecimal.ZERO) > 0) {
-
- final InvoicePayment invoicePayment = new InvoicePayment();
- invoicePayment.setPurchasedAmount(invoice.getAmount());
- invoicePayment.setAccountId(accountJson.getAccountId());
- invoicePayment.setTargetInvoiceId(invoice.getInvoiceId());
- killBillClient.createInvoicePayment(invoicePayment, true, createdBy, reason, comment);
- }
- }
+ postExternalPayments(accountJson);
// Wait a bit for overdue to pick up the payment events...
crappyWaitForLackOfProperSynchonization();
// Verify we're in clear state
- Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getIsClearState());
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
}
@Test(groups = "slow", description = "Allow overdue condition by control tag defined in overdue config xml file")
@@ -179,4 +163,128 @@ public class TestOverdue extends TestJaxrsBase {
// This account is expected to move to OD1 state because it matches with exclusion controlTag defined
Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJsonNoTag.getAccountId(), requestOptions).getName(), "OD1");
}
+
+ @Test(groups = "slow", description = "Allow overdue condition by number of unpaid invoices defined in overdue config xml file")
+ public void testOverdueStatusWithNumberOfUnpaidInvoicesCondition() throws Exception {
+ final String overdueConfigPath = Resources.getResource("overdueWithNumberOfUnpaidInvoicesCondition.xml").getPath();
+ killBillClient.uploadXMLOverdueConfig(overdueConfigPath, requestOptions);
+
+ // Create an account without a payment method
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ // 2 invoices but look for the non zero dollar one
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions).size(), 2);
+
+ // We're still clear - see the configuration
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
+
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+ // 3 invoices, 2 unpaid, must be inside OD1
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions).size(), 3);
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD1");
+
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+ // 4 invoices, 3 unpaid, must be inside OD2
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions).size(), 4);
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD2");
+
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+ // 5 invoices, 4 unpaid, must be still in OD2
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions).size(), 5);
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD2");
+
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+ // 6 invoices, 5 unpaid, must be inside OD3
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions).size(), 6);
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD3");
+
+ // Post external payments, paying the most recent invoice first: this is to avoid a race condition where
+ // a refresh overdue notification kicks in after the first payment, which makes the account goes CLEAR and
+ // triggers an AUTO_INVOICE_OFF tag removal (hence adjustment of the other invoices balance).
+ postExternalPayments(accountJson);
+
+ // Wait a bit for overdue to pick up the payment events...
+ crappyWaitForLackOfProperSynchonization();
+
+ // Verify we're in clear state
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
+ }
+
+ @Test(groups = "slow", description = "Allow overdue condition by total unpaid invoice balance defined in overdue config xml file")
+ public void testOverdueStatusWithTotalUnpaidInvoiceBalanceCondition() throws Exception {
+ final String overdueConfigPath = Resources.getResource("overdueWithTotalUnpaidInvoiceBalanceCondition.xml").getPath();
+ killBillClient.uploadXMLOverdueConfig(overdueConfigPath, requestOptions);
+
+ // Create an account without a payment method
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Amount balance should be USD 249.95
+ assertEquals(killBillClient.getAccount(accountJson.getAccountId(), true, false, requestOptions).getAccountBalance().compareTo(BigDecimal.valueOf(249.95)), 0);
+ // We're still clear - see the configuration
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
+
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+ // Amount balance should be USD 499.90
+ assertEquals(killBillClient.getAccount(accountJson.getAccountId(), true, false, requestOptions).getAccountBalance().compareTo(BigDecimal.valueOf(499.90)), 0);
+ // State must be inside OD1
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD1");
+
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+ // Amount balance should be USD 749.85
+ assertEquals(killBillClient.getAccount(accountJson.getAccountId(), true, false, requestOptions).getAccountBalance().compareTo(BigDecimal.valueOf(749.85)), 0);
+ // State must be inside OD2
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD2");
+
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+ // Amount balance should be USD 999.80
+ assertEquals(killBillClient.getAccount(accountJson.getAccountId(), true, false, requestOptions).getAccountBalance().compareTo(BigDecimal.valueOf(999.80)), 0);
+ // State must be still OD2
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD2");
+
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+ // Amount balance should be USD 1249.75
+ assertEquals(killBillClient.getAccount(accountJson.getAccountId(), true, false, requestOptions).getAccountBalance().compareTo(BigDecimal.valueOf(1249.75)), 0);
+ // State must be inside OD3
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD3");
+
+ // Post external payments, paying the most recent invoice first: this is to avoid a race condition where
+ // a refresh overdue notification kicks in after the first payment, which makes the account goes CLEAR and
+ // triggers an AUTO_INVOICE_OFF tag removal (hence adjustment of the other invoices balance).
+ postExternalPayments(accountJson);
+
+ // Wait a bit for overdue to pick up the payment events...
+ crappyWaitForLackOfProperSynchonization();
+
+ // Verify we're in clear state
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
+ }
+
+ private void postExternalPayments(final Account accountJson) throws KillBillClientException {
+ final Invoices invoicesForAccount = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
+ final List<Invoice> mostRecentInvoiceFirst = Ordering.<Invoice>from(new Comparator<Invoice>() {
+ @Override
+ public int compare(final Invoice invoice1, final Invoice invoice2) {
+ return invoice1.getInvoiceDate().compareTo(invoice2.getInvoiceDate());
+ }
+ }).reverse().sortedCopy(invoicesForAccount);
+ for (final Invoice invoice : mostRecentInvoiceFirst) {
+ if (invoice.getBalance().compareTo(BigDecimal.ZERO) > 0) {
+
+ final InvoicePayment invoicePayment = new InvoicePayment();
+ invoicePayment.setPurchasedAmount(invoice.getAmount());
+ invoicePayment.setAccountId(accountJson.getAccountId());
+ invoicePayment.setTargetInvoiceId(invoice.getInvoiceId());
+ killBillClient.createInvoicePayment(invoicePayment, true, requestOptions);
+ }
+ }
+ }
}