killbill-aplcache
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java 194(+194 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java 5(+2 -3)
payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentRoutingProviderPluginRegistryProvider.java 2(+1 -1)
payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentRoutingPluginApi.java 4(+2 -2)
payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentRoutingProviderPluginRegistry.java 4(+1 -3)
Details
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index 329873b..1b8af6b 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -37,6 +37,7 @@ import org.killbill.billing.util.entity.Pagination;
public interface SubscriptionBaseInternalApi {
+
public SubscriptionBase createSubscription(UUID bundleId, PlanPhaseSpecifier spec, List<PlanPhasePriceOverride> overrides, DateTime requestedDateWithMs,
InternalCallContext context) throws SubscriptionBaseApiException;
@@ -80,4 +81,6 @@ public interface SubscriptionBaseInternalApi {
public void updateExternalKey(UUID bundleId, String newExternalKey, InternalCallContext context);
+ public Iterable<DateTime> getFutureNotificationsForAccount(InternalCallContext context);
+
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
index 9982a0a..63977ad 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
@@ -120,6 +120,7 @@ public class TestIntegration extends TestIntegrationBase {
expectedInvoices);
checkNoMoreInvoiceToGenerate(account);
+
}
@Test(groups = "slow")
@@ -170,9 +171,12 @@ public class TestIntegration extends TestIntegrationBase {
DateTime nextDate = clock.getUTCNow().plusDays(1);
dryRun = new TestDryRunArguments();
- dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(nextDate, testTimeZone), dryRun, callContext);
+
+
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 2), new LocalDate(2012, 3, 31), InvoiceItemType.RECURRING, new BigDecimal("561.24")));
+ // Verify first next targetDate
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(nextDate, testTimeZone), dryRun, callContext);
invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
new file mode 100644
index 0000000..a7a0b4c
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+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.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.invoice.api.DryRunArguments;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.testng.annotations.Test;
+
+public class TestIntegrationInvoice extends TestIntegrationBase {
+
+ //
+ // Basic test with one subscription that verifies the behavior of using invoice dryRun api with no date
+ //
+ @Test(groups = "slow")
+ public void testDryRunWithNoTargetDate() throws Exception {
+
+ final int billingDay = 14;
+ final DateTime initialCreationDate = new DateTime(2015, 5, 15, 0, 0, 0, 0, testTimeZone);
+
+ log.info("Beginning test with BCD of " + billingDay);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+
+ // set clock to the initial start date
+ clock.setTime(initialCreationDate);
+ int invoiceItemCount = 1;
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ //
+ DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ DefaultSubscriptionBase subscription = subscriptionDataFromSubscription(baseEntitlement.getSubscriptionBase());
+ invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, new ExpectedInvoiceItemCheck(initialCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
+ invoiceChecker.checkChargedThroughDate(subscription.getId(), clock.getUTCToday(), callContext);
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 6, 14), new LocalDate(2015, 7, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+
+ // This will verify that the upcoming Phase is found and the invoice is generated at the right date, with correct items
+ DryRunArguments dryRun = new TestDryRunArguments();
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
+ // Move through time and verify we get the same invoice
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // This will verify that the upcoming invoice notification is found and the invoice is generated at the right date, with correct items
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 7, 14), new LocalDate(2015, 8, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
+
+ // Move through time and verify we get the same invoice
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // One more time, this will verify that the upcoming invoice notification is found and the invoice is generated at the right date, with correct items
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 8, 14), new LocalDate(2015, 9, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+ }
+
+
+ //
+ // More sophisticated test with two non aligned subscriptions that verifies the behavior of using invoice dryRun api with no date
+ // - The first subscription is an annual (SUBSCRIPTION aligned) whose billingDate is the first (we start on Jan 2nd to take into account the 30 days trial)
+ // - The second subscription is a monthly (ACCOUNT aligned) whose billingDate is the 14 (we start on Dec 15 to also take into account the 30 days trial)
+ //
+ // The test verifies that the dryRun invoice with supplied date will always take into account the 'closest' invoice that should be generated
+ //
+ @Test(groups = "slow")
+ public void testDryRunWithNoTargetDateAndMultipleNonAlignedSubscriptions() throws Exception {
+
+ // billing date for the monthly
+ final int billingDay = 14;
+
+ // Set in such a way that annual billing date will be the 1st
+ final DateTime initialCreationDate = new DateTime(2014, 1, 2, 0, 0, 0, 0, testTimeZone);
+ clock.setTime(initialCreationDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+
+ int invoiceItemCount = 1;
+
+ // Create ANNUAL BP
+ DefaultEntitlement baseEntitlementAnnual = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKeyAnnual", "Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.INVOICE);
+ DefaultSubscriptionBase subscriptionAnnual = subscriptionDataFromSubscription(baseEntitlementAnnual.getSubscriptionBase());
+ invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, new ExpectedInvoiceItemCheck(initialCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
+ invoiceChecker.checkChargedThroughDate(subscriptionAnnual.getId(), clock.getUTCToday(), callContext);
+
+
+ // Verify next dryRun invoice and then move the clock to that date to also verify real invoice is the same
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2014, 2, 1), new LocalDate(2015, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+
+ DryRunArguments dryRun = new TestDryRunArguments();
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+ // 2014-1-2
+ clock.addDays(30);
+ assertListenerStatus();
+ invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Since we only have one subscription next dryRun will show the annual
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 1), new LocalDate(2016, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2014-12-15
+ final DateTime secondSubscriptionCreationDate = new DateTime(2014, 12, 15, 0, 0, 0, 0, testTimeZone);
+ clock.setTime(secondSubscriptionCreationDate);
+
+ // Create the monthly
+ DefaultEntitlement baseEntitlementMonthly = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ DefaultSubscriptionBase subscriptionMonthly = subscriptionDataFromSubscription(baseEntitlementMonthly.getSubscriptionBase());
+ invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, new ExpectedInvoiceItemCheck(secondSubscriptionCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
+ invoiceChecker.checkChargedThroughDate(subscriptionMonthly.getId(), clock.getUTCToday(), callContext);
+
+ // Verify next dryRun invoice and then move the clock to that date to also verify real invoice is the same
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 1, 14), new LocalDate(2015, 2, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+ // 2015-1-14
+ clock.addDays(30);
+ assertListenerStatus();
+ invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ //
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 1), new LocalDate(2016, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT);
+ // 2015-2-1
+ clock.addDays(18);
+ assertListenerStatus();
+ invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
+
+ }
+
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java
index 007b7bc..1b0123b 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java
@@ -31,7 +31,6 @@ import org.killbill.billing.catalog.api.PriceListSet;
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.InvoiceUserApi;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.util.config.PaymentConfig;
@@ -144,7 +143,7 @@ public class TestIntegrationWithAutoPayOff extends TestIntegrationBase {
}
assertListenerStatus();
- int nbDaysBeforeRetry = paymentConfig.getPaymentRetryDays().get(0);
+ int nbDaysBeforeRetry = paymentConfig.getPaymentFailureRetryDays().get(0);
// MOVE TIME FOR RETRY TO HAPPEN
busHandler.pushExpectedEvents(NextEvent.PAYMENT);
@@ -205,7 +204,7 @@ public class TestIntegrationWithAutoPayOff extends TestIntegrationBase {
assertListenerStatus();
// RE-ADD AUTO_PAY_OFF to ON
- int nbDaysBeforeRetry = paymentConfig.getPaymentRetryDays().get(0);
+ int nbDaysBeforeRetry = paymentConfig.getPaymentFailureRetryDays().get(0);
add_AUTO_PAY_OFF_Tag(account.getId(), ObjectType.ACCOUNT);
// MOVE TIME FOR RETRY TO HAPPEN -> WILL BE DISCARDED SINCE AUTO_PAY_OFF IS SET
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index b62ab84..fc0b942 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -200,7 +200,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
}
@Override
- public Invoice triggerInvoiceGeneration(final UUID accountId, final LocalDate targetDate, final DryRunArguments dryRunArguments,
+ public Invoice triggerInvoiceGeneration(final UUID accountId, @Nullable final LocalDate targetDate, final DryRunArguments dryRunArguments,
final CallContext context) throws InvoiceApiException {
final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(accountId, context);
@@ -211,10 +211,10 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
throw new InvoiceApiException(e, ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, e.toString());
}
- final DateTime processingDateTime = targetDate.toDateTimeAtCurrentTime(account.getTimeZone());
+ final DateTime processingDateTime = targetDate != null ? targetDate.toDateTimeAtCurrentTime(account.getTimeZone()) : null;
final Invoice result = dispatcher.processAccount(accountId, processingDateTime, dryRunArguments, internalContext);
if (result == null) {
- throw new InvoiceApiException(ErrorCode.INVOICE_NOTHING_TO_DO, accountId, targetDate);
+ throw new InvoiceApiException(ErrorCode.INVOICE_NOTHING_TO_DO, accountId, targetDate != null ? targetDate : "null");
} else {
return result;
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 3d05980..88bd542 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -51,6 +51,7 @@ import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.events.InvoiceAdjustmentInternalEvent;
import org.killbill.billing.events.InvoiceInternalEvent;
import org.killbill.billing.events.InvoiceNotificationInternalEvent;
+import org.killbill.billing.invoice.api.DefaultInvoiceService;
import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
@@ -70,6 +71,8 @@ import org.killbill.billing.invoice.model.DefaultInvoice;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.InvoiceItemFactory;
import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier;
+import org.killbill.billing.invoice.notification.NextBillingDateNotificationKey;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.junction.BillingEventSet;
import org.killbill.billing.junction.BillingInternalApi;
@@ -86,16 +89,22 @@ import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLock;
import org.killbill.commons.locker.GlobalLocker;
import org.killbill.commons.locker.LockFailedException;
+import org.killbill.notificationq.api.NotificationEventWithMetadata;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
import com.google.inject.Inject;
public class InvoiceDispatcher {
@@ -103,6 +112,8 @@ public class InvoiceDispatcher {
private static final Logger log = LoggerFactory.getLogger(InvoiceDispatcher.class);
private static final int NB_LOCK_TRY = 5;
+ private static final Ordering<DateTime> UPCOMING_NOTIFICATION_DATE_ORDERING = Ordering.natural();
+
private static final NullDryRunArguments NULL_DRY_RUN_ARGUMENTS = new NullDryRunArguments();
private final InvoiceGenerator generator;
@@ -116,6 +127,7 @@ public class InvoiceDispatcher {
private final GlobalLocker locker;
private final PersistentBus eventBus;
private final Clock clock;
+ private final NotificationQueueService notificationQueueService;
@Inject
public InvoiceDispatcher(final InvoiceGenerator generator,
@@ -128,6 +140,7 @@ public class InvoiceDispatcher {
final InvoicePluginDispatcher invoicePluginDispatcher,
final GlobalLocker locker,
final PersistentBus eventBus,
+ final NotificationQueueService notificationQueueService,
final Clock clock) {
this.generator = generator;
this.billingApi = billingApi;
@@ -140,6 +153,7 @@ public class InvoiceDispatcher {
this.locker = locker;
this.eventBus = eventBus;
this.clock = clock;
+ this.notificationQueueService = notificationQueueService;
}
public void processSubscriptionForInvoiceGeneration(final EffectiveSubscriptionInternalEvent transition,
@@ -166,7 +180,6 @@ public class InvoiceDispatcher {
}
}
-
private Invoice processSubscriptionInternal(final UUID subscriptionId, final DateTime targetDate, final boolean dryRunForNotification, final InternalCallContext context) throws InvoiceApiException {
try {
if (subscriptionId == null) {
@@ -184,7 +197,6 @@ public class InvoiceDispatcher {
}
}
-
public Invoice processAccount(final UUID accountId, final DateTime targetDate,
@Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException {
GlobalLock lock = null;
@@ -204,34 +216,52 @@ public class InvoiceDispatcher {
return null;
}
- private Invoice processAccountWithLock(final UUID accountId, final DateTime targetDateTime,
+ private Invoice processAccountWithLock(final UUID accountId, @Nullable final DateTime inputTargetDateTime,
@Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException {
final boolean isDryRun = dryRunArguments != null;
- try {
+ // inputTargetDateTime is only allowed in dryRun mode to have the system compute it
+ Preconditions.checkArgument(inputTargetDateTime != null || isDryRun);
+ try {
// Make sure to first set the BCD if needed then get the account object (to have the BCD set)
final BillingEventSet billingEvents = billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId, dryRunArguments, context);
+ final List<DateTime> candidateDateTimes = (inputTargetDateTime != null) ? ImmutableList.of(inputTargetDateTime) : getUpcomingInvoiceCandidateDates(context);
+ for (final DateTime curTargetDateTime : candidateDateTimes) {
+ final Invoice invoice = processAccountWithLockAndInputTargetDate(accountId, curTargetDateTime, billingEvents, isDryRun, context);
+ if (invoice != null) {
+ return invoice;
+ }
+ }
+ return null;
+ } catch (CatalogApiException e) {
+ log.error("Failed handling SubscriptionBase change.", e);
+ return null;
+ }
+ }
+
+ private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId, final DateTime targetDateTime,
+ final BillingEventSet billingEvents, final boolean isDryRun, final InternalCallContext context) throws InvoiceApiException {
+ try {
final Account account = accountApi.getAccountById(accountId, context);
final DateAndTimeZoneContext dateAndTimeZoneContext = billingEvents.iterator().hasNext() ?
new DateAndTimeZoneContext(billingEvents.iterator().next().getEffectiveDate(), account.getTimeZone(), clock) :
null;
- List<Invoice> invoices = new ArrayList<Invoice>();
- if (!billingEvents.isAccountAutoInvoiceOff()) {
- invoices = ImmutableList.<Invoice>copyOf(Collections2.transform(invoiceDao.getInvoicesByAccount(context),
- new Function<InvoiceModelDao, Invoice>() {
- @Override
- public Invoice apply(final InvoiceModelDao input) {
- return new DefaultInvoice(input);
- }
- })); //no need to fetch, invoicing is off on this account
- }
+ final List<Invoice> invoices = billingEvents.isAccountAutoInvoiceOff() ?
+ ImmutableList.<Invoice>of() :
+ ImmutableList.<Invoice>copyOf(Collections2.transform(invoiceDao.getInvoicesByAccount(context),
+ new Function<InvoiceModelDao, Invoice>() {
+ @Override
+ public Invoice apply(final InvoiceModelDao input) {
+ return new DefaultInvoice(input);
+ }
+ }));
final Currency targetCurrency = account.getCurrency();
- final LocalDate targetDate = dateAndTimeZoneContext != null ? dateAndTimeZoneContext.computeTargetDate(targetDateTime) : null;
+ final LocalDate targetDate = (dateAndTimeZoneContext != null && targetDateTime != null) ? dateAndTimeZoneContext.computeTargetDate(targetDateTime) : null;
final Invoice invoice = targetDate != null ? generator.generateInvoice(account, billingEvents, invoices, targetDate, targetCurrency, context) : null;
//
// If invoice comes back null, there is nothing new to generate, we can bail early
@@ -257,97 +287,96 @@ public class InvoiceDispatcher {
//
final CallContext callContext = buildCallContext(context);
invoice.addInvoiceItems(invoicePluginDispatcher.getAdditionalInvoiceItems(invoice, callContext));
-
- boolean isRealInvoiceWithNonEmptyItems = false;
if (!isDryRun) {
- // Extract the set of invoiceId for which we see items that don't belong to current generated invoice
- final Set<UUID> adjustedUniqueOtherInvoiceId = new TreeSet<UUID>();
- adjustedUniqueOtherInvoiceId.addAll(Collections2.transform(invoice.getInvoiceItems(), new Function<InvoiceItem, UUID>() {
- @Nullable
- @Override
- public UUID apply(@Nullable final InvoiceItem input) {
- return input.getInvoiceId();
- }
- }));
- boolean isRealInvoiceWithItems = adjustedUniqueOtherInvoiceId.remove(invoice.getId());
- if (isRealInvoiceWithItems) {
- log.info("Generated invoice {} with {} items for accountId {} and targetDate {} (targetDateTime {})", new Object[]{invoice.getId(), invoice.getNumberOfItems(), accountId, targetDate, targetDateTime});
- } else {
- final Joiner joiner = Joiner.on(",");
- final String adjustedInvoices = joiner.join(adjustedUniqueOtherInvoiceId.toArray(new UUID[adjustedUniqueOtherInvoiceId.size()]));
- log.info("Adjusting existing invoices {} with {} items for accountId {} and targetDate {} (targetDateTime {})", new Object[]{adjustedInvoices, invoice.getNumberOfItems(),
- accountId, targetDate, targetDateTime});
- }
-
- // Transformation to Invoice -> InvoiceModelDao
- final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
- final Iterable<InvoiceItemModelDao> invoiceItemModelDaos = Iterables.transform(invoice.getInvoiceItems(),
- new Function<InvoiceItem, InvoiceItemModelDao>() {
- @Override
- public InvoiceItemModelDao apply(final InvoiceItem input) {
- return new InvoiceItemModelDao(input);
- }
- });
- final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceItemModelDaos, billingEvents, dateAndTimeZoneContext);
-
- // We filter any zero amount for USAGE items prior we generate the invoice, which may leave us with an invoice with no items;
- // we recompute the isRealInvoiceWithItems flag based on what is left (the call to invoice is still necessary to set the future notifications).
- final Iterable<InvoiceItemModelDao> filteredInvoiceItemModelDaos = Iterables.filter(invoiceItemModelDaos, new Predicate<InvoiceItemModelDao>() {
- @Override
- public boolean apply(@Nullable final InvoiceItemModelDao input) {
- return (input.getType() != InvoiceItemType.USAGE || input.getAmount().compareTo(BigDecimal.ZERO) != 0);
- }
- });
-
- final boolean isThereAnyItemsLeft = filteredInvoiceItemModelDaos.iterator().hasNext();
- isRealInvoiceWithNonEmptyItems = isThereAnyItemsLeft ? isRealInvoiceWithItems : false;
-
- if (isThereAnyItemsLeft) {
- invoiceDao.createInvoice(invoiceModelDao, ImmutableList.copyOf(filteredInvoiceItemModelDaos), isRealInvoiceWithItems, futureAccountNotifications, context);
- } else {
- invoiceDao.setFutureAccountNotificationsForEmptyInvoice(accountId, futureAccountNotifications, context);
- }
-
- final List<InvoiceItem> fixedPriceInvoiceItems = invoice.getInvoiceItems(FixedPriceInvoiceItem.class);
- final List<InvoiceItem> recurringInvoiceItems = invoice.getInvoiceItems(RecurringInvoiceItem.class);
- setChargedThroughDates(dateAndTimeZoneContext, fixedPriceInvoiceItems, recurringInvoiceItems, context);
-
- final List<InvoiceInternalEvent> events = new ArrayList<InvoiceInternalEvent>();
- if (isRealInvoiceWithNonEmptyItems) {
- events.add(new DefaultInvoiceCreationEvent(invoice.getId(), invoice.getAccountId(),
- invoice.getBalance(), invoice.getCurrency(),
- context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()));
- }
- for (final UUID cur : adjustedUniqueOtherInvoiceId) {
- final InvoiceAdjustmentInternalEvent event = new DefaultInvoiceAdjustmentEvent(cur, invoice.getAccountId(),
- context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
- events.add(event);
- }
-
- for (final InvoiceInternalEvent event : events) {
- postEvent(event, accountId, context);
- }
- }
-
- if (account.isNotifiedForInvoices() && isRealInvoiceWithNonEmptyItems && !isDryRun) {
- // Need to re-hydrate the invoice object to get the invoice number (record id)
- // API_FIX InvoiceNotifier public API?
- invoiceNotifier.notify(account, new DefaultInvoice(invoiceDao.getById(invoice.getId(), context)), buildTenantContext(context));
+ commitInvoiceStateAndNotifyAccountIfConfugured(account, invoice, billingEvents, dateAndTimeZoneContext, targetDate, context);
}
-
return invoice;
} catch (final AccountApiException e) {
log.error("Failed handling SubscriptionBase change.", e);
return null;
- } catch (CatalogApiException e) {
- log.error("Failed handling SubscriptionBase change.", e);
- return null;
} catch (SubscriptionBaseApiException e) {
log.error("Failed handling SubscriptionBase change.", e);
return null;
}
}
+ private void commitInvoiceStateAndNotifyAccountIfConfugured(final Account account, final Invoice invoice, final BillingEventSet billingEvents, final DateAndTimeZoneContext dateAndTimeZoneContext, final LocalDate targetDate, final InternalCallContext context) throws SubscriptionBaseApiException, InvoiceApiException {
+ boolean isRealInvoiceWithNonEmptyItems = false;
+ // Extract the set of invoiceId for which we see items that don't belong to current generated invoice
+ final Set<UUID> adjustedUniqueOtherInvoiceId = new TreeSet<UUID>();
+ adjustedUniqueOtherInvoiceId.addAll(Collections2.transform(invoice.getInvoiceItems(), new Function<InvoiceItem, UUID>() {
+ @Nullable
+ @Override
+ public UUID apply(@Nullable final InvoiceItem input) {
+ return input.getInvoiceId();
+ }
+ }));
+ boolean isRealInvoiceWithItems = adjustedUniqueOtherInvoiceId.remove(invoice.getId());
+ if (isRealInvoiceWithItems) {
+ log.info("Generated invoice {} with {} items for accountId {} and targetDate {}", new Object[]{invoice.getId(), invoice.getNumberOfItems(), account.getId(), targetDate});
+ } else {
+ final Joiner joiner = Joiner.on(",");
+ final String adjustedInvoices = joiner.join(adjustedUniqueOtherInvoiceId.toArray(new UUID[adjustedUniqueOtherInvoiceId.size()]));
+ log.info("Adjusting existing invoices {} with {} items for accountId {} and targetDate {})", new Object[]{adjustedInvoices, invoice.getNumberOfItems(),
+ account.getId(), targetDate});
+ }
+
+ // Transformation to Invoice -> InvoiceModelDao
+ final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
+ final Iterable<InvoiceItemModelDao> invoiceItemModelDaos = Iterables.transform(invoice.getInvoiceItems(),
+ new Function<InvoiceItem, InvoiceItemModelDao>() {
+ @Override
+ public InvoiceItemModelDao apply(final InvoiceItem input) {
+ return new InvoiceItemModelDao(input);
+ }
+ });
+ final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceItemModelDaos, billingEvents, dateAndTimeZoneContext);
+
+ // We filter any zero amount for USAGE items prior we generate the invoice, which may leave us with an invoice with no items;
+ // we recompute the isRealInvoiceWithItems flag based on what is left (the call to invoice is still necessary to set the future notifications).
+ final Iterable<InvoiceItemModelDao> filteredInvoiceItemModelDaos = Iterables.filter(invoiceItemModelDaos, new Predicate<InvoiceItemModelDao>() {
+ @Override
+ public boolean apply(@Nullable final InvoiceItemModelDao input) {
+ return (input.getType() != InvoiceItemType.USAGE || input.getAmount().compareTo(BigDecimal.ZERO) != 0);
+ }
+ });
+
+ final boolean isThereAnyItemsLeft = filteredInvoiceItemModelDaos.iterator().hasNext();
+ isRealInvoiceWithNonEmptyItems = isThereAnyItemsLeft ? isRealInvoiceWithItems : false;
+
+ if (isThereAnyItemsLeft) {
+ invoiceDao.createInvoice(invoiceModelDao, ImmutableList.copyOf(filteredInvoiceItemModelDaos), isRealInvoiceWithItems, futureAccountNotifications, context);
+ } else {
+ invoiceDao.setFutureAccountNotificationsForEmptyInvoice(account.getId(), futureAccountNotifications, context);
+ }
+
+ final List<InvoiceItem> fixedPriceInvoiceItems = invoice.getInvoiceItems(FixedPriceInvoiceItem.class);
+ final List<InvoiceItem> recurringInvoiceItems = invoice.getInvoiceItems(RecurringInvoiceItem.class);
+ setChargedThroughDates(dateAndTimeZoneContext, fixedPriceInvoiceItems, recurringInvoiceItems, context);
+
+ final List<InvoiceInternalEvent> events = new ArrayList<InvoiceInternalEvent>();
+ if (isRealInvoiceWithNonEmptyItems) {
+ events.add(new DefaultInvoiceCreationEvent(invoice.getId(), invoice.getAccountId(),
+ invoice.getBalance(), invoice.getCurrency(),
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()));
+ }
+ for (final UUID cur : adjustedUniqueOtherInvoiceId) {
+ final InvoiceAdjustmentInternalEvent event = new DefaultInvoiceAdjustmentEvent(cur, invoice.getAccountId(),
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ events.add(event);
+ }
+
+ for (final InvoiceInternalEvent event : events) {
+ postEvent(event, account.getId(), context);
+ }
+
+ if (account.isNotifiedForInvoices() && isRealInvoiceWithNonEmptyItems) {
+ // Need to re-hydrate the invoice object to get the invoice number (record id)
+ // API_FIX InvoiceNotifier public API?
+ invoiceNotifier.notify(account, new DefaultInvoice(invoiceDao.getById(invoice.getId(), context)), buildTenantContext(context));
+ }
+ }
+
private InvoiceItem computeCBAOnExistingInvoice(final Invoice invoice, final InternalCallContext context) throws InvoiceApiException {
// Transformation to Invoice -> InvoiceModelDao
final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
@@ -371,7 +400,6 @@ public class InvoiceDispatcher {
return internalCallContextFactory.createCallContext(context);
}
-
@VisibleForTesting
FutureAccountNotifications createNextFutureNotificationDate(final Iterable<InvoiceItemModelDao> invoiceItems, final BillingEventSet billingEvents, final DateAndTimeZoneContext dateAndTimeZoneContext) {
@@ -429,9 +457,8 @@ public class InvoiceDispatcher {
private DateTime getNextUsageBillingDate(final UUID subscriptionId, final String usageName, final LocalDate chargedThroughDate, final DateAndTimeZoneContext dateAndTimeZoneContext, final BillingEventSet billingEvents) {
-
final Usage usage = billingEvents.getUsages().get(usageName);
- final BillingEvent billingEventSubscription = Iterables.tryFind(billingEvents, new Predicate<BillingEvent>() {
+ final BillingEvent billingEventSubscription = Iterables.tryFind(billingEvents, new Predicate<BillingEvent>() {
@Override
public boolean apply(@Nullable final BillingEvent input) {
return input.getSubscription().getId().equals(subscriptionId);
@@ -486,6 +513,7 @@ public class InvoiceDispatcher {
}
public static class FutureAccountNotifications {
+
private final DateAndTimeZoneContext accountDateAndTimeZoneContext;
private final Map<UUID, List<DateTime>> notifications;
@@ -503,32 +531,73 @@ public class InvoiceDispatcher {
}
}
+ private List<DateTime> getUpcomingInvoiceCandidateDates(final InternalCallContext internalCallContext) {
+ final Iterable<DateTime> nextScheduledInvoiceDates = getNextScheduledInvoiceEffectiveDate(internalCallContext);
+ final Iterable<DateTime> nextScheduledSubscriptionsEventDates = subscriptionApi.getFutureNotificationsForAccount(internalCallContext);
+ Iterables.concat(nextScheduledInvoiceDates, nextScheduledSubscriptionsEventDates);
+ return UPCOMING_NOTIFICATION_DATE_ORDERING.sortedCopy(Iterables.concat(nextScheduledInvoiceDates, nextScheduledSubscriptionsEventDates));
+ }
+
+ private Iterable<DateTime> getNextScheduledInvoiceEffectiveDate(final InternalCallContext internalCallContext) {
+ try {
+ final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
+ DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
+ final List<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+
+ final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> filtered = Iterables.filter(futureNotifications, new Predicate<NotificationEventWithMetadata<NextBillingDateNotificationKey>>() {
+ @Override
+ public boolean apply(@Nullable final NotificationEventWithMetadata<NextBillingDateNotificationKey> input) {
+
+ final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
+ input.getEvent().isDryRunForInvoiceNotification() : false;
+ return !isEventDryRunForNotifications;
+ }
+ });
+
+ return Iterables.transform(filtered, new Function<NotificationEventWithMetadata<NextBillingDateNotificationKey>, DateTime>() {
+ @Nullable
+ @Override
+ public DateTime apply(@Nullable final NotificationEventWithMetadata<NextBillingDateNotificationKey> input) {
+ return input.getEffectiveDate();
+ }
+ });
+ } catch (final NoSuchNotificationQueue noSuchNotificationQueue) {
+ throw new IllegalStateException(noSuchNotificationQueue);
+ }
+ }
private final static class NullDryRunArguments implements DryRunArguments {
+
@Override
public PlanPhaseSpecifier getPlanPhaseSpecifier() {
return null;
}
+
@Override
public SubscriptionEventType getAction() {
return null;
}
+
@Override
public UUID getSubscriptionId() {
return null;
}
+
@Override
public DateTime getEffectiveDate() {
return null;
}
+
@Override
public UUID getBundleId() {
return null;
}
+
@Override
public BillingActionPolicy getBillingActionPolicy() {
return null;
}
+
@Override
public List<PlanPhasePriceOverride> getPlanPhasePriceoverrides() {
return null;
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
index 521ea31..6bcc93b 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -96,7 +96,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
- clock);
+ null, clock);
Invoice invoice = dispatcher.processAccount(accountId, target, new DryRunFutureDateArguments(), context);
Assert.assertNotNull(invoice);
@@ -149,7 +149,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
- clock);
+ null, clock);
final Invoice invoice = dispatcher.processAccount(account.getId(), new DateTime("2012-07-30T00:00:00.000Z"), null, context);
Assert.assertNotNull(invoice);
@@ -207,7 +207,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
- clock);
+ null, clock);
final FutureAccountNotifications futureAccountNotifications = dispatcher.createNextFutureNotificationDate(Collections.singletonList(item), null, dateAndTimeZoneContext);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
index 2cd3410..808abb6 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -198,7 +198,7 @@ public class TestInvoiceHelper {
final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi,
invoiceDao, internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
- clock);
+ null, clock);
Invoice invoice = dispatcher.processAccount(account.getId(), targetDate, new DryRunFutureDateArguments(), internalCallContext);
Assert.assertNotNull(invoice);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateJson.java
index a5446ce..ed0fd75 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateJson.java
@@ -56,7 +56,7 @@ public class OverdueStateJson {
public OverdueStateJson(final OverdueState overdueState, final PaymentConfig paymentConfig) {
this.name = overdueState.getName();
this.externalMessage = overdueState.getExternalMessage();
- this.daysBetweenPaymentRetries = paymentConfig.getPaymentRetryDays();
+ this.daysBetweenPaymentRetries = paymentConfig.getPaymentFailureRetryDays();
this.disableEntitlementAndChangesBlocked = overdueState.isDisableEntitlementAndChangesBlocked();
this.blockChanges = overdueState.isBlockChanges();
this.isClearState = overdueState.isClearState();
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
index 497756c..97e4fb7 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
@@ -283,14 +283,14 @@ public class InvoiceResource extends JaxRsResourceBase {
@ApiOperation(value = "Trigger an invoice generation", response = InvoiceJson.class)
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id or target datetime supplied")})
public Response createFutureInvoice(@QueryParam(QUERY_ACCOUNT_ID) final String accountId,
- @QueryParam(QUERY_TARGET_DATE) final String targetDateTime,
+ @QueryParam(QUERY_TARGET_DATE) final String targetDate,
@HeaderParam(HDR_CREATED_BY) final String createdBy,
@HeaderParam(HDR_REASON) final String reason,
@HeaderParam(HDR_COMMENT) final String comment,
@javax.ws.rs.core.Context final HttpServletRequest request,
@javax.ws.rs.core.Context final UriInfo uriInfo) throws AccountApiException, InvoiceApiException {
final CallContext callContext = context.createContext(createdBy, reason, comment, request);
- final LocalDate inputDate = toLocalDate(UUID.fromString(accountId), targetDateTime, callContext);
+ final LocalDate inputDate = toLocalDate(UUID.fromString(accountId), targetDate, callContext);
try {
final Invoice generatedInvoice = invoiceApi.triggerInvoiceGeneration(UUID.fromString(accountId), inputDate, null,
@@ -304,6 +304,7 @@ public class InvoiceResource extends JaxRsResourceBase {
}
}
+
@Timed
@POST
@Path("/" + DRY_RUN)
@@ -313,14 +314,15 @@ public class InvoiceResource extends JaxRsResourceBase {
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id or target datetime supplied")})
public Response generateDryRunInvoice(@Nullable final InvoiceDryRunJson dryRunSubscriptionSpec,
@QueryParam(QUERY_ACCOUNT_ID) final String accountId,
- @QueryParam(QUERY_TARGET_DATE) final String targetDateTime,
+ @Nullable @QueryParam(QUERY_TARGET_DATE) final String targetDate,
@HeaderParam(HDR_CREATED_BY) final String createdBy,
@HeaderParam(HDR_REASON) final String reason,
@HeaderParam(HDR_COMMENT) final String comment,
@javax.ws.rs.core.Context final HttpServletRequest request,
@javax.ws.rs.core.Context final UriInfo uriInfo) throws AccountApiException, InvoiceApiException {
final CallContext callContext = context.createContext(createdBy, reason, comment, request);
- final LocalDate inputDate = toLocalDate(UUID.fromString(accountId), targetDateTime, callContext);
+ // We allow special value UPCOMING_INVOICE_TARGET_DATE where the system will automatically generate the resulting targetDate for upcoming invoice
+ final LocalDate inputDate = targetDate != null && targetDate.equals(UPCOMING_INVOICE_TARGET_DATE) ? null : toLocalDate(UUID.fromString(accountId), targetDate, callContext);
// Passing a null or empty body means we are trying to generate an invoice with a (future) targetDate
// On the other hand if body is not null, we are attempting a dryRun subscription operation
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
index 872296c..e7a52cf 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -218,4 +218,6 @@ public interface JaxrsResource {
public static final String INVOICE_TRANSLATION = "translation";
public static final String INVOICE_CATALOG_TRANSLATION = "catalogTranslation";
+ public static final String UPCOMING_INVOICE_TARGET_DATE = "upcomingInvoiceTargetDate";
+
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentRoutingProviderPluginRegistryProvider.java b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentRoutingProviderPluginRegistryProvider.java
index f2b428a..edecd0b 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentRoutingProviderPluginRegistryProvider.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentRoutingProviderPluginRegistryProvider.java
@@ -44,7 +44,7 @@ public class DefaultPaymentRoutingProviderPluginRegistryProvider implements Prov
@Override
public OSGIServiceRegistration<PaymentRoutingPluginApi> get() {
- final DefaultPaymentRoutingProviderPluginRegistry pluginRegistry = new DefaultPaymentRoutingProviderPluginRegistry(paymentConfig);
+ final DefaultPaymentRoutingProviderPluginRegistry pluginRegistry = new DefaultPaymentRoutingProviderPluginRegistry();
// Make the external payment provider plugin available by default
final OSGIServiceDescriptor desc = new OSGIServiceDescriptor() {
diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentRoutingPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentRoutingPluginApi.java
index 4567bbf..e6a4ad4 100644
--- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentRoutingPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentRoutingPluginApi.java
@@ -366,7 +366,7 @@ public final class InvoicePaymentRoutingPluginApi implements PaymentRoutingPlugi
private DateTime getNextRetryDateForPaymentFailure(final List<PaymentTransactionModelDao> purchasedTransactions) {
DateTime result = null;
- final List<Integer> retryDays = paymentConfig.getPaymentRetryDays();
+ final List<Integer> retryDays = paymentConfig.getPaymentFailureRetryDays();
final int attemptsInState = getNumberAttemptsInState(purchasedTransactions, TransactionStatus.PAYMENT_FAILURE);
final int retryCount = (attemptsInState - 1) >= 0 ? (attemptsInState - 1) : 0;
if (retryCount < retryDays.size()) {
@@ -389,7 +389,7 @@ public final class InvoicePaymentRoutingPluginApi implements PaymentRoutingPlugi
final int retryAttempt = (attemptsInState - 1) >= 0 ? (attemptsInState - 1) : 0;
if (retryAttempt < paymentConfig.getPluginFailureRetryMaxAttempts()) {
- int nbSec = paymentConfig.getPluginFailureRetryStart();
+ int nbSec = paymentConfig.getPluginFailureInitialRetryInSec();
int remainingAttempts = retryAttempt;
while (--remainingAttempts > 0) {
nbSec = nbSec * paymentConfig.getPluginFailureRetryMultiplier();
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentRoutingProviderPluginRegistry.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentRoutingProviderPluginRegistry.java
index 36e08be..2d13746 100644
--- a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentRoutingProviderPluginRegistry.java
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentRoutingProviderPluginRegistry.java
@@ -33,12 +33,10 @@ public class DefaultPaymentRoutingProviderPluginRegistry implements OSGIServiceR
private final static Logger log = LoggerFactory.getLogger(DefaultPaymentProviderPluginRegistry.class);
- private final String defaultPlugin;
private final Map<String, PaymentRoutingPluginApi> pluginsByName = new ConcurrentHashMap<String, PaymentRoutingPluginApi>();
@Inject
- public DefaultPaymentRoutingProviderPluginRegistry(final PaymentConfig config) {
- this.defaultPlugin = config.getDefaultRetryProvider();
+ public DefaultPaymentRoutingProviderPluginRegistry() {
}
@Override
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
index 9d19b6e..2a1ae78 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
@@ -109,12 +109,12 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testFailedPaymentWithLastRetrySuccess() throws Exception {
- testSchedulesRetryInternal(paymentConfig.getPaymentRetryDays().size(), true, FailureType.PAYMENT_FAILURE);
+ testSchedulesRetryInternal(paymentConfig.getPaymentFailureRetryDays().size(), true, FailureType.PAYMENT_FAILURE);
}
@Test(groups = "fast")
public void testAbortedPayment() throws Exception {
- testSchedulesRetryInternal(paymentConfig.getPaymentRetryDays().size(), false, FailureType.PAYMENT_FAILURE);
+ testSchedulesRetryInternal(paymentConfig.getPaymentFailureRetryDays().size(), false, FailureType.PAYMENT_FAILURE);
}
private void testSchedulesRetryInternal(final int maxTries, final boolean lastSuccess, final FailureType failureType) throws Exception {
@@ -231,7 +231,7 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
private void moveClockForFailureType(final FailureType failureType, final int curFailure) throws InterruptedException {
final int nbDays;
if (failureType == FailureType.PAYMENT_FAILURE) {
- nbDays = paymentConfig.getPaymentRetryDays().get(curFailure) + 1;
+ nbDays = paymentConfig.getPaymentFailureRetryDays().get(curFailure) + 1;
} else {
nbDays = 1;
}
@@ -240,7 +240,7 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
private int getMaxRetrySizeForFailureType(final FailureType failureType) {
if (failureType == FailureType.PAYMENT_FAILURE) {
- return paymentConfig.getPaymentRetryDays().size();
+ return paymentConfig.getPaymentFailureRetryDays().size();
} else {
return paymentConfig.getPluginFailureRetryMaxAttempts();
}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
index 03b3482..b0c427d 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
@@ -26,6 +26,8 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.ProductCategory;
@@ -91,10 +93,17 @@ public class TestInvoice extends TestJaxrsBase {
final Invoice firstInvoiceByNumberJson = killBillClient.getInvoice(invoiceJson.getInvoiceNumber());
assertEquals(firstInvoiceByNumberJson, invoiceJson);
- // Then create a dryRun Invoice
- final DateTime futureDate = clock.getUTCNow().plusMonths(1).plusDays(3);
- killBillClient.createDryRunInvoice(accountJson.getAccountId(), futureDate, null, createdBy, reason, comment);
+ // Then create a dryRun for next upcoming invoice
+ LocalDate futureDate = null;
+ final Invoice dryRunInvoice = killBillClient.createDryRunInvoice(accountJson.getAccountId(), futureDate, null, createdBy, reason, comment);
+ assertEquals(dryRunInvoice.getBalance(), new BigDecimal("249.95"));
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2012, 6, 25));
+ assertEquals(dryRunInvoice.getItems().size(), 1);
+ assertEquals(dryRunInvoice.getItems().get(0).getStartDate(), new LocalDate(2012, 6, 25));
+ assertEquals(dryRunInvoice.getItems().get(0).getEndDate(), new LocalDate(2012, 7, 25));
+ assertEquals(dryRunInvoice.getItems().get(0).getAmount(), new BigDecimal("249.95"));
+ futureDate = dryRunInvoice.getTargetDate();
// The one more time with no DryRun
killBillClient.createInvoice(accountJson.getAccountId(), futureDate, createdBy, reason, comment);
@@ -113,7 +122,7 @@ public class TestInvoice extends TestJaxrsBase {
final Account accountJson = createAccountWithDefaultPaymentMethod();
final InvoiceDryRun dryRunArg = new InvoiceDryRun(SubscriptionEventType.START_BILLING,
null, "Assault-Rifle", ProductCategory.BASE, BillingPeriod.ANNUAL, null, null, null, null, null, null);
- final Invoice dryRunInvoice = killBillClient.createDryRunInvoice(accountJson.getAccountId(), initialDate, dryRunArg, createdBy, reason, comment);
+ final Invoice dryRunInvoice = killBillClient.createDryRunInvoice(accountJson.getAccountId(), new LocalDate(initialDate, DateTimeZone.forID(accountJson.getTimeZone())), dryRunArg, createdBy, reason, comment);
assertEquals(dryRunInvoice.getItems().size(), 1);
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index adfe092..561e8cb 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -20,6 +20,7 @@ package org.killbill.billing.subscription.api.svcs;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -62,6 +63,7 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
@@ -73,12 +75,18 @@ import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
import org.killbill.clock.Clock;
import org.killbill.clock.DefaultClock;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationEventWithMetadata;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import com.google.inject.Inject;
import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
@@ -90,9 +98,12 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
private final AddonUtils addonUtils;
private final InternalCallContextFactory internalCallContextFactory;
+ private final NotificationQueueService notificationQueueService;
+
@Inject
public DefaultSubscriptionInternalApi(final SubscriptionDao dao,
final DefaultSubscriptionBaseApiService apiService,
+ final NotificationQueueService notificationQueueService,
final Clock clock,
final CatalogService catalogService,
final AddonUtils addonUtils,
@@ -100,8 +111,10 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
super(dao, apiService, clock, catalogService);
this.addonUtils = addonUtils;
this.internalCallContextFactory = internalCallContextFactory;
+ this.notificationQueueService = notificationQueueService;
}
+
@Override
public SubscriptionBase createSubscription(final UUID bundleId, final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDateWithMs, final InternalCallContext context) throws SubscriptionBaseApiException {
try {
@@ -478,6 +491,25 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
}
+ @Override
+ public Iterable<DateTime> getFutureNotificationsForAccount(final InternalCallContext internalCallContext) {
+ try {
+ final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultSubscriptionBaseService.SUBSCRIPTION_SERVICE_NAME,
+ DefaultSubscriptionBaseService.NOTIFICATION_QUEUE_NAME);
+ final List<NotificationEventWithMetadata<NotificationEvent>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+ return Iterables.transform(futureNotifications, new Function<NotificationEventWithMetadata<NotificationEvent>, DateTime>() {
+ @Nullable
+ @Override
+ public DateTime apply(final NotificationEventWithMetadata<NotificationEvent> input) {
+ return input.getEffectiveDate();
+ }
+ });
+ } catch(NoSuchNotificationQueue noSuchNotificationQueue) {
+ throw new IllegalStateException(noSuchNotificationQueue);
+ }
+ }
+
+
private DateTime getBundleStartDateWithSanity(final UUID bundleId, @Nullable final DefaultSubscriptionBase baseSubscription, final Plan plan,
final DateTime requestedDate, final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException {
switch (plan.getProduct().getCategory()) {
diff --git a/util/src/main/java/org/killbill/billing/util/config/CatalogConfig.java b/util/src/main/java/org/killbill/billing/util/config/CatalogConfig.java
index 28c09ef..e27ae6c 100644
--- a/util/src/main/java/org/killbill/billing/util/config/CatalogConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/CatalogConfig.java
@@ -24,6 +24,6 @@ public interface CatalogConfig extends KillbillConfig {
@Config("org.killbill.catalog.uri")
@Default("SpyCarBasic.xml")
- @Description("Catalog location. Either in the classpath or in the filesystem")
+ @Description("Default Catalog location, either in the classpath or in the filesystem. For multi-tenancy, one should use APIs to load per-tenant catalog")
String getCatalogURI();
}
diff --git a/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java
index c3fd9b7..f42a635 100644
--- a/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java
@@ -41,7 +41,7 @@ public interface InvoiceConfig extends KillbillConfig {
@Config("org.killbill.invoice.readMaxRawUsagePreviousPeriod")
@Default("2")
- @Description("Maximum number of billingPeriod we read when retrieving raw usage data")
+ @Description("Maximum number of past billing periods we use to fetch raw usage data (usage optimization)")
public int getMaxRawUsagePreviousPeriod();
}
diff --git a/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java b/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java
index 86a0014..4c90b6e 100644
--- a/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java
@@ -31,28 +31,24 @@ public interface PaymentConfig extends KillbillConfig {
@Description("Default payment provider to use")
public String getDefaultPaymentProvider();
- // STEPH_RETRY unique property (does not match payment one)
- @Config("org.killbill.payment.retry.provider.default")
- @Default("__external_retry__")
- @Description("Default retry provider to use")
- public String getDefaultRetryProvider();
-
@Config("org.killbill.payment.retry.days")
@Default("8,8,8")
- @Description("Interval in days between payment retries")
- public List<Integer> getPaymentRetryDays();
+ @Description("Specify the number of payment retries along with the interval in days between payment retries when payment failures occur")
+ public List<Integer> getPaymentFailureRetryDays();
@Config("org.killbill.payment.failure.retry.start.sec")
@Default("300")
- public int getPluginFailureRetryStart();
+ @Description("Specify the interval of time in seconds before retrying a payment that failed due to a plugin failure (gateway is down, transient error, ...")
+ public int getPluginFailureInitialRetryInSec();
@Config("org.killbill.payment.failure.retry.multiplier")
@Default("2")
+ @Description("Specify the multiplier to apply between in retry before retrying a payment that failed due to a plugin failure (gateway is down, transient error, ...")
public int getPluginFailureRetryMultiplier();
@Config("org.killbill.payment.failure.retry.max.attempts")
@Default("8")
- @Description("Maximum number of retries for failed payments")
+ @Description("Specify the max number of attempts before retrying a payment that failed due to a plugin failure (gateway is down, transient error, ...\"")
public int getPluginFailureRetryMaxAttempts();
@Config("org.killbill.payment.plugin.timeout")