diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java
index a80c2fa..f770663 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java
@@ -18,6 +18,7 @@
package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
+import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
@@ -34,10 +35,13 @@ 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.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
import org.killbill.billing.invoice.api.DefaultInvoiceService;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
@@ -63,6 +67,7 @@ import org.killbill.notificationq.api.NotificationQueueService;
import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
import org.killbill.queue.retry.RetryNotificationEvent;
import org.killbill.queue.retry.RetryableService;
+import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
@@ -113,6 +118,7 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
testInvoicePluginApi.additionalInvoiceItem = null;
testInvoicePluginApi.shouldAddTaxItem = true;
testInvoicePluginApi.isAborted = false;
+ testInvoicePluginApi.shouldUpdateDescription = false;
testInvoicePluginApi.rescheduleDate = null;
testInvoicePluginApi.wasRescheduled = false;
testInvoicePluginApi.invocationCount = 0;
@@ -338,6 +344,53 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
}
@Test(groups = "slow")
+ public void testUpdateDescription() throws Exception {
+ testInvoicePluginApi.shouldAddTaxItem = false;
+ testInvoicePluginApi.shouldUpdateDescription = true;
+
+ // 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 AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ // Create original subscription (Trial PHASE) -> $0 invoice but plugin added one item
+ final Entitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ final Invoice firstInvoice = invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
+ checkInvoiceDescriptions(firstInvoice);
+
+ // Move to Evergreen PHASE
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+ final Invoice secondInvoice = invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ checkInvoiceDescriptions(secondInvoice);
+
+ // Cancel START_OF_TERM to make sure odd items like CBA are updated too
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE);
+ bpSubscription.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE,
+ BillingActionPolicy.START_OF_TERM,
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+ assertListenerStatus();
+ final Invoice thirdInvoice = invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("29.95").negate()),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("29.95")));
+ checkInvoiceDescriptions(thirdInvoice);
+ }
+
+ private void checkInvoiceDescriptions(final Invoice invoice) {
+ for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+ assertEquals(invoiceItem.getDescription(), String.format("[plugin] %s", invoiceItem.getId()));
+ }
+ }
+
+ @Test(groups = "slow")
public void testRescheduledViaNotification() throws Exception {
testInvoicePluginApi.shouldAddTaxItem = false;
@@ -649,6 +702,7 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
InvoiceItem additionalInvoiceItem;
boolean shouldAddTaxItem = true;
boolean isAborted = false;
+ boolean shouldUpdateDescription = false;
DateTime rescheduleDate;
boolean wasRescheduled = false;
int invocationCount = 0;
@@ -679,6 +733,14 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
return ImmutableList.<InvoiceItem>of(additionalInvoiceItem);
} else if (shouldAddTaxItem) {
return ImmutableList.<InvoiceItem>of(createTaxInvoiceItem(invoice));
+ } else if (shouldUpdateDescription) {
+ final List<InvoiceItem> updatedInvoiceItems = new LinkedList<InvoiceItem>();
+ for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+ final InvoiceItem updatedInvoiceItem = Mockito.spy(invoiceItem);
+ Mockito.when(updatedInvoiceItem.getDescription()).thenReturn(String.format("[plugin] %s", invoiceItem.getId()));
+ updatedInvoiceItems.add(updatedInvoiceItem);
+ }
+ return updatedInvoiceItems;
} else {
return ImmutableList.<InvoiceItem>of();
}
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 dcd487e..320fcfd 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -75,12 +75,10 @@ import org.killbill.billing.invoice.generator.InvoiceWithMetadata;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates.UsageDef;
import org.killbill.billing.invoice.model.DefaultInvoice;
-import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.InvoiceItemCatalogBase;
import org.killbill.billing.invoice.model.InvoiceItemFactory;
import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
import org.killbill.billing.invoice.model.ParentInvoiceItem;
-import org.killbill.billing.invoice.model.RecurringInvoiceItem;
import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier;
import org.killbill.billing.invoice.notification.NextBillingDateNotificationKey;
import org.killbill.billing.junction.BillingEvent;
@@ -579,39 +577,16 @@ public class InvoiceDispatcher {
} else {
// Add or update items from generated invoice
for (final InvoiceItem cur : additionalInvoiceItemsFromPlugins) {
- final InvoiceItem exitingItem = Iterables.tryFind(tmpInvoiceForInvoicePlugins.getInvoiceItems(), new Predicate<InvoiceItem>() {
+ final InvoiceItem existingItem = Iterables.tryFind(tmpInvoiceForInvoicePlugins.getInvoiceItems(), new Predicate<InvoiceItem>() {
@Override
public boolean apply(final InvoiceItem input) {
return input.getId().equals(cur.getId());
}
}).orNull();
- if (exitingItem != null) {
- invoice.removeInvoiceItem(exitingItem);
+ if (existingItem != null) {
+ invoice.removeInvoiceItem(existingItem);
}
-
- final InvoiceItem sanitizedInvoiceItemFromPlugin = new InvoiceItemCatalogBase(cur.getId(),
- cur.getCreatedDate(),
- MoreObjects.firstNonNull(cur.getInvoiceId(), invoice.getId()),
- cur.getAccountId(),
- cur.getBundleId(),
- cur.getSubscriptionId(),
- cur.getDescription(),
- cur.getPlanName(),
- cur.getPhaseName(),
- cur.getUsageName(),
- cur.getPrettyPlanName(),
- cur.getPrettyPhaseName(),
- cur.getPrettyUsageName(),
- cur.getStartDate(),
- cur.getEndDate(),
- cur.getAmount(),
- cur.getRate(),
- cur.getCurrency(),
- cur.getLinkedItemId(),
- cur.getQuantity(),
- cur.getItemDetails(),
- cur.getInvoiceItemType());
- invoice.addInvoiceItem(sanitizedInvoiceItemFromPlugin);
+ invoice.addInvoiceItem(cur);
}
// Use credit after we call the plugin (https://github.com/killbill/killbill/issues/637)
@@ -639,7 +614,7 @@ public class InvoiceDispatcher {
success = true;
try {
- setChargedThroughDates(invoice.getInvoiceItems(FixedPriceInvoiceItem.class), invoice.getInvoiceItems(RecurringInvoiceItem.class), internalCallContext);
+ setChargedThroughDates(invoice, internalCallContext);
} catch (final SubscriptionBaseApiException e) {
log.error("Failed handling SubscriptionBase change.", e);
return null;
@@ -854,9 +829,23 @@ public class InvoiceDispatcher {
return internalCallContextFactory.createCallContext(context);
}
- private void setChargedThroughDates(final Collection<InvoiceItem> fixedPriceItems,
- final Collection<InvoiceItem> recurringItems,
- final InternalCallContext context) throws SubscriptionBaseApiException {
+ private void setChargedThroughDates(final Invoice invoice, final InternalCallContext context) throws SubscriptionBaseApiException {
+ // Don't use invoice.getInvoiceItems(final Class<T> clazz) as some items can come from plugins
+ final Collection<InvoiceItem> fixedPriceItems = new LinkedList<InvoiceItem>();
+ final Collection<InvoiceItem> recurringItems = new LinkedList<InvoiceItem>();
+ for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+ switch (invoiceItem.getInvoiceItemType()) {
+ case FIXED:
+ fixedPriceItems.add(invoiceItem);
+ break;
+ case RECURRING:
+ recurringItems.add(invoiceItem);
+ break;
+ default:
+ break;
+ }
+ }
+
final Map<UUID, DateTime> chargeThroughDates = new HashMap<UUID, DateTime>();
addInvoiceItemsToChargeThroughDates(chargeThroughDates, fixedPriceItems, context);
addInvoiceItemsToChargeThroughDates(chargeThroughDates, recurringItems, context);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
index 6bbdaf1..ec32fd3 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
@@ -24,6 +24,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
import javax.annotation.Nullable;
import javax.inject.Inject;
@@ -38,6 +39,7 @@ import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.InvoiceItemCatalogBase;
import org.killbill.billing.invoice.plugin.api.InvoiceContext;
import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
import org.killbill.billing.invoice.plugin.api.PriorInvoiceResult;
@@ -49,7 +51,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
public class InvoicePluginDispatcher {
@@ -167,22 +172,77 @@ public class InvoicePluginDispatcher {
// We clone the original invoice so plugins don't remove/add items
final Invoice clonedInvoice = (Invoice) ((DefaultInvoice) originalInvoice).clone();
for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
- final List<InvoiceItem> items = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.<PluginProperty>of(), callContext);
- if (items != null) {
- for (final InvoiceItem item : items) {
- validateInvoiceItemFromPlugin(item, invoicePlugin);
- additionalInvoiceItems.add(item);
+ final List<InvoiceItem> additionalInvoiceItemsForPlugin = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.<PluginProperty>of(), callContext);
+ if (additionalInvoiceItemsForPlugin != null) {
+ for (final InvoiceItem additionalInvoiceItem : additionalInvoiceItemsForPlugin) {
+ final InvoiceItem sanitizedInvoiceItem = validateAndSanitizeInvoiceItemFromPlugin(originalInvoice, additionalInvoiceItem, invoicePlugin);
+ additionalInvoiceItems.add(sanitizedInvoiceItem);
}
}
}
return additionalInvoiceItems;
}
- private void validateInvoiceItemFromPlugin(final InvoiceItem invoiceItem, final InvoicePluginApi invoicePlugin) throws InvoiceApiException {
- if (!ALLOWED_INVOICE_ITEM_TYPES.contains(invoiceItem.getInvoiceItemType())) {
- log.warn("Ignoring invoice item of type {} from InvoicePlugin {}: {}", invoiceItem.getInvoiceItemType(), invoicePlugin, invoiceItem);
- throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_TYPE_INVALID, invoiceItem.getInvoiceItemType());
+ private InvoiceItem validateAndSanitizeInvoiceItemFromPlugin(final Invoice originalInvoice, final InvoiceItem additionalInvoiceItem, final InvoicePluginApi invoicePlugin) throws InvoiceApiException {
+ final InvoiceItem existingItem = Iterables.<InvoiceItem>tryFind(originalInvoice.getInvoiceItems(),
+ new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem originalInvoiceItem) {
+ return originalInvoiceItem.getId().equals(additionalInvoiceItem.getId());
+ }
+ }).orNull();
+
+ if (!ALLOWED_INVOICE_ITEM_TYPES.contains(additionalInvoiceItem.getInvoiceItemType()) && existingItem == null) {
+ log.warn("Ignoring invoice item of type {} from InvoicePlugin {}: {}", additionalInvoiceItem.getInvoiceItemType(), invoicePlugin, additionalInvoiceItem);
+ throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_TYPE_INVALID, additionalInvoiceItem.getInvoiceItemType());
+ }
+
+ final UUID invoiceId = MoreObjects.firstNonNull(mutableField("invoiceId", existingItem != null ? existingItem.getInvoiceId() : null, additionalInvoiceItem.getInvoiceId(), invoicePlugin),
+ originalInvoice.getId());
+ return new InvoiceItemCatalogBase(additionalInvoiceItem.getId(),
+ mutableField("createdDate", existingItem != null ? existingItem.getCreatedDate() : null, additionalInvoiceItem.getCreatedDate(), invoicePlugin),
+ invoiceId,
+ immutableField("accountId", existingItem, existingItem != null ? existingItem.getAccountId() : null, additionalInvoiceItem.getAccountId(), invoicePlugin),
+ immutableField("bundleId", existingItem, existingItem != null ? existingItem.getBundleId() : null, additionalInvoiceItem.getBundleId(), invoicePlugin),
+ immutableField("subscriptionId", existingItem, existingItem != null ? existingItem.getSubscriptionId() : null, additionalInvoiceItem.getSubscriptionId(), invoicePlugin),
+ mutableField("description", existingItem != null ? existingItem.getDescription() : null, additionalInvoiceItem.getDescription(), invoicePlugin),
+ immutableField("planName", existingItem, existingItem != null ? existingItem.getPlanName() : null, additionalInvoiceItem.getPlanName(), invoicePlugin),
+ immutableField("phaseName", existingItem, existingItem != null ? existingItem.getPhaseName() : null, additionalInvoiceItem.getPhaseName(), invoicePlugin),
+ immutableField("usageName", existingItem, existingItem != null ? existingItem.getUsageName() : null, additionalInvoiceItem.getUsageName(), invoicePlugin),
+ mutableField("prettyPlanName", existingItem != null ? existingItem.getPrettyPlanName() : null, additionalInvoiceItem.getPrettyPlanName(), invoicePlugin),
+ mutableField("prettyPhaseName", existingItem != null ? existingItem.getPrettyPhaseName() : null, additionalInvoiceItem.getPrettyPhaseName(), invoicePlugin),
+ mutableField("prettyUsageName", existingItem != null ? existingItem.getPrettyUsageName() : null, additionalInvoiceItem.getPrettyUsageName(), invoicePlugin),
+ immutableField("startDate", existingItem, existingItem != null ? existingItem.getStartDate() : null, additionalInvoiceItem.getStartDate(), invoicePlugin),
+ immutableField("endDate", existingItem, existingItem != null ? existingItem.getEndDate() : null, additionalInvoiceItem.getEndDate(), invoicePlugin),
+ immutableField("amount", existingItem, existingItem != null ? existingItem.getAmount() : null, additionalInvoiceItem.getAmount(), invoicePlugin),
+ immutableField("rate", existingItem, existingItem != null ? existingItem.getRate() : null, additionalInvoiceItem.getRate(), invoicePlugin),
+ immutableField("currency", existingItem, existingItem != null ? existingItem.getCurrency() : null, additionalInvoiceItem.getCurrency(), invoicePlugin),
+ immutableField("linkedItemId", existingItem, existingItem != null ? existingItem.getLinkedItemId() : null, additionalInvoiceItem.getLinkedItemId(), invoicePlugin),
+ immutableField("quantity", existingItem, existingItem != null ? existingItem.getQuantity() : null, additionalInvoiceItem.getQuantity(), invoicePlugin),
+ mutableField("itemDetails", existingItem != null ? existingItem.getItemDetails() : null, additionalInvoiceItem.getItemDetails(), invoicePlugin),
+ immutableField("invoiceItemType", existingItem, existingItem != null ? existingItem.getInvoiceItemType() : null, additionalInvoiceItem.getInvoiceItemType(), invoicePlugin));
+ }
+
+ private <T> T mutableField(final String fieldName, @Nullable final T existingValue, @Nullable final T updatedValue, final InvoicePluginApi invoicePlugin) {
+ if (updatedValue != null) {
+ log.debug("Overriding mutable invoice item value from InvoicePlugin {} for fieldName='{}': existingValue='{}', updatedValue='{}'",
+ invoicePlugin, fieldName, existingValue, updatedValue);
+ return updatedValue;
+ } else {
+ return existingValue;
+ }
+ }
+
+ private <T> T immutableField(final String fieldName, @Nullable final InvoiceItem existingItem, @Nullable final T existingValue, @Nullable final T updatedValue, final InvoicePluginApi invoicePlugin) {
+ if (existingItem == null) {
+ return updatedValue;
+ }
+
+ if (updatedValue != null && !updatedValue.equals(existingValue)) {
+ log.warn("Ignoring immutable invoice item value from InvoicePlugin {} for fieldName='{}': existingValue='{}', updatedValue='{}'",
+ invoicePlugin, fieldName, existingValue, updatedValue);
}
+ return existingValue;
}
private Map<String, InvoicePluginApi> getInvoicePlugins(final InternalTenantContext tenantContext) {