Skip to content

Commit

Permalink
invoice: Invoice Plugin Api Enhancements - initial changes
Browse files Browse the repository at this point in the history
See #893.

Signed-off-by: Pierre-Alexandre Meyer <pierre@mouraf.org>
  • Loading branch information
pierre committed Mar 20, 2018
1 parent 5b75959 commit 71735bb
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 21 deletions.
Expand Up @@ -45,8 +45,12 @@
import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
import org.killbill.billing.invoice.model.TaxInvoiceItem;
import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier;
import org.killbill.billing.invoice.plugin.api.InvoiceContext;
import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
import org.killbill.billing.invoice.plugin.api.InvoicePluginApiRetryException;
import org.killbill.billing.invoice.plugin.api.OnFailureInvoiceResult;
import org.killbill.billing.invoice.plugin.api.OnSuccessInvoiceResult;
import org.killbill.billing.invoice.plugin.api.PriorInvoiceResult;
import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.payment.api.PluginProperty;
Expand Down Expand Up @@ -401,6 +405,11 @@ public class TestInvoicePluginApi implements InvoicePluginApi {
boolean shouldThrowException = false;
InvoiceItem additionalInvoiceItem;

@Override
public PriorInvoiceResult priorCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
return null;
}

@Override
public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable<PluginProperty> pluginProperties, final CallContext callContext) {
if (shouldThrowException) {
Expand All @@ -412,6 +421,16 @@ public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final
}
}

@Override
public OnSuccessInvoiceResult onSuccessCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
return null;
}

@Override
public OnFailureInvoiceResult onFailureCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
return null;
}

private InvoiceItem createTaxInvoiceItem(final Invoice invoice) {
return new TaxInvoiceItem(invoice.getId(), invoice.getAccountId(), null, "Tax Item", clock.getUTCNow().toLocalDate(), BigDecimal.ONE, invoice.getCurrency());
}
Expand Down
Expand Up @@ -40,7 +40,11 @@
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.invoice.model.TaxInvoiceItem;
import org.killbill.billing.invoice.plugin.api.InvoiceContext;
import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
import org.killbill.billing.invoice.plugin.api.OnFailureInvoiceResult;
import org.killbill.billing.invoice.plugin.api.OnSuccessInvoiceResult;
import org.killbill.billing.invoice.plugin.api.PriorInvoiceResult;
import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.payment.api.PluginProperty;
Expand Down Expand Up @@ -292,23 +296,37 @@ public TestInvoicePluginApi() {
taxItems = new ArrayList<TaxInvoiceItem>();
}

@Override
public PriorInvoiceResult priorCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
return null;
}

@Override
public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable<PluginProperty> pluginProperties, final CallContext callContext) {
final List<InvoiceItem> result = new ArrayList<InvoiceItem>();
for (TaxInvoiceItem item : taxItems) {
for (final TaxInvoiceItem item : taxItems) {
result.add(new TaxInvoiceItem(item.getId(), invoice.getId(), invoice.getAccountId(), item.getBundleId(), "Tax Item", item.getStartDate(), item.getAmount(), invoice.getCurrency()));
}
taxItems.clear();
return result;
}

@Override
public OnSuccessInvoiceResult onSuccessCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
return null;
}

@Override
public OnFailureInvoiceResult onFailureCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
return null;
}

public void reset() {
taxItems.clear();
}

public void addTaxItem(final TaxInvoiceItem item) {
taxItems.add(item);
}

}
}
Expand Up @@ -509,11 +509,17 @@ private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId,
final List<Invoice> existingInvoices,
final boolean isDryRun,
final InternalCallContext internalCallContext) throws InvoiceApiException {
final boolean isRescheduled = false; // TODO

final CallContext callContext = buildCallContext(internalCallContext);
invoicePluginDispatcher.priorCall(targetDate, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);

final ImmutableAccountData account;
try {
account = accountApi.getImmutableAccountDataById(accountId, internalCallContext);
} catch (final AccountApiException e) {
log.error("Unable to generate invoice for accountId='{}', a future notification has NOT been recorded", accountId, e);
invoicePluginDispatcher.onFailureCall(targetDate, null, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
return null;
}

Expand All @@ -525,6 +531,8 @@ private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId,

// If invoice comes back null, there is nothing new to generate, we can bail early
if (invoice == null) {
invoicePluginDispatcher.onSuccessCall(targetDate, null, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);

if (isDryRun) {
log.info("Generated null dryRun invoice for accountId='{}', targetDate='{}'", accountId, targetDate);
} else {
Expand All @@ -551,15 +559,13 @@ private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId,
//
// Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice
//
final CallContext callContext = buildCallContext(internalCallContext);
final List<InvoiceItem> additionalInvoiceItemsFromPlugins = invoicePluginDispatcher.getAdditionalInvoiceItems(tmpInvoiceForInvoicePlugins, isDryRun, callContext, internalCallContext);
if (additionalInvoiceItemsFromPlugins.isEmpty()) {
// PERF: avoid re-computing the CBA if no change was made
if (cbaItemPreInvoicePlugins != null) {
invoice.addInvoiceItem(cbaItemPreInvoicePlugins);
}
} else {

// Add or update items from generated invoice
for (final InvoiceItem cur : additionalInvoiceItemsFromPlugins) {
final InvoiceItem exitingItem = Iterables.tryFind(tmpInvoiceForInvoicePlugins.getInvoiceItems(), new Predicate<InvoiceItem>() {
Expand Down Expand Up @@ -605,7 +611,6 @@ public boolean apply(final InvoiceItem input) {
}

if (!isDryRun) {

// Compute whether this is a new invoice object (or just some adjustments on an existing invoice), and extract invoiceIds for later use
final Set<UUID> uniqueInvoiceIds = getUniqueInvoiceIds(invoice);
final boolean isRealInvoiceWithItems = uniqueInvoiceIds.remove(invoice.getId());
Expand Down Expand Up @@ -634,6 +639,13 @@ public boolean apply(final InvoiceItem input) {
if (!isDryRun && !success) {
commitInvoiceAndSetFutureNotifications(account, null, futureAccountNotifications, internalCallContext);
}

if (success) {
final DefaultInvoice refreshedInvoice = new DefaultInvoice(invoiceDao.getById(invoice.getId(), internalCallContext));
invoicePluginDispatcher.onSuccessCall(targetDate, refreshedInvoice, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
} else {
invoicePluginDispatcher.onFailureCall(targetDate, invoice, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
}
}

return invoice;
Expand Down
@@ -1,6 +1,6 @@
/*
* Copyright 2014-2015 Groupon, Inc
* Copyright 2014-2015 The Billing Project, LLC
* Copyright 2014-2018 Groupon, Inc
* Copyright 2014-2018 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
Expand All @@ -25,13 +25,16 @@

import javax.inject.Inject;

import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.invoice.api.DefaultInvoiceContext;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.model.DefaultInvoice;
import org.killbill.billing.invoice.plugin.api.InvoiceContext;
import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.payment.api.PluginProperty;
Expand All @@ -48,30 +51,92 @@ public class InvoicePluginDispatcher {
private static final Logger log = LoggerFactory.getLogger(InvoicePluginDispatcher.class);

public static final Collection<InvoiceItemType> ALLOWED_INVOICE_ITEM_TYPES = ImmutableList.<InvoiceItemType>of(InvoiceItemType.EXTERNAL_CHARGE,
InvoiceItemType.ITEM_ADJ,
InvoiceItemType.CREDIT_ADJ,
InvoiceItemType.TAX);
InvoiceItemType.ITEM_ADJ,
InvoiceItemType.CREDIT_ADJ,
InvoiceItemType.TAX);

private final OSGIServiceRegistration<InvoicePluginApi> pluginRegistry;
private final InvoiceConfig invoiceConfig;


@Inject
public InvoicePluginDispatcher(final OSGIServiceRegistration<InvoicePluginApi> pluginRegistry,
final InvoiceConfig invoiceConfig) {
this.pluginRegistry = pluginRegistry;
this.invoiceConfig = invoiceConfig;
}

public void priorCall(final LocalDate targetDate, final List<Invoice> existingInvoices, final boolean isDryRun, final boolean isRescheduled, final CallContext callContext, final InternalTenantContext internalTenantContext) {
final List<InvoicePluginApi> invoicePlugins = getInvoicePlugins(internalTenantContext);
if (invoicePlugins.isEmpty()) {
return;
}

final InvoiceContext invoiceContext = new DefaultInvoiceContext(targetDate, null, existingInvoices, isDryRun, isRescheduled, callContext);
for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
invoicePlugin.priorCall(invoiceContext, ImmutableList.<PluginProperty>of());
}
}

public void onSuccessCall(final LocalDate targetDate,
final DefaultInvoice invoice,
final List<Invoice> existingInvoices,
final boolean isDryRun,
final boolean isRescheduled,
final CallContext callContext,
final InternalTenantContext internalTenantContext) {
onCompletionCall(true, targetDate, invoice, existingInvoices, isDryRun, isRescheduled, callContext, internalTenantContext);
}

public void onFailureCall(final LocalDate targetDate,
final DefaultInvoice invoice,
final List<Invoice> existingInvoices,
final boolean isDryRun,
final boolean isRescheduled,
final CallContext callContext,
final InternalTenantContext internalTenantContext) {
onCompletionCall(false, targetDate, invoice, existingInvoices, isDryRun, isRescheduled, callContext, internalTenantContext);
}

private void onCompletionCall(final boolean isSuccess,
final LocalDate targetDate,
final DefaultInvoice originalInvoice,
final List<Invoice> existingInvoices,
final boolean isDryRun,
final boolean isRescheduled,
final CallContext callContext,
final InternalTenantContext internalTenantContext) {
final List<InvoicePluginApi> invoicePlugins = getInvoicePlugins(internalTenantContext);
if (invoicePlugins.isEmpty()) {
return;
}

// We clone the original invoice so plugins don't remove/add items
final Invoice clonedInvoice = (Invoice) originalInvoice.clone();
final InvoiceContext invoiceContext = new DefaultInvoiceContext(targetDate, clonedInvoice, existingInvoices, isDryRun, isRescheduled, callContext);

for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
if (isSuccess) {
invoicePlugin.onSuccessCall(invoiceContext, ImmutableList.<PluginProperty>of());
} else {
invoicePlugin.onFailureCall(invoiceContext, ImmutableList.<PluginProperty>of());
}
}
}

//
// If we have multiple plugins there is a question of plugin ordering and also a 'product' questions to decide whether
// subsequent plugins should have access to items added by previous plugins
//
public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice originalInvoice, final boolean isDryRun, final CallContext callContext, final InternalTenantContext tenantContext) throws InvoiceApiException {
// We clone the original invoice so plugins don't remove/add items
final Invoice clonedInvoice = (Invoice) ((DefaultInvoice) originalInvoice).clone();
final List<InvoiceItem> additionalInvoiceItems = new LinkedList<InvoiceItem>();

final List<InvoicePluginApi> invoicePlugins = getInvoicePlugins(tenantContext);
if (invoicePlugins.isEmpty()) {
return additionalInvoiceItems;
}

// 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) {
Expand All @@ -93,7 +158,6 @@ private void validateInvoiceItemFromPlugin(final InvoiceItem invoiceItem, final

private List<InvoicePluginApi> getInvoicePlugins(final InternalTenantContext tenantContext) {


final Collection<String> resultingPluginList = getResultingPluginNameList(tenantContext);

final List<InvoicePluginApi> invoicePlugins = new ArrayList<InvoicePluginApi>();
Expand All @@ -112,7 +176,7 @@ final Collection<String> getResultingPluginNameList(final InternalTenantContext
if (configuredPlugins == null || configuredPlugins.isEmpty()) {
return registeredPlugins;
} else {
final List<String> result = new ArrayList<String>(configuredPlugins.size());
final List<String> result = new ArrayList<String>(configuredPlugins.size());
for (final String name : configuredPlugins) {
if (pluginRegistry.getServiceForName(name) != null) {
result.add(name);
Expand Down

0 comments on commit 71735bb

Please sign in to comment.