Skip to content

Commit

Permalink
invoice: code review integration
Browse files Browse the repository at this point in the history
See #934 (comment).

Signed-off-by: Pierre-Alexandre Meyer <pierre@mouraf.org>
  • Loading branch information
pierre committed Apr 4, 2018
1 parent 6d5af5e commit 15964b0
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 63 deletions.
Expand Up @@ -75,7 +75,6 @@
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates; import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates.UsageDef; import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates.UsageDef;
import org.killbill.billing.invoice.model.DefaultInvoice; import org.killbill.billing.invoice.model.DefaultInvoice;
import org.killbill.billing.invoice.model.InvoiceItemCatalogBase;
import org.killbill.billing.invoice.model.InvoiceItemFactory; import org.killbill.billing.invoice.model.InvoiceItemFactory;
import org.killbill.billing.invoice.model.ItemAdjInvoiceItem; import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
import org.killbill.billing.invoice.model.ParentInvoiceItem; import org.killbill.billing.invoice.model.ParentInvoiceItem;
Expand Down Expand Up @@ -558,35 +557,20 @@ private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId,


boolean success = false; boolean success = false;
try { try {
// Generate missing credit (> 0 for generation and < 0 for use) prior we call the plugin // Generate missing credit (> 0 for generation and < 0 for use) prior we call the plugin(s)
final InvoiceItem cbaItemPreInvoicePlugins = computeCBAOnExistingInvoice(invoice, internalCallContext); final InvoiceItem cbaItemPreInvoicePlugins = computeCBAOnExistingInvoice(invoice, internalCallContext);
DefaultInvoice tmpInvoiceForInvoicePlugins = invoice;
if (cbaItemPreInvoicePlugins != null) { if (cbaItemPreInvoicePlugins != null) {
tmpInvoiceForInvoicePlugins = (DefaultInvoice) tmpInvoiceForInvoicePlugins.clone(); invoice.addInvoiceItem(cbaItemPreInvoicePlugins);
tmpInvoiceForInvoicePlugins.addInvoiceItem(cbaItemPreInvoicePlugins);
} }

// //
// Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice // Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice
// //
final List<InvoiceItem> additionalInvoiceItemsFromPlugins = invoicePluginDispatcher.getAdditionalInvoiceItems(tmpInvoiceForInvoicePlugins, isDryRun, callContext, internalCallContext); final boolean invoiceUpdated = invoicePluginDispatcher.updateOriginalInvoiceWithPluginInvoiceItems(invoice, isDryRun, callContext, internalCallContext);
if (additionalInvoiceItemsFromPlugins.isEmpty()) { if (invoiceUpdated) {
// PERF: avoid re-computing the CBA if no change was made // Remove the temporary CBA item as we need to re-compute CBA
if (cbaItemPreInvoicePlugins != null) { if (cbaItemPreInvoicePlugins != null) {
invoice.addInvoiceItem(cbaItemPreInvoicePlugins); invoice.removeInvoiceItem(cbaItemPreInvoicePlugins);
}
} else {
// Add or update items from generated invoice
for (final InvoiceItem cur : additionalInvoiceItemsFromPlugins) {
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 (existingItem != null) {
invoice.removeInvoiceItem(existingItem);
}
invoice.addInvoiceItem(cur);
} }


// Use credit after we call the plugin (https://github.com/killbill/killbill/issues/637) // Use credit after we call the plugin (https://github.com/killbill/killbill/issues/637)
Expand Down
Expand Up @@ -155,32 +155,54 @@ private void onCompletionCall(final boolean isSuccess,
} }
} }


// public boolean updateOriginalInvoiceWithPluginInvoiceItems(final DefaultInvoice originalInvoice, final boolean isDryRun, final CallContext callContext, final InternalTenantContext tenantContext) throws InvoiceApiException {
// 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 {
log.debug("Invoking invoice plugins getAdditionalInvoiceItems: isDryRun='{}', originalInvoice='{}'", isDryRun, originalInvoice); log.debug("Invoking invoice plugins getAdditionalInvoiceItems: isDryRun='{}', originalInvoice='{}'", isDryRun, originalInvoice);


final List<InvoiceItem> additionalInvoiceItems = new LinkedList<InvoiceItem>();

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


// We clone the original invoice so plugins don't remove/add items boolean invoiceUpdated = false;
final Invoice clonedInvoice = (Invoice) ((DefaultInvoice) originalInvoice).clone();
for (final InvoicePluginApi invoicePlugin : invoicePlugins) { for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
// We clone the original invoice so plugins don't remove/add items
final Invoice clonedInvoice = (Invoice) originalInvoice.clone();
final List<InvoiceItem> additionalInvoiceItemsForPlugin = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.<PluginProperty>of(), callContext); final List<InvoiceItem> additionalInvoiceItemsForPlugin = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.<PluginProperty>of(), callContext);
if (additionalInvoiceItemsForPlugin != null) {
if (additionalInvoiceItemsForPlugin != null && !additionalInvoiceItemsForPlugin.isEmpty()) {
final Collection<InvoiceItem> additionalInvoiceItems = new LinkedList<InvoiceItem>();
for (final InvoiceItem additionalInvoiceItem : additionalInvoiceItemsForPlugin) { for (final InvoiceItem additionalInvoiceItem : additionalInvoiceItemsForPlugin) {
final InvoiceItem sanitizedInvoiceItem = validateAndSanitizeInvoiceItemFromPlugin(originalInvoice, additionalInvoiceItem, invoicePlugin); final InvoiceItem sanitizedInvoiceItem = validateAndSanitizeInvoiceItemFromPlugin(originalInvoice, additionalInvoiceItem, invoicePlugin);
additionalInvoiceItems.add(sanitizedInvoiceItem); additionalInvoiceItems.add(sanitizedInvoiceItem);
} }
invoiceUpdated = invoiceUpdated || updateOriginalInvoiceWithPluginInvoiceItems(originalInvoice, additionalInvoiceItems);
} }
} }
return additionalInvoiceItems;
return invoiceUpdated;
}

private boolean updateOriginalInvoiceWithPluginInvoiceItems(final DefaultInvoice originalInvoice, final Collection<InvoiceItem> additionalInvoiceItems) {
if (additionalInvoiceItems.isEmpty()) {
return false;
}

// Add or update items from generated invoice
for (final InvoiceItem additionalInvoiceItem : additionalInvoiceItems) {
final InvoiceItem existingItem = Iterables.tryFind(originalInvoice.getInvoiceItems(),
new Predicate<InvoiceItem>() {
@Override
public boolean apply(final InvoiceItem originalInvoiceItem) {
return originalInvoiceItem.getId().equals(additionalInvoiceItem.getId());
}
}).orNull();
if (existingItem != null) {
originalInvoice.removeInvoiceItem(existingItem);
}
originalInvoice.addInvoiceItem(additionalInvoiceItem);
}

return true;
} }


private InvoiceItem validateAndSanitizeInvoiceItemFromPlugin(final Invoice originalInvoice, final InvoiceItem additionalInvoiceItem, final InvoicePluginApi invoicePlugin) throws InvoiceApiException { private InvoiceItem validateAndSanitizeInvoiceItemFromPlugin(final Invoice originalInvoice, final InvoiceItem additionalInvoiceItem, final InvoicePluginApi invoicePlugin) throws InvoiceApiException {
Expand Down
Expand Up @@ -92,18 +92,17 @@ public List<InvoiceItem> dispatchToInvoicePluginsAndInsertItems(final UUID accou


boolean success = false; boolean success = false;
GlobalLock lock = null; GlobalLock lock = null;
Iterable<Invoice> invoicesForPlugins = null; Iterable<DefaultInvoice> invoicesForPlugins = null;
try { try {
lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), invoiceConfig.getMaxGlobalLockRetries()); lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), invoiceConfig.getMaxGlobalLockRetries());


invoicesForPlugins = withAccountLock.prepareInvoices(); invoicesForPlugins = withAccountLock.prepareInvoices();


final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(accountId, context); final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(accountId, context);
final List<InvoiceModelDao> invoiceModelDaos = new LinkedList<InvoiceModelDao>(); final List<InvoiceModelDao> invoiceModelDaos = new LinkedList<InvoiceModelDao>();
for (final Invoice invoiceForPlugin : invoicesForPlugins) { for (final DefaultInvoice invoiceForPlugin : invoicesForPlugins) {
// Call plugin // Call plugin(s)
final List<InvoiceItem> additionalInvoiceItems = invoicePluginDispatcher.getAdditionalInvoiceItems(invoiceForPlugin, isDryRun, context, internalCallContext); invoicePluginDispatcher.updateOriginalInvoiceWithPluginInvoiceItems(invoiceForPlugin, isDryRun, context, internalCallContext);
invoiceForPlugin.addInvoiceItems(additionalInvoiceItems);


// Transformation to InvoiceModelDao // Transformation to InvoiceModelDao
final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoiceForPlugin); final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoiceForPlugin);
Expand Down
@@ -1,6 +1,6 @@
/* /*
* Copyright 2015 Groupon, Inc * Copyright 2014-2018 Groupon, Inc
* Copyright 2015 The Billing Project, LLC * Copyright 2014-2018 The Billing Project, LLC
* *
* The Billing Project licenses this file to you under the Apache License, version 2.0 * 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 * (the "License"); you may not use this file except in compliance with the
Expand All @@ -17,7 +17,9 @@


package org.killbill.billing.invoice.api; package org.killbill.billing.invoice.api;


import org.killbill.billing.invoice.model.DefaultInvoice;

public interface WithAccountLock { public interface WithAccountLock {


public Iterable<Invoice> prepareInvoices() throws InvoiceApiException; public Iterable<DefaultInvoice> prepareInvoices() throws InvoiceApiException;
} }
@@ -1,7 +1,7 @@
/* /*
* Copyright 2010-2013 Ning, Inc. * Copyright 2010-2013 Ning, Inc.
* Copyright 2014-2016 Groupon, Inc * Copyright 2014-2018 Groupon, Inc
* Copyright 2014-2016 The Billing Project, LLC * Copyright 2014-2018 The Billing Project, LLC
* *
* The Billing Project licenses this file to you under the Apache License, version 2.0 * 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 * (the "License"); you may not use this file except in compliance with the
Expand Down Expand Up @@ -36,7 +36,6 @@
import org.killbill.billing.invoice.api.InvoiceApiException; import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceApiHelper; import org.killbill.billing.invoice.api.InvoiceApiHelper;
import org.killbill.billing.invoice.api.InvoiceInternalApi; 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.InvoicePayment;
import org.killbill.billing.invoice.api.InvoicePaymentType; import org.killbill.billing.invoice.api.InvoicePaymentType;
import org.killbill.billing.invoice.api.InvoiceStatus; import org.killbill.billing.invoice.api.InvoiceStatus;
Expand Down Expand Up @@ -76,6 +75,10 @@ public DefaultInvoiceInternalApi(final InvoiceDao dao,


@Override @Override
public Invoice getInvoiceById(final UUID invoiceId, final InternalTenantContext context) throws InvoiceApiException { public Invoice getInvoiceById(final UUID invoiceId, final InternalTenantContext context) throws InvoiceApiException {
return getInvoiceByIdInternal(invoiceId, context);
}

private DefaultInvoice getInvoiceByIdInternal(final UUID invoiceId, final InternalTenantContext context) {
return new DefaultInvoice(dao.getById(invoiceId, context)); return new DefaultInvoice(dao.getById(invoiceId, context));
} }


Expand Down Expand Up @@ -132,12 +135,12 @@ public InvoicePayment recordRefund(final UUID paymentId, final BigDecimal amount


// See https://github.com/killbill/killbill/issues/265 // See https://github.com/killbill/killbill/issues/265
final CallContext callContext = internalCallContextFactory.createCallContext(context); final CallContext callContext = internalCallContextFactory.createCallContext(context);
final Invoice invoice = getInvoiceById(refund.getInvoiceId(), context); final DefaultInvoice invoice = getInvoiceByIdInternal(refund.getInvoiceId(), context);
final UUID accountId = invoice.getAccountId(); final UUID accountId = invoice.getAccountId();
final WithAccountLock withAccountLock = new WithAccountLock() { final WithAccountLock withAccountLock = new WithAccountLock() {
@Override @Override
public Iterable<Invoice> prepareInvoices() throws InvoiceApiException { public Iterable<DefaultInvoice> prepareInvoices() throws InvoiceApiException {
return ImmutableList.<Invoice>of(invoice); return ImmutableList.<DefaultInvoice>of(invoice);
} }
}; };
invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, callContext); invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, callContext);
Expand Down
Expand Up @@ -204,6 +204,10 @@ public BigDecimal getAccountCBA(final UUID accountId, final TenantContext contex


@Override @Override
public Invoice getInvoice(final UUID invoiceId, final TenantContext context) throws InvoiceApiException { public Invoice getInvoice(final UUID invoiceId, final TenantContext context) throws InvoiceApiException {
return getInvoiceInternal(invoiceId, context);
}

private DefaultInvoice getInvoiceInternal(final UUID invoiceId, final TenantContext context) {
final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(invoiceId, ObjectType.INVOICE, context); final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(invoiceId, ObjectType.INVOICE, context);
return new DefaultInvoice(dao.getById(invoiceId, internalTenantContext), getCatalogSafelyForPrettyNames(internalTenantContext)); return new DefaultInvoice(dao.getById(invoiceId, internalTenantContext), getCatalogSafelyForPrettyNames(internalTenantContext));
} }
Expand Down Expand Up @@ -285,13 +289,13 @@ public List<InvoiceItem> insertExternalCharges(final UUID accountId, final Local
final WithAccountLock withAccountLock = new WithAccountLock() { final WithAccountLock withAccountLock = new WithAccountLock() {


@Override @Override
public Iterable<Invoice> prepareInvoices() throws InvoiceApiException { public Iterable<DefaultInvoice> prepareInvoices() throws InvoiceApiException {
final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, context); final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, context);
final LocalDate invoiceDate = internalTenantContext.toLocalDate(context.getCreatedDate()); final LocalDate invoiceDate = internalTenantContext.toLocalDate(context.getCreatedDate());


// Group all new external charges on the same invoice (per currency) // Group all new external charges on the same invoice (per currency)
final Map<Currency, Invoice> newInvoicesForExternalCharges = new HashMap<Currency, Invoice>(); final Map<Currency, DefaultInvoice> newInvoicesForExternalCharges = new HashMap<Currency, DefaultInvoice>();
final Map<UUID, Invoice> existingInvoicesForExternalCharges = new HashMap<UUID, Invoice>(); final Map<UUID, DefaultInvoice> existingInvoicesForExternalCharges = new HashMap<UUID, DefaultInvoice>();


for (final InvoiceItem charge : charges) { for (final InvoiceItem charge : charges) {
final Invoice invoiceForExternalCharge; final Invoice invoiceForExternalCharge;
Expand All @@ -301,13 +305,13 @@ public Iterable<Invoice> prepareInvoices() throws InvoiceApiException {
final Currency currency = charge.getCurrency(); final Currency currency = charge.getCurrency();
if (newInvoicesForExternalCharges.get(currency) == null) { if (newInvoicesForExternalCharges.get(currency) == null) {
final InvoiceStatus status = autoCommit ? InvoiceStatus.COMMITTED : InvoiceStatus.DRAFT; final InvoiceStatus status = autoCommit ? InvoiceStatus.COMMITTED : InvoiceStatus.DRAFT;
final Invoice newInvoiceForExternalCharge = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status); final DefaultInvoice newInvoiceForExternalCharge = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status);
newInvoicesForExternalCharges.put(currency, newInvoiceForExternalCharge); newInvoicesForExternalCharges.put(currency, newInvoiceForExternalCharge);
} }
invoiceForExternalCharge = newInvoicesForExternalCharges.get(currency); invoiceForExternalCharge = newInvoicesForExternalCharges.get(currency);
} else { } else {
if (existingInvoicesForExternalCharges.get(invoiceIdForExternalCharge) == null) { if (existingInvoicesForExternalCharges.get(invoiceIdForExternalCharge) == null) {
final Invoice existingInvoiceForExternalCharge = getInvoice(invoiceIdForExternalCharge, context); final DefaultInvoice existingInvoiceForExternalCharge = getInvoiceInternal(invoiceIdForExternalCharge, context);
if (InvoiceStatus.COMMITTED.equals(existingInvoiceForExternalCharge.getStatus())) { if (InvoiceStatus.COMMITTED.equals(existingInvoiceForExternalCharge.getStatus())) {
throw new InvoiceApiException(ErrorCode.INVOICE_ALREADY_COMMITTED, existingInvoiceForExternalCharge.getId()); throw new InvoiceApiException(ErrorCode.INVOICE_ALREADY_COMMITTED, existingInvoiceForExternalCharge.getId());
} }
Expand Down Expand Up @@ -341,7 +345,7 @@ public Iterable<Invoice> prepareInvoices() throws InvoiceApiException {
invoiceForExternalCharge.addInvoiceItem(externalCharge); invoiceForExternalCharge.addInvoiceItem(externalCharge);
} }


return Iterables.<Invoice>concat(newInvoicesForExternalCharges.values(), existingInvoicesForExternalCharges.values()); return Iterables.<DefaultInvoice>concat(newInvoicesForExternalCharges.values(), existingInvoicesForExternalCharges.values());
} }
}; };


Expand Down Expand Up @@ -382,12 +386,12 @@ private InvoiceItem insertCreditForInvoice(final UUID accountId, final UUID invo
private InvoiceItem creditItem; private InvoiceItem creditItem;


@Override @Override
public List<Invoice> prepareInvoices() throws InvoiceApiException { public List<DefaultInvoice> prepareInvoices() throws InvoiceApiException {
final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, context); final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, context);
final LocalDate invoiceDate = internalTenantContext.toLocalDate(context.getCreatedDate()); final LocalDate invoiceDate = internalTenantContext.toLocalDate(context.getCreatedDate());


// Create an invoice for that credit if it doesn't exist // Create an invoice for that credit if it doesn't exist
final Invoice invoiceForCredit; final DefaultInvoice invoiceForCredit;
if (invoiceId == null) { if (invoiceId == null) {
final InvoiceStatus status = autoCommit ? InvoiceStatus.COMMITTED : InvoiceStatus.DRAFT; final InvoiceStatus status = autoCommit ? InvoiceStatus.COMMITTED : InvoiceStatus.DRAFT;
invoiceForCredit = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status); invoiceForCredit = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status);
Expand All @@ -411,7 +415,7 @@ public List<Invoice> prepareInvoices() throws InvoiceApiException {
itemDetails); itemDetails);
invoiceForCredit.addInvoiceItem(creditItem); invoiceForCredit.addInvoiceItem(creditItem);


return ImmutableList.<Invoice>of(invoiceForCredit); return ImmutableList.<DefaultInvoice>of(invoiceForCredit);
} }
}; };


Expand Down Expand Up @@ -443,8 +447,8 @@ public InvoiceItem insertInvoiceItemAdjustment(final UUID accountId, final UUID


final WithAccountLock withAccountLock = new WithAccountLock() { final WithAccountLock withAccountLock = new WithAccountLock() {
@Override @Override
public Iterable<Invoice> prepareInvoices() throws InvoiceApiException { public Iterable<DefaultInvoice> prepareInvoices() throws InvoiceApiException {
final Invoice invoice = getInvoiceAndCheckCurrency(invoiceId, currency, context); final DefaultInvoice invoice = getInvoiceAndCheckCurrency(invoiceId, currency, context);
final InvoiceItem adjustmentItem = invoiceApiHelper.createAdjustmentItem(invoice, final InvoiceItem adjustmentItem = invoiceApiHelper.createAdjustmentItem(invoice,
invoiceItemId, invoiceItemId,
amount, amount,
Expand All @@ -457,7 +461,7 @@ public Iterable<Invoice> prepareInvoices() throws InvoiceApiException {
invoice.addInvoiceItem(adjustmentItem); invoice.addInvoiceItem(adjustmentItem);
} }


return ImmutableList.<Invoice>of(invoice); return ImmutableList.<DefaultInvoice>of(invoice);
} }
}; };


Expand Down Expand Up @@ -560,8 +564,8 @@ public Invoice apply(final InvoiceModelDao input) {
})); }));
} }


private Invoice getInvoiceAndCheckCurrency(final UUID invoiceId, @Nullable final Currency currency, final TenantContext context) throws InvoiceApiException { private DefaultInvoice getInvoiceAndCheckCurrency(final UUID invoiceId, @Nullable final Currency currency, final TenantContext context) throws InvoiceApiException {
final Invoice invoice = getInvoice(invoiceId, context); final DefaultInvoice invoice = getInvoiceInternal(invoiceId, context);
// Check the specified currency matches the one of the existing invoice // Check the specified currency matches the one of the existing invoice
if (currency != null && invoice.getCurrency() != currency) { if (currency != null && invoice.getCurrency() != currency) {
throw new InvoiceApiException(ErrorCode.CURRENCY_INVALID, currency, invoice.getCurrency()); throw new InvoiceApiException(ErrorCode.CURRENCY_INVALID, currency, invoice.getCurrency());
Expand Down

0 comments on commit 15964b0

Please sign in to comment.