/*
* Copyright 2010-2013 Ning, Inc.
* Copyright 2014-2017 Groupon, Inc
* Copyright 2014-2017 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.dao;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
import org.killbill.billing.util.tag.ControlTagType;
import org.killbill.billing.util.tag.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
public class InvoiceDaoHelper {
private static final Logger log = LoggerFactory.getLogger(InvoiceDaoHelper.class);
private final InternalCallContextFactory internalCallContextFactory;
@Inject
public InvoiceDaoHelper(final InternalCallContextFactory internalCallContextFactory) {
this.internalCallContextFactory = internalCallContextFactory;
}
/**
* Find amounts to adjust for individual items, if not specified.
* The user gives us a list of items to adjust associated with a given amount (how much to refund per invoice item).
* In case of full adjustments, the amount can be null: in this case, we retrieve the original amount for the invoice
* item.
*
* @param invoiceId original invoice id
* @param entitySqlDaoWrapperFactory the EntitySqlDaoWrapperFactory from the current transaction
* @param invoiceItemIdsWithNullAmounts the original mapping between invoice item ids and amount to refund (contains null)
* @param context the tenant callcontext
* @return the final mapping between invoice item ids and amount to refund
* @throws org.killbill.billing.invoice.api.InvoiceApiException
*/
public Map<UUID, BigDecimal> computeItemAdjustments(final String invoiceId,
final List<Tag> invoicesTags,
final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
final Map<UUID, BigDecimal> invoiceItemIdsWithNullAmounts,
final InternalTenantContext context) throws InvoiceApiException {
// Populate the missing amounts for individual items, if needed
final Map<UUID, BigDecimal> outputItemIdsWithAmounts = new HashMap<UUID, BigDecimal>();
// Retrieve invoice before the Refund
final InvoiceModelDao invoice = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class).getById(invoiceId, context);
if (invoice != null) {
populateChildren(invoice, invoicesTags, entitySqlDaoWrapperFactory, context);
} else {
throw new IllegalStateException("Invoice shouldn't be null for id " + invoiceId);
}
//
// If we have an item amount, we 'd like to use it, but we need to check first that it is lesser or equal than maximum allowed
//If, not we compute maximum value we can adjust per item
for (final UUID invoiceItemId : invoiceItemIdsWithNullAmounts.keySet()) {
final List<InvoiceItemModelDao> adjustedOrRepairedItems = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class).getAdjustedOrRepairedInvoiceItemsByLinkedId(invoiceItemId.toString(), context);
computeItemAdjustmentsForTargetInvoiceItem(getInvoiceItemForId(invoice, invoiceItemId), adjustedOrRepairedItems, invoiceItemIdsWithNullAmounts, outputItemIdsWithAmounts);
}
return outputItemIdsWithAmounts;
}
private static void computeItemAdjustmentsForTargetInvoiceItem(final InvoiceItemModelDao targetInvoiceItem, final List<InvoiceItemModelDao> adjustedOrRepairedItems, final Map<UUID, BigDecimal> inputAdjInvoiceItem, final Map<UUID, BigDecimal> outputAdjInvoiceItem) throws InvoiceApiException {
final BigDecimal originalItemAmount = targetInvoiceItem.getAmount();
final BigDecimal maxAdjLeftAmount = computeItemAdjustmentAmount(originalItemAmount, adjustedOrRepairedItems);
final BigDecimal proposedItemAmount = inputAdjInvoiceItem.get(targetInvoiceItem.getId());
if (proposedItemAmount != null && proposedItemAmount.compareTo(maxAdjLeftAmount) > 0) {
throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_ADJUSTMENT_AMOUNT_INVALID, proposedItemAmount, maxAdjLeftAmount);
}
final BigDecimal itemAmountToAdjust = MoreObjects.firstNonNull(proposedItemAmount, maxAdjLeftAmount);
if (itemAmountToAdjust.compareTo(BigDecimal.ZERO) > 0) {
outputAdjInvoiceItem.put(targetInvoiceItem.getId(), itemAmountToAdjust);
}
}
/**
* @param requestedPositiveAmountToAdjust amount we are adjusting for that item
* @param adjustedOrRepairedItems list of all adjusted or repaired linking to this item
* @return the amount we should really adjust based on whether or not the item got repaired
*/
private static BigDecimal computeItemAdjustmentAmount(final BigDecimal requestedPositiveAmountToAdjust, final List<InvoiceItemModelDao> adjustedOrRepairedItems) {
BigDecimal positiveAdjustedOrRepairedAmount = BigDecimal.ZERO;
for (final InvoiceItemModelDao cur : adjustedOrRepairedItems) {
// Adjustment or repair items are negative so we negate to make it positive
positiveAdjustedOrRepairedAmount = positiveAdjustedOrRepairedAmount.add(cur.getAmount().negate());
}
return (positiveAdjustedOrRepairedAmount.compareTo(requestedPositiveAmountToAdjust) >= 0) ? BigDecimal.ZERO : requestedPositiveAmountToAdjust.subtract(positiveAdjustedOrRepairedAmount);
}
private InvoiceItemModelDao getInvoiceItemForId(final InvoiceModelDao invoice, final UUID invoiceItemId) throws InvoiceApiException {
for (final InvoiceItemModelDao invoiceItem : invoice.getInvoiceItems()) {
if (invoiceItem.getId().equals(invoiceItemId)) {
return invoiceItem;
}
}
throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, invoiceItemId);
}
public BigDecimal computePositiveRefundAmount(final InvoicePaymentModelDao payment, final BigDecimal requestedRefundAmount, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts) throws InvoiceApiException {
final BigDecimal maxRefundAmount = payment.getAmount() == null ? BigDecimal.ZERO : payment.getAmount();
final BigDecimal requestedPositiveAmount = requestedRefundAmount == null ? maxRefundAmount : requestedRefundAmount;
// This check is good but not enough, we need to also take into account previous refunds
// (But that should have been checked in the payment call already)
if (requestedPositiveAmount.compareTo(maxRefundAmount) > 0) {
throw new InvoiceApiException(ErrorCode.REFUND_AMOUNT_TOO_HIGH, requestedPositiveAmount, maxRefundAmount);
}
// Verify if the requested amount matches the invoice items to adjust, if specified
BigDecimal amountFromItems = BigDecimal.ZERO;
for (final BigDecimal itemAmount : invoiceItemIdsWithAmounts.values()) {
amountFromItems = amountFromItems.add(itemAmount);
}
// Sanity check: if some items were specified, then the sum should be equal to specified refund amount, if specified
if (amountFromItems.compareTo(BigDecimal.ZERO) != 0 && requestedPositiveAmount.compareTo(amountFromItems) < 0) {
throw new InvoiceApiException(ErrorCode.REFUND_AMOUNT_DONT_MATCH_ITEMS_TO_ADJUST, requestedPositiveAmount, amountFromItems);
}
return requestedPositiveAmount;
}
public List<InvoiceModelDao> getUnpaidInvoicesByAccountFromTransaction(final UUID accountId, final List<Tag> invoicesTags, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final LocalDate upToDate, final InternalTenantContext context) {
final List<InvoiceModelDao> invoices = getAllInvoicesByAccountFromTransaction(invoicesTags, entitySqlDaoWrapperFactory, context);
log.debug("Found invoices={} for accountId={}", invoices, accountId);
return getUnpaidInvoicesByAccountFromTransaction(invoices, upToDate);
}
public List<InvoiceModelDao> getUnpaidInvoicesByAccountFromTransaction(final List<InvoiceModelDao> invoices, @Nullable final LocalDate upToDate) {
final Collection<InvoiceModelDao> unpaidInvoices = Collections2.filter(invoices, new Predicate<InvoiceModelDao>() {
@Override
public boolean apply(final InvoiceModelDao in) {
final InvoiceModelDao invoice = (in.getParentInvoice() == null) ? in : in.getParentInvoice();
final BigDecimal balance = InvoiceModelDaoHelper.getBalance(invoice);
log.debug("Computed balance={} for invoice={}", balance, in);
return InvoiceStatus.COMMITTED.equals(in.getStatus()) && (balance.compareTo(BigDecimal.ZERO) >= 1) &&
(upToDate == null || in.getTargetDate() == null || !in.getTargetDate().isAfter(upToDate));
}
});
return new ArrayList<InvoiceModelDao>(unpaidInvoices);
}
/**
* Create an adjustment for a given invoice item. This just creates the object in memory, it doesn't write it to disk.
*
* @param invoiceId the invoice id
* @param invoiceItemId the invoice item id to adjust
* @param effectiveDate adjustment effective date, in the account timezone
* @param positiveAdjAmount the amount to adjust. Pass null to adjust the full amount of the original item
* @param currency the currency of the amount. Pass null to default to the original currency used
* @return the adjustment item
*/
public InvoiceItemModelDao createAdjustmentItem(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID invoiceId, final UUID invoiceItemId,
final BigDecimal positiveAdjAmount, final Currency currency,
final LocalDate effectiveDate, final InternalCallContext context) throws InvoiceApiException {
// First, retrieve the invoice item in question
final InvoiceItemSqlDao invoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
final InvoiceItemModelDao invoiceItemToBeAdjusted = invoiceItemSqlDao.getById(invoiceItemId.toString(), context);
if (invoiceItemToBeAdjusted == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, invoiceItemId);
}
// Validate the invoice it belongs to
if (!invoiceItemToBeAdjusted.getInvoiceId().equals(invoiceId)) {
throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_FOR_INVOICE_ITEM_ADJUSTMENT, invoiceItemId, invoiceId);
}
// Retrieve the amount and currency if needed
final BigDecimal amountToAdjust = MoreObjects.firstNonNull(positiveAdjAmount, invoiceItemToBeAdjusted.getAmount());
// TODO - should we enforce the currency (and respect the original one) here if the amount passed was null?
final Currency currencyForAdjustment = MoreObjects.firstNonNull(currency, invoiceItemToBeAdjusted.getCurrency());
// Finally, create the adjustment
// Note! The amount is negated here!
return new InvoiceItemModelDao(context.getCreatedDate(), InvoiceItemType.ITEM_ADJ, invoiceItemToBeAdjusted.getInvoiceId(), invoiceItemToBeAdjusted.getAccountId(),
null, null, null, null, null, null, effectiveDate, effectiveDate, amountToAdjust.negate(), null, currencyForAdjustment, invoiceItemToBeAdjusted.getId());
}
public void populateChildren(final InvoiceModelDao invoice, final List<Tag> invoicesTags, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
populateChildren(ImmutableList.<InvoiceModelDao>of(invoice), invoicesTags, entitySqlDaoWrapperFactory, context);
}
public void populateChildren(final Iterable<InvoiceModelDao> invoices, final List<Tag> invoicesTags, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
if (Iterables.<InvoiceModelDao>isEmpty(invoices)) {
return;
}
getInvoiceItemsWithinTransaction(invoices, entitySqlDaoWrapperFactory, context);
getInvoicePaymentsWithinTransaction(invoices, entitySqlDaoWrapperFactory, context);
setInvoicesWrittenOff(invoices, invoicesTags);
final Iterable<InvoiceModelDao> nonParentInvoices = Iterables.<InvoiceModelDao>filter(invoices,
new Predicate<InvoiceModelDao>() {
@Override
public boolean apply(final InvoiceModelDao invoice) {
return !invoice.isParentInvoice();
}
});
if (!Iterables.<InvoiceModelDao>isEmpty(nonParentInvoices)) {
setParentInvoice(nonParentInvoices,
invoicesTags,
entitySqlDaoWrapperFactory,
context);
}
}
public List<InvoiceModelDao> getAllInvoicesByAccountFromTransaction(final List<Tag> invoicesTags, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
final List<InvoiceModelDao> invoices = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class).getByAccountRecordId(context);
populateChildren(invoices, invoicesTags, entitySqlDaoWrapperFactory, context);
return invoices;
}
public BigDecimal getRemainingAmountPaidFromTransaction(final UUID invoicePaymentId, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
final BigDecimal amount = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class).getRemainingAmountPaid(invoicePaymentId.toString(), context);
return amount == null ? BigDecimal.ZERO : amount;
}
private void getInvoiceItemsWithinTransaction(final Iterable<InvoiceModelDao> invoices, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
final InvoiceItemSqlDao invoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
final List<InvoiceItemModelDao> invoiceItemsForAccount = invoiceItemSqlDao.getByAccountRecordId(context);
final Map<UUID, List<InvoiceItemModelDao>> invoiceItemsPerInvoiceId = new HashMap<UUID, List<InvoiceItemModelDao>>();
for (final InvoiceItemModelDao item : invoiceItemsForAccount) {
if (invoiceItemsPerInvoiceId.get(item.getInvoiceId()) == null) {
invoiceItemsPerInvoiceId.put(item.getInvoiceId(), new LinkedList<InvoiceItemModelDao>());
}
invoiceItemsPerInvoiceId.get(item.getInvoiceId()).add(item);
}
for (final InvoiceModelDao invoice : invoices) {
// Make sure to set invoice items to a non-null value
final List<InvoiceItemModelDao> invoiceItemsForInvoice = MoreObjects.firstNonNull(invoiceItemsPerInvoiceId.get(invoice.getId()), ImmutableList.<InvoiceItemModelDao>of());
log.debug("Found items={} for invoice={}", invoiceItemsForInvoice, invoice);
invoice.addInvoiceItems(invoiceItemsForInvoice);
}
}
private void getInvoicePaymentsWithinTransaction(final Iterable<InvoiceModelDao> invoices, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
final InvoicePaymentSqlDao invoicePaymentSqlDao = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class);
final List<InvoicePaymentModelDao> invoicePaymentsForAccount = invoicePaymentSqlDao.getByAccountRecordId(context);
final Map<UUID, List<InvoicePaymentModelDao>> invoicePaymentsPerInvoiceId = new HashMap<UUID, List<InvoicePaymentModelDao>>();
for (final InvoicePaymentModelDao invoicePayment : invoicePaymentsForAccount) {
if (invoicePaymentsPerInvoiceId.get(invoicePayment.getInvoiceId()) == null) {
invoicePaymentsPerInvoiceId.put(invoicePayment.getInvoiceId(), new LinkedList<InvoicePaymentModelDao>());
}
invoicePaymentsPerInvoiceId.get(invoicePayment.getInvoiceId()).add(invoicePayment);
}
for (final InvoiceModelDao invoice : invoices) {
// Make sure to set payments to a non-null value
final List<InvoicePaymentModelDao> invoicePaymentsForInvoice = MoreObjects.firstNonNull(invoicePaymentsPerInvoiceId.get(invoice.getId()), ImmutableList.<InvoicePaymentModelDao>of());
log.debug("Found payments={} for invoice={}", invoicePaymentsForInvoice, invoice);
invoice.addPayments(invoicePaymentsForInvoice);
for (final InvoicePaymentModelDao invoicePayment : invoicePaymentsForInvoice) {
if (invoicePayment.getCurrency() != invoicePayment.getProcessedCurrency()) {
// If any entry is set with a different processed currency, we use it as a processed currency.
invoice.setProcessedCurrency(invoicePayment.getProcessedCurrency());
break;
}
}
}
}
private void setInvoicesWrittenOff(final Iterable<InvoiceModelDao> invoices, final List<Tag> invoicesTags) {
final Iterable<Tag> writtenOffTags = filterForWrittenOff(invoicesTags);
for (final Tag cur : writtenOffTags) {
final InvoiceModelDao foundInvoice = Iterables.tryFind(invoices, new Predicate<InvoiceModelDao>() {
@Override
public boolean apply(final InvoiceModelDao input) {
return input.getId().equals(cur.getObjectId());
}
}).orNull();
if (foundInvoice != null) {
foundInvoice.setIsWrittenOff(true);
}
}
}
private Iterable<Tag> filterForWrittenOff(final List<Tag> tags) {
return Iterables.filter(tags, new Predicate<Tag>() {
@Override
public boolean apply(final Tag input) {
return input.getTagDefinitionId().equals(ControlTagType.WRITTEN_OFF.getId());
}
});
}
private void setParentInvoice(final Iterable<InvoiceModelDao> childInvoices, final List<Tag> invoicesTags, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext childContext) {
final Collection<String> childInvoiceIds = new HashSet<String>();
for (final InvoiceModelDao childInvoice : childInvoices) {
childInvoiceIds.add(childInvoice.getId().toString());
}
// DAO: retrieve the mappings between parent and child invoices
final InvoiceParentChildrenSqlDao invoiceParentChildrenSqlDao = entitySqlDaoWrapperFactory.become(InvoiceParentChildrenSqlDao.class);
final List<InvoiceParentChildModelDao> mappings = invoiceParentChildrenSqlDao.getParentChildMappingsByChildInvoiceIds(childInvoiceIds, childContext);
if (mappings.isEmpty()) {
return;
}
final Map<UUID, InvoiceParentChildModelDao> mappingPerChildInvoiceId = new HashMap<UUID, InvoiceParentChildModelDao>();
final Collection<String> parentInvoiceIdsAsStrings = new HashSet<String>();
for (final InvoiceParentChildModelDao mapping : mappings) {
mappingPerChildInvoiceId.put(mapping.getChildInvoiceId(), mapping);
parentInvoiceIdsAsStrings.add(mapping.getParentInvoiceId().toString());
}
// DAO: retrieve all parents invoices in bulk, for all child invoices
final InvoiceSqlDao invoiceSqlDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final List<InvoiceModelDao> parentInvoices = invoiceSqlDao.getByIds(parentInvoiceIdsAsStrings, childContext);
// Group the parent invoices by (parent) account id (most likely, we only have one parent account group, except in re-parenting cases)
final Map<UUID, List<InvoiceModelDao>> parentInvoicesGroupedByParentAccountId = new HashMap<UUID, List<InvoiceModelDao>>();
// Create also a convenient mapping (needed below later)
final Map<UUID, InvoiceModelDao> parentInvoiceByParentInvoiceId = new HashMap<UUID, InvoiceModelDao>();
for (final InvoiceModelDao parentInvoice : parentInvoices) {
if (parentInvoicesGroupedByParentAccountId.get(parentInvoice.getAccountId()) == null) {
parentInvoicesGroupedByParentAccountId.put(parentInvoice.getAccountId(), new LinkedList<InvoiceModelDao>());
}
parentInvoicesGroupedByParentAccountId.get(parentInvoice.getAccountId()).add(parentInvoice);
parentInvoiceByParentInvoiceId.put(parentInvoice.getId(), parentInvoice);
}
// DAO: populate the parent invoices in bulk
for (final UUID parentAccountId : parentInvoicesGroupedByParentAccountId.keySet()) {
final List<InvoiceModelDao> parentInvoicesForOneParentAccountId = parentInvoicesGroupedByParentAccountId.get(parentAccountId);
final Long parentAccountRecordId = internalCallContextFactory.getRecordIdFromObject(parentAccountId, ObjectType.ACCOUNT, internalCallContextFactory.createTenantContext(childContext));
final InternalTenantContext parentContext = internalCallContextFactory.createInternalTenantContext(childContext.getTenantRecordId(), parentAccountRecordId);
// Note the misnomer here, populateChildren simply populates the content of these invoices (unrelated to HA)
populateChildren(parentInvoicesForOneParentAccountId, invoicesTags, entitySqlDaoWrapperFactory, parentContext);
}
for (final InvoiceModelDao invoice : childInvoices) {
final InvoiceParentChildModelDao mapping = mappingPerChildInvoiceId.get(invoice.getId());
if (mapping == null) {
continue;
}
final InvoiceModelDao parentInvoice = parentInvoiceByParentInvoiceId.get(mapping.getParentInvoiceId());
if (parentInvoice != null) {
invoice.addParentInvoice(parentInvoice);
}
}
}
}