Details
diff --git a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
index d1c1b5d..8e30d0c 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
@@ -92,6 +92,7 @@ import com.ning.billing.util.svcsapi.bus.InternalBus;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import static org.testng.Assert.fail;
@@ -254,7 +255,7 @@ public class TestAnalyticsService extends AnalyticsTestSuiteWithEmbeddedDB {
}
}));
- invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, true, internalCallContext);
+ invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, true, ImmutableMap.<UUID, DateTime>of(), internalCallContext);
final List<InvoiceModelDao> invoices = invoiceDao.getInvoicesByAccount(account.getId(), internalCallContext);
Assert.assertEquals(invoices.size(), 1);
Assert.assertEquals(invoices.get(0).getInvoiceItems().size(), 1);
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
index e613e9d..2e48838 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
@@ -19,6 +19,7 @@ package com.ning.billing.invoice.api.migration;
import java.math.BigDecimal;
import java.util.UUID;
+import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,6 +39,7 @@ import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.svcapi.account.AccountInternalApi;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
public class DefaultInvoiceMigrationApi implements InvoiceMigrationApi {
@@ -74,7 +76,7 @@ public class DefaultInvoiceMigrationApi implements InvoiceMigrationApi {
MigrationPlan.MIGRATION_PLAN_NAME, MigrationPlan.MIGRATION_PLAN_PHASE_NAME,
targetDate, null, balance, null, currency, null);
dao.createInvoice(migrationInvoice, ImmutableList.<InvoiceItemModelDao>of(migrationInvoiceItem),
- ImmutableList.<InvoicePaymentModelDao>of(), true, internalCallContextFactory.createInternalCallContext(accountId, context));
+ ImmutableList.<InvoicePaymentModelDao>of(), true, ImmutableMap.<UUID, DateTime>of(), internalCallContextFactory.createInternalCallContext(accountId, context));
return migrationInvoice.getId();
}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
index 142ac74..fe64a71 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
@@ -25,6 +25,7 @@ import java.util.UUID;
import javax.annotation.Nullable;
+import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.skife.jdbi.v2.IDBI;
import org.slf4j.Logger;
@@ -177,7 +178,8 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
@Override
public void createInvoice(final InvoiceModelDao invoice, final List<InvoiceItemModelDao> invoiceItems,
- final List<InvoicePaymentModelDao> invoicePayments, final boolean isRealInvoice, final InternalCallContext context) {
+ final List<InvoicePaymentModelDao> invoicePayments, final boolean isRealInvoice, final Map<UUID, DateTime> callbackDateTimePerSubscriptions,
+ final InternalCallContext context) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
@@ -197,14 +199,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
transInvoiceItemSqlDao.create(invoiceItemModelDao, context);
}
- // Add entries in the notification queue for recurring items
- final List<InvoiceItemModelDao> recurringInvoiceItems = ImmutableList.<InvoiceItemModelDao>copyOf(Collections2.filter(invoiceItems, new Predicate<InvoiceItemModelDao>() {
- @Override
- public boolean apply(@Nullable final InvoiceItemModelDao item) {
- return item.getType() == InvoiceItemType.RECURRING;
- }
- }));
- notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), recurringInvoiceItems);
+ notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), callbackDateTimePerSubscriptions);
// Create associated payments
final InvoicePaymentSqlDao invoicePaymentSqlDao = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class);
@@ -944,20 +939,12 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
invoice.addPayments(invoicePayments);
}
- private void notifyOfFutureBillingEvents(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final UUID accountId, final List<InvoiceItemModelDao> invoiceItems) {
+ private void notifyOfFutureBillingEvents(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final UUID accountId, final Map<UUID, DateTime> callbackDateTimePerSubscriptions) {
- for (final InvoiceItemModelDao item : invoiceItems) {
- if (item.getType() == InvoiceItemType.RECURRING) {
- if ((item.getEndDate() != null) &&
- (item.getAmount() == null ||
- item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
- //
- // We insert a future notification for each recurring subscription at the end of the service period = new CTD of the subscription
- //
- nextBillingDatePoster.insertNextBillingNotification(entitySqlDaoWrapperFactory, accountId, item.getSubscriptionId(),
- item.getEndDate().toDateTimeAtCurrentTime());
- }
- }
+
+ for (UUID subscriptionId : callbackDateTimePerSubscriptions.keySet()) {
+ final DateTime callbackDateTimeUTC = callbackDateTimePerSubscriptions.get(subscriptionId);
+ nextBillingDatePoster.insertNextBillingNotification(entitySqlDaoWrapperFactory, accountId, subscriptionId, callbackDateTimeUTC);
}
}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
index 1d51614..dbaa474 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
@@ -23,6 +23,7 @@ import java.util.UUID;
import javax.annotation.Nullable;
+import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import com.ning.billing.catalog.api.Currency;
@@ -33,7 +34,7 @@ import com.ning.billing.util.callcontext.InternalTenantContext;
public interface InvoiceDao {
void createInvoice(InvoiceModelDao invoice, List<InvoiceItemModelDao> invoiceItems,
- List<InvoicePaymentModelDao> invoicePayments, boolean isRealInvoice, InternalCallContext context);
+ List<InvoicePaymentModelDao> invoicePayments, boolean isRealInvoice, final Map<UUID, DateTime> callbackDateTimePerSubscriptions, InternalCallContext context);
InvoiceModelDao getById(UUID id, InternalTenantContext context);
diff --git a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
index b7aefc8..7fb537a 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
@@ -16,6 +16,7 @@
package com.ning.billing.invoice;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -26,6 +27,7 @@ import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
+import org.joda.time.LocalTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,6 +40,7 @@ import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoiceApiException;
import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
import com.ning.billing.invoice.api.InvoiceNotifier;
import com.ning.billing.invoice.api.InvoicePayment;
import com.ning.billing.invoice.api.user.DefaultInvoiceCreationEvent;
@@ -53,6 +56,8 @@ import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
import com.ning.billing.invoice.model.RecurringInvoiceItem;
import com.ning.billing.util.callcontext.InternalCallContext;
import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.entity.dao.EntitySqlDao;
+import com.ning.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
import com.ning.billing.util.events.BusInternalEvent;
import com.ning.billing.util.events.EffectiveSubscriptionInternalEvent;
import com.ning.billing.util.events.InvoiceCreationInternalEvent;
@@ -208,7 +213,9 @@ public class InvoiceDispatcher {
return new InvoicePaymentModelDao(input);
}
}));
- invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, isRealInvoiceWithItems, context);
+
+ final Map<UUID, DateTime> callbackDateTimePerSubscriptions = createNextFutureNotificationDate(invoiceItemModelDaos, account.getTimeZone());
+ invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, isRealInvoiceWithItems, callbackDateTimePerSubscriptions, context);
final List<InvoiceItem> fixedPriceInvoiceItems = invoice.getInvoiceItems(FixedPriceInvoiceItem.class);
final List<InvoiceItem> recurringInvoiceItems = invoice.getInvoiceItems(RecurringInvoiceItem.class);
@@ -239,6 +246,37 @@ public class InvoiceDispatcher {
}
}
+
+ Map<UUID, DateTime> createNextFutureNotificationDate(final List<InvoiceItemModelDao> invoiceItems, final DateTimeZone accountTimeZone) {
+
+ final Map<UUID, DateTime> result = new HashMap<UUID, DateTime>();
+
+ // For each subscription that has a positive (amount) recurring item, create the date
+ // at which we should be called back for next invoice.
+ //
+ for (final InvoiceItemModelDao item : invoiceItems) {
+ if (item.getType() == InvoiceItemType.RECURRING) {
+ if ((item.getEndDate() != null) &&
+ (item.getAmount() == null ||
+ item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
+
+ //
+ // Since we create the targetDate for next invoice using that date (on the way back), we need to make sure
+ // that this datetime once transformed into a LocalDate points to the correct day.
+ // e.g If accountTimeZone is -8 and we want to invoice on the 16, with a toDateTimeAtCurrentTime = 00:00:23,
+ // we will generate a datetime that is 16T08:00:23 => LocalDate in that timeZone stays on the 16.
+ //
+ int deltaMs = accountTimeZone.getOffset(clock.getUTCNow());
+ int negativeDeltaMs = -1 * deltaMs;
+
+ final LocalTime localTime = clock.getUTCNow().toLocalTime();
+ result.put(item.getSubscriptionId(), item.getEndDate().toDateTime(localTime, DateTimeZone.UTC).plusMillis(negativeDeltaMs));
+ }
+ }
+ }
+ return result;
+ }
+
private void setChargedThroughDates(final BillCycleDay billCycleDay,
final DateTimeZone accountTimeZone,
final Collection<InvoiceItem> fixedPriceItems,
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
index 2d1bb07..8ca2f72 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
@@ -21,6 +21,7 @@ import java.net.URL;
import java.util.List;
import java.util.UUID;
+import org.joda.time.DateTime;
import org.skife.jdbi.v2.IDBI;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
@@ -49,6 +50,7 @@ import com.ning.billing.util.svcsapi.bus.InternalBus;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
@@ -156,7 +158,8 @@ public class InvoiceDaoTestBase extends InvoicingTestBase {
}
}));
- invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, isRealInvoiceWithItems, internalCallContext);
+ // The test does not use the invoice callback notifier hence the empty map
+ invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, isRealInvoiceWithItems, ImmutableMap.<UUID, DateTime>of(), internalCallContext);
}
protected void verifyInvoice(final UUID invoiceId, final double balance, final double cbaAmount) throws InvoiceApiException {
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
index cdcc683..6861d13 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
@@ -26,6 +26,7 @@ import java.util.UUID;
import javax.annotation.Nullable;
+import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import com.ning.billing.catalog.api.Currency;
@@ -52,7 +53,7 @@ public class MockInvoiceDao implements InvoiceDao {
@Override
public void createInvoice(final InvoiceModelDao invoice, final List<InvoiceItemModelDao> invoiceItems,
- final List<InvoicePaymentModelDao> invoicePayments, final boolean isRealInvoice, final InternalCallContext context) {
+ final List<InvoicePaymentModelDao> invoicePayments, final boolean isRealInvoice, final Map<UUID, DateTime> callbackDateTimePerSubscriptions, final InternalCallContext context) {
synchronized (monitor) {
invoices.put(invoice.getId(), invoice);
for (final InvoiceItemModelDao invoiceItemModelDao : invoiceItems) {
diff --git a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
index 95341d8..fc03430 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
@@ -17,10 +17,13 @@
package com.ning.billing.invoice;
import java.math.BigDecimal;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.mockito.Mockito;
import org.slf4j.Logger;
@@ -49,8 +52,10 @@ import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.invoice.api.InvoiceItemType;
import com.ning.billing.invoice.api.InvoiceNotifier;
import com.ning.billing.invoice.dao.InvoiceDao;
+import com.ning.billing.invoice.dao.InvoiceItemModelDao;
import com.ning.billing.invoice.dao.InvoiceModelDao;
import com.ning.billing.invoice.generator.InvoiceGenerator;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
import com.ning.billing.invoice.notification.NextBillingDateNotifier;
import com.ning.billing.invoice.notification.NullInvoiceNotifier;
import com.ning.billing.invoice.tests.InvoicingTestBase;
@@ -60,6 +65,7 @@ import com.ning.billing.util.callcontext.InternalCallContext;
import com.ning.billing.util.callcontext.InternalCallContextFactory;
import com.ning.billing.util.callcontext.InternalTenantContext;
import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
import com.ning.billing.util.globallocker.GlobalLocker;
import com.ning.billing.util.svcapi.account.AccountInternalApi;
import com.ning.billing.util.svcapi.entitlement.EntitlementInternalApi;
@@ -97,7 +103,7 @@ public class TestInvoiceDispatcher extends InvoicingTestBase {
private BillingInternalApi billingApi;
@Inject
- private Clock clock;
+ private ClockMock clock;
@Inject
private AccountInternalApi accountInternalApi;
@@ -263,5 +269,39 @@ public class TestInvoiceDispatcher extends InvoicingTestBase {
}
}
+ @Test(groups= "slow")
+ public void testCreateNextFutureNotificationDate() throws Exception {
+
+ final LocalDate startDate = new LocalDate("2012-10-26");
+ final LocalDate endDate = new LocalDate("2012-11-26");
+
+ clock.setTime(new DateTime(2012, 10, 26, 1, 12, 23, DateTimeZone.UTC));
+ final InvoiceItemModelDao item = new InvoiceItemModelDao(UUID.randomUUID(), clock.getUTCNow(), InvoiceItemType.RECURRING, UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(),
+ "planName", "phaseName", startDate, endDate, new BigDecimal("23.9"), new BigDecimal("23.9"), Currency.EUR, null);
+
+ final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
+ final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountInternalApi, billingApi, entitlementInternalApi, invoiceDao,
+ invoiceNotifier, locker, busService.getBus(),
+ clock);
+
+ final DateTime expectedBefore = clock.getUTCNow();
+ final Map<UUID, DateTime> result = dispatcher.createNextFutureNotificationDate(Collections.singletonList(item), DateTimeZone.forID("Pacific/Pitcairn"));
+ final DateTime expectedAfter = clock.getUTCNow();
+
+ Assert.assertEquals(result.size(), 1);
+
+ final DateTime receivedDate = result.get(item.getSubscriptionId());
+
+ final LocalDate receivedTargetDate = new LocalDate(receivedDate, DateTimeZone.forID("Pacific/Pitcairn"));
+ Assert.assertEquals(receivedTargetDate, endDate);
+
+
+
+ Assert.assertTrue(receivedDate.compareTo(new DateTime(2012, 11, 26, 9 /* 1 + 8 for Pitcairn */ , 12, 23, DateTimeZone.UTC)) >= 0);
+ Assert.assertTrue(receivedDate.compareTo(new DateTime(2012, 11, 26, 9, 13, 0, DateTimeZone.UTC)) <= 0);
+
+ }
+
+
//MDW add a test to cover when the account auto-invoice-off tag is present
}