/*
* 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.Comparator;
import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.entity.EntityPersistenceException;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
import org.killbill.billing.util.tag.Tag;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import br.ufrgs.inf.prosoft.cache.*;
public class CBADao {
private final InvoiceDaoHelper invoiceDaoHelper;
@Inject
public CBADao(final InvoiceDaoHelper invoiceDaoHelper) {
this.invoiceDaoHelper = invoiceDaoHelper;
}
public static CaffeineSingleCache<Parameters, BigDecimal> getAccountCBAFromTransactionCache = new CaffeineSingleCache<>("22#static-single:CBADao.getAccountCBAFromTransaction", 20000);
// PERF: Compute the CBA directly in the database (faster than re-constructing all invoices)
public BigDecimal getAccountCBAFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
return getAccountCBAFromTransactionCache.computeIfAbsent(new Parameters(entitySqlDaoWrapperFactory, context), () -> {
final InvoiceItemSqlDao invoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
return invoiceItemSqlDao.getAccountCBA(context);
}, 20000);
}
// We expect a clean up to date invoice, with all the items except the cba, that we will compute in that method
public InvoiceItemModelDao computeCBAComplexity(final InvoiceModelDao invoice,
@Nullable final BigDecimal accountCBAOrNull,
@Nullable final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
final BigDecimal balance = getInvoiceBalance(invoice);
if (balance.compareTo(BigDecimal.ZERO) < 0) {
// Current balance is negative, we need to generate a credit (positive CBA amount)
return buildCBAItem(invoice, balance, context);
} else if (balance.compareTo(BigDecimal.ZERO) > 0 && invoice.getStatus() == InvoiceStatus.COMMITTED && !invoice.isWrittenOff()) {
// Current balance is positive and the invoice is COMMITTED, we need to use some of the existing if available (negative CBA amount)
// PERF: in some codepaths, the CBA maybe have already been computed
BigDecimal accountCBA = accountCBAOrNull;
if (accountCBAOrNull == null) {
accountCBA = getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
}
if (accountCBA.compareTo(BigDecimal.ZERO) <= 0) {
return null;
}
final BigDecimal positiveCreditAmount = accountCBA.compareTo(balance) > 0 ? balance : accountCBA;
return buildCBAItem(invoice, positiveCreditAmount, context);
} else {
// 0 balance, nothing to do.
return null;
}
}
private BigDecimal getInvoiceBalance(final InvoiceModelDao invoice) {
final InvoiceModelDao parentInvoice = invoice.getParentInvoice();
if ((parentInvoice != null) && (InvoiceModelDaoHelper.getRawBalanceForRegularInvoice(parentInvoice).compareTo(BigDecimal.ZERO) == 0)) {
final Iterable<InvoiceItemModelDao> items = Iterables.filter(parentInvoice.getInvoiceItems(), new Predicate<InvoiceItemModelDao>() {
@Override
public boolean apply(@Nullable final InvoiceItemModelDao input) {
return input.getChildAccountId().equals(invoice.getAccountId());
}
});
final BigDecimal childInvoiceAmountCharged = InvoiceModelDaoHelper.getAmountCharged(invoice);
BigDecimal parentInvoiceAmountChargedForChild = BigDecimal.ZERO;
for (InvoiceItemModelDao itemModel : items) {
parentInvoiceAmountChargedForChild = parentInvoiceAmountChargedForChild.add(itemModel.getAmount());
}
return childInvoiceAmountCharged.add(parentInvoiceAmountChargedForChild.negate());
}
return InvoiceModelDaoHelper.getRawBalanceForRegularInvoice(invoice);
}
// We let the code below rehydrate the invoice before we can add the CBA item
// PERF: when possible, prefer the method below to avoid re-fetching the invoice
public void doCBAComplexityFromTransaction(final UUID invoiceId,
final List<Tag> invoicesTags,
final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
final InvoiceSqlDao transInvoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final InvoiceModelDao invoice = transInvoiceDao.getById(invoiceId.toString(), context);
invoiceDaoHelper.populateChildren(invoice, invoicesTags, entitySqlDaoWrapperFactory, context);
doCBAComplexityFromTransaction(invoice, invoicesTags, entitySqlDaoWrapperFactory, context);
}
public void doCBAComplexityFromTransaction(final List<Tag> invoicesTags,
final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
doCBAComplexityFromTransaction((InvoiceModelDao) null, invoicesTags, entitySqlDaoWrapperFactory, context);
}
// Note! We expect an *up-to-date* invoice, with all the items and payments except the CBA, that we will compute in that method
public void doCBAComplexityFromTransaction(@Nullable final InvoiceModelDao invoice,
final List<Tag> invoicesTags,
final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
// PERF: It is expensive to retrieve and construct all invoice objects. To check if there is effectively something to use, compute the CBA by the database first
BigDecimal remainingAccountCBA = getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
if (invoice != null) {
// Generate or use CBA for that specific invoice
remainingAccountCBA = computeCBAComplexityAndCreateCBAItem(remainingAccountCBA, invoice, entitySqlDaoWrapperFactory, context);
}
useExistingCBAFromTransaction(remainingAccountCBA, invoicesTags, entitySqlDaoWrapperFactory, context);
}
// Distribute account CBA across all COMMITTED unpaid invoices
private void useExistingCBAFromTransaction(final BigDecimal accountCBA,
final List<Tag> invoicesTags,
final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
final InternalCallContext context) throws InvoiceApiException, EntityPersistenceException {
if (accountCBA.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
// PERF: Computing the invoice balance is difficult to do in the DB, so we effectively need to retrieve all invoices on the account and filter the unpaid ones in memory.
// This should be infrequent though because of the account CBA check above.
final List<InvoiceModelDao> allInvoices = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(false, invoicesTags, entitySqlDaoWrapperFactory, context);
final List<InvoiceModelDao> unpaidInvoices = invoiceDaoHelper.getUnpaidInvoicesByAccountFromTransaction(allInvoices, null);
// We order the same os BillingStateCalculator-- should really share the comparator
final List<InvoiceModelDao> orderedUnpaidInvoices = Ordering.from(new Comparator<InvoiceModelDao>() {
@Override
public int compare(final InvoiceModelDao i1, final InvoiceModelDao i2) {
return i1.getInvoiceDate().compareTo(i2.getInvoiceDate());
}
}).immutableSortedCopy(unpaidInvoices);
BigDecimal remainingAccountCBA = accountCBA;
for (final InvoiceModelDao unpaidInvoice : orderedUnpaidInvoices) {
remainingAccountCBA = computeCBAComplexityAndCreateCBAItem(remainingAccountCBA, unpaidInvoice, entitySqlDaoWrapperFactory, context);
if (remainingAccountCBA.compareTo(BigDecimal.ZERO) <= 0) {
break;
}
}
}
// Return the updated account CBA
private BigDecimal computeCBAComplexityAndCreateCBAItem(final BigDecimal accountCBA,
final InvoiceModelDao invoice,
final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
final InvoiceItemModelDao cbaItem = computeCBAComplexity(invoice, accountCBA, entitySqlDaoWrapperFactory, context);
if (cbaItem != null) {
createCBAItem(invoice, cbaItem, entitySqlDaoWrapperFactory, context);
return accountCBA.add(cbaItem.getAmount());
} else {
return accountCBA;
}
}
private void createCBAItem(final InvoiceModelDao invoiceModelDao,
final InvoiceItemModelDao cbaItem,
final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
final InternalCallContext context) throws EntityPersistenceException {
final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
transInvoiceItemDao.create(cbaItem, context);
// Refresh the in-memory item
invoiceModelDao.addInvoiceItem(cbaItem);
}
private InvoiceItemModelDao buildCBAItem(final InvoiceModelDao invoice,
final BigDecimal amount,
final InternalCallContext context) {
return new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(),
invoice.getAccountId(),
context.getCreatedDate().toLocalDate(),
amount.negate(),
invoice.getCurrency()));
}
}