killbill-memoizeit
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java 64(+64 -0)
invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceNotificationInternalEvent.java 114(+114 -0)
invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java 16(+14 -2)
invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java 16(+15 -1)
invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java 19(+18 -1)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java 6(+6 -0)
invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java 56(+56 -0)
invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java 3(+1 -2)
pom.xml 2(+1 -1)
Details
diff --git a/api/src/main/java/org/killbill/billing/events/BusInternalEvent.java b/api/src/main/java/org/killbill/billing/events/BusInternalEvent.java
index 66f849e..b756b7a 100644
--- a/api/src/main/java/org/killbill/billing/events/BusInternalEvent.java
+++ b/api/src/main/java/org/killbill/billing/events/BusInternalEvent.java
@@ -35,6 +35,7 @@ public interface BusInternalEvent extends BusEvent {
ENTITLEMENT_TRANSITION,
INVOICE_ADJUSTMENT,
INVOICE_CREATION,
+ INVOICE_NOTIFICATION,
INVOICE_EMPTY,
OVERDUE_CHANGE,
PAYMENT_ERROR,
diff --git a/api/src/main/java/org/killbill/billing/events/InvoiceNotificationInternalEvent.java b/api/src/main/java/org/killbill/billing/events/InvoiceNotificationInternalEvent.java
new file mode 100644
index 0000000..6f2b7a1
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/events/InvoiceNotificationInternalEvent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.events;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.Currency;
+
+public interface InvoiceNotificationInternalEvent extends InvoiceInternalEvent {
+
+ public BigDecimal getAmountOwed();
+
+ public Currency getCurrency();
+
+ public DateTime getTargetDate();
+
+}
diff --git a/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java b/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java
index 73123dd..7e914ce 100644
--- a/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java
+++ b/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java
@@ -38,6 +38,7 @@ import org.killbill.billing.events.CustomFieldDeletionEvent;
import org.killbill.billing.events.EntitlementInternalEvent;
import org.killbill.billing.events.InvoiceAdjustmentInternalEvent;
import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.events.InvoiceNotificationInternalEvent;
import org.killbill.billing.events.OverdueChangeInternalEvent;
import org.killbill.billing.events.PaymentErrorInternalEvent;
import org.killbill.billing.events.PaymentInfoInternalEvent;
@@ -159,6 +160,15 @@ public class BeatrixListener {
eventBusType = ExtBusEventType.INVOICE_CREATION;
break;
+ case INVOICE_NOTIFICATION:
+ final InvoiceNotificationInternalEvent realEventInvNotification = (InvoiceNotificationInternalEvent) event;
+ objectType = ObjectType.INVOICE;
+ objectId = null;
+ accountId = realEventInvNotification.getAccountId(); // has to be set here because objectId is null with a dryRun Invoice
+ eventBusType = ExtBusEventType.INVOICE_NOTIFICATION;
+ break;
+
+
case INVOICE_ADJUSTMENT:
final InvoiceAdjustmentInternalEvent realEventInvAdj = (InvoiceAdjustmentInternalEvent) event;
objectType = ObjectType.INVOICE;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java
new file mode 100644
index 0000000..613ce72
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+public class TestInvoiceNotifications extends TestIntegrationBase {
+
+ @Override
+ protected KillbillConfigSource getConfigSource() {
+ ImmutableMap additionalProperties = new ImmutableMap.Builder()
+ .put("org.killbill.invoice.dryRunNotificationSchedule", "7d")
+ .build();
+ return getConfigSource("/beatrix.properties", additionalProperties);
+ }
+
+ @Test(groups = "slow")
+ public void testInvoiceNotificationBasic() throws Exception {
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ // We take april as it has 30 days (easier to play with BCD)
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+
+ // Move to end of trial => 2012, 5, 1
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+
+ // Next invoice is scheduled for 2012, 6, 1 so we should have a NOTIFICATION event 7 days before, on 2012, 5, 25
+ addDaysAndCheckForCompletion(24, NextEvent.INVOICE_NOTIFICATION);
+
+ // And then verify the invoice is correctly generated
+ addDaysAndCheckForCompletion(7, NextEvent.INVOICE, NextEvent.PAYMENT);
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceNotificationInternalEvent.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceNotificationInternalEvent.java
new file mode 100644
index 0000000..e969fa9
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceNotificationInternalEvent.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.user;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.InvoiceNotificationInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultInvoiceNotificationInternalEvent extends BusEventBase implements InvoiceNotificationInternalEvent {
+
+ private final DateTime targetDate;
+ private final UUID accountId;
+ private final BigDecimal amountOwed;
+ private final Currency currency;
+
+ @JsonCreator
+ public DefaultInvoiceNotificationInternalEvent(@JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("amountOwed") final BigDecimal amountOwed,
+ @JsonProperty("currency") final Currency currency,
+ @JsonProperty("targetDate") final DateTime targetDate,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.targetDate = targetDate;
+ this.accountId = accountId;
+ this.amountOwed = amountOwed;
+ this.currency = currency;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.INVOICE_NOTIFICATION;
+ }
+
+ @Override
+ public BigDecimal getAmountOwed() {
+ return amountOwed;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public DateTime getTargetDate() {
+ return targetDate;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DefaultInvoiceNotificationInternalEvent)) {
+ return false;
+ }
+
+ final DefaultInvoiceNotificationInternalEvent that = (DefaultInvoiceNotificationInternalEvent) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (amountOwed != null ? !amountOwed.equals(that.amountOwed) : that.amountOwed != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (targetDate != null ? targetDate.compareTo(that.targetDate) != 0 : that.targetDate != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = targetDate != null ? targetDate.hashCode() : 0;
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (amountOwed != null ? amountOwed.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ return result;
+ }
+}
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 5add0ff..0b3f65f 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
@@ -43,6 +43,7 @@ import org.killbill.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent;
import org.killbill.billing.invoice.notification.NextBillingDatePoster;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.config.InvoiceConfig;
import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
@@ -89,6 +90,8 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
private final InternalCallContextFactory internalCallContextFactory;
private final InvoiceDaoHelper invoiceDaoHelper;
private final CBADao cbaDao;
+ private final InvoiceConfig invoiceConfig;
+ private final Clock clock;
@Inject
public DefaultInvoiceDao(final IDBI dbi,
@@ -97,13 +100,16 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
final Clock clock,
final CacheControllerDispatcher cacheControllerDispatcher,
final NonEntityDao nonEntityDao,
+ final InvoiceConfig invoiceConfig,
final InternalCallContextFactory internalCallContextFactory) {
super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao), InvoiceSqlDao.class);
this.nextBillingDatePoster = nextBillingDatePoster;
this.eventBus = eventBus;
+ this.invoiceConfig = invoiceConfig;
this.internalCallContextFactory = internalCallContextFactory;
this.invoiceDaoHelper = new InvoiceDaoHelper();
this.cbaDao = new CBADao();
+ this.clock = clock;
}
@Override
@@ -770,9 +776,20 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
private void notifyOfFutureBillingEvents(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
final Map<UUID, List<DateTime>> callbackDateTimePerSubscriptions, final InternalCallContext internalCallContext) {
+
+
+ final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule().getMillis();
+ final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
for (final UUID subscriptionId : callbackDateTimePerSubscriptions.keySet()) {
final List<DateTime> callbackDateTimeUTC = callbackDateTimePerSubscriptions.get(subscriptionId);
for (final DateTime cur : callbackDateTimeUTC) {
+ if (isInvoiceNotificationEnabled) {
+ final DateTime curDryRunNotificationTime = cur.minus(dryRunNotificationTime);
+ // Only schedule if the date is in the future
+ if (curDryRunNotificationTime.isAfter(clock.getUTCNow())) {
+ nextBillingDatePoster.insertNextBillingDryRunNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionId, curDryRunNotificationTime, cur, internalCallContext);
+ }
+ }
nextBillingDatePoster.insertNextBillingNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionId, cur, 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 321c00a..11a0813 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -38,14 +38,19 @@ import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
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.BillingMode;
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.catalog.api.Usage;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.events.BusInternalEvent;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.events.InvoiceAdjustmentInternalEvent;
import org.killbill.billing.events.InvoiceInternalEvent;
+import org.killbill.billing.events.InvoiceNotificationInternalEvent;
import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
@@ -54,6 +59,7 @@ import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoiceNotifier;
import org.killbill.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent;
import org.killbill.billing.invoice.api.user.DefaultInvoiceCreationEvent;
+import org.killbill.billing.invoice.api.user.DefaultInvoiceNotificationInternalEvent;
import org.killbill.billing.invoice.api.user.DefaultNullInvoiceEvent;
import org.killbill.billing.invoice.dao.InvoiceDao;
import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
@@ -93,6 +99,8 @@ public class InvoiceDispatcher {
private static final Logger log = LoggerFactory.getLogger(InvoiceDispatcher.class);
private static final int NB_LOCK_TRY = 5;
+ private static final NullDryRunArguments NULL_DRY_RUN_ARGUMENTS = new NullDryRunArguments();
+
private final InvoiceGenerator generator;
private final BillingInternalApi billingApi;
private final AccountInternalApi accountApi;
@@ -130,27 +138,49 @@ public class InvoiceDispatcher {
this.clock = clock;
}
- public void processSubscription(final EffectiveSubscriptionInternalEvent transition,
- final InternalCallContext context) throws InvoiceApiException {
+ public void processSubscriptionForInvoiceGeneration(final EffectiveSubscriptionInternalEvent transition,
+ final InternalCallContext context) throws InvoiceApiException {
final UUID subscriptionId = transition.getSubscriptionId();
final DateTime targetDate = transition.getEffectiveTransitionTime();
- processSubscription(subscriptionId, targetDate, context);
+ processSubscriptionForInvoiceGeneration(subscriptionId, targetDate, context);
+ }
+
+ public void processSubscriptionForInvoiceGeneration(final UUID subscriptionId, final DateTime targetDate, final InternalCallContext context) throws InvoiceApiException {
+ processSubscriptionInternal(subscriptionId, targetDate, false, context);
+ }
+
+ public void processSubscriptionForInvoiceNotification(final UUID subscriptionId, final DateTime targetDate, final InternalCallContext context) throws InvoiceApiException {
+ final Invoice dryRunInvoice = processSubscriptionInternal(subscriptionId, targetDate, true, context);
+ if (dryRunInvoice != null && dryRunInvoice.getBalance().compareTo(BigDecimal.ZERO) > 0) {
+ final InvoiceNotificationInternalEvent event = new DefaultInvoiceNotificationInternalEvent(dryRunInvoice.getAccountId(), dryRunInvoice.getBalance(), dryRunInvoice.getCurrency(),
+ targetDate, context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ try {
+ eventBus.post(event);
+ } catch (EventBusException e) {
+ log.error("Failed to post event " + event, e);
+ }
+ }
}
- public void processSubscription(final UUID subscriptionId, final DateTime targetDate, final InternalCallContext context) throws InvoiceApiException {
+
+ private Invoice processSubscriptionInternal(final UUID subscriptionId, final DateTime targetDate, final boolean dryRunForNotification, final InternalCallContext context) throws InvoiceApiException {
try {
if (subscriptionId == null) {
log.error("Failed handling SubscriptionBase change.", new InvoiceApiException(ErrorCode.INVOICE_INVALID_TRANSITION));
- return;
+ return null;
}
final UUID accountId = subscriptionApi.getAccountIdFromSubscriptionId(subscriptionId, context);
- processAccount(accountId, targetDate, null, context);
+ final DryRunArguments dryRunArguments = dryRunForNotification ? NULL_DRY_RUN_ARGUMENTS : null;
+
+ return processAccount(accountId, targetDate, dryRunArguments, context);
} catch (final SubscriptionBaseApiException e) {
log.error("Failed handling SubscriptionBase change.",
new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, subscriptionId.toString()));
+ return null;
}
}
+
public Invoice processAccount(final UUID accountId, final DateTime targetDate,
@Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException {
GlobalLock lock = null;
@@ -426,4 +456,35 @@ public class InvoiceDispatcher {
}
}
}
+
+ private final static class NullDryRunArguments implements DryRunArguments {
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return null;
+ }
+ @Override
+ public SubscriptionEventType getAction() {
+ return null;
+ }
+ @Override
+ public UUID getSubscriptionId() {
+ return null;
+ }
+ @Override
+ public DateTime getEffectiveDate() {
+ return null;
+ }
+ @Override
+ public UUID getBundleId() {
+ return null;
+ }
+ @Override
+ public BillingActionPolicy getBillingActionPolicy() {
+ return null;
+ }
+ @Override
+ public List<PlanPhasePriceOverride> getPlanPhasePriceoverrides() {
+ return null;
+ }
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
index 74ef87f..86d77d9 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
@@ -83,7 +83,7 @@ public class InvoiceListener {
return;
}
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
- dispatcher.processSubscription(event, context);
+ dispatcher.processSubscriptionForInvoiceGeneration(event, context);
} catch (InvoiceApiException e) {
log.error(e.getMessage());
}
@@ -122,7 +122,16 @@ public class InvoiceListener {
public void handleNextBillingDateEvent(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
try {
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "Next Billing Date", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
- dispatcher.processSubscription(subscriptionId, eventDateTime, context);
+ dispatcher.processSubscriptionForInvoiceGeneration(subscriptionId, eventDateTime, context);
+ } catch (InvoiceApiException e) {
+ log.error(e.getMessage());
+ }
+ }
+
+ public void handleEventForInvoiceNotification(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ try {
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "Next Billing Date", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
+ dispatcher.processSubscriptionForInvoiceNotification(subscriptionId, eventDateTime, context);
} catch (InvoiceApiException e) {
log.error(e.getMessage());
}
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 27db49b..e5ec2ae 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
@@ -77,12 +77,20 @@ public class DefaultNextBillingDateNotifier implements NextBillingDateNotifier {
}
final NextBillingDateNotificationKey key = (NextBillingDateNotificationKey) notificationKey;
+
+ // 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;
try {
final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(key.getUuidKey(), callContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
if (subscription == null) {
log.warn("Next Billing Date Notification Queue handled spurious notification (key: " + 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);
} else {
- processEvent(key.getUuidKey(), eventDate, userToken, accountRecordId, tenantRecordId);
+ processEventForInvoiceGeneration(key.getUuidKey(), targetDate, userToken, accountRecordId, tenantRecordId);
}
} catch (SubscriptionBaseApiException e) {
log.warn("Next Billing Date Notification Queue handled spurious notification (key: " + key + ")", e);
@@ -111,7 +119,11 @@ public class DefaultNextBillingDateNotifier implements NextBillingDateNotifier {
}
}
- private void processEvent(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ private void processEventForInvoiceGeneration(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
listener.handleNextBillingDateEvent(subscriptionId, eventDateTime, userToken, accountRecordId, tenantRecordId);
}
+
+ private void processEventForInvoiceNotification(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ listener.handleEventForInvoiceNotification(subscriptionId, eventDateTime, userToken, accountRecordId, tenantRecordId);
+ }
}
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 d490d0d..e76a1f5 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
@@ -47,6 +47,19 @@ 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, accountId, subscriptionId, 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, accountId, subscriptionId, Boolean.TRUE, futureNotificationTime, targetDate, internalCallContext);
+ }
+
+
+ private void insertNextBillingFromTransactionInternal(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
+ final UUID subscriptionId, final Boolean isDryRunForInvoiceNotification, final DateTime futureNotificationTime, final DateTime targetDate, final InternalCallContext internalCallContext) {
final NotificationQueue nextBillingQueue;
try {
nextBillingQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
@@ -54,7 +67,7 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
log.info("Queuing next billing date notification at {} for subscriptionId {}", futureNotificationTime.toString(), subscriptionId.toString());
nextBillingQueue.recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), futureNotificationTime,
- new NextBillingDateNotificationKey(subscriptionId), internalCallContext.getUserToken(),
+ new NextBillingDateNotificationKey(subscriptionId, targetDate, isDryRunForInvoiceNotification), internalCallContext.getUserToken(),
internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
} catch (final NoSuchNotificationQueue e) {
log.error("Attempting to put items on a non-existent queue (NextBillingDateNotifier).", e);
@@ -62,4 +75,5 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
log.error("Failed to serialize notificationKey for subscriptionId {}", subscriptionId);
}
}
+
}
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 b8349c0..664d415 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
@@ -18,6 +18,7 @@ package org.killbill.billing.invoice.notification;
import java.util.UUID;
+import org.joda.time.DateTime;
import org.killbill.notificationq.DefaultUUIDNotificationKey;
import com.fasterxml.jackson.annotation.JsonCreator;
@@ -25,8 +26,24 @@ import com.fasterxml.jackson.annotation.JsonProperty;
public class NextBillingDateNotificationKey extends DefaultUUIDNotificationKey {
+ private Boolean isDryRunForInvoiceNotification;
+ private DateTime targetDate;
+
@JsonCreator
- public NextBillingDateNotificationKey(@JsonProperty("uuidKey") final UUID uuidKey) {
+ public NextBillingDateNotificationKey(@JsonProperty("uuidKey") final UUID uuidKey,
+ @JsonProperty("targetDate") final DateTime targetDate,
+ @JsonProperty("isDryRunForInvoiceNotification") final Boolean isDryRunForInvoiceNotification) {
super(uuidKey);
+ this.targetDate = targetDate;
+ this.isDryRunForInvoiceNotification = isDryRunForInvoiceNotification;
+ }
+
+ @JsonProperty("isDryRunForInvoiceNotification")
+ public Boolean isDryRunForInvoiceNotification() {
+ return isDryRunForInvoiceNotification;
+ }
+
+ public DateTime getTargetDate() {
+ return targetDate;
}
}
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 9e6312c..b702434 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
@@ -28,4 +28,8 @@ public interface NextBillingDatePoster {
void insertNextBillingNotificationFromTransaction(EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, UUID accountId,
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);
+
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestEventJson.java b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestEventJson.java
index 6fa5710..1d44414 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestEventJson.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestEventJson.java
@@ -19,7 +19,9 @@ package org.killbill.billing.invoice.api.user;
import java.math.BigDecimal;
import java.util.UUID;
+import org.joda.time.DateTime;
import org.joda.time.LocalDate;
+import org.killbill.billing.events.InvoiceNotificationInternalEvent;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -42,6 +44,16 @@ public class TestEventJson extends InvoiceTestSuiteNoDB {
Assert.assertEquals(obj, e);
}
+
+ @Test(groups = "fast")
+ public void testInvoiceNotificationEvent() throws Exception {
+ final InvoiceNotificationInternalEvent e = new DefaultInvoiceNotificationInternalEvent(UUID.randomUUID(), new BigDecimal(12.0), Currency.USD, new DateTime(), 1L, 2L, null);
+ final String json = mapper.writeValueAsString(e);
+
+ final Object obj = mapper.readValue(json, DefaultInvoiceNotificationInternalEvent.class);
+ Assert.assertEquals(obj, e);
+ }
+
@Test(groups = "fast")
public void testEmptyInvoiceEvent() throws Exception {
final NullInvoiceInternalEvent e = new DefaultNullInvoiceEvent(UUID.randomUUID(), new LocalDate(), 1L, 2L, null);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index 0343fbc..bf63597 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -65,6 +65,7 @@ import org.killbill.billing.util.currency.KillBillMoney;
import org.killbill.clock.Clock;
import org.killbill.clock.DefaultClock;
import org.mockito.Mockito;
+import org.skife.config.TimeSpan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.Test;
@@ -113,6 +114,11 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
public boolean isInsertZeroUsageItems() {
return true;
}
+
+ @Override
+ public TimeSpan getDryRunNotificationSchedule() {
+ return new TimeSpan("0s");
+ }
};
this.generator = new DefaultInvoiceGenerator(clock, null, invoiceConfig, internalCallContextFactory);
}
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
new file mode 100644
index 0000000..d3bf3ed
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class TestNextBillingDateNotificationKey {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ @Test(groups = "fast")
+ public void testBasic() 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 String json = mapper.writeValueAsString(key);
+
+ final NextBillingDateNotificationKey result = mapper.readValue(json, NextBillingDateNotificationKey.class);
+ Assert.assertEquals(result.getUuidKey(), uuidKey);
+ Assert.assertEquals(result.getTargetDate().compareTo(targetDate), 0);
+ Assert.assertEquals(result.isDryRunForInvoiceNotification(), isDryRunForInvoiceNotification);
+ }
+
+ @Test(groups = "fast")
+ public void testWithMissingFields() throws Exception {
+ final String json = "{\"uuidKey\":\"a38c363f-b25b-4287-8ebc-55964e116d2f\"}";
+ final NextBillingDateNotificationKey result = mapper.readValue(json, NextBillingDateNotificationKey.class);
+ Assert.assertEquals(result.getUuidKey().toString(), "a38c363f-b25b-4287-8ebc-55964e116d2f");
+ Assert.assertNull(result.getTargetDate());
+ Assert.assertNull(result.isDryRunForInvoiceNotification());
+
+ }
+}
\ No newline at end of file
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 cb6c553..6d083d1 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
@@ -37,7 +37,6 @@ public class TestNextBillingDateNotifier extends InvoiceTestSuiteWithEmbeddedDB
@Test(groups = "slow")
public void testInvoiceNotifier() throws Exception {
- final UUID accountId = UUID.randomUUID();
final SubscriptionBase subscription = invoiceUtil.createSubscription();
final UUID subscriptionId = subscription.getId();
final DateTime now = clock.getUTCNow();
@@ -46,7 +45,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), internalCallContext.getUserToken(), internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+ nextBillingQueue.recordFutureNotification(now, new NextBillingDateNotificationKey(subscriptionId, now, Boolean.FALSE), internalCallContext.getUserToken(), internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
// Move time in the future after the notification effectiveDate
((ClockMock) clock).setDeltaFromReality(3000);
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index b2c9cef..96c2026 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.10.1</version>
+ <version>0.10.2</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.13.6-SNAPSHOT</version>
diff --git a/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java
index c32ec52..6d5a205 100644
--- a/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java
@@ -19,6 +19,7 @@ package org.killbill.billing.util.config;
import org.skife.config.Config;
import org.skife.config.Default;
import org.skife.config.Description;
+import org.skife.config.TimeSpan;
public interface InvoiceConfig extends KillbillConfig {
@@ -37,4 +38,8 @@ public interface InvoiceConfig extends KillbillConfig {
@Description("Whether to insert usage items with a zero amount")
public boolean isInsertZeroUsageItems();
+ @Config("org.killbill.invoice.dryRunNotificationSchedule")
+ @Default("0s")
+ @Description("DryRun invoice notification time before targetDate (ignored if set to 0s)")
+ public TimeSpan getDryRunNotificationSchedule();
}
diff --git a/util/src/test/java/org/killbill/billing/api/TestApiListener.java b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
index a1d7fbd..0f92505 100644
--- a/util/src/test/java/org/killbill/billing/api/TestApiListener.java
+++ b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
@@ -25,6 +25,7 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.joda.time.DateTime;
+import org.killbill.billing.events.InvoiceNotificationInternalEvent;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.IDBI;
import org.skife.jdbi.v2.tweak.HandleCallback;
@@ -102,6 +103,7 @@ public class TestApiListener {
PHASE,
BLOCK,
INVOICE,
+ INVOICE_NOTIFICATION,
INVOICE_ADJUSTMENT,
PAYMENT,
PAYMENT_ERROR,
@@ -208,6 +210,13 @@ public class TestApiListener {
}
@Subscribe
+ public void handleInvoiceNotificationEvents(final InvoiceNotificationInternalEvent event) {
+ log.info(String.format("Got Invoice notification event %s", event.toString()));
+ assertEqualsNicely(NextEvent.INVOICE_NOTIFICATION);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
public void handleInvoiceEvents(final InvoiceCreationInternalEvent event) {
log.info(String.format("Got Invoice event %s", event.toString()));
assertEqualsNicely(NextEvent.INVOICE);