killbill-memoizeit
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationDryRunInvoice.java 554(+554 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java 307(+2 -305)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java 4(+1 -3)
invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java 13(+7 -6)
invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java 48(+32 -16)
invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java 25(+24 -1)
invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java 33(+31 -2)
invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java 4(+3 -1)
NEWS 3(+3 -0)
payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java 3(+2 -1)
payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java 74(+73 -1)
pom.xml 2(+1 -1)
subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java 53(+0 -53)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java 18(+0 -18)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java 25(+24 -1)
Details
diff --git a/api/src/main/java/org/killbill/billing/callcontext/DefaultCallContext.java b/api/src/main/java/org/killbill/billing/callcontext/DefaultCallContext.java
index 865f58f..2d43bcd 100644
--- a/api/src/main/java/org/killbill/billing/callcontext/DefaultCallContext.java
+++ b/api/src/main/java/org/killbill/billing/callcontext/DefaultCallContext.java
@@ -20,9 +20,9 @@ import java.util.UUID;
import org.joda.time.DateTime;
-import org.killbill.clock.Clock;
import org.killbill.billing.util.callcontext.CallOrigin;
import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.clock.Clock;
public class DefaultCallContext extends CallContextBase {
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 33c4db0..a896c01 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
@@ -93,11 +93,6 @@ public interface SubscriptionBaseInternalApi {
public void updateExternalKey(UUID bundleId, String newExternalKey, InternalCallContext context);
- public Iterable<DateTime> getFutureNotificationsForAccount(InternalCallContext context);
-
- public Map<UUID, DateTime> getNextFutureEventForSubscriptions(final SubscriptionBaseTransitionType eventType, final InternalCallContext internalCallContext);
-
-
public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException;
public int getDefaultBillCycleDayLocal(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final PlanPhaseSpecifier planPhaseSpecifier, final int accountBillCycleDayLocal, final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationDryRunInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationDryRunInvoice.java
new file mode 100644
index 0000000..559a626
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationDryRunInvoice.java
@@ -0,0 +1,554 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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.ErrorCode;
+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.DefaultPlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+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.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
+import org.killbill.billing.invoice.api.DryRunArguments;
+import org.killbill.billing.invoice.api.DryRunType;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import static com.tc.util.Assert.fail;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+public class TestIntegrationDryRunInvoice extends TestIntegrationBase {
+
+ private static final DryRunArguments DRY_RUN_UPCOMING_INVOICE_ARG = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE);
+ private static final DryRunArguments DRY_RUN_TARGET_DATE_ARG = new TestDryRunArguments(DryRunType.TARGET_DATE);
+
+ //
+ // 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);
+ // set clock to the initial start date
+ clock.setTime(initialCreationDate);
+
+ log.info("Beginning test with BCD of " + billingDay);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+
+ int invoiceNumber = 1;
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE, NextEvent.BLOCK NextEvent.INVOICE
+ //
+ DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ DefaultSubscriptionBase subscription = subscriptionDataFromSubscription(baseEntitlement.getSubscriptionBase());
+ invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, 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
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+ // Move through time and verify we get the same invoice
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, 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, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+ // Move through time and verify we get the same invoice
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, 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, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, 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 and third subscriptions are monthly (ACCOUNT aligned) whose billingDate are 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 {
+ // 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);
+
+ // billing date for the monthly
+ final int billingDay = 14;
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+
+ int invoiceNumber = 1;
+
+ // Create ANNUAL BP
+ DefaultEntitlement baseEntitlementAnnual = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKeyAnnual", "Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ DefaultSubscriptionBase subscriptionAnnual = subscriptionDataFromSubscription(baseEntitlementAnnual.getSubscriptionBase());
+ invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, 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")));
+
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // 2014-2-1
+ clock.addDays(30);
+ assertListenerStatus();
+ invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, 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, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2014-12-15
+ final DateTime secondSubscriptionCreationDate = new DateTime(2014, 12, 15, 0, 0, 0, 0, testTimeZone);
+ clock.setTime(secondSubscriptionCreationDate);
+
+ // Create the first monthly
+ DefaultEntitlement baseEntitlementMonthly1 = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey1", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ DefaultSubscriptionBase subscriptionMonthly1 = subscriptionDataFromSubscription(baseEntitlementMonthly1.getSubscriptionBase());
+ invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, 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(subscriptionMonthly1.getId(), clock.getUTCToday(), callContext);
+
+ // Create the second monthly
+ DefaultEntitlement baseEntitlementMonthly2 = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey2", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ DefaultSubscriptionBase subscriptionMonthly2 = subscriptionDataFromSubscription(baseEntitlementMonthly2.getSubscriptionBase());
+ invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, 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(subscriptionMonthly2.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")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 1, 14), new LocalDate(2015, 2, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.NULL_INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // 2015-1-14
+ clock.addDays(30);
+ assertListenerStatus();
+ invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // We test first the next expected invoice for a specific subscription: We can see the targetDate is 2015-2-14 and not 2015-2-1
+ final DryRunArguments dryRunUpcomingInvoiceWithFilterArg1 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, subscriptionMonthly1.getId(), null, null, null);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg1, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 14));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ final DryRunArguments dryRunUpcomingInvoiceWithFilterArg2 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, subscriptionMonthly2.getId(), null, null, null);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg2, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 14));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Then we test first the next expected invoice at the account level
+ 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, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // 2015-2-1
+ clock.addDays(18);
+ assertListenerStatus();
+ invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ }
+
+ @Test(groups = "slow")
+ public void testDryRunWithPendingSubscription() throws Exception {
+
+ final LocalDate initialDate = new LocalDate(2017, 4, 1);
+ clock.setDay(initialDate);
+
+ // Create account with non BCD to force junction BCD logic to activate
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+ final LocalDate futureDate = new LocalDate(2017, 5, 1);
+
+ // No CREATE event as this is set in the future
+ final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, futureDate, futureDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(createdEntitlement.getState(), Entitlement.EntitlementState.PENDING);
+ assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(futureDate), 0);
+ assertEquals(createdEntitlement.getEffectiveEndDate(), null);
+ assertListenerStatus();
+
+ // Generate a dryRun invoice on the billing startDate
+ final Invoice dryRunInvoice1 = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, DRY_RUN_TARGET_DATE_ARG, callContext);
+ assertEquals(dryRunInvoice1.getInvoiceItems().size(), 1);
+ assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getStartDate(), futureDate);
+ assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getPlanName(), "shotgun-annual");
+
+ // Generate a dryRun invoice with a plan change
+ final DryRunArguments dryRunSubscriptionActionArg = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null,
+ SubscriptionEventType.CHANGE, createdEntitlement.getId(), createdEntitlement.getBundleId(), futureDate, BillingActionPolicy.IMMEDIATE);
+
+ // First one day prior subscription starts
+ try {
+ invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate.minusDays(1), dryRunSubscriptionActionArg, callContext);
+ fail("Should fail to trigger dryRun invoice prior subscription starts");
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.INVOICE_NOTHING_TO_DO.getCode());
+ }
+
+ // Second, on the startDate
+ final Invoice dryRunInvoice2 = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, dryRunSubscriptionActionArg, callContext);
+ assertEquals(dryRunInvoice2.getInvoiceItems().size(), 1);
+ assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getStartDate(), futureDate);
+ assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getPlanName(), "pistol-monthly");
+
+ // Check BCD is not yet set
+ final Account refreshedAccount1 = accountUserApi.getAccountById(account.getId(), callContext);
+ assertEquals(refreshedAccount1.getBillCycleDayLocal(), new Integer(0));
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ final Invoice realInvoice = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, null, callContext);
+ assertListenerStatus();
+
+ assertEquals(realInvoice.getInvoiceItems().size(), 1);
+ assertEquals(realInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(realInvoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(realInvoice.getInvoiceItems().get(0).getStartDate(), futureDate);
+ assertEquals(realInvoice.getInvoiceItems().get(0).getPlanName(), "shotgun-annual");
+
+ // Check BCD is now set
+ final Account refreshedAccount2 = accountUserApi.getAccountById(account.getId(), callContext);
+ assertEquals(refreshedAccount2.getBillCycleDayLocal(), new Integer(31));
+
+ // Move clock past startDate to check nothing happens
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
+ clock.addDays(31);
+ assertListenerStatus();
+
+ // Move clock after PHASE event
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(12);
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testDryRunWithPendingCancelledSubscription() throws Exception {
+
+ final LocalDate initialDate = new LocalDate(2017, 4, 1);
+ clock.setDay(initialDate);
+
+ // Create account with non BCD to force junction BCD logic to activate
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("pistol-monthly-notrial", null);
+
+ final LocalDate futureStartDate = new LocalDate(2017, 5, 1);
+
+ // No CREATE event as this is set in the future
+ final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, futureStartDate, futureStartDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(createdEntitlement.getState(), Entitlement.EntitlementState.PENDING);
+ assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(futureStartDate), 0);
+ assertEquals(createdEntitlement.getEffectiveEndDate(), null);
+ assertListenerStatus();
+
+ // Generate an invoice using a future targetDate
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ final Invoice firstInvoice = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureStartDate, null, callContext);
+ assertListenerStatus();
+
+ assertEquals(firstInvoice.getInvoiceItems().size(), 1);
+ assertEquals(firstInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(firstInvoice.getInvoiceItems().get(0).getAmount().compareTo(new BigDecimal("19.95")), 0);
+ assertEquals(firstInvoice.getInvoiceItems().get(0).getStartDate(), futureStartDate);
+ assertEquals(firstInvoice.getInvoiceItems().get(0).getPlanName(), "pistol-monthly-notrial");
+
+ // Cancel subscription on its pending startDate
+ createdEntitlement.cancelEntitlementWithDate(futureStartDate, true, ImmutableList.<PluginProperty>of(), callContext);
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-19.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 4, 1), new LocalDate(2017, 4, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("19.95")));
+
+ final Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2017, 5, 1));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Move to startDate/cancel Date
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.NULL_INVOICE, NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-19.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("19.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, expectedInvoices);
+ }
+
+
+ @Test(groups = "slow", description = "See https://github.com/killbill/killbill/issues/774")
+ public void testDryRunTargetDateWithIntermediateInvoice() throws Exception {
+ final DateTime initialCreationDate = new DateTime(2014, 1, 2, 0, 0, 0, 0, testTimeZone);
+ clock.setTime(initialCreationDate);
+
+ // billing date for the monthly
+ final int billingDay = 14;
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+
+ // Create first ANNUAL BP -> BCD = 1
+ createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKeyAnnual1", "Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2014-1-4
+ clock.addDays(2);
+ // Create second ANNUAL BP -> BCD = 3
+ createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKeyAnnual2", "Pistol", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2014-2-1
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(28);
+ assertListenerStatus();
+
+ // 2014-2-3
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(2);
+ assertListenerStatus();
+
+ // 2014-12-15
+ final DateTime monthlySubscriptionCreationDate = new DateTime(2014, 12, 15, 0, 0, 0, 0, testTimeZone);
+ clock.setTime(monthlySubscriptionCreationDate);
+
+ // Create the monthly
+ createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2015-1-14
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+
+ // At this point (2015-1-14), we have 3 pending invoice notifications:
+ //
+ // - The 1st ANNUAL on 2015-2-1
+ // - The 2nd ANNUAL on 2015-2-3
+ // - The MONTHLY on 2015-2-14
+ //
+ // 1. We verify that a DryRunType.TARGET_DATE for 2015-2-14 leads to an invoice that **only** contains the MONTHLY item (fix for #774)
+ //
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(2015, 2, 14), DRY_RUN_TARGET_DATE_ARG, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 14));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2. We verify that a DryRunType.TARGET_DATE for 2015-2-3 leads to an invoice that **only** contains the 2nd ANNUAL item (fix for #774)
+ //
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 3), new LocalDate(2016, 2, 3), InvoiceItemType.RECURRING, new BigDecimal("199.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(2015, 2, 3), DRY_RUN_TARGET_DATE_ARG, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 3));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 3. We verify that UPCOMING_INVOICE leads to next invoice fo 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, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 1));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ }
+
+ @Test(groups = "slow")
+ public void testDryRunWithAOs() throws Exception {
+ final LocalDate initialDate = new LocalDate(2017, 12, 1);
+ clock.setDay(initialDate);
+
+ // Create account with non BCD to force junction BCD logic to activate
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
+
+ // No CREATE event as this is set in the future
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("pistol-monthly-notrial", null);
+ final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, null, null, false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ final DefaultEntitlement aoEntitlement = addAOEntitlementAndCheckForCompletion(baseEntitlement.getBundleId(), "Refurbish-Maintenance", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2018, 2, 1), new LocalDate(2018, 3, 1), InvoiceItemType.RECURRING, new BigDecimal("19.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2018, 2, 1), new LocalDate(2018, 3, 1), InvoiceItemType.RECURRING, new BigDecimal("199.95")));
+
+ // Specify AO subscriptionId filter
+ final DryRunArguments dryRunUpcomingInvoiceWithFilterArg1 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, aoEntitlement.getId(), null, null, null);
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg1, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2018, 2, 1));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+ // Specify BP subscriptionId filter
+ final DryRunArguments dryRunUpcomingInvoiceWithFilterArg2 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, baseEntitlement.getId(), null, null, null);
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg2, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2018, 2, 1));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+ // Specify bundleId filter
+ final DryRunArguments dryRunUpcomingInvoiceWithFilterArg3 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, null, baseEntitlement.getBundleId(), null, null);
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg3, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2018, 2, 1));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+ }
+
+ @Test(groups = "slow")
+ public void testDryRunWithUpcomingSubscriptionEvents() throws Exception {
+
+ final DateTime initialDate = new DateTime(2017, 11, 1, 0, 3, 42, 0, testTimeZone);
+
+ // set clock to the initial start date
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ assertNotNull(baseEntitlement);
+
+ // Verify the next invoice based on the PHASE event is correctly seen in the dryRun scenario
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 1), new LocalDate(2018, 1, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 1), new LocalDate(2018, 1, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+
+ // Future pause the subscription
+ LocalDate effectivePauseDate = new LocalDate(2017, 12, 15);
+ entitlementApi.pause(baseEntitlement.getBundleId(), effectivePauseDate, ImmutableList.<PluginProperty>of(), callContext);
+
+ // Verify the next invoice based on the PAUSE event is correctly seen in the dryRun scenario
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 15), new LocalDate(2018, 1, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-137.07")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 1), new LocalDate(2017, 12, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("137.07")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Hit the pause effective date 2017-12-15)
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.INVOICE);
+ clock.addDays(14);
+ assertListenerStatus();
+
+ // Unfortunately we can't reuse *exactly the items from the dryRun invoice because the effective date for the CBA is set with current date.
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 15), new LocalDate(2018, 1, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-137.07")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 15), new LocalDate(2017, 12, 15), InvoiceItemType.CBA_ADJ, new BigDecimal("137.07")));
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Future resume the subscription
+ LocalDate effectiveResumeDate = new LocalDate(2017, 12, 25);
+ entitlementApi.resume(baseEntitlement.getBundleId(), effectiveResumeDate, ImmutableList.<PluginProperty>of(), callContext);
+
+ // Verify the next invoice based on the RESUME event is correctly seen in the dryRun scenario
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 25), new LocalDate(2018, 1, 1), InvoiceItemType.RECURRING, new BigDecimal("56.44")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 15), new LocalDate(2017, 12, 15), InvoiceItemType.CBA_ADJ, new BigDecimal("-56.44")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+ expectedInvoices.clear();
+
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.INVOICE);
+ clock.addDays(10);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 25), new LocalDate(2018, 1, 1), InvoiceItemType.RECURRING, new BigDecimal("56.44")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 25), new LocalDate(2017, 12, 25), InvoiceItemType.CBA_ADJ, new BigDecimal("-56.44")));
+
+ }
+
+}
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
index 8155e6d..c9c9157 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
@@ -25,26 +25,19 @@ import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
-import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
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.DefaultPlanPhasePriceOverride;
-import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
-import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.catalog.api.UsagePriceOverride;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.entitlement.api.Entitlement;
-import org.killbill.billing.entitlement.api.SubscriptionEventType;
-import org.killbill.billing.invoice.api.DryRunArguments;
-import org.killbill.billing.invoice.api.DryRunType;
import org.killbill.billing.invoice.api.Invoice;
-import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
@@ -55,170 +48,11 @@ import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
-import static com.tc.util.Assert.fail;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
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);
- // set clock to the initial start date
- clock.setTime(initialCreationDate);
-
- log.info("Beginning test with BCD of " + billingDay);
- final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
-
- int invoiceItemCount = 1;
-
- //
- // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE, NextEvent.BLOCK NextEvent.INVOICE
- //
- DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, 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(DryRunType.UPCOMING_INVOICE);
- Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
- invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-
- // Move through time and verify we get the same invoice
- busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
- clock.addDays(30);
- assertListenerStatus();
- List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, 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, expectedInvoices);
-
-
- // Move through time and verify we get the same invoice
- busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
- clock.addMonths(1);
- assertListenerStatus();
- invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, 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, 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 {
- // 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);
-
- // billing date for the monthly
- final int billingDay = 14;
-
- 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.BLOCK, 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(DryRunType.UPCOMING_INVOICE);
- Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
- invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-
- busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
- // 2014-2-1
- 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, 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.BLOCK, 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, expectedInvoices);
-
- busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
- // 2015-1-14
- clock.addDays(30);
- assertListenerStatus();
- invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
- expectedInvoices.clear();
-
-
- // We test first the next expected invoice for a specific subscription: We can see the targetDate is 2015-2-14 and not 2015-2-1
- final DryRunArguments dryRunWIthSubscription = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, subscriptionMonthly.getId(), null, null, null);
- 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, dryRunWIthSubscription, callContext);
- assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 14));
- invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
- expectedInvoices.clear();
-
- // Then we test first the next expected invoice at the account level
- 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, expectedInvoices);
-
- busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_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, expectedInvoices);
- }
-
@Test(groups = "slow")
public void testApplyCreditOnExistingBalance() throws Exception {
final DateTime initialCreationDate = new DateTime(2015, 5, 15, 0, 0, 0, 0, testTimeZone);
@@ -268,7 +102,6 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
final BigDecimal accountBalance3 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
assertTrue(accountBalance3.compareTo(BigDecimal.ZERO) == 0);
-
final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, false, ImmutableList.<PluginProperty>of(), callContext);
assertEquals(payments.size(), 1);
@@ -289,7 +122,7 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
int invoiceItemCount = 1;
- DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
DefaultSubscriptionBase subscription = subscriptionDataFromSubscription(baseEntitlement.getSubscriptionBase());
final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
@@ -336,8 +169,7 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
assertEquals(accountPayments.get(2).getPurchasedAmount(), new BigDecimal("10.00"));
}
-
- @Test(groups = "slow", description= "See https://github.com/killbill/killbill/issues/127#issuecomment-292445089")
+ @Test(groups = "slow", description = "See https://github.com/killbill/killbill/issues/127#issuecomment-292445089")
public void testIntegrationWithBCDLargerThanEndMonth() throws Exception {
final int billingDay = 31;
@@ -369,141 +201,6 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
assertListenerStatus();
}
- @Test(groups = "slow")
- public void testDryRunWithPendingSubscription() throws Exception {
-
- final LocalDate initialDate = new LocalDate(2017, 4, 1);
- clock.setDay(initialDate);
-
- // Create account with non BCD to force junction BCD logic to activate
- final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
-
- final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null);
-
- final LocalDate futureDate = new LocalDate(2017, 5, 1);
-
- // No CREATE event as this is set in the future
- final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, futureDate, futureDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
- assertEquals(createdEntitlement.getState(), Entitlement.EntitlementState.PENDING);
- assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(futureDate), 0);
- assertEquals(createdEntitlement.getEffectiveEndDate(), null);
- assertListenerStatus();
-
- // Generate a dryRun invoice on the billing startDate
- final DryRunArguments dryRunArguments1 = new TestDryRunArguments(DryRunType.TARGET_DATE);
- final Invoice dryRunInvoice1 = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, dryRunArguments1, callContext);
- assertEquals(dryRunInvoice1.getInvoiceItems().size(), 1);
- assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
- assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
- assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getStartDate(), futureDate);
- assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getPlanName(), "shotgun-annual");
-
-
- // Generate a dryRun invoice with a plan change
- final DryRunArguments dryRunArguments = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null,
- SubscriptionEventType.CHANGE, createdEntitlement.getId(), createdEntitlement.getBundleId(), futureDate, BillingActionPolicy.IMMEDIATE);
-
- // First one day prior subscription starts
- try {
- invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate.minusDays(1), dryRunArguments, callContext);
- fail("Should fail to trigger dryRun invoice prior subscription starts");
- } catch (final InvoiceApiException e) {
- assertEquals(e.getCode(),ErrorCode.INVOICE_NOTHING_TO_DO.getCode());
- }
-
- // Second, on the startDate
- final Invoice dryRunInvoice2 = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, dryRunArguments, callContext);
- assertEquals(dryRunInvoice2.getInvoiceItems().size(), 1);
- assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
- assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
- assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getStartDate(), futureDate);
- assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getPlanName(), "pistol-monthly");
-
-
- // Check BCD is not yet set
- final Account refreshedAccount1 = accountUserApi.getAccountById(account.getId(), callContext);
- assertEquals(refreshedAccount1.getBillCycleDayLocal(), new Integer(0));
-
-
- busHandler.pushExpectedEvents(NextEvent.INVOICE);
- final Invoice realInvoice = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, null, callContext);
- assertListenerStatus();
-
- assertEquals(realInvoice.getInvoiceItems().size(), 1);
- assertEquals(realInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
- assertEquals(realInvoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
- assertEquals(realInvoice.getInvoiceItems().get(0).getStartDate(), futureDate);
- assertEquals(realInvoice.getInvoiceItems().get(0).getPlanName(), "shotgun-annual");
-
- // Check BCD is now set
- final Account refreshedAccount2 = accountUserApi.getAccountById(account.getId(), callContext);
- assertEquals(refreshedAccount2.getBillCycleDayLocal(), new Integer(31));
-
-
- // Move clock past startDate to check nothing happens
- busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
- clock.addDays(31);
- assertListenerStatus();
-
- // Move clock after PHASE event
- busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
- clock.addMonths(12);
- assertListenerStatus();
- }
-
-
- @Test(groups = "slow")
- public void testDryRunWithPendingCancelledSubscription() throws Exception {
-
- final LocalDate initialDate = new LocalDate(2017, 4, 1);
- clock.setDay(initialDate);
-
- // Create account with non BCD to force junction BCD logic to activate
- final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
-
- final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("pistol-monthly-notrial", null);
-
- final LocalDate futureDate = new LocalDate(2017, 5, 1);
-
- // No CREATE event as this is set in the future
- final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, futureDate, futureDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
- assertEquals(createdEntitlement.getState(), Entitlement.EntitlementState.PENDING);
- assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(futureDate), 0);
- assertEquals(createdEntitlement.getEffectiveEndDate(), null);
- assertListenerStatus();
-
- // Generate an invoice using a future targetDate
- busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
- final Invoice firstInvoice = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, null, callContext);
- assertListenerStatus();
-
- assertEquals(firstInvoice.getInvoiceItems().size(), 1);
- assertEquals(firstInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
- assertEquals(firstInvoice.getInvoiceItems().get(0).getAmount().compareTo(new BigDecimal("19.95")), 0);
- assertEquals(firstInvoice.getInvoiceItems().get(0).getStartDate(), futureDate);
- assertEquals(firstInvoice.getInvoiceItems().get(0).getPlanName(), "pistol-monthly-notrial");
-
-
- // Cancel subscription on its pending startDate
- createdEntitlement.cancelEntitlementWithDate(futureDate, true, ImmutableList.<PluginProperty>of(), callContext);
-
- // Move to startDate/cancel Date
- busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.NULL_INVOICE, NextEvent.INVOICE);
- clock.addMonths(1);
- assertListenerStatus();
-
- final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
- assertEquals(invoices.size(), 2);
-
- final List<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
- new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-19.95")),
- new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("19.95")));
- invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-
-
-
- }
@Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/783")
public void testIntegrationWithRecurringFreePlan() throws Exception {
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
index a755f9c..2ed5d58 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
@@ -41,7 +41,6 @@ import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoiceStatus;
@@ -51,7 +50,6 @@ import org.killbill.billing.invoice.dao.InvoiceModelDao;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionStatus;
-import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
@@ -722,7 +720,7 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
newItems.add(recurring3);
newItems.add(repair3);
shellInvoice.addInvoiceItems(newItems);
- invoiceDao.createInvoice(shellInvoice, new FutureAccountNotifications(new HashMap<UUID, List<SubscriptionNotification>>()), internalCallContext);
+ invoiceDao.createInvoice(shellInvoice, new FutureAccountNotifications(), internalCallContext);
// Move ahead one month, verify nothing from previous data was generated
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 33d55ad..f060623 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -26,6 +26,7 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
@@ -40,7 +41,6 @@ import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entity.EntityPersistenceException;
import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
import org.killbill.billing.invoice.InvoicePluginDispatcher;
import org.killbill.billing.invoice.api.DefaultInvoicePaymentErrorEvent;
import org.killbill.billing.invoice.api.DefaultInvoicePaymentInfoEvent;
@@ -286,7 +286,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
@Override
public List<InvoiceItemModelDao> createInvoices(final List<InvoiceModelDao> invoices,
final InternalCallContext context) {
- return createInvoices(invoices, new FutureAccountNotifications(ImmutableMap.<UUID, List<SubscriptionNotification>>of()), context);
+ return createInvoices(invoices, new FutureAccountNotifications(), context);
}
private List<InvoiceItemModelDao> createInvoices(final Iterable<InvoiceModelDao> invoices,
@@ -999,19 +999,18 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
private void notifyOfFutureBillingEvents(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
final FutureAccountNotifications callbackDateTimePerSubscriptions, final InternalCallContext internalCallContext) {
+ for (final LocalDate notificationDate : callbackDateTimePerSubscriptions.getNotificationsForTrigger().keySet()) {
+ final DateTime notificationDateTime = internalCallContext.toUTCDateTime(notificationDate);
+ final Set<UUID> subscriptionIds = callbackDateTimePerSubscriptions.getNotificationsForTrigger().get(notificationDate);
+ nextBillingDatePoster.insertNextBillingNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionIds, notificationDateTime, internalCallContext);
+ }
+
final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(internalCallContext).getMillis();
- final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
- for (final UUID subscriptionId : callbackDateTimePerSubscriptions.getNotifications().keySet()) {
- final List<SubscriptionNotification> callbackDateTimeUTC = callbackDateTimePerSubscriptions.getNotifications().get(subscriptionId);
- for (final SubscriptionNotification cur : callbackDateTimeUTC) {
- if (isInvoiceNotificationEnabled) {
- final DateTime curDryRunNotificationTime = cur.getEffectiveDate().minus(dryRunNotificationTime);
- final DateTime effectiveCurDryRunNotificationTime = (curDryRunNotificationTime.isAfter(clock.getUTCNow())) ? curDryRunNotificationTime : clock.getUTCNow();
- nextBillingDatePoster.insertNextBillingDryRunNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionId, effectiveCurDryRunNotificationTime, cur.getEffectiveDate(), internalCallContext);
- }
- if (cur.isForInvoiceNotificationTrigger()) {
- nextBillingDatePoster.insertNextBillingNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionId, cur.getEffectiveDate(), internalCallContext);
- }
+ if (dryRunNotificationTime > 0) {
+ for (final LocalDate notificationDate : callbackDateTimePerSubscriptions.getNotificationsForDryRun().keySet()) {
+ final DateTime notificationDateTime = internalCallContext.toUTCDateTime(notificationDate);
+ final Set<UUID> subscriptionIds = callbackDateTimePerSubscriptions.getNotificationsForDryRun().get(notificationDate);
+ nextBillingDatePoster.insertNextBillingDryRunNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionIds, notificationDateTime, notificationDateTime.plusMillis((int) dryRunNotificationTime), internalCallContext);
}
}
}
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 46d742e..fabf9c7 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -24,7 +24,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
-import java.util.Iterator;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -46,14 +46,12 @@ import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.CatalogApiException;
-import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.events.BusInternalEvent;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.events.InvoiceNotificationInternalEvent;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
import org.killbill.billing.invoice.api.DefaultInvoiceService;
import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.DryRunType;
@@ -86,7 +84,6 @@ import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.junction.BillingEventSet;
import org.killbill.billing.junction.BillingInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
-import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.api.TagApiException;
@@ -114,6 +111,7 @@ 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.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
@@ -264,7 +262,7 @@ public class InvoiceDispatcher {
final boolean upcomingInvoiceDryRun = isDryRun && DryRunType.UPCOMING_INVOICE.equals(dryRunArguments.getDryRunType());
LocalDate inputTargetDate = inputTargetDateMaybeNull;
- // A null inputTargetDate is only allowed in dryRun mode to have the system compute it
+ // A null inputTargetDate is only allowed in UPCOMING_INVOICE dryRun mode to have the system compute it
if (inputTargetDate == null && !upcomingInvoiceDryRun) {
inputTargetDate = clock.getUTCToday();
}
@@ -276,28 +274,59 @@ public class InvoiceDispatcher {
if (billingEvents.isEmpty()) {
return null;
}
- final Iterable<UUID> filteredSubscriptionIdsForDryRun = getFilteredSubscriptionIdsForDryRun(dryRunArguments, billingEvents);
- final List<LocalDate> candidateTargetDates = (inputTargetDate != null) ?
- ImmutableList.<LocalDate>of(inputTargetDate) :
- getUpcomingInvoiceCandidateDates(filteredSubscriptionIdsForDryRun, context);
- for (final LocalDate curTargetDate : candidateTargetDates) {
- final Invoice invoice = processAccountWithLockAndInputTargetDate(accountId, curTargetDate, billingEvents, isDryRun, context);
- if (invoice != null) {
- filterInvoiceItemsForDryRun(filteredSubscriptionIdsForDryRun, invoice);
-
- if (!isDryRun && parkedAccount) {
- try {
- log.info("Illegal invoicing state fixed for accountId='{}', unparking account", accountId);
- parkedAccountsManager.unparkAccount(accountId, context);
- } catch (final TagApiException ignored) {
- log.warn("Unable to unpark account", ignored);
- }
+
+ // Avoid pulling all invoices when AUTO_INVOICING_OFF is set since we will disable invoicing later
+ // (Note that we can't return right away as we send a NullInvoice event)
+ final List<Invoice> existingInvoices = 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);
+ }
+ }));
+ Invoice invoice;
+ if (!isDryRun) {
+ invoice = processAccountWithLockAndInputTargetDate(accountId, inputTargetDate, billingEvents, existingInvoices, false, context);
+ if (parkedAccount) {
+ try {
+ log.info("Illegal invoicing state fixed for accountId='{}', unparking account", accountId);
+ parkedAccountsManager.unparkAccount(accountId, context);
+ } catch (final TagApiException ignored) {
+ log.warn("Unable to unpark account", ignored);
}
+ }
+ } else /* Dry run use cases */ {
+
+ final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
+ DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
+ final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(context.getAccountRecordId(), context.getTenantRecordId());
+
+ final Map<UUID, DateTime> nextScheduledSubscriptionsEventMap = getNextTransitionsForSubscriptions(billingEvents);
+
+ // List of all existing invoice notifications
+ final List<LocalDate> allCandidateTargetDates = getUpcomingInvoiceCandidateDates(futureNotifications, nextScheduledSubscriptionsEventMap, ImmutableList.<UUID>of(), context);
+
+ if (dryRunArguments.getDryRunType() == DryRunType.UPCOMING_INVOICE) {
+
+ final Iterable<UUID> filteredSubscriptionIdsForDryRun = getFilteredSubscriptionIdsFor_UPCOMING_INVOICE_DryRun(dryRunArguments, billingEvents);
- return invoice;
+ // List of existing invoice notifications associated to the filter set of subscriptionIds
+ final List<LocalDate> filteredCandidateTargetDates = Iterables.isEmpty(filteredSubscriptionIdsForDryRun) ?
+ allCandidateTargetDates :
+ getUpcomingInvoiceCandidateDates(futureNotifications, nextScheduledSubscriptionsEventMap, filteredSubscriptionIdsForDryRun, context);
+
+ if (Iterables.isEmpty(filteredSubscriptionIdsForDryRun)) {
+ invoice = processDryRun_UPCOMING_INVOICE_Invoice(accountId, allCandidateTargetDates, billingEvents, existingInvoices, context);
+ } else {
+ invoice = processDryRun_UPCOMING_INVOICE_FILTERING_Invoice(accountId, filteredCandidateTargetDates, allCandidateTargetDates, billingEvents, existingInvoices, context);
+ }
+ } else /* DryRunType.TARGET_DATE, SUBSCRIPTION_ACTION */ {
+ invoice = processDryRun_TARGET_DATE_Invoice(accountId, inputTargetDate, allCandidateTargetDates, billingEvents, existingInvoices, context);
}
}
- return null;
+ return invoice;
} catch (final CatalogApiException e) {
log.warn("Failed to retrieve BillingEvents for accountId='{}', dryRunArguments='{}'", accountId, dryRunArguments, e);
return null;
@@ -313,32 +342,89 @@ public class InvoiceDispatcher {
parkAccount(accountId, context);
}
throw e;
+ } catch (final NoSuchNotificationQueue e) {
+ // Should not happen, notificationQ is only used for dry run mode
+ if (!isDryRun) {
+ log.warn("Missing notification queue, accountId='{}', dryRunArguments='{}', parking account", accountId, dryRunArguments, e);
+ parkAccount(accountId, context);
+ }
+ throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, "Failed to retrieve future notifications from notificationQ");
}
}
- private void parkAccount(final UUID accountId, final InternalCallContext context) {
- try {
- parkedAccountsManager.parkAccount(accountId, context);
- } catch (final TagApiException ignored) {
- log.warn("Unable to park account", ignored);
+ // Return a map of subscriptionId / localDate identifying what is the next upcoming billing transition (PHASE, PAUSE, ..)
+ private Map<UUID, DateTime> getNextTransitionsForSubscriptions(final BillingEventSet billingEvents) {
+
+ final DateTime now = clock.getUTCNow();
+ final Map<UUID, DateTime> result = new HashMap<UUID, DateTime>();
+ for (final BillingEvent evt : billingEvents) {
+ final UUID subscriptionId = evt.getSubscription().getId();
+ final DateTime evtEffectiveDate = evt.getEffectiveDate();
+ if (evtEffectiveDate.compareTo(now) <= 0) {
+ continue;
+ }
+ final DateTime nextUpcomingPerSubscriptionDate = result.get(subscriptionId);
+ if (nextUpcomingPerSubscriptionDate == null || nextUpcomingPerSubscriptionDate.compareTo(evtEffectiveDate) > 0) {
+ result.put(subscriptionId, evtEffectiveDate);
+ }
}
+ return result;
}
- private void filterInvoiceItemsForDryRun(final Iterable<UUID> filteredSubscriptionIdsForDryRun, final Invoice invoice) {
- if (!filteredSubscriptionIdsForDryRun.iterator().hasNext()) {
- return;
+ private Invoice processDryRun_UPCOMING_INVOICE_Invoice(final UUID accountId, final List<LocalDate> allCandidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+ for (final LocalDate curTargetDate : allCandidateTargetDates) {
+ final Invoice invoice = processAccountWithLockAndInputTargetDate(accountId, curTargetDate, billingEvents, existingInvoices, true, context);
+ if (invoice != null) {
+ return invoice;
+ }
}
+ return null;
+ }
- final Iterator<InvoiceItem> it = invoice.getInvoiceItems().iterator();
- while (it.hasNext()) {
- final InvoiceItem cur = it.next();
- if (!Iterables.contains(filteredSubscriptionIdsForDryRun, cur.getSubscriptionId())) {
- it.remove();
+ private Invoice processDryRun_UPCOMING_INVOICE_FILTERING_Invoice(final UUID accountId, final List<LocalDate> filteringCandidateTargetDates, final List<LocalDate> allCandidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+ for (final LocalDate curTargetDate : filteringCandidateTargetDates) {
+ final Invoice invoice = processDryRun_TARGET_DATE_Invoice(accountId, curTargetDate, allCandidateTargetDates, billingEvents, existingInvoices, context);
+ if (invoice != null) {
+ return invoice;
}
}
+ return null;
}
- private Iterable<UUID> getFilteredSubscriptionIdsForDryRun(@Nullable final DryRunArguments dryRunArguments, final BillingEventSet billingEvents) {
+ private Invoice processDryRun_TARGET_DATE_Invoice(final UUID accountId, final LocalDate targetDate, final List<LocalDate> allCandidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+
+ LocalDate prevLocalDate = null;
+ for (final LocalDate cur : allCandidateTargetDates) {
+ if (cur.compareTo(targetDate) < 0) {
+ prevLocalDate = cur;
+ } else {
+ break;
+ }
+ }
+
+ // Generate a dryRun invoice for such date if required in such a way that dryRun invoice on our targetDate only contains items that we expect to see
+ final Invoice additionalInvoice = prevLocalDate != null ?
+ processAccountWithLockAndInputTargetDate(accountId, prevLocalDate, billingEvents, existingInvoices, true, context) :
+ null;
+
+ final List<Invoice> augmentedExistingInvoices = additionalInvoice != null ?
+ new ImmutableList.Builder().addAll(existingInvoices).add(additionalInvoice).build() :
+ existingInvoices;
+
+ final Invoice targetInvoice = processAccountWithLockAndInputTargetDate(accountId, targetDate, billingEvents, augmentedExistingInvoices, true, context);
+ // If our targetDate -- user specified -- did not align with any boundary, we return previous 'additionalInvoice' invoice
+ return targetInvoice != null ? targetInvoice : additionalInvoice;
+ }
+
+ private void parkAccount(final UUID accountId, final InternalCallContext context) {
+ try {
+ parkedAccountsManager.parkAccount(accountId, context);
+ } catch (final TagApiException ignored) {
+ log.warn("Unable to park account", ignored);
+ }
+ }
+
+ private Iterable<UUID> getFilteredSubscriptionIdsFor_UPCOMING_INVOICE_DryRun(@Nullable final DryRunArguments dryRunArguments, final BillingEventSet billingEvents) {
if (dryRunArguments == null ||
!dryRunArguments.getDryRunType().equals(DryRunType.UPCOMING_INVOICE) ||
(dryRunArguments.getSubscriptionId() == null && dryRunArguments.getBundleId() == null)) {
@@ -362,8 +448,12 @@ public class InvoiceDispatcher {
});
}
- private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId, final LocalDate targetDate,
- final BillingEventSet billingEvents, final boolean isDryRun, final InternalCallContext internalCallContext) throws InvoiceApiException {
+ private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId,
+ final LocalDate targetDate,
+ final BillingEventSet billingEvents,
+ final List<Invoice> existingInvoices,
+ final boolean isDryRun,
+ final InternalCallContext internalCallContext) throws InvoiceApiException {
final ImmutableAccountData account;
try {
account = accountApi.getImmutableAccountDataById(accountId, internalCallContext);
@@ -372,11 +462,11 @@ public class InvoiceDispatcher {
return null;
}
- final InvoiceWithMetadata invoiceWithMetadata = generateKillBillInvoice(account, targetDate, billingEvents, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generateKillBillInvoice(account, targetDate, billingEvents, existingInvoices, internalCallContext);
final DefaultInvoice invoice = invoiceWithMetadata.getInvoice();
// Compute future notifications
- final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceWithMetadata, internalCallContext);
+ final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceWithMetadata, billingEvents, internalCallContext);
// If invoice comes back null, there is nothing new to generate, we can bail early
if (invoice == null) {
@@ -471,20 +561,11 @@ public class InvoiceDispatcher {
return invoice;
}
- private InvoiceWithMetadata generateKillBillInvoice(final ImmutableAccountData account, final LocalDate targetDate, final BillingEventSet billingEvents, final InternalCallContext context) throws InvoiceApiException {
- 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);
- }
- }));
+ private InvoiceWithMetadata generateKillBillInvoice(final ImmutableAccountData account, final LocalDate targetDate, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
final UUID targetInvoiceId;
// Filter out DRAFT invoices for computation of existing items unless Account is in AUTO_INVOICING_REUSE_DRAFT
if (billingEvents.isAccountAutoInvoiceReuseDraft()) {
- final Invoice existingDraft = Iterables.tryFind(invoices, new Predicate<Invoice>() {
+ final Invoice existingDraft = Iterables.tryFind(existingInvoices, new Predicate<Invoice>() {
@Override
public boolean apply(final Invoice input) {
return input.getStatus() == InvoiceStatus.DRAFT;
@@ -495,49 +576,71 @@ public class InvoiceDispatcher {
targetInvoiceId = null;
}
- return generator.generateInvoice(account, billingEvents, invoices, targetInvoiceId, targetDate, account.getCurrency(), context);
+ return generator.generateInvoice(account, billingEvents, existingInvoices, targetInvoiceId, targetDate, account.getCurrency(), context);
}
- private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final InternalCallContext context) {
- final Map<UUID, List<SubscriptionNotification>> result = new HashMap<UUID, List<SubscriptionNotification>>();
+ private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final BillingEventSet billingEvents, final InternalCallContext context) {
- for (final UUID subscriptionId : invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().keySet()) {
+ final Map<LocalDate, Set<UUID>> notificationListForTrigger = new HashMap<LocalDate, Set<UUID>>();
- final List<SubscriptionNotification> perSubscriptionNotifications = new ArrayList<SubscriptionNotification>();
+ for (final UUID subscriptionId : invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().keySet()) {
final SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().get(subscriptionId);
- // Add next recurring date if any
+
if (subscriptionFutureNotificationDates.getNextRecurringDate() != null) {
- perSubscriptionNotifications.add(new SubscriptionNotification(context.toUTCDateTime(subscriptionFutureNotificationDates.getNextRecurringDate()), true));
+ Set<UUID> subscriptionsForDates = notificationListForTrigger.get(subscriptionFutureNotificationDates.getNextRecurringDate());
+ if (subscriptionsForDates == null) {
+ subscriptionsForDates = new HashSet<UUID>();
+ notificationListForTrigger.put(subscriptionFutureNotificationDates.getNextRecurringDate(), subscriptionsForDates);
+ }
+ subscriptionsForDates.add(subscriptionId);
}
- // Add next usage dates if any
+
if (subscriptionFutureNotificationDates.getNextUsageDates() != null) {
for (final UsageDef usageDef : subscriptionFutureNotificationDates.getNextUsageDates().keySet()) {
+
final LocalDate nextNotificationDateForUsage = subscriptionFutureNotificationDates.getNextUsageDates().get(usageDef);
- final DateTime subscriptionUsageCallbackDate = nextNotificationDateForUsage != null ? context.toUTCDateTime(nextNotificationDateForUsage) : null;
- perSubscriptionNotifications.add(new SubscriptionNotification(subscriptionUsageCallbackDate, true));
+ Set<UUID> subscriptionsForDates = notificationListForTrigger.get(nextNotificationDateForUsage);
+ if (subscriptionsForDates == null) {
+ subscriptionsForDates = new HashSet<UUID>();
+ notificationListForTrigger.put(nextNotificationDateForUsage, subscriptionsForDates);
+ }
+ subscriptionsForDates.add(subscriptionId);
}
}
- if (!perSubscriptionNotifications.isEmpty()) {
- result.put(subscriptionId, perSubscriptionNotifications);
- }
}
- // If dryRunNotification is enabled we also need to fetch the upcoming PHASE dates (we add SubscriptionNotification with isForInvoiceNotificationTrigger = false)
- final boolean isInvoiceNotificationEnabled = invoiceConfig.getDryRunNotificationSchedule(context).getMillis() > 0;
+ final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(context).getMillis();
+ final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
+
+ final Map<LocalDate, Set<UUID>> notificationListForDryRun = isInvoiceNotificationEnabled ? new HashMap<LocalDate, Set<UUID>>() : ImmutableMap.<LocalDate, Set<UUID>>of();
if (isInvoiceNotificationEnabled) {
- final Map<UUID, DateTime> upcomingPhasesForSubscriptions = subscriptionApi.getNextFutureEventForSubscriptions(SubscriptionBaseTransitionType.PHASE, context);
- for (final UUID cur : upcomingPhasesForSubscriptions.keySet()) {
- final DateTime curDate = upcomingPhasesForSubscriptions.get(cur);
- List<SubscriptionNotification> resultValue = result.get(cur);
- if (resultValue == null) {
- resultValue = new ArrayList<SubscriptionNotification>();
+ for (final LocalDate curDate : notificationListForTrigger.keySet()) {
+ final LocalDate curDryRunDate = context.toLocalDate(context.toUTCDateTime(curDate).minus(dryRunNotificationTime));
+ Set<UUID> subscriptionsForDryRunDates = notificationListForDryRun.get(curDryRunDate);
+ if (subscriptionsForDryRunDates == null) {
+ subscriptionsForDryRunDates = new HashSet<UUID>();
+ notificationListForDryRun.put(curDryRunDate, subscriptionsForDryRunDates);
}
- resultValue.add(new SubscriptionNotification(curDate, false));
- result.put(cur, resultValue);
+ subscriptionsForDryRunDates.addAll(notificationListForTrigger.get(curDate));
+ }
+
+ final Map<UUID, DateTime> upcomingPhasesForSubscriptions = isInvoiceNotificationEnabled ?
+ getNextTransitionsForSubscriptions(billingEvents) :
+ ImmutableMap.<UUID, DateTime>of();
+
+ for (UUID curId : upcomingPhasesForSubscriptions.keySet()) {
+ final LocalDate curDryRunDate = context.toLocalDate(upcomingPhasesForSubscriptions.get(curId).minus(dryRunNotificationTime));
+ Set<UUID> subscriptionsForDryRunDates = notificationListForDryRun.get(curDryRunDate);
+ if (subscriptionsForDryRunDates == null) {
+ subscriptionsForDryRunDates = new HashSet<UUID>();
+ notificationListForDryRun.put(curDryRunDate, subscriptionsForDryRunDates);
+ }
+ subscriptionsForDryRunDates.add(curId);
}
}
- return new FutureAccountNotifications(result);
+
+ return new FutureAccountNotifications(notificationListForTrigger, notificationListForDryRun);
}
private List<InvoiceItemModelDao> transformToInvoiceModelDao(final List<InvoiceItem> invoiceItems) {
@@ -656,40 +759,48 @@ public class InvoiceDispatcher {
public static class FutureAccountNotifications {
- private final Map<UUID, List<SubscriptionNotification>> notifications;
+ private final Map<LocalDate, Set<UUID>> notificationListForTrigger;
+ private final Map<LocalDate, Set<UUID>> notificationListForDryRun;
- public FutureAccountNotifications(final Map<UUID, List<SubscriptionNotification>> notifications) {
- this.notifications = notifications;
+ public FutureAccountNotifications() {
+ this(ImmutableMap.<LocalDate, Set<UUID>>of(), ImmutableMap.<LocalDate, Set<UUID>>of());
}
- public Map<UUID, List<SubscriptionNotification>> getNotifications() {
- return notifications;
+ public FutureAccountNotifications(final Map<LocalDate, Set<UUID>> notificationListForTrigger, final Map<LocalDate, Set<UUID>> notificationListForDryRun) {
+ this.notificationListForTrigger = notificationListForTrigger;
+ this.notificationListForDryRun = notificationListForDryRun;
}
- public static class SubscriptionNotification {
+ public Map<LocalDate, Set<UUID>> getNotificationsForTrigger() {
+ return notificationListForTrigger;
+ }
- private final DateTime effectiveDate;
- private final boolean isForNotificationTrigger;
+ public Map<LocalDate, Set<UUID>> getNotificationsForDryRun() {
+ return notificationListForDryRun;
+ }
+ }
- public SubscriptionNotification(final DateTime effectiveDate, final boolean isForNotificationTrigger) {
- this.effectiveDate = effectiveDate;
- this.isForNotificationTrigger = isForNotificationTrigger;
- }
+ private List<LocalDate> getUpcomingInvoiceCandidateDates(final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications,
+ final Map<UUID, DateTime> nextScheduledSubscriptionsEventMap,
+ final Iterable<UUID> filteredSubscriptionIds,
+ final InternalCallContext internalCallContext) {
- public DateTime getEffectiveDate() {
- return effectiveDate;
- }
+ final Iterable<DateTime> nextScheduledInvoiceDates = getNextScheduledInvoiceEffectiveDate(futureNotifications, filteredSubscriptionIds);
- public boolean isForInvoiceNotificationTrigger() {
- return isForNotificationTrigger;
+ final Iterable<DateTime> nextScheduledSubscriptionsEvents;
+ if (!Iterables.isEmpty(filteredSubscriptionIds)) {
+ List<DateTime> tmp = new ArrayList<DateTime>();
+ for (final UUID curSubscriptionId : nextScheduledSubscriptionsEventMap.keySet()) {
+ if (Iterables.contains(filteredSubscriptionIds, curSubscriptionId)) {
+ tmp.add(nextScheduledSubscriptionsEventMap.get(curSubscriptionId));
+ }
}
+ nextScheduledSubscriptionsEvents = tmp;
+ } else {
+ nextScheduledSubscriptionsEvents = nextScheduledSubscriptionsEventMap.values();
}
- }
- private List<LocalDate> getUpcomingInvoiceCandidateDates(final Iterable<UUID> filteredSubscriptionIds, final InternalCallContext internalCallContext) {
- final Iterable<DateTime> nextScheduledInvoiceDates = getNextScheduledInvoiceEffectiveDate(filteredSubscriptionIds, internalCallContext);
- final Iterable<DateTime> nextScheduledSubscriptionsEventDates = subscriptionApi.getFutureNotificationsForAccount(internalCallContext);
- return Lists.<DateTime, LocalDate>transform(UPCOMING_NOTIFICATION_DATE_ORDERING.sortedCopy(Iterables.<DateTime>concat(nextScheduledInvoiceDates, nextScheduledSubscriptionsEventDates)),
+ return Lists.<DateTime, LocalDate>transform(UPCOMING_NOTIFICATION_DATE_ORDERING.sortedCopy(Iterables.<DateTime>concat(nextScheduledInvoiceDates, nextScheduledSubscriptionsEvents)),
new Function<DateTime, LocalDate>() {
@Override
public LocalDate apply(final DateTime input) {
@@ -698,28 +809,30 @@ public class InvoiceDispatcher {
});
}
- private Iterable<DateTime> getNextScheduledInvoiceEffectiveDate(final Iterable<UUID> filteredSubscriptionIds, final InternalCallContext internalCallContext) {
- try {
- final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
- DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
- final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
-
- final Collection<DateTime> effectiveDates = new LinkedList<DateTime>();
- for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
- final boolean isEventForSubscription = !filteredSubscriptionIds.iterator().hasNext() || Iterables.contains(filteredSubscriptionIds, input.getEvent().getUuidKey());
-
- final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
- input.getEvent().isDryRunForInvoiceNotification() : false;
- if (isEventForSubscription && !isEventDryRunForNotifications) {
- effectiveDates.add(input.getEffectiveDate());
+ private Iterable<DateTime> getNextScheduledInvoiceEffectiveDate(final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications,
+ final Iterable<UUID> filteredSubscriptionIds) {
+ final Collection<DateTime> effectiveDates = new LinkedList<DateTime>();
+ for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
+
+ // If we don't specify a filter list of subscriptionIds, we look at all events.
+ boolean isEventForSubscription = Iterables.isEmpty(filteredSubscriptionIds);
+ // If we specify a filter, we keep the date if at least one of the subscriptions from the event list matches one of the subscription from our filter list
+ if (!Iterables.isEmpty(filteredSubscriptionIds)) {
+ for (final UUID curSubscriptionId : filteredSubscriptionIds) {
+ if (Iterables.contains(input.getEvent().getUuidKeys(), curSubscriptionId)) {
+ isEventForSubscription = true;
+ break;
+ }
}
-
}
- return effectiveDates;
- } catch (final NoSuchNotificationQueue noSuchNotificationQueue) {
- throw new IllegalStateException(noSuchNotificationQueue);
+ final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
+ input.getEvent().isDryRunForInvoiceNotification() : false;
+ if (isEventForSubscription && !isEventDryRunForNotifications) {
+ effectiveDates.add(input.getEffectiveDate());
+ }
}
+ return effectiveDates;
}
private static final class TargetDateDryRunArguments implements DryRunArguments {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
index 85391a5..47dc673 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
@@ -83,20 +83,21 @@ public class DefaultNextBillingDateNotifier extends RetryableService implements
// Just to ensure compatibility with json that might not have that targetDate field (old versions < 0.13.6)
final DateTime targetDate = key.getTargetDate() != null ? key.getTargetDate() : eventDate;
+ final UUID firstSubscriptionId = key.getUuidKeys().iterator().next();
try {
- final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(key.getUuidKey(), internalCallContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
+ final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(firstSubscriptionId, internalCallContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
if (subscription == null) {
- log.warn("Unable to retrieve subscriptionId='{}' for event {}", key.getUuidKey(), key);
+ log.warn("Unable to retrieve subscriptionId='{}' for event {}", firstSubscriptionId, key);
return;
}
if (key.isDryRunForInvoiceNotification() != null && // Just to ensure compatibility with json that might not have that field (old versions < 0.13.6)
key.isDryRunForInvoiceNotification()) {
- processEventForInvoiceNotification(key.getUuidKey(), targetDate, userToken, accountRecordId, tenantRecordId);
+ processEventForInvoiceNotification(firstSubscriptionId, targetDate, userToken, accountRecordId, tenantRecordId);
} else {
- processEventForInvoiceGeneration(key.getUuidKey(), targetDate, userToken, accountRecordId, tenantRecordId);
+ processEventForInvoiceGeneration(firstSubscriptionId, targetDate, userToken, accountRecordId, tenantRecordId);
}
- } catch (final SubscriptionBaseApiException e) {
- log.warn("Error retrieving subscriptionId='{}'", key.getUuidKey(), e);
+ } catch (SubscriptionBaseApiException e) {
+ log.warn("Error retrieving subscriptionId='{}'", firstSubscriptionId, e);
}
}
};
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
index 6dac86e..fb269c5 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
@@ -33,12 +33,16 @@ import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificatio
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
private static final Logger log = LoggerFactory.getLogger(DefaultNextBillingDatePoster.class);
+ private static Joiner JOINER = Joiner.on(",");
+
private final NotificationQueueService notificationQueueService;
@Inject
@@ -47,20 +51,29 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
}
@Override
- public void insertNextBillingNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
- final UUID subscriptionId, final DateTime futureNotificationTime, final InternalCallContext internalCallContext) {
- insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionId, Boolean.FALSE, futureNotificationTime, futureNotificationTime, internalCallContext);
+ public void insertNextBillingNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final UUID accountId,
+ final Iterable<UUID> subscriptionIds,
+ final DateTime futureNotificationTime,
+ final InternalCallContext internalCallContext) {
+ insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionIds, Boolean.FALSE, futureNotificationTime, futureNotificationTime, internalCallContext);
}
@Override
- public void insertNextBillingDryRunNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
- final UUID subscriptionId, final DateTime futureNotificationTime, final DateTime targetDate, final InternalCallContext internalCallContext) {
- insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionId, Boolean.TRUE, futureNotificationTime, targetDate, internalCallContext);
+ public void insertNextBillingDryRunNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final UUID accountId,
+ final Iterable<UUID> subscriptionIds,
+ final DateTime futureNotificationTime,
+ final DateTime targetDate,
+ final InternalCallContext internalCallContext) {
+ insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionIds, Boolean.TRUE, futureNotificationTime, targetDate, internalCallContext);
}
private void insertNextBillingFromTransactionInternal(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
- final UUID subscriptionId, final Boolean isDryRunForInvoiceNotification,
- final DateTime futureNotificationTime, final DateTime targetDate,
+ final Iterable<UUID> subscriptionIds,
+ final Boolean isDryRunForInvoiceNotification,
+ final DateTime futureNotificationTime,
+ final DateTime targetDate,
final InternalCallContext internalCallContext) {
final NotificationQueue nextBillingQueue;
try {
@@ -70,7 +83,7 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
// If we see existing notification for the same date (and isDryRunForInvoiceNotification mode), we don't insert a new notification
final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = nextBillingQueue.getFutureNotificationFromTransactionForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), entitySqlDaoWrapperFactory.getHandle().getConnection());
- boolean existingFutureNotificationWithSameDate = false;
+ NotificationEventWithMetadata<NextBillingDateNotificationKey> existingNotificationForEffectiveDate = null;
for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
input.getEvent().isDryRunForInvoiceNotification() : false;
@@ -81,25 +94,28 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 &&
((isDryRunForInvoiceNotification && isEventDryRunForNotifications) ||
(!isDryRunForInvoiceNotification && !isEventDryRunForNotifications))) {
- existingFutureNotificationWithSameDate = true;
+ existingNotificationForEffectiveDate = input;
}
// Go through all results to close the connection
}
- if (!existingFutureNotificationWithSameDate) {
- log.info("Queuing next billing date notification at {} for subscriptionId {}", futureNotificationTime.toString(), subscriptionId.toString());
+ if (existingNotificationForEffectiveDate == null) {
+ log.info("Queuing next billing date notification at {} for subscriptionId {}", futureNotificationTime.toString(), JOINER.join(subscriptionIds));
+ final NextBillingDateNotificationKey newNotificationEvent = new NextBillingDateNotificationKey(null, subscriptionIds, targetDate, isDryRunForInvoiceNotification);
nextBillingQueue.recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), futureNotificationTime,
- new NextBillingDateNotificationKey(subscriptionId, targetDate, isDryRunForInvoiceNotification), internalCallContext.getUserToken(),
+ newNotificationEvent, internalCallContext.getUserToken(),
internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
- } else if (log.isDebugEnabled()) {
- log.debug("********************* SKIPPING Queuing next billing date notification at {} for subscriptionId {} *******************", futureNotificationTime.toString(), subscriptionId.toString());
+ } else {
+ log.info("Updating next billing date notification event at {} for subscriptionId {}", futureNotificationTime.toString(), JOINER.join(subscriptionIds));
+ final NextBillingDateNotificationKey updateNotificationEvent = new NextBillingDateNotificationKey(existingNotificationForEffectiveDate.getEvent(), subscriptionIds);
+ nextBillingQueue.updateFutureNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), existingNotificationForEffectiveDate.getRecordId(), updateNotificationEvent, internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
}
} catch (final NoSuchNotificationQueue e) {
log.error("Attempting to put items on a non-existent queue (NextBillingDateNotifier).", e);
} catch (final IOException e) {
- log.error("Failed to serialize notificationKey for subscriptionId {}", subscriptionId);
+ log.error("Failed to serialize notificationKey for subscriptionId {}", subscriptionIds);
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java
index 664d415..1b64c60 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java
@@ -23,21 +23,35 @@ import org.killbill.notificationq.DefaultUUIDNotificationKey;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
public class NextBillingDateNotificationKey extends DefaultUUIDNotificationKey {
private Boolean isDryRunForInvoiceNotification;
private DateTime targetDate;
+ private final Iterable<UUID> uuidKeys;
@JsonCreator
- public NextBillingDateNotificationKey(@JsonProperty("uuidKey") final UUID uuidKey,
+ public NextBillingDateNotificationKey(@Deprecated @JsonProperty("uuidKey") final UUID uuidKey,
+ @JsonProperty("uuidKeys") final Iterable<UUID> uuidKeys,
@JsonProperty("targetDate") final DateTime targetDate,
@JsonProperty("isDryRunForInvoiceNotification") final Boolean isDryRunForInvoiceNotification) {
super(uuidKey);
+ this.uuidKeys = uuidKeys;
this.targetDate = targetDate;
this.isDryRunForInvoiceNotification = isDryRunForInvoiceNotification;
}
+ public NextBillingDateNotificationKey(NextBillingDateNotificationKey existing,
+ final Iterable<UUID> newUUIDKeys) {
+ super(null);
+ this.uuidKeys = ImmutableSet.copyOf(Iterables.concat(existing.getUuidKeys(), newUUIDKeys));
+ this.targetDate = existing.getTargetDate();
+ this.isDryRunForInvoiceNotification = existing.isDryRunForInvoiceNotification();
+ }
+
@JsonProperty("isDryRunForInvoiceNotification")
public Boolean isDryRunForInvoiceNotification() {
return isDryRunForInvoiceNotification;
@@ -46,4 +60,13 @@ public class NextBillingDateNotificationKey extends DefaultUUIDNotificationKey {
public DateTime getTargetDate() {
return targetDate;
}
+
+ public final Iterable<UUID> getUuidKeys() {
+ // Deprecated mode
+ if (uuidKeys == null || !uuidKeys.iterator().hasNext()) {
+ return ImmutableList.of(getUuidKey());
+ } else {
+ return uuidKeys;
+ }
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java
index 96836bd..d2dce63 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java
@@ -27,8 +27,8 @@ import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
public interface NextBillingDatePoster {
void insertNextBillingNotificationFromTransaction(EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, UUID accountId,
- UUID subscriptionId, DateTime futureNotificationTime, InternalCallContext internalCallContext);
+ Iterable<UUID> subscriptionId, DateTime futureNotificationTime, InternalCallContext internalCallContext);
void insertNextBillingDryRunNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
- final UUID subscriptionId, final DateTime futureNotificationTime, final DateTime targetDate, final InternalCallContext internalCallContext);
+ final Iterable<UUID> subscriptionId, final DateTime futureNotificationTime, final DateTime targetDate, final InternalCallContext internalCallContext);
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java
index a7c6830..b837bdf 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java
@@ -24,18 +24,21 @@ import org.killbill.billing.util.jackson.ObjectMapper;
import org.testng.Assert;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
public class TestNextBillingDateNotificationKey {
private static final ObjectMapper mapper = new ObjectMapper();
@Test(groups = "fast")
- public void testBasic() throws Exception {
+ public void testBasicWithUUIDKey() throws Exception {
final UUID uuidKey = UUID.randomUUID();
final DateTime targetDate = new DateTime();
final Boolean isDryRunForInvoiceNotification = Boolean.FALSE;
- final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(uuidKey, targetDate, isDryRunForInvoiceNotification);
+ final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(uuidKey, null, targetDate, isDryRunForInvoiceNotification);
final String json = mapper.writeValueAsString(key);
final NextBillingDateNotificationKey result = mapper.readValue(json, NextBillingDateNotificationKey.class);
@@ -44,6 +47,28 @@ public class TestNextBillingDateNotificationKey {
Assert.assertEquals(result.isDryRunForInvoiceNotification(), isDryRunForInvoiceNotification);
}
+
+ @Test(groups = "fast")
+ public void testBasicWithUUIDKeys() throws Exception {
+
+ final UUID uuidKey1 = UUID.randomUUID();
+ final UUID uuidKey2 = UUID.randomUUID();
+ final DateTime targetDate = new DateTime();
+ final Boolean isDryRunForInvoiceNotification = Boolean.FALSE;
+
+ final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(null, ImmutableList.of(uuidKey1, uuidKey2), targetDate, isDryRunForInvoiceNotification);
+ final String json = mapper.writeValueAsString(key);
+
+ final NextBillingDateNotificationKey result = mapper.readValue(json, NextBillingDateNotificationKey.class);
+ Assert.assertNull(result.getUuidKey());
+ Assert.assertEquals(result.getTargetDate().compareTo(targetDate), 0);
+ Assert.assertEquals(result.isDryRunForInvoiceNotification(), isDryRunForInvoiceNotification);
+ Assert.assertNotNull(result.getUuidKeys());
+
+ Assert.assertTrue(Iterables.contains(result.getUuidKeys(), uuidKey1));
+ Assert.assertTrue(Iterables.contains(result.getUuidKeys(), uuidKey2));
+ }
+
@Test(groups = "fast")
public void testWithMissingFields() throws Exception {
final String json = "{\"uuidKey\":\"a38c363f-b25b-4287-8ebc-55964e116d2f\"}";
@@ -52,5 +77,9 @@ public class TestNextBillingDateNotificationKey {
Assert.assertNull(result.getTargetDate());
Assert.assertNull(result.isDryRunForInvoiceNotification());
+ // Compatibility mode : Although the uuidKeys is not in the json, we verify the getter return the right result
+ Assert.assertNotNull(result.getUuidKeys());
+ Assert.assertEquals(result.getUuidKeys().iterator().next().toString(), "a38c363f-b25b-4287-8ebc-55964e116d2f");
+
}
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
index 763bd45..b620fab 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
@@ -31,6 +31,8 @@ import org.killbill.notificationq.api.NotificationQueue;
import org.testng.Assert;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
+
import static org.awaitility.Awaitility.await;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -47,7 +49,7 @@ public class TestNextBillingDateNotifier extends InvoiceTestSuiteWithEmbeddedDB
final NotificationQueue nextBillingQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME, DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
- nextBillingQueue.recordFutureNotification(now, new NextBillingDateNotificationKey(subscriptionId, now, Boolean.FALSE), internalCallContext.getUserToken(), accountRecordId, internalCallContext.getTenantRecordId());
+ nextBillingQueue.recordFutureNotification(now, new NextBillingDateNotificationKey(null, ImmutableList.<UUID>of(subscriptionId), now, Boolean.FALSE), internalCallContext.getUserToken(), accountRecordId, internalCallContext.getTenantRecordId());
// Move time in the future after the notification effectiveDate
clock.setDeltaFromReality(3000);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
index 693d0b7..42fccdb 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
@@ -28,8 +28,6 @@ import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entity.EntityPersistenceException;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
import org.killbill.billing.invoice.TestInvoiceHelper;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
@@ -47,7 +45,6 @@ import org.mockito.Mockito;
import org.testng.Assert;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
public class InvoiceTestUtils {
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 af1ceb2..a07977a 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -66,6 +66,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
private Account account;
private SubscriptionBase subscription;
private InternalCallContext context;
+ private InvoiceDispatcher dispatcher;
@Override
@BeforeMethod(groups = "slow")
@@ -74,6 +75,11 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
account = invoiceUtil.createAccount(callContext);
subscription = invoiceUtil.createSubscription();
context = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+
+ dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
+ internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
+ notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
+
}
@Test(groups = "slow")
@@ -96,7 +102,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
- null, invoiceConfig, clock, parkedAccountsManager);
+ notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
Assert.assertNotNull(invoice);
@@ -139,7 +145,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
- null, invoiceConfig, clock, parkedAccountsManager);
+ notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
// Verify initial tags state for account
Assert.assertTrue(tagUserApi.getTagsForAccount(accountId, true, callContext).isEmpty());
@@ -286,8 +292,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
- null, invoiceConfig, clock, parkedAccountsManager);
-
+ notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
final Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), new LocalDate("2012-07-30"), null, context);
Assert.assertNotNull(invoice);
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 6cf3f1a..81c6db8 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -84,6 +84,7 @@ import org.killbill.billing.util.currency.KillBillMoney;
import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.notificationq.api.NotificationQueueService;
import org.mockito.Mockito;
import org.skife.jdbi.v2.IDBI;
import org.testng.Assert;
@@ -161,6 +162,7 @@ public class TestInvoiceHelper {
private final MutableInternalCallContext internalCallContext;
private final InternalCallContextFactory internalCallContextFactory;
private final InvoiceConfig invoiceConfig;
+ private final NotificationQueueService notificationQueueService;
// Low level SqlDao used by the tests to directly insert rows
private final InvoicePaymentSqlDao invoicePaymentSqlDao;
private final InvoiceItemSqlDao invoiceItemSqlDao;
@@ -170,7 +172,7 @@ public class TestInvoiceHelper {
@Inject
public TestInvoiceHelper(final InvoiceGenerator generator, final IDBI dbi,
final BillingInternalApi billingApi, final AccountInternalApi accountApi, final ImmutableAccountInternalApi immutableAccountApi, final InvoicePluginDispatcher invoicePluginDispatcher, final AccountUserApi accountUserApi, final SubscriptionBaseInternalApi subscriptionApi, final BusService busService,
- final InvoiceDao invoiceDao, final GlobalLocker locker, final Clock clock, final NonEntityDao nonEntityDao, final CacheControllerDispatcher cacheControllerDispatcher, final MutableInternalCallContext internalCallContext, final InvoiceConfig invoiceConfig,
+ final InvoiceDao invoiceDao, final GlobalLocker locker, final Clock clock, final NonEntityDao nonEntityDao, final NotificationQueueService notificationQueueService, final MutableInternalCallContext internalCallContext, final InvoiceConfig invoiceConfig,
final ParkedAccountsManager parkedAccountsManager, final InternalCallContextFactory internalCallContextFactory) {
this.generator = generator;
this.billingApi = billingApi;
@@ -184,6 +186,7 @@ public class TestInvoiceHelper {
this.locker = locker;
this.clock = clock;
this.nonEntityDao = nonEntityDao;
+ this.notificationQueueService = notificationQueueService;
this.parkedAccountsManager = parkedAccountsManager;
this.internalCallContext = internalCallContext;
this.internalCallContextFactory = internalCallContextFactory;
NEWS 3(+3 -0)
diff --git a/NEWS b/NEWS
index c1e7da5..9b7028a 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,6 @@
+0.18.14
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.18.14
+
0.18.13
See https://github.com/killbill/killbill/releases/tag/killbill-0.18.13
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
index 88fd895..6887cc5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
@@ -175,7 +175,8 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
final Boolean result = doJanitorOperationWithAccountLock(new JanitorIterationCallback() {
@Override
public Boolean doIteration() {
- return updatePaymentAndTransactionInternal(payment, null, null, paymentTransaction, paymentTransactionInfoPlugin, internalTenantContext);
+ final PaymentTransactionModelDao refreshedPaymentTransaction = paymentDao.getPaymentTransaction(paymentTransaction.getId(), internalTenantContext);
+ return updatePaymentAndTransactionInternal(payment, null, null, refreshedPaymentTransaction, paymentTransactionInfoPlugin, internalTenantContext);
}
}, internalTenantContext);
return result != null && result;
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
index e46ad72..168e06e 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
@@ -18,7 +18,6 @@
package org.killbill.billing.payment.core.janitor;
import java.math.BigDecimal;
-import java.util.List;
import java.util.UUID;
import org.killbill.billing.account.api.Account;
@@ -27,7 +26,13 @@ import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
+import org.killbill.billing.payment.api.TransactionType;
+import org.killbill.billing.payment.dao.PaymentModelDao;
+import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin;
+import org.killbill.billing.payment.provider.DefaultNoOpPaymentInfoPlugin;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.killbill.billing.util.globallocker.LockerType;
import org.killbill.commons.locker.GlobalLock;
@@ -106,4 +111,71 @@ public class TestIncompletePaymentTransactionTaskWithDB extends PaymentTestSuite
}
}
}
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/809")
+ public void testUpdateWithinLock() throws PaymentApiException {
+ final Payment payment = paymentApi.createAuthorization(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ null,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED.toString(), false)),
+ callContext);
+ final PaymentModelDao paymentModel = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final UUID transactionId = payment.getTransactions().get(0).getId();
+ final PaymentTransactionModelDao transactionModel = paymentDao.getPaymentTransaction(transactionId, internalCallContext);
+
+ Assert.assertEquals(paymentModel.getStateName(), "AUTH_ERRORED");
+ Assert.assertEquals(transactionModel.getTransactionStatus().toString(), "UNKNOWN");
+
+ paymentDao.updatePaymentAndTransactionOnCompletion(
+ account.getId(),
+ null,
+ payment.getId(),
+ TransactionType.AUTHORIZE,
+ "AUTH_SUCCESS",
+ "AUTH_SUCCESS",
+ transactionId,
+ TransactionStatus.SUCCESS,
+ BigDecimal.TEN,
+ Currency.EUR,
+ "200",
+ "Ok",
+ internalCallContext);
+
+ paymentApi.createCapture(account,
+ payment.getId(),
+ BigDecimal.TEN,
+ Currency.EUR,
+ null,
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PROCESSED.toString(), false)),
+ callContext);
+
+ final PaymentModelDao paymentAfterCapture = paymentDao.getPayment(payment.getId(), internalCallContext);
+ Assert.assertEquals(paymentAfterCapture.getStateName(), "CAPTURE_SUCCESS");
+
+ PaymentTransactionInfoPlugin paymentTransactionInfoPlugin = new DefaultNoOpPaymentInfoPlugin(
+ payment.getId(),
+ transactionId,
+ TransactionType.AUTHORIZE,
+ BigDecimal.TEN,
+ Currency.EUR,
+ transactionModel.getEffectiveDate(),
+ transactionModel.getCreatedDate(),
+ PaymentPluginStatus.PROCESSED,
+ "200",
+ "OK");
+ incompletePaymentTransactionTask.updatePaymentAndTransactionIfNeededWithAccountLock(
+ paymentModel,
+ transactionModel,
+ paymentTransactionInfoPlugin,
+ internalCallContext);
+
+ final PaymentModelDao paymentAfterJanitor = paymentDao.getPayment(payment.getId(), internalCallContext);
+ Assert.assertEquals(paymentAfterJanitor.getStateName(), "CAPTURE_SUCCESS");
+ }
}
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 2537129..58b57c1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.141.12</version>
+ <version>0.141.13-SNAPSHOT</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.19.0-SNAPSHOT</version>
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 6768cec..fab57eb 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
@@ -58,7 +58,6 @@ import org.killbill.billing.subscription.api.SubscriptionApiBase;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
-import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOns;
import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
@@ -72,7 +71,6 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData
import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
import org.killbill.billing.subscription.api.user.SubscriptionSpecifier;
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;
@@ -89,20 +87,14 @@ 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.annotations.VisibleForTesting;
import com.google.common.base.Function;
-import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.inject.Inject;
@@ -780,51 +772,6 @@ 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 Iterable<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 (final NoSuchNotificationQueue noSuchNotificationQueue) {
- throw new IllegalStateException(noSuchNotificationQueue);
- }
- }
-
- @Override
- public Map<UUID, DateTime> getNextFutureEventForSubscriptions(final SubscriptionBaseTransitionType eventType, final InternalCallContext internalCallContext) {
- final Iterable<SubscriptionBaseEvent> events = dao.getFutureEventsForAccount(internalCallContext);
- final Iterable<SubscriptionBaseEvent> filteredEvents = Iterables.filter(events, new Predicate<SubscriptionBaseEvent>() {
- @Override
- public boolean apply(final SubscriptionBaseEvent input) {
- switch (input.getType()) {
- case PHASE:
- return eventType == SubscriptionBaseTransitionType.PHASE;
- case BCD_UPDATE:
- return eventType == SubscriptionBaseTransitionType.BCD_CHANGE;
- case API_USER:
- default:
- return true;
- }
- }
- });
- final Map<UUID, DateTime> result = filteredEvents.iterator().hasNext() ? new HashMap<UUID, DateTime>() : ImmutableMap.<UUID, DateTime>of();
- for (final SubscriptionBaseEvent cur : filteredEvents) {
- final DateTime targetDate = result.get(cur.getSubscriptionId());
- if (targetDate == null || targetDate.compareTo(cur.getEffectiveDate()) > 0) {
- result.put(cur.getSubscriptionId(), cur.getEffectiveDate());
- }
- }
- return result;
- }
-
- @Override
public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
final Catalog catalog;
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index 322f804..3af29b0 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -535,24 +535,6 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
}
@Override
- public Iterable<SubscriptionBaseEvent> getFutureEventsForAccount(final InternalTenantContext context) {
- return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Iterable<SubscriptionBaseEvent>>() {
- @Override
- public Iterable<SubscriptionBaseEvent> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
- final SubscriptionEventSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
- final List<SubscriptionEventModelDao> activeEvents = transactional.getFutureActiveEventsForAccount(clock.getUTCNow().toDate(), context);
- return Iterables.transform(activeEvents, new Function<SubscriptionEventModelDao, SubscriptionBaseEvent>() {
-
- @Override
- public SubscriptionBaseEvent apply(final SubscriptionEventModelDao input) {
- return SubscriptionEventModelDao.toSubscriptionEvent(input);
- }
- });
- }
- });
- }
-
- @Override
public List<SubscriptionBaseEvent> getPendingEventsForSubscription(final UUID subscriptionId, final InternalTenantContext context) {
final Date now = clock.getUTCNow().toDate();
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
index 9063bba..c5d14bc 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
@@ -19,6 +19,7 @@ package org.killbill.billing.subscription.engine.dao.model;
import java.util.UUID;
import org.joda.time.DateTime;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.events.EventBaseBuilder;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
@@ -35,7 +36,6 @@ import org.killbill.billing.util.entity.dao.EntityModelDao;
import org.killbill.billing.util.entity.dao.EntityModelDaoBase;
public class SubscriptionEventModelDao extends EntityModelDaoBase implements EntityModelDao<SubscriptionBaseEvent> {
-
private long totalOrdering;
private EventType eventType;
private ApiEventType userType;
@@ -216,6 +216,29 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
return result;
}
+ public boolean isOfSubscriptionBaseTransitionType(final SubscriptionBaseTransitionType type) {
+ switch(type) {
+ case CREATE:
+ return eventType == EventType.API_USER && userType == ApiEventType.CREATE;
+ case TRANSFER:
+ return eventType == EventType.API_USER && userType == ApiEventType.TRANSFER;
+ case CHANGE:
+ return eventType == EventType.API_USER && userType == ApiEventType.CHANGE;
+ case CANCEL:
+ return eventType == EventType.API_USER && userType == ApiEventType.CANCEL;
+ case UNCANCEL:
+ return eventType == EventType.API_USER && userType == ApiEventType.UNCANCEL;
+ case PHASE:
+ return eventType == EventType.PHASE;
+ case BCD_CHANGE:
+ return eventType == EventType.BCD_UPDATE;
+ case START_BILLING_DISABLED:
+ case END_BILLING_DISABLED:
+ default:
+ return false;
+ }
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
index 4393e40..0939c7f 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -26,6 +26,7 @@ import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.entitlement.api.SubscriptionApiException;
import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOns;
import org.killbill.billing.subscription.api.transfer.BundleTransferData;
import org.killbill.billing.subscription.api.transfer.TransferCancelData;
@@ -75,8 +76,6 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
public SubscriptionBaseEvent getEventById(UUID eventId, InternalTenantContext context);
- public Iterable<SubscriptionBaseEvent> getFutureEventsForAccount(InternalTenantContext context);
-
public List<SubscriptionBaseEvent> getEventsForSubscription(UUID subscriptionId, InternalTenantContext context);
public List<SubscriptionBaseEvent> getPendingEventsForSubscription(UUID subscriptionId, InternalTenantContext context);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index 8bf6081..5d7c296 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -508,10 +508,6 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
}
}
- @Override
- public Iterable<SubscriptionBaseEvent> getFutureEventsForAccount(final InternalTenantContext context) {
- return null;
- }
@Override
public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleTransferData data,