killbill-memoizeit
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java 20(+9 -11)
invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentNotificationKey.java 34(+34 -0)
invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentNotifier.java 102(+102 -0)
invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java 92(+92 -0)
Details
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
index b50879e..7c930cd 100644
--- a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
@@ -75,4 +75,6 @@ public interface InvoiceInternalApi {
public void consumeExistingCBAOnAccountWithUnpaidInvoices(final UUID accountId, final InternalCallContext context) throws InvoiceApiException;
public Map<UUID, BigDecimal> validateInvoiceItemAdjustments(final UUID paymentId, final Map<UUID, BigDecimal> idWithAmount, final InternalTenantContext context) throws InvoiceApiException;
+
+ public void commitInvoice(UUID invoiceId, InternalCallContext context) throws InvoiceApiException;
}
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 9bf78b2..678f286 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
@@ -366,26 +366,23 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
assertEquals(parentInvoice.getNumberOfItems(), 2);
assertEquals(parentInvoice.getStatus(), InvoiceStatus.DRAFT);
assertTrue(parentInvoice.isParentInvoice());
- //assertEquals(parentInvoice.getBalance(), BigDecimal.ZERO);
+ assertEquals(parentInvoice.getBalance().toString(), "0.00");
- // No payment is expected because balance is 0
+ // Moving a day the NotificationQ calls the commitInvoice. No payment is expected because balance is 0
busHandler.pushExpectedEvents(NextEvent.INVOICE);
- invoiceUserApi.commitInvoice(parentInvoice.getId(), callContext);
+ clock.addDays(1);
assertListenerStatus();
parentInvoice = invoiceUserApi.getInvoice(parentInvoice.getId(), callContext);
assertEquals(parentInvoice.getStatus(), InvoiceStatus.COMMITTED);
- // Move through time and verify new parent Invoice
+ // Move through time and verify new parent Invoice. No payments are expected.
busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.PHASE,
- NextEvent.INVOICE, NextEvent.INVOICE,
- NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT,
- NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT); // TODO fix when refactor invoice.getBalance
+ NextEvent.INVOICE, NextEvent.INVOICE);
clock.addDays(31);
assertListenerStatus();
// Second Parent invoice over Recurring period
-
parentInvoices = invoiceUserApi.getInvoicesByAccount(parentAccount.getId(), callContext);
assertEquals(parentInvoices.size(), 2);
@@ -393,10 +390,11 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
assertEquals(parentInvoice.getNumberOfItems(), 2);
assertEquals(parentInvoice.getStatus(), InvoiceStatus.DRAFT);
assertTrue(parentInvoice.isParentInvoice());
+ assertEquals(parentInvoice.getBalance().toString(), "279.90");
- // now payment is expected
- busHandler.pushExpectedEvents(NextEvent.INVOICE); // TODO NextEvent.PAYMENT
- invoiceUserApi.commitInvoice(parentInvoice.getId(), callContext);
+ // Moving a day the NotificationQ calls the commitInvoice. Now payment is expected
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(1);
assertListenerStatus();
parentInvoice = invoiceUserApi.getInvoice(parentInvoice.getId(), callContext);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java b/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java
index 961d3e7..82dab56 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java
@@ -18,6 +18,7 @@
package org.killbill.billing.invoice.api;
+import org.killbill.billing.invoice.notification.ParentInvoiceCommitmentNotifier;
import org.killbill.bus.api.PersistentBus;
import org.killbill.billing.invoice.InvoiceListener;
import org.killbill.billing.invoice.InvoiceTagHandler;
@@ -36,13 +37,16 @@ public class DefaultInvoiceService implements InvoiceService {
private final InvoiceListener invoiceListener;
private final InvoiceTagHandler tagHandler;
private final PersistentBus eventBus;
+ private final ParentInvoiceCommitmentNotifier parentInvoiceNotifier;
@Inject
- public DefaultInvoiceService(final InvoiceListener invoiceListener, final InvoiceTagHandler tagHandler, final PersistentBus eventBus, final NextBillingDateNotifier dateNotifier) {
+ public DefaultInvoiceService(final InvoiceListener invoiceListener, final InvoiceTagHandler tagHandler, final PersistentBus eventBus,
+ final NextBillingDateNotifier dateNotifier, final ParentInvoiceCommitmentNotifier parentInvoiceNotifier) {
this.invoiceListener = invoiceListener;
this.tagHandler = tagHandler;
this.eventBus = eventBus;
this.dateNotifier = dateNotifier;
+ this.parentInvoiceNotifier = parentInvoiceNotifier;
}
@Override
@@ -59,11 +63,13 @@ public class DefaultInvoiceService implements InvoiceService {
throw new RuntimeException("Unable to register to the EventBus!", e);
}
dateNotifier.initialize();
+ parentInvoiceNotifier.initialize();
}
@LifecycleHandlerType(LifecycleLevel.START_SERVICE)
public void start() {
dateNotifier.start();
+ parentInvoiceNotifier.start();
}
@LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
@@ -75,5 +81,6 @@ public class DefaultInvoiceService implements InvoiceService {
throw new RuntimeException("Unable to unregister to the EventBus!", e);
}
dateNotifier.stop();
+ parentInvoiceNotifier.stop();
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
index 80472a9..fcdd838 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
@@ -40,6 +40,7 @@ import org.killbill.billing.invoice.api.InvoiceInternalApi;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.invoice.api.InvoicePaymentType;
+import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.invoice.api.WithAccountLock;
import org.killbill.billing.invoice.dao.InvoiceDao;
import org.killbill.billing.invoice.dao.InvoiceModelDao;
@@ -182,4 +183,9 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
}
}).orNull();
}
+
+ @Override
+ public void commitInvoice(final UUID invoiceId, final InternalCallContext context) throws InvoiceApiException {
+ dao.changeInvoiceStatus(invoiceId, InvoiceStatus.COMMITTED, context);
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
index eceab78..23b11ef 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
@@ -56,6 +56,11 @@ public abstract class InvoiceCalculatorUtils {
return InvoiceItemType.CBA_ADJ.equals(invoiceItem.getInvoiceItemType());
}
+ // Item from Parent Invoice
+ public static boolean isParentSummaryItem(final InvoiceItem invoiceItem) {
+ return InvoiceItemType.PARENT_SUMMARY.equals(invoiceItem.getInvoiceItemType());
+ }
+
// Regular line item (charges)
public static boolean isCharge(final InvoiceItem invoiceItem) {
return InvoiceItemType.TAX.equals(invoiceItem.getInvoiceItemType()) ||
@@ -124,7 +129,8 @@ public abstract class InvoiceCalculatorUtils {
if (isCharge(invoiceItem) ||
isInvoiceAdjustmentItem(invoiceItem, otherInvoiceItems) ||
- isInvoiceItemAdjustmentItem(invoiceItem)) {
+ isInvoiceItemAdjustmentItem(invoiceItem) ||
+ isParentSummaryItem(invoiceItem)) {
amountCharged = amountCharged.add(invoiceItem.getAmount());
}
}
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 04ab792..c91a235 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
@@ -29,6 +29,7 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
@@ -48,6 +49,8 @@ import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent;
import org.killbill.billing.invoice.api.user.DefaultInvoiceCreationEvent;
import org.killbill.billing.invoice.notification.NextBillingDatePoster;
+import org.killbill.billing.invoice.notification.ParentInvoiceCommitmentPoster;
+import org.killbill.billing.util.AccountDateAndTimeZoneContext;
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.cache.Cachable.CacheType;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
@@ -60,6 +63,7 @@ import org.killbill.billing.util.entity.dao.EntityDaoBase;
import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+import org.killbill.billing.util.timezone.DefaultAccountDateAndTimeZoneContext;
import org.killbill.bus.api.BusEvent;
import org.killbill.bus.api.PersistentBus;
import org.killbill.bus.api.PersistentBus.EventBusException;
@@ -104,6 +108,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
private final Clock clock;
private final CacheControllerDispatcher cacheControllerDispatcher;
private final NonEntityDao nonEntityDao;
+ private final ParentInvoiceCommitmentPoster parentInvoiceCommitmentPoster;
@Inject
public DefaultInvoiceDao(final IDBI dbi,
@@ -115,6 +120,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
final InvoiceConfig invoiceConfig,
final InvoiceDaoHelper invoiceDaoHelper,
final CBADao cbaDao,
+ final ParentInvoiceCommitmentPoster parentInvoiceCommitmentPoster,
final InternalCallContextFactory internalCallContextFactory) {
super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao), InvoiceSqlDao.class);
this.nextBillingDatePoster = nextBillingDatePoster;
@@ -126,6 +132,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
this.clock = clock;
this.cacheControllerDispatcher = cacheControllerDispatcher;
this.nonEntityDao = nonEntityDao;
+ this.parentInvoiceCommitmentPoster = parentInvoiceCommitmentPoster;
}
@Override
@@ -271,6 +278,9 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
if (InvoiceStatus.COMMITTED.equals(invoice.getStatus())) {
notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), callbackDateTimePerSubscriptions, context);
}
+ if (invoice.isParentInvoice()) {
+ notifyOfParentInvoiceCreation(entitySqlDaoWrapperFactory, invoice, callbackDateTimePerSubscriptions, context);
+ }
}
return null;
}
@@ -948,7 +958,6 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
if (InvoiceStatus.COMMITTED.equals(newStatus)) {
// notify invoice creation event
notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, invoice, context);
- // notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), callbackDateTimePerSubscriptions, context);
}
return null;
@@ -969,6 +978,18 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
}
+ private void notifyOfParentInvoiceCreation(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InvoiceModelDao parentInvoice,
+ final FutureAccountNotifications callbackDateTime, final InternalCallContext context) {
+ DateTime futureNotificationDate = parentInvoice.getCreatedDate();
+ AccountDateAndTimeZoneContext accountDateAndTimeZone;
+ if ((callbackDateTime != null) && (callbackDateTime.getAccountDateAndTimeZoneContext() != null)) {
+ accountDateAndTimeZone = callbackDateTime.getAccountDateAndTimeZoneContext();
+ } else {
+ accountDateAndTimeZone = new DefaultAccountDateAndTimeZoneContext(futureNotificationDate, DateTimeZone.UTC);
+ }
+ parentInvoiceCommitmentPoster.insertParentInvoiceFromTransactionInternal(entitySqlDaoWrapperFactory, parentInvoice.getId(), futureNotificationDate, accountDateAndTimeZone, context);
+ }
+
@Override
public void createParentChildInvoiceRelation(final InvoiceParentChildModelDao invoiceRelation, final InternalCallContext context) throws InvoiceApiException {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@@ -982,6 +1003,17 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
@Override
+ public List<InvoiceParentChildModelDao> getChildInvoicesByParentInvoiceId(final UUID parentInvoiceId, final InternalCallContext context) throws InvoiceApiException {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoiceParentChildModelDao>>() {
+ @Override
+ public List<InvoiceParentChildModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+ final InvoiceParentChildrenSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoiceParentChildrenSqlDao.class);
+ return transactional.getChildInvoicesByParentInvoiceId(parentInvoiceId.toString(), context);
+ }
+ });
+ }
+
+ @Override
public InvoiceModelDao getParentDraftInvoice(final UUID parentAccountId, final InternalCallContext context) throws InvoiceApiException {
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoiceModelDao>() {
@Override
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
index 4dec8ec..ab6ee1a 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
@@ -160,6 +160,16 @@ public interface InvoiceDao extends EntityDao<InvoiceModelDao, Invoice, InvoiceA
void createParentChildInvoiceRelation(final InvoiceParentChildModelDao invoiceRelation, final InternalCallContext context) throws InvoiceApiException;
/**
+ * Get parent/child invoice relationship by parent invoice id
+ *
+ * @param parentInvoiceId the parent invoice id
+ * @param context the tenant context
+ * @throws InvoiceApiException
+ */
+ List<InvoiceParentChildModelDao> getChildInvoicesByParentInvoiceId(UUID parentInvoiceId, final InternalCallContext context) throws InvoiceApiException;
+
+
+ /**
* Retrieve parent invoice by the parent account id
*
* @param parentAccountId the parent account id
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
index 0919880..ce4da0d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
@@ -83,7 +83,7 @@ public class InvoiceModelDao extends EntityModelDaoBase implements EntityModelDa
}
public InvoiceModelDao(final UUID accountId, final LocalDate invoiceDate, final Currency currency, final InvoiceStatus status, final boolean isParentInvoice) {
- this(UUIDs.randomUUID(), null, accountId, null, invoiceDate, null, currency, false, status, isParentInvoice);
+ this(UUIDs.randomUUID(), invoiceDate.toDateTimeAtCurrentTime(), accountId, null, invoiceDate, null, currency, false, status, isParentInvoice);
}
public InvoiceModelDao(final Invoice invoice) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceParentChildrenSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceParentChildrenSqlDao.java
index 3d62a1a..2e82ba8 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceParentChildrenSqlDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceParentChildrenSqlDao.java
@@ -17,13 +17,22 @@
package org.killbill.billing.invoice.dao;
+import java.util.List;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.invoice.api.InvoiceParentChild;
import org.killbill.billing.util.entity.dao.EntitySqlDao;
import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
@EntitySqlDaoStringTemplate
public interface InvoiceParentChildrenSqlDao extends EntitySqlDao<InvoiceParentChildModelDao, InvoiceParentChild> {
+ @SqlQuery
+ List<InvoiceParentChildModelDao> getChildInvoicesByParentInvoiceId(@Bind("parentInvoiceId") final String parentInvoiceId,
+ @BindBean final InternalTenantContext context);
}
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 d35ee7e..7aa89dc 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -94,6 +94,7 @@ import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.billing.util.config.InvoiceConfig;
import org.killbill.billing.util.globallocker.LockerType;
+import org.killbill.billing.util.timezone.DefaultAccountDateAndTimeZoneContext;
import org.killbill.bus.api.PersistentBus;
import org.killbill.bus.api.PersistentBus.EventBusException;
import org.killbill.clock.Clock;
@@ -716,17 +717,23 @@ public class InvoiceDispatcher {
final Long parentAccountRecordId = internalCallContextFactory.getRecordIdFromObject(account.getParentAccountId(), ObjectType.ACCOUNT, buildTenantContext(context));
final InternalCallContext parentContext = new InternalCallContext(context, parentAccountRecordId);
+ final DateTime today = clock.getNow(account.getTimeZone());
if (parentInvoice != null) {
- InvoiceItem invoiceItem = new ParentInvoiceItem(UUID.randomUUID(), DateTime.now(), parentInvoice.getId(), account.getParentAccountId(), account.getId(), invoiceAmount, account.getCurrency());
+ InvoiceItem invoiceItem = new ParentInvoiceItem(UUID.randomUUID(), today, parentInvoice.getId(), account.getParentAccountId(), account.getId(), invoiceAmount, account.getCurrency());
parentInvoice.addInvoiceItem(new InvoiceItemModelDao(invoiceItem));
List<InvoiceModelDao> invoices = new ArrayList<InvoiceModelDao>();
invoices.add(parentInvoice);
invoiceDao.createInvoices(invoices, parentContext);
} else {
- parentInvoice = new InvoiceModelDao(account.getParentAccountId(), LocalDate.now(), account.getCurrency(), InvoiceStatus.DRAFT, true);
- InvoiceItem invoiceItem = new ParentInvoiceItem(UUID.randomUUID(), DateTime.now(), parentInvoice.getId(), account.getParentAccountId(), account.getId(), invoiceAmount, account.getCurrency());
+ parentInvoice = new InvoiceModelDao(account.getParentAccountId(), today.toLocalDate(), account.getCurrency(), InvoiceStatus.DRAFT, true);
+ InvoiceItem invoiceItem = new ParentInvoiceItem(UUID.randomUUID(), today, parentInvoice.getId(), account.getParentAccountId(), account.getId(), invoiceAmount, account.getCurrency());
parentInvoice.addInvoiceItem(new InvoiceItemModelDao(invoiceItem));
- invoiceDao.createInvoice(parentInvoice, parentInvoice.getInvoiceItems(), true, null, parentContext);
+
+ // build account date time zone
+ final AccountDateAndTimeZoneContext accountDateTimeZone = new DefaultAccountDateAndTimeZoneContext(today, account.getTimeZone());
+ final FutureAccountNotifications futureAccountNotifications = new FutureAccountNotifications(accountDateTimeZone, null);
+
+ invoiceDao.createInvoice(parentInvoice, parentInvoice.getInvoiceItems(), true, futureAccountNotifications, parentContext);
}
// save parent child invoice relation
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 db59d84..7642e72 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
@@ -19,7 +19,6 @@ package org.killbill.billing.invoice;
import java.util.UUID;
import org.joda.time.DateTime;
-import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.account.api.ImmutableAccountData;
@@ -171,4 +170,13 @@ public class InvoiceListener {
return account.getParentAccountId() != null && account.isPaymentDelegatedToParent();
}
+ public void handleParentInvoiceCommitmentEvent(final UUID invoiceId, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ try {
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "Commit Invoice", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
+ invoiceApi.commitInvoice(invoiceId, context);
+ } catch (InvoiceApiException e) {
+ log.error(e.getMessage());
+ }
+ }
+
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentNotificationKey.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentNotificationKey.java
new file mode 100644
index 0000000..3e927f7
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentNotificationKey.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.util.UUID;
+
+import org.killbill.notificationq.DefaultUUIDNotificationKey;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class ParentInvoiceCommitmentNotificationKey extends DefaultUUIDNotificationKey {
+
+ @JsonCreator
+ public ParentInvoiceCommitmentNotificationKey(@JsonProperty("uuidKey") final UUID uuidKey) {
+ super(uuidKey);
+ }
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentNotifier.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentNotifier.java
new file mode 100644
index 0000000..61437c2
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentNotifier.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.invoice.InvoiceListener;
+import org.killbill.billing.invoice.api.DefaultInvoiceService;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.config.InvoiceConfig;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+
+public class ParentInvoiceCommitmentNotifier implements NextBillingDateNotifier {
+
+ private static final Logger log = LoggerFactory.getLogger(ParentInvoiceCommitmentNotifier.class);
+
+ public static final String PARENT_INVOICE_COMMITMENT_NOTIFIER_QUEUE = "parent-invoice-commitment-queue";
+
+ private final NotificationQueueService notificationQueueService;
+ private final InvoiceConfig config;
+ private final InvoiceListener listener;
+ private final InternalCallContextFactory callContextFactory;
+
+ private NotificationQueue commitInvoiceQueue;
+
+ @Inject
+ public ParentInvoiceCommitmentNotifier(final NotificationQueueService notificationQueueService,
+ final InvoiceConfig config,
+ final InvoiceListener listener,
+ final InternalCallContextFactory callContextFactory) {
+ this.notificationQueueService = notificationQueueService;
+ this.config = config;
+ this.listener = listener;
+ this.callContextFactory = callContextFactory;
+ }
+
+ @Override
+ public void initialize() throws NotificationQueueAlreadyExists {
+
+ final NotificationQueueHandler notificationQueueHandler = new NotificationQueueHandler() {
+ @Override
+ public void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ try {
+ if (!(notificationKey instanceof ParentInvoiceCommitmentNotificationKey)) {
+ log.error("Invoice service received an unexpected event type {}", notificationKey.getClass().getName());
+ return;
+ }
+
+ final ParentInvoiceCommitmentNotificationKey key = (ParentInvoiceCommitmentNotificationKey) notificationKey;
+
+ listener.handleParentInvoiceCommitmentEvent(key.getUuidKey(), userToken, accountRecordId, tenantRecordId);
+
+ } catch (IllegalArgumentException e) {
+ log.error("The key returned from the ParentInvoiceCommitmentQueue is not a valid UUID", e);
+ }
+ }
+ };
+
+ commitInvoiceQueue = notificationQueueService.createNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
+ PARENT_INVOICE_COMMITMENT_NOTIFIER_QUEUE,
+ notificationQueueHandler);
+ }
+
+ @Override
+ public void start() {
+ commitInvoiceQueue.startQueue();
+ }
+
+ @Override
+ public void stop() throws NoSuchNotificationQueue {
+ if (commitInvoiceQueue != null) {
+ commitInvoiceQueue.stopQueue();
+ notificationQueueService.deleteNotificationQueue(commitInvoiceQueue.getServiceName(), commitInvoiceQueue.getQueueName());
+ }
+ }
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java
new file mode 100644
index 0000000..58f4ca6
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.invoice.api.DefaultInvoiceService;
+import org.killbill.billing.util.AccountDateAndTimeZoneContext;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+import org.killbill.notificationq.api.NotificationEventWithMetadata;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+public class ParentInvoiceCommitmentPoster {
+
+ private static final Logger log = LoggerFactory.getLogger(ParentInvoiceCommitmentPoster.class);
+
+ private final NotificationQueueService notificationQueueService;
+
+ @Inject
+ public ParentInvoiceCommitmentPoster(final NotificationQueueService notificationQueueService) {
+ this.notificationQueueService = notificationQueueService;
+ }
+
+ public void insertParentInvoiceFromTransactionInternal(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+ final UUID invoiceId,
+ final DateTime futureNotificationTime,
+ final AccountDateAndTimeZoneContext accountDateAndTimeZoneContext,
+ final InternalCallContext internalCallContext) {
+ final NotificationQueue commitInvoiceQueue;
+ try {
+ commitInvoiceQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
+ ParentInvoiceCommitmentNotifier.PARENT_INVOICE_COMMITMENT_NOTIFIER_QUEUE);
+
+ // If we see existing notification for the same date we don't insert a new notification
+ final List<NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey>> futureNotifications = commitInvoiceQueue.getFutureNotificationFromTransactionForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), entitySqlDaoWrapperFactory.getHandle().getConnection());
+ final NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey> existingFutureNotificationWithSameDate = Iterables.tryFind(futureNotifications, new Predicate<NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey>>() {
+ @Override
+ public boolean apply(final NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey> input) {
+
+ final LocalDate notificationEffectiveLocaleDate = accountDateAndTimeZoneContext.computeLocalDateFromFixedAccountOffset(futureNotificationTime);
+ final LocalDate eventEffectiveLocaleDate = accountDateAndTimeZoneContext.computeLocalDateFromFixedAccountOffset(input.getEffectiveDate());
+
+ return notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0;
+ }
+ }).orNull();
+
+ if (existingFutureNotificationWithSameDate == null) {
+ log.info("Queuing parent invoice commitment notification at {} for invoiceId {}", futureNotificationTime.toString(), invoiceId.toString());
+
+ commitInvoiceQueue.recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), futureNotificationTime,
+ new ParentInvoiceCommitmentNotificationKey(invoiceId), internalCallContext.getUserToken(),
+ internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+ } else if (log.isDebugEnabled()) {
+ log.debug("********************* SKIPPING Queuing parent invoice commitment notification at {} for invoiceId {} *******************", futureNotificationTime.toString(), invoiceId.toString());
+ }
+
+ } catch (final NoSuchNotificationQueue e) {
+ log.error("Attempting to put items on a non-existent queue (ParentInvoiceCommitmentNotifier).", e);
+ } catch (final IOException e) {
+ log.error("Failed to serialize notificationKey for invoiceId {}", invoiceId);
+ }
+ }
+
+}
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceParentChildrenSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceParentChildrenSqlDao.sql.stg
index 813154d..4c780d9 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceParentChildrenSqlDao.sql.stg
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceParentChildrenSqlDao.sql.stg
@@ -28,6 +28,9 @@ allTableValues() ::= <<
, <tableValues()>
>>
-
-
-
+getChildInvoicesByParentInvoiceId() ::= <<
+ SELECT <allTableFields()>
+ FROM <tableName()>
+ WHERE parent_invoice_id = :parentInvoiceId
+ <AND_CHECK_TENANT()>
+ >>
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
index 8c8310f..23650a6 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
@@ -377,4 +377,9 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice,
public InvoiceModelDao getParentDraftInvoice(final UUID parentAccountId, final InternalCallContext context) throws InvoiceApiException {
throw new UnsupportedOperationException();
}
+
+ @Override
+ public List<InvoiceParentChildModelDao> getChildInvoicesByParentInvoiceId(final UUID parentInvoiceId, final InternalCallContext context) throws InvoiceApiException {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
index 9006337..af3e3a2 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
@@ -1723,4 +1723,23 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
invoiceModelDao.addInvoiceItem(new InvoiceItemModelDao(invoiceItem));
invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceModelDao), context);
}
+
+ @Test(groups = "slow")
+ public void testCreateParentChildInvoiceRelation() throws InvoiceApiException {
+
+ final UUID parentInvoiceId = UUID.randomUUID();
+ final UUID childInvoiceId = UUID.randomUUID();
+ final UUID childAccountId = UUID.randomUUID();
+ InvoiceParentChildModelDao invoiceRelation = new InvoiceParentChildModelDao(parentInvoiceId, childInvoiceId, childAccountId);
+ invoiceDao.createParentChildInvoiceRelation(invoiceRelation, context);
+
+ final List<InvoiceParentChildModelDao> relations = invoiceDao.getChildInvoicesByParentInvoiceId(parentInvoiceId, context);
+ assertEquals(relations.size(), 1);
+ final InvoiceParentChildModelDao parentChildRelation = relations.get(0);
+ assertEquals(parentChildRelation.getChildAccountId(), childAccountId);
+ assertEquals(parentChildRelation.getChildInvoiceId(), childInvoiceId);
+ assertEquals(parentChildRelation.getParentInvoiceId(), parentInvoiceId);
+
+ }
+
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
index 066bd7f..a86eb99 100644
--- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
@@ -31,6 +31,9 @@ import javax.inject.Named;
import org.joda.time.DateTime;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult;
@@ -97,6 +100,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
private final RetryServiceScheduler retryServiceScheduler;
private final InternalCallContextFactory internalCallContextFactory;
private final Clock clock;
+ private final AccountInternalApi accountApi;
private final Logger log = LoggerFactory.getLogger(InvoicePaymentControlPluginApi.class);
@@ -105,7 +109,8 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
final InvoiceInternalApi invoiceApi, final TagUserApi tagApi,
final PaymentDao paymentDao, final InvoicePaymentControlDao invoicePaymentControlDao,
@Named(PaymentModule.RETRYABLE_NAMED) final RetryServiceScheduler retryServiceScheduler,
- final InternalCallContextFactory internalCallContextFactory, final Clock clock) {
+ final InternalCallContextFactory internalCallContextFactory, final Clock clock,
+ final AccountInternalApi accountApi) {
this.paymentConfig = paymentConfig;
this.invoiceApi = invoiceApi;
this.tagApi = tagApi;
@@ -114,6 +119,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
this.retryServiceScheduler = retryServiceScheduler;
this.internalCallContextFactory = internalCallContextFactory;
this.clock = clock;
+ this.accountApi = accountApi;
}
@Override
@@ -263,6 +269,12 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
return new DefaultPriorPaymentControlResult(true);
}
+ // get immutable account and check if it is child and payment is delegated to parent => abort
+ final ImmutableAccountData accountData = accountApi.getImmutableAccountDataById(invoice.getAccountId(), internalContext);
+ if ((accountData != null) && (accountData.getParentAccountId() != null) && accountData.isPaymentDelegatedToParent()) {
+ return new DefaultPriorPaymentControlResult(true);
+ }
+
final BigDecimal requestedAmount = validateAndComputePaymentAmount(invoice, paymentControlPluginContext.getAmount(), paymentControlPluginContext.isApiPayment());
final boolean isAborted = requestedAmount.compareTo(BigDecimal.ZERO) == 0;
@@ -283,6 +295,8 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
throw new PaymentControlApiException(e);
} catch (final IllegalArgumentException e) {
throw new PaymentControlApiException(e);
+ } catch (AccountApiException e) {
+ throw new PaymentControlApiException(e);
}
}