killbill-uncached
Changes
account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java 12(+12 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java 8(+6 -2)
Details
diff --git a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
index 57203fc..829e000 100644
--- a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
+++ b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
@@ -29,6 +29,7 @@ import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountEmail;
import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.DefaultAccount;
import org.killbill.billing.account.api.DefaultAccountEmail;
import org.killbill.billing.account.api.DefaultMutableAccountData;
import org.killbill.billing.account.api.ImmutableAccountData;
@@ -174,4 +175,15 @@ public class DefaultAccountInternalApi extends DefaultAccountApiBase implements
final ObjectType irrelevant = null;
return new CacheLoaderArgument(irrelevant, args, context);
}
+
+ @Override
+ public List<Account> getChildrenAccounts(final UUID parentAccountId, final InternalCallContext context) throws AccountApiException {
+ return ImmutableList.<Account>copyOf(Collections2.transform(accountDao.getAccountsByParentId(parentAccountId, context),
+ new Function<AccountModelDao, Account>() {
+ @Override
+ public Account apply(final AccountModelDao input) {
+ return new DefaultAccount(input);
+ }
+ }));
+ }
}
diff --git a/api/src/main/java/org/killbill/billing/account/api/AccountInternalApi.java b/api/src/main/java/org/killbill/billing/account/api/AccountInternalApi.java
index ad70272..f935ae7 100644
--- a/api/src/main/java/org/killbill/billing/account/api/AccountInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/account/api/AccountInternalApi.java
@@ -43,4 +43,6 @@ public interface AccountInternalApi extends ImmutableAccountInternalApi {
void updatePaymentMethod(UUID accountId, UUID paymentMethodId, InternalCallContext context) throws AccountApiException;
UUID getByRecordId(Long recordId, InternalTenantContext context) throws AccountApiException;
+
+ List<Account> getChildrenAccounts(UUID parentAccountId, InternalCallContext context) throws AccountApiException;
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
index 29a302c..f7fb6b3 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
@@ -76,6 +76,10 @@ public abstract class TestOverdueBase extends TestIntegrationBase {
}
protected void checkODState(final String expected) {
+ checkODState(expected, account.getId());
+ }
+
+ protected void checkODState(final String expected, final UUID accountId) {
try {
// This will test the overdue notification queue: when we move the clock, the overdue system
// should get notified to refresh its state.
@@ -85,13 +89,13 @@ public abstract class TestOverdueBase extends TestIntegrationBase {
await().atMost(10, SECONDS).until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
- final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(account.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
+ final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
return expected.equals(stateName);
}
});
} catch (final Exception e) {
- final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(account.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
+ final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
Assert.assertEquals(stateName, expected, "Got exception: " + e.toString());
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueChildParentRelationship.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueChildParentRelationship.java
new file mode 100644
index 0000000..fba90a7
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueChildParentRelationship.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.beatrix.integration.overdue;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
+import org.testng.annotations.Test;
+import org.weakref.jmx.internal.guava.collect.Iterables;
+
+import static org.testng.Assert.assertEquals;
+
+// For all the tests, we set the the property org.killbill.payment.retry.days=8,8,8,8,8,8,8,8 so that Payment retry logic does not end with an ABORTED state
+// preventing final instant payment to succeed.
+//
+// The tests are difficult to follow because there are actually two tracks of retry in logic:
+// - The payment retries
+// - The overdue notifications
+//
+
+public class TestOverdueChildParentRelationship extends TestOverdueBase {
+
+ @Override
+ public String getOverdueConfig() {
+ final String configXml = "<overdueConfig>" +
+ " <accountOverdueStates>" +
+ " <initialReevaluationInterval>" +
+ " <unit>DAYS</unit><number>10</number>" +
+ " </initialReevaluationInterval>" +
+ " <state name=\"OD3\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>26</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD3</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>" +
+ " </state>" +
+ " <state name=\"OD2\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>18</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD2</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>" +
+ " <autoReevaluationInterval>" +
+ " <unit>DAYS</unit><number>8</number>" +
+ " </autoReevaluationInterval>" +
+ " </state>" +
+ " <state name=\"OD1\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>10</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+ " <autoReevaluationInterval>" +
+ " <unit>DAYS</unit><number>8</number>" +
+ " </autoReevaluationInterval>" +
+ " </state>" +
+ " </accountOverdueStates>" +
+ "</overdueConfig>";
+
+ return configXml;
+ }
+
+ @Test(groups = "slow", description = "Test overdue stages and return to clear on CTD for Parent and Child accounts")
+ public void testOverdueStagesParentChildAccounts() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+ setupAccount();
+ final Account childAccount = createAccountWithNonOsgiPaymentMethod(getChildAccountData(0, account.getId(), true));
+
+ // Set next invoice to fail and create subscription
+ paymentPlugin.makeAllInvoicesFailWithError(true);
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(childAccount.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ bundle = subscriptionApi.getSubscriptionBundle(baseEntitlement.getBundleId(), callContext);
+
+ invoiceChecker.checkInvoice(childAccount.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ //invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.PARENT_SUMMARY, new BigDecimal("0")));
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 5, 1), callContext);
+
+ // 2012-05-2 => DAY 1 : Parent Invoice commit status
+ addDaysAndCheckForCompletion(1, NextEvent.INVOICE);
+
+ // 2012-05-31 => DAY 30 have to get out of trial {I0, P0}
+ addDaysAndCheckForCompletion(29, NextEvent.PHASE, NextEvent.INVOICE);
+
+ // 2012-06-01 => Parent Invoice payment attempt
+ addDaysAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ invoiceChecker.checkInvoice(childAccount.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ //invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.PARENT_SUMMARY, new BigDecimal("249.95")));
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
+
+ // 2012-06-09 => DAY 8 : Retry P0
+ addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, childAccount.getId());
+
+ // 2012-06-11 => Day 10 - Retry P0 - Move to OD1 state
+ addDaysAndCheckForCompletion(2, NextEvent.BLOCK, NextEvent.BLOCK);
+ checkODState("OD1", account.getId());
+ checkODState("OD1", childAccount.getId());
+
+ // 2012-06-17 => DAY 16 - Retry P1
+ addDaysAndCheckForCompletion(6, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ checkODState("OD1", account.getId());
+ checkODState("OD1", childAccount.getId());
+
+ // 2012-06-19 => Day 18 - Retry P0 - Move to OD2 state
+ addDaysAndCheckForCompletion(2, NextEvent.TAG, NextEvent.BLOCK, NextEvent.TAG, NextEvent.BLOCK);
+ checkODState("OD2", account.getId());
+ checkODState("OD2", childAccount.getId());
+
+ // 2012-06-25 => DAY 24 - Retry P2
+ addDaysAndCheckForCompletion(6, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ checkODState("OD2", account.getId());
+ checkODState("OD2", childAccount.getId());
+
+ // 2012-06-27 => Day 26 - Retry P2 - Move to OD3 state
+ addDaysAndCheckForCompletion(2, NextEvent.BLOCK, NextEvent.BLOCK);
+ checkODState("OD3", account.getId());
+ checkODState("OD3", childAccount.getId());
+
+ // Make sure the 'invoice-service:next-billing-date-queue' gets processed before we continue and since we are in AUTO_INVOICING_OFF
+ // no event (NULL_INVOICE) will be generated and so we can't synchronize on any event, and we need to add a small amount of sleep
+ Thread.sleep(1000);
+
+ // Verify the account balance is 0
+ assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoiceUserApi.getAccountBalance(childAccount.getId(), callContext).compareTo(new BigDecimal("249.95")), 0);
+
+ allowPaymentsAndResetOverdueToClearByPayingAllUnpaidInvoices(1, 1, childAccount);
+
+ // check invoice generated after clear child account
+ invoiceChecker.checkInvoice(childAccount.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 19), new LocalDate(2012, 6, 27), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-66.65")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 27), new LocalDate(2012, 6, 27), InvoiceItemType.CBA_ADJ, new BigDecimal("66.65")));
+
+ // Verify the account balance is now 0
+ assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.ZERO), 0);
+ assertEquals(invoiceUserApi.getAccountBalance(childAccount.getId(), callContext).compareTo(BigDecimal.valueOf(-66.65)), 0);
+ }
+
+ @Test(groups = "slow", description = "Test overdue stages and return to clear on CTD for Parent and Child accounts")
+ public void testOverdueStagesParentMultiChildAccounts() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+ setupAccount();
+ final Account childAccount1 = createAccountWithNonOsgiPaymentMethod(getChildAccountData(0, account.getId(), true));
+ final Account childAccount2 = createAccountWithNonOsgiPaymentMethod(getChildAccountData(0, account.getId(), true));
+ final Account childAccount3 = createAccountWithNonOsgiPaymentMethod(getChildAccountData(0, account.getId(), true));
+ final Account childAccountNoPaymentDelegated = createAccountWithNonOsgiPaymentMethod(getChildAccountData(0, account.getId(), false));
+
+ // Set next invoice to fail and create subscription
+ paymentPlugin.makeAllInvoicesFailWithError(true);
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(childAccount1.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ bundle = subscriptionApi.getSubscriptionBundle(baseEntitlement.getBundleId(), callContext);
+
+ invoiceChecker.checkInvoice(childAccount1.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 5, 1), callContext);
+
+ // 2012-05-2 => DAY 1 : Parent Invoice commit status
+ addDaysAndCheckForCompletion(1, NextEvent.INVOICE);
+
+ // 2012-05-2 => DAY 2
+ addDaysAndCheckForCompletion(1);
+ final DefaultEntitlement baseEntitlement2 = createBaseEntitlementAndCheckForCompletion(childAccount2.getId(), "externalKey2", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ invoiceChecker.checkInvoice(childAccount2.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 3), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ invoiceChecker.checkChargedThroughDate(baseEntitlement2.getId(), new LocalDate(2012, 5, 3), callContext);
+
+ // 2012-05-3 => DAY 3 : Parent Invoice commit status (Sub.2)
+ addDaysAndCheckForCompletion(1, NextEvent.INVOICE);
+
+ // 2012-05-31 => DAY 30 have to get out of trial {I0, P0}
+ addDaysAndCheckForCompletion(27, NextEvent.PHASE, NextEvent.INVOICE);
+
+ // 2012-06-01 => Parent Invoice payment attempt
+ addDaysAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ // 2012-06-02 => DAY 30 have to get out of trial {I0, P0} (Sub.2)
+ addDaysAndCheckForCompletion(1, NextEvent.PHASE, NextEvent.INVOICE);
+
+ // 2012-06-03 => Parent Invoice payment attempt (Sub.2)
+ addDaysAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ invoiceChecker.checkInvoice(childAccount1.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
+
+ // 2012-06-09 => DAY 8 : Retry P0
+ addDaysAndCheckForCompletion(6, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, childAccount1.getId());
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, childAccount2.getId());
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, childAccount3.getId());
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, childAccountNoPaymentDelegated.getId());
+
+ // 2012-06-11 => Day 10 - Retry P0 - Move to OD1 state
+ addDaysAndCheckForCompletion(2, NextEvent.BLOCK, NextEvent.BLOCK, NextEvent.BLOCK, NextEvent.BLOCK,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ checkODState("OD1", account.getId());
+ checkODState("OD1", childAccount1.getId());
+ checkODState("OD1", childAccount2.getId());
+ checkODState("OD1", childAccount3.getId());
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, childAccountNoPaymentDelegated.getId());
+
+ // 2012-06-17 => DAY 16 : Retry P0
+ addDaysAndCheckForCompletion(6, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ checkODState("OD1", account.getId());
+ checkODState("OD1", childAccount1.getId());
+ checkODState("OD1", childAccount2.getId());
+ checkODState("OD1", childAccount3.getId());
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, childAccountNoPaymentDelegated.getId());
+
+ // 2012-06-19 => Day 18 - Retry P0 - Move to OD2 state
+ addDaysAndCheckForCompletion(2, NextEvent.TAG, NextEvent.BLOCK, NextEvent.TAG, NextEvent.BLOCK, NextEvent.TAG,
+ NextEvent.BLOCK, NextEvent.TAG, NextEvent.BLOCK,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ checkODState("OD2", account.getId());
+ checkODState("OD2", childAccount1.getId());
+ checkODState("OD2", childAccount2.getId());
+ checkODState("OD2", childAccount3.getId());
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, childAccountNoPaymentDelegated.getId());
+
+ allowPaymentsAndResetOverdueToClearByPayingAllUnpaidInvoices(0, 4, childAccount1, childAccount2, childAccount3);
+
+ }
+
+ private void allowPaymentsAndResetOverdueToClearByPayingAllUnpaidInvoices(final int expectedInvoicesCount, final int expectedNullInvoicesCount, final Account... childAccounts) {
+
+ // Reset plugin so payments should now succeed
+ paymentPlugin.makeAllInvoicesFailWithError(false);
+
+ // build expected event list
+ List<NextEvent> nextEventList = new ArrayList<NextEvent>();
+ nextEventList.addAll(Collections.nCopies(childAccounts.length + 1, NextEvent.BLOCK));
+ nextEventList.addAll(Collections.nCopies(childAccounts.length + 1, NextEvent.TAG));
+ nextEventList.addAll(Collections.nCopies(expectedInvoicesCount, NextEvent.INVOICE));
+ nextEventList.addAll(Collections.nCopies(expectedNullInvoicesCount, NextEvent.NULL_INVOICE));
+ nextEventList.addAll(Arrays.asList(NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT));
+
+ //
+ // We now pay all unpaid invoices.
+ //
+ // Upon paying the last invoice, the overdue system will clear the state and notify invoice that it should re-generate a new invoice
+ // for the part that was unblocked, which explains why on the last payment we expect an additional invoice (and payment if needed).
+ //
+ final List<Invoice> sortedInvoices = getUnpaidInvoicesOrderFromRecent();
+
+ int remainingUnpaidInvoices = sortedInvoices.size();
+ for (final Invoice invoice : sortedInvoices) {
+ if (invoice.getBalance().compareTo(BigDecimal.ZERO) > 0) {
+ remainingUnpaidInvoices--;
+ if (remainingUnpaidInvoices > 0) {
+ createPaymentAndCheckForCompletion(account, invoice, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ } else {
+ createPaymentAndCheckForCompletion(account, invoice, Iterables.toArray(nextEventList, NextEvent.class));
+ }
+ }
+ }
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
+ for (Account childAccount : childAccounts) {
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, childAccount.getId());
+ }
+ }
+
+ private List<Invoice> getUnpaidInvoicesOrderFromRecent() {
+ final Collection<Invoice> invoices = invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext);
+ // Sort in reverse order to first pay most recent invoice-- that way overdue state may only flip when we reach the last one.
+ final List<Invoice> sortedInvoices = new LinkedList<Invoice>(invoices);
+ Collections.sort(sortedInvoices, new Comparator<Invoice>() {
+ @Override
+ public int compare(final Invoice i1, final Invoice i2) {
+ return i2.getInvoiceDate().compareTo(i1.getInvoiceDate());
+ }
+ });
+ return sortedInvoices;
+ }
+
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java
index 6d579da..139ac52 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java
@@ -179,7 +179,8 @@ public class InvoiceDaoHelper {
final InvoiceModelDao invoice = (in.getParentInvoice() == null) ? in : in.getParentInvoice();
final BigDecimal balance = InvoiceModelDaoHelper.getBalance(invoice);
log.debug("Computed balance={} for invoice={}", balance, in);
- return InvoiceStatus.COMMITTED.equals(in.getStatus()) && (balance.compareTo(BigDecimal.ZERO) >= 1) && (upToDate == null || !in.getTargetDate().isAfter(upToDate));
+ return InvoiceStatus.COMMITTED.equals(in.getStatus()) && (balance.compareTo(BigDecimal.ZERO) >= 1) &&
+ (upToDate == null || in.getTargetDate() == null || !in.getTargetDate().isAfter(upToDate));
}
});
return new ArrayList<InvoiceModelDao>(unpaidInvoices);
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
index 867b81f..a0abc46 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
@@ -18,11 +18,14 @@
package org.killbill.billing.overdue.listener;
+import java.util.List;
import java.util.UUID;
import javax.inject.Named;
import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.events.ControlTagCreationInternalEvent;
@@ -66,6 +69,7 @@ public class OverdueListener {
private final OverduePoster asyncPoster;
private final OverdueConfigCache overdueConfigCache;
private final NonEntityDao nonEntityDao;
+ private final AccountInternalApi accountApi;
@Inject
public OverdueListener(final NonEntityDao nonEntityDao,
@@ -73,13 +77,15 @@ public class OverdueListener {
final Clock clock,
@Named(DefaultOverdueModule.OVERDUE_NOTIFIER_ASYNC_BUS_NAMED) final OverduePoster asyncPoster,
final OverdueConfigCache overdueConfigCache,
- final InternalCallContextFactory internalCallContextFactory) {
+ final InternalCallContextFactory internalCallContextFactory,
+ final AccountInternalApi accountApi) {
this.nonEntityDao = nonEntityDao;
this.clock = clock;
this.asyncPoster = asyncPoster;
this.overdueConfigCache = overdueConfigCache;
this.cacheControllerDispatcher = cacheControllerDispatcher;
this.internalCallContextFactory = internalCallContextFactory;
+ this.accountApi = accountApi;
}
@AllowConcurrentEvents
@@ -139,8 +145,26 @@ public class OverdueListener {
final boolean shouldInsertNotification = shouldInsertNotification(callContext);
if (shouldInsertNotification) {
- final OverdueAsyncBusNotificationKey notificationKey = new OverdueAsyncBusNotificationKey(accountId, action);
+ OverdueAsyncBusNotificationKey notificationKey = new OverdueAsyncBusNotificationKey(accountId, action);
asyncPoster.insertOverdueNotification(accountId, clock.getUTCNow(), OverdueAsyncBusNotifier.OVERDUE_ASYNC_BUS_NOTIFIER_QUEUE, notificationKey, callContext);
+
+ try {
+ final List<Account> childrenAccounts = accountApi.getChildrenAccounts(accountId, callContext);
+ if (childrenAccounts != null) {
+ for (Account childAccount : childrenAccounts) {
+
+ if (childAccount.isPaymentDelegatedToParent()) {
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(childAccount.getId(), callContext);
+ final InternalCallContext accountContext = internalCallContextFactory.createInternalCallContext(internalTenantContext.getAccountRecordId(), callContext);
+ notificationKey = new OverdueAsyncBusNotificationKey(childAccount.getId(), action);
+ asyncPoster.insertOverdueNotification(childAccount.getId(), clock.getUTCNow(), OverdueAsyncBusNotifier.OVERDUE_ASYNC_BUS_NOTIFIER_QUEUE, notificationKey, accountContext);
+ }
+ }
+ }
+ } catch (Exception e) {
+ log.error("Error loading child accounts from account " + accountId);
+ }
+
}
}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
index 163786d..0c1e151 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
@@ -33,6 +33,7 @@ import org.killbill.billing.overdue.calculator.BillingStateCalculator;
import org.killbill.billing.overdue.config.api.BillingState;
import org.killbill.billing.overdue.config.api.OverdueException;
import org.killbill.billing.overdue.config.api.OverdueStateSet;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.globallocker.LockerType;
import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLock;
@@ -41,8 +42,6 @@ import org.killbill.commons.locker.LockFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.common.base.MoreObjects;
-
public class OverdueWrapper {
@@ -60,6 +59,7 @@ public class OverdueWrapper {
private final OverdueStateSet overdueStateSet;
private final BillingStateCalculator billingStateCalcuator;
private final OverdueStateApplicator overdueStateApplicator;
+ private final InternalCallContextFactory internalCallContextFactory;
public OverdueWrapper(final ImmutableAccountData overdueable,
final BlockingInternalApi api,
@@ -67,7 +67,8 @@ public class OverdueWrapper {
final GlobalLocker locker,
final Clock clock,
final BillingStateCalculator billingStateCalcuator,
- final OverdueStateApplicator overdueStateApplicator) {
+ final OverdueStateApplicator overdueStateApplicator,
+ final InternalCallContextFactory internalCallContextFactory) {
this.overdueable = overdueable;
this.overdueStateSet = overdueStateSet;
this.api = api;
@@ -75,6 +76,7 @@ public class OverdueWrapper {
this.clock = clock;
this.billingStateCalcuator = billingStateCalcuator;
this.overdueStateApplicator = overdueStateApplicator;
+ this.internalCallContextFactory = internalCallContextFactory;
}
public OverdueState refresh(final DateTime effectiveDate, final InternalCallContext context) throws OverdueException, OverdueApiException {
@@ -131,7 +133,13 @@ public class OverdueWrapper {
overdueStateApplicator.clear(effectiveDate, overdueable, previousOverdueState, overdueStateSet.getClearState(), context);
}
- public BillingState billingState(final InternalTenantContext context) throws OverdueException {
+ public BillingState billingState(final InternalCallContext context) throws OverdueException {
+ if ((overdueable.getParentAccountId() != null) && (overdueable.isPaymentDelegatedToParent())) {
+ // calculate billing state from parent account
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(overdueable.getParentAccountId(), context);
+ final InternalCallContext parentAccountContext = internalCallContextFactory.createInternalCallContext(internalTenantContext.getAccountRecordId(), context);
+ return billingStateCalcuator.calculateBillingState(overdueable, parentAccountContext);
+ }
return billingStateCalcuator.calculateBillingState(overdueable, context);
}
}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java
index bec4c53..aa43f69 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java
@@ -36,6 +36,7 @@ import org.killbill.billing.overdue.config.DefaultOverdueState;
import org.killbill.billing.overdue.config.DefaultOverdueStateSet;
import org.killbill.billing.overdue.config.api.OverdueException;
import org.killbill.billing.overdue.config.api.OverdueStateSet;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
import org.slf4j.Logger;
@@ -54,6 +55,7 @@ public class OverdueWrapperFactory {
private final GlobalLocker locker;
private final Clock clock;
private final OverdueConfigCache overdueConfigCache;
+ private final InternalCallContextFactory internalCallContextFactory;
@Inject
public OverdueWrapperFactory(final BlockingInternalApi api,
@@ -62,7 +64,8 @@ public class OverdueWrapperFactory {
final BillingStateCalculator billingStateCalculator,
final OverdueStateApplicator overdueStateApplicatorBundle,
final OverdueConfigCache overdueConfigCache,
- final AccountInternalApi accountApi) {
+ final AccountInternalApi accountApi,
+ final InternalCallContextFactory internalCallContextFactory) {
this.billingStateCalculator = billingStateCalculator;
this.overdueStateApplicator = overdueStateApplicatorBundle;
this.accountApi = accountApi;
@@ -70,16 +73,17 @@ public class OverdueWrapperFactory {
this.locker = locker;
this.clock = clock;
this.overdueConfigCache = overdueConfigCache;
+ this.internalCallContextFactory = internalCallContextFactory;
}
public OverdueWrapper createOverdueWrapperFor(final ImmutableAccountData blockable, final InternalTenantContext context) throws OverdueException {
- return new OverdueWrapper(blockable, api, getOverdueStateSet(context), locker, clock, billingStateCalculator, overdueStateApplicator);
+ return new OverdueWrapper(blockable, api, getOverdueStateSet(context), locker, clock, billingStateCalculator, overdueStateApplicator, internalCallContextFactory);
}
public OverdueWrapper createOverdueWrapperFor(final UUID id, final InternalTenantContext context) throws OverdueException {
try {
final ImmutableAccountData account = accountApi.getImmutableAccountDataById(id, context);
- return new OverdueWrapper(account, api, getOverdueStateSet(context), locker, clock, billingStateCalculator, overdueStateApplicator);
+ return new OverdueWrapper(account, api, getOverdueStateSet(context), locker, clock, billingStateCalculator, overdueStateApplicator, internalCallContextFactory);
} catch (final AccountApiException e) {
throw new OverdueException(e);
}