/*
* Copyright 2010-2013 Ning, Inc.
* Copyright 2014-2015 Groupon, Inc
* Copyright 2014-2015 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.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entity.EntityPersistenceException;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoicePaymentType;
import org.killbill.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent;
import org.killbill.billing.invoice.notification.NextBillingDatePoster;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.config.InvoiceConfig;
import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
import org.killbill.billing.util.entity.dao.EntityDaoBase;
import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
import org.killbill.bus.api.PersistentBus;
import org.killbill.bus.api.PersistentBus.EventBusException;
import org.killbill.clock.Clock;
import org.skife.jdbi.v2.IDBI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Ordering;
import com.google.inject.Inject;
public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, InvoiceApiException> implements InvoiceDao {
private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceDao.class);
private static final Ordering<InvoiceModelDao> INVOICE_MODEL_DAO_ORDERING = Ordering.natural()
.onResultOf(new Function<InvoiceModelDao, Comparable>() {
@Override
public Comparable apply(final InvoiceModelDao invoice) {
return invoice.getTargetDate();
}
});
private static final Collection<InvoiceItemType> INVOICE_ITEM_TYPES_ADJUSTABLE = ImmutableList.<InvoiceItemType>of(InvoiceItemType.EXTERNAL_CHARGE,
InvoiceItemType.FIXED,
InvoiceItemType.RECURRING,
InvoiceItemType.TAX,
InvoiceItemType.USAGE);
private final NextBillingDatePoster nextBillingDatePoster;
private final PersistentBus eventBus;
private final InternalCallContextFactory internalCallContextFactory;
private final InvoiceDaoHelper invoiceDaoHelper;
private final CBADao cbaDao;
private final InvoiceConfig invoiceConfig;
private final Clock clock;
@Inject
public DefaultInvoiceDao(final IDBI dbi,
final NextBillingDatePoster nextBillingDatePoster,
final PersistentBus eventBus,
final Clock clock,
final CacheControllerDispatcher cacheControllerDispatcher,
final NonEntityDao nonEntityDao,
final InvoiceConfig invoiceConfig,
final InternalCallContextFactory internalCallContextFactory) {
super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao), InvoiceSqlDao.class);
this.nextBillingDatePoster = nextBillingDatePoster;
this.eventBus = eventBus;
this.invoiceConfig = invoiceConfig;
this.internalCallContextFactory = internalCallContextFactory;
this.invoiceDaoHelper = new InvoiceDaoHelper();
this.cbaDao = new CBADao();
this.clock = clock;
}
@Override
protected InvoiceApiException generateAlreadyExistsException(final InvoiceModelDao entity, final InternalCallContext context) {
return new InvoiceApiException(ErrorCode.INVOICE_ACCOUNT_ID_INVALID, entity.getId());
}
@Override
public List<InvoiceModelDao> getInvoicesByAccount(final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoiceModelDao>>() {
@Override
public List<InvoiceModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceSqlDao invoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final List<InvoiceModelDao> invoices = ImmutableList.<InvoiceModelDao>copyOf(INVOICE_MODEL_DAO_ORDERING.sortedCopy(Iterables.<InvoiceModelDao>filter(invoiceDao.getByAccountRecordId(context),
new Predicate<InvoiceModelDao>() {
@Override
public boolean apply(final InvoiceModelDao invoice) {
return !invoice.isMigrated();
}
})));
invoiceDaoHelper.populateChildren(invoices, entitySqlDaoWrapperFactory, context);
return invoices;
}
});
}
@Override
public List<InvoiceModelDao> getAllInvoicesByAccount(final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoiceModelDao>>() {
@Override
public List<InvoiceModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
return invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
}
});
}
@Override
public List<InvoiceModelDao> getInvoicesByAccount(final LocalDate fromDate, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoiceModelDao>>() {
@Override
public List<InvoiceModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceSqlDao invoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final List<InvoiceModelDao> invoices = getAllNonMigratedInvoicesByAccountAfterDate(invoiceDao, fromDate, context);
invoiceDaoHelper.populateChildren(invoices, entitySqlDaoWrapperFactory, context);
return invoices;
}
});
}
private List<InvoiceModelDao> getAllNonMigratedInvoicesByAccountAfterDate(final InvoiceSqlDao invoiceSqlDao, final LocalDate fromDate, final InternalTenantContext context) {
return ImmutableList.<InvoiceModelDao>copyOf(INVOICE_MODEL_DAO_ORDERING.sortedCopy(Iterables.<InvoiceModelDao>filter(invoiceSqlDao.getByAccountRecordId(context),
new Predicate<InvoiceModelDao>() {
@Override
public boolean apply(final InvoiceModelDao invoice) {
return !invoice.isMigrated() && invoice.getTargetDate().compareTo(fromDate) >= 0;
}
})));
}
@Override
public InvoiceModelDao getById(final UUID invoiceId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<InvoiceModelDao>() {
@Override
public InvoiceModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceSqlDao invoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final InvoiceModelDao invoice = invoiceDao.getById(invoiceId.toString(), context);
if (invoice == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_NOT_FOUND, invoiceId);
}
invoiceDaoHelper.populateChildren(invoice, entitySqlDaoWrapperFactory, context);
return invoice;
}
});
}
@Override
public InvoiceModelDao getByNumber(final Integer number, final InternalTenantContext context) throws InvoiceApiException {
if (number == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_NUMBER, "(null)");
}
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoiceModelDao>() {
@Override
public InvoiceModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceSqlDao invoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final InvoiceModelDao invoice = invoiceDao.getByRecordId(number.longValue(), context);
if (invoice == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_NUMBER_NOT_FOUND, number.longValue());
}
// The context may not contain the account record id at this point - we couldn't do it in the API above
// as we couldn't get access to the invoice object until now.
final InternalTenantContext contextWithAccountRecordId = internalCallContextFactory.createInternalTenantContext(invoice.getAccountId(), context);
invoiceDaoHelper.populateChildren(invoice, entitySqlDaoWrapperFactory, contextWithAccountRecordId);
return invoice;
}
});
}
@Override
public void createInvoice(final InvoiceModelDao invoice, final List<InvoiceItemModelDao> invoiceItems,
final boolean isRealInvoice, final Map<UUID, List<DateTime>> callbackDateTimePerSubscriptions,
final InternalCallContext context) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final InvoiceModelDao currentInvoice = transactional.getById(invoice.getId().toString(), context);
if (currentInvoice == null) {
// We only want to insert that invoice if there are real invoiceItems associated to it -- if not, this is just
// a shell invoice and we only need to insert the invoiceItems -- for the already existing invoices
if (isRealInvoice) {
transactional.create(invoice, context);
}
// Create the invoice items
final InvoiceItemSqlDao transInvoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
for (final InvoiceItemModelDao invoiceItemModelDao : invoiceItems) {
createInvoiceItemFromTransaction(transInvoiceItemSqlDao, invoiceItemModelDao, context);
}
cbaDao.addCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), callbackDateTimePerSubscriptions, context);
}
return null;
}
});
}
@Override
public List<InvoiceItemModelDao> createInvoices(final List<InvoiceModelDao> invoices, final InternalCallContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoiceItemModelDao>>() {
@Override
public List<InvoiceItemModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceSqlDao invoiceSqlDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final InvoiceItemSqlDao transInvoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
final List<InvoiceItemModelDao> createdInvoiceItems = new LinkedList<InvoiceItemModelDao>();
for (final InvoiceModelDao invoiceModelDao : invoices) {
boolean madeChanges = false;
// Create the invoice if needed
if (invoiceSqlDao.getById(invoiceModelDao.getId().toString(), context) == null) {
invoiceSqlDao.create(invoiceModelDao, context);
madeChanges = true;
}
// Create the invoice items if needed
for (final InvoiceItemModelDao invoiceItemModelDao : invoiceModelDao.getInvoiceItems()) {
if (transInvoiceItemSqlDao.getById(invoiceItemModelDao.getId().toString(), context) == null) {
createInvoiceItemFromTransaction(transInvoiceItemSqlDao, invoiceItemModelDao, context);
createdInvoiceItems.add(transInvoiceItemSqlDao.getById(invoiceItemModelDao.getId().toString(), context));
madeChanges = true;
}
}
if (madeChanges) {
cbaDao.addCBAComplexityFromTransaction(invoiceModelDao.getId(), entitySqlDaoWrapperFactory, context);
// Notify the bus since the balance of the invoice changed
// TODO should we post an InvoiceCreationInternalEvent event instead? Note! This will trigger a payment (see InvoiceHandler)
notifyBusOfInvoiceAdjustment(entitySqlDaoWrapperFactory, invoiceModelDao.getId(), invoiceModelDao.getAccountId(), context.getUserToken(), context);
}
}
return createdInvoiceItems;
}
});
}
@Override
public List<InvoiceModelDao> getInvoicesBySubscription(final UUID subscriptionId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoiceModelDao>>() {
@Override
public List<InvoiceModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceSqlDao invoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final List<InvoiceModelDao> invoices = invoiceDao.getInvoicesBySubscription(subscriptionId.toString(), context);
invoiceDaoHelper.populateChildren(invoices, entitySqlDaoWrapperFactory, context);
return invoices;
}
});
}
@Override
public Pagination<InvoiceModelDao> searchInvoices(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
Integer invoiceNumberParsed = null;
try {
invoiceNumberParsed = Integer.parseInt(searchKey);
} catch (final NumberFormatException ignored) {
}
final Integer invoiceNumber = invoiceNumberParsed;
return paginationHelper.getPagination(InvoiceSqlDao.class,
new PaginationIteratorBuilder<InvoiceModelDao, Invoice, InvoiceSqlDao>() {
@Override
public Long getCount(final InvoiceSqlDao invoiceSqlDao, final InternalTenantContext context) {
return invoiceNumber != null ? 1L : invoiceSqlDao.getSearchCount(searchKey, String.format("%%%s%%", searchKey), context);
}
@Override
public Iterator<InvoiceModelDao> build(final InvoiceSqlDao invoiceSqlDao, final Long limit, final InternalTenantContext context) {
try {
return invoiceNumber != null ?
ImmutableList.<InvoiceModelDao>of(getByNumber(invoiceNumber, context)).iterator() :
invoiceSqlDao.search(searchKey, String.format("%%%s%%", searchKey), offset, limit, context);
} catch (final InvoiceApiException ignored) {
return Iterators.<InvoiceModelDao>emptyIterator();
}
}
},
offset,
limit,
context);
}
@Override
public BigDecimal getAccountBalance(final UUID accountId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<BigDecimal>() {
@Override
public BigDecimal inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
BigDecimal cba = BigDecimal.ZERO;
BigDecimal accountBalance = BigDecimal.ZERO;
final List<InvoiceModelDao> invoices = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
for (final InvoiceModelDao cur : invoices) {
accountBalance = accountBalance.add(InvoiceModelDaoHelper.getBalance(cur));
cba = cba.add(InvoiceModelDaoHelper.getCBAAmount(cur));
}
return accountBalance.subtract(cba);
}
});
}
@Override
public BigDecimal getAccountCBA(final UUID accountId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<BigDecimal>() {
@Override
public BigDecimal inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
return cbaDao.getAccountCBAFromTransaction(accountId, entitySqlDaoWrapperFactory, context);
}
});
}
@Override
public List<InvoiceModelDao> getUnpaidInvoicesByAccountId(final UUID accountId, @Nullable final LocalDate upToDate, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoiceModelDao>>() {
@Override
public List<InvoiceModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
return invoiceDaoHelper.getUnpaidInvoicesByAccountFromTransaction(accountId, entitySqlDaoWrapperFactory, upToDate, context);
}
});
}
@Override
public UUID getInvoiceIdByPaymentId(final UUID paymentId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<UUID>() {
@Override
public UUID inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
return entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class).getInvoiceIdByPaymentId(paymentId.toString(), context);
}
});
}
@Override
public List<InvoicePaymentModelDao> getInvoicePayments(final UUID paymentId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoicePaymentModelDao>>() {
@Override
public List<InvoicePaymentModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
return entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class).getInvoicePayments(paymentId.toString(), context);
}
});
}
@Override
public List<InvoicePaymentModelDao> getInvoicePaymentsByAccount(final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoicePaymentModelDao>>() {
@Override
public List<InvoicePaymentModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
return entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class).getByAccountRecordId(context);
}
});
}
@Override
public InvoicePaymentModelDao createRefund(final UUID paymentId, final BigDecimal requestedRefundAmount, final boolean isInvoiceAdjusted,
final Map<UUID, BigDecimal> invoiceItemIdsWithNullAmounts, final String transactionExternalKey,
final InternalCallContext context) throws InvoiceApiException {
final boolean isInvoiceItemAdjusted = isInvoiceAdjusted && invoiceItemIdsWithNullAmounts.size() > 0;
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoicePaymentModelDao>() {
@Override
public InvoicePaymentModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoicePaymentSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class);
final InvoiceSqlDao transInvoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
final List<InvoicePaymentModelDao> paymentsForId = transactional.getByPaymentId(paymentId.toString(), context);
final InvoicePaymentModelDao payment = Iterables.tryFind(paymentsForId, new Predicate<InvoicePaymentModelDao>() {
@Override
public boolean apply(final InvoicePaymentModelDao input) {
return input.getType() == InvoicePaymentType.ATTEMPT;
}
}).orNull();
if (payment == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_PAYMENT_BY_ATTEMPT_NOT_FOUND, paymentId);
}
// Retrieve the amounts to adjust, if needed
final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts = invoiceDaoHelper.computeItemAdjustments(payment.getInvoiceId().toString(),
entitySqlDaoWrapperFactory,
invoiceItemIdsWithNullAmounts,
context);
// Compute the actual amount to refund
final BigDecimal requestedPositiveAmount = invoiceDaoHelper.computePositiveRefundAmount(payment, requestedRefundAmount, invoiceItemIdsWithAmounts);
// Before we go further, check if that refund already got inserted -- the payment system keeps a state machine
// and so this call may be called several time for the same paymentCookieId (which is really the refundId)
final InvoicePaymentModelDao existingRefund = transactional.getPaymentsForCookieId(transactionExternalKey, context);
if (existingRefund != null) {
return existingRefund;
}
final InvoicePaymentModelDao refund = new InvoicePaymentModelDao(UUID.randomUUID(), context.getCreatedDate(), InvoicePaymentType.REFUND,
payment.getInvoiceId(), paymentId,
context.getCreatedDate(), requestedPositiveAmount.negate(),
payment.getCurrency(), payment.getProcessedCurrency(), transactionExternalKey, payment.getId());
transactional.create(refund, context);
// Retrieve invoice after the Refund
final InvoiceModelDao invoice = transInvoiceDao.getById(payment.getInvoiceId().toString(), context);
Preconditions.checkState(invoice != null, "Invoice shouldn't be null for payment " + payment.getId());
invoiceDaoHelper.populateChildren(invoice, entitySqlDaoWrapperFactory, context);
final BigDecimal invoiceBalanceAfterRefund = InvoiceModelDaoHelper.getBalance(invoice);
final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
// At this point, we created the refund which made the invoice balance positive and applied any existing
// available CBA to that invoice.
// We now need to adjust the invoice and/or invoice items if needed and specified.
if (isInvoiceAdjusted && !isInvoiceItemAdjusted) {
// Invoice adjustment
final BigDecimal maxBalanceToAdjust = (invoiceBalanceAfterRefund.compareTo(BigDecimal.ZERO) <= 0) ? BigDecimal.ZERO : invoiceBalanceAfterRefund;
final BigDecimal requestedPositiveAmountToAdjust = requestedPositiveAmount.compareTo(maxBalanceToAdjust) > 0 ? maxBalanceToAdjust : requestedPositiveAmount;
if (requestedPositiveAmountToAdjust.compareTo(BigDecimal.ZERO) > 0) {
final InvoiceItemModelDao adjItem = new InvoiceItemModelDao(context.getCreatedDate(), InvoiceItemType.REFUND_ADJ, invoice.getId(), invoice.getAccountId(),
null, null, null, null, null, null, context.getCreatedDate().toLocalDate(), null,
requestedPositiveAmountToAdjust.negate(), null, invoice.getCurrency(), null);
createInvoiceItemFromTransaction(transInvoiceItemDao, adjItem, context);
invoice.addInvoiceItem(adjItem);
}
} else if (isInvoiceAdjusted) {
// Invoice item adjustment
for (final UUID invoiceItemId : invoiceItemIdsWithAmounts.keySet()) {
final BigDecimal adjAmount = invoiceItemIdsWithAmounts.get(invoiceItemId);
final InvoiceItemModelDao item = invoiceDaoHelper.createAdjustmentItem(entitySqlDaoWrapperFactory, invoice.getId(), invoiceItemId, adjAmount,
invoice.getCurrency(), context.getCreatedDate().toLocalDate(),
context);
createInvoiceItemFromTransaction(transInvoiceItemDao, item, context);
invoice.addInvoiceItem(item);
}
}
cbaDao.addCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
// Notify the bus since the balance of the invoice changed
notifyBusOfInvoiceAdjustment(entitySqlDaoWrapperFactory, invoice.getId(), invoice.getAccountId(), context.getUserToken(), context);
return refund;
}
});
}
@Override
public InvoicePaymentModelDao postChargeback(final UUID paymentId, final BigDecimal amount, final Currency currency, final InternalCallContext context) throws InvoiceApiException {
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoicePaymentModelDao>() {
@Override
public InvoicePaymentModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoicePaymentSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class);
final List<InvoicePaymentModelDao> invoicePayments = transactional.getByPaymentId(paymentId.toString(), context);
final InvoicePaymentModelDao invoicePayment = Iterables.tryFind(invoicePayments, new Predicate<InvoicePaymentModelDao>() {
@Override
public boolean apply(final InvoicePaymentModelDao input) {
return input.getType() == InvoicePaymentType.ATTEMPT;
}
}).orNull();
if (invoicePayment == null) {
throw new InvoiceApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT, paymentId);
}
// We expect the code to correctly pass the account currency -- the payment code, more generic accept chargeBack in different currencies,
// but this is only for direct payment (no invoice)
Preconditions.checkArgument(invoicePayment.getCurrency() == currency);
final UUID invoicePaymentId = invoicePayment.getId();
final BigDecimal maxChargedBackAmount = invoiceDaoHelper.getRemainingAmountPaidFromTransaction(invoicePaymentId, entitySqlDaoWrapperFactory, context);
final BigDecimal requestedChargedBackAmount = (amount == null) ? maxChargedBackAmount : amount;
if (requestedChargedBackAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new InvoiceApiException(ErrorCode.CHARGE_BACK_AMOUNT_IS_NEGATIVE);
}
if (requestedChargedBackAmount.compareTo(maxChargedBackAmount) > 0) {
throw new InvoiceApiException(ErrorCode.CHARGE_BACK_AMOUNT_TOO_HIGH, requestedChargedBackAmount, maxChargedBackAmount);
}
final InvoicePaymentModelDao payment = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class).getById(invoicePaymentId.toString(), context);
if (payment == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_PAYMENT_NOT_FOUND, invoicePaymentId.toString());
}
final InvoicePaymentModelDao chargeBack = new InvoicePaymentModelDao(UUID.randomUUID(), context.getCreatedDate(), InvoicePaymentType.CHARGED_BACK,
payment.getInvoiceId(), payment.getPaymentId(), context.getCreatedDate(),
requestedChargedBackAmount.negate(), payment.getCurrency(), payment.getProcessedCurrency(),
null, payment.getId());
transactional.create(chargeBack, context);
// Notify the bus since the balance of the invoice changed
final UUID accountId = transactional.getAccountIdFromInvoicePaymentId(chargeBack.getId().toString(), context);
cbaDao.addCBAComplexityFromTransaction(payment.getInvoiceId(), entitySqlDaoWrapperFactory, context);
notifyBusOfInvoiceAdjustment(entitySqlDaoWrapperFactory, payment.getInvoiceId(), accountId, context.getUserToken(), context);
return chargeBack;
}
});
}
@Override
public InvoiceItemModelDao doCBAComplexity(final InvoiceModelDao invoice, final InternalCallContext context) throws InvoiceApiException {
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoiceItemModelDao>() {
@Override
public InvoiceItemModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceItemModelDao cbaNewItem = cbaDao.computeCBAComplexity(invoice, entitySqlDaoWrapperFactory, context);
return cbaNewItem;
}
});
}
@Override
public BigDecimal getRemainingAmountPaid(final UUID invoicePaymentId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<BigDecimal>() {
@Override
public BigDecimal inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
return invoiceDaoHelper.getRemainingAmountPaidFromTransaction(invoicePaymentId, entitySqlDaoWrapperFactory, context);
}
});
}
@Override
public UUID getAccountIdFromInvoicePaymentId(final UUID invoicePaymentId, final InternalTenantContext context) throws InvoiceApiException {
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<UUID>() {
@Override
public UUID inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final UUID accountId = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class).getAccountIdFromInvoicePaymentId(invoicePaymentId.toString(), context);
if (accountId == null) {
throw new InvoiceApiException(ErrorCode.CHARGE_BACK_COULD_NOT_FIND_ACCOUNT_ID, invoicePaymentId);
} else {
return accountId;
}
}
});
}
@Override
public List<InvoicePaymentModelDao> getChargebacksByAccountId(final UUID accountId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoicePaymentModelDao>>() {
@Override
public List<InvoicePaymentModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
return entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class).getChargeBacksByAccountId(accountId.toString(), context);
}
});
}
@Override
public List<InvoicePaymentModelDao> getChargebacksByPaymentId(final UUID paymentId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoicePaymentModelDao>>() {
@Override
public List<InvoicePaymentModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
return entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class).getChargebacksByPaymentId(paymentId.toString(), context);
}
});
}
@Override
public InvoicePaymentModelDao getChargebackById(final UUID chargebackId, final InternalTenantContext context) throws InvoiceApiException {
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoicePaymentModelDao>() {
@Override
public InvoicePaymentModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoicePaymentModelDao chargeback = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class).getById(chargebackId.toString(), context);
if (chargeback == null) {
throw new InvoiceApiException(ErrorCode.CHARGE_BACK_DOES_NOT_EXIST, chargebackId);
} else {
return chargeback;
}
}
});
}
@Override
public InvoiceItemModelDao getExternalChargeById(final UUID externalChargeId, final InternalTenantContext context) throws InvoiceApiException {
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoiceItemModelDao>() {
@Override
public InvoiceItemModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceItemSqlDao invoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
final InvoiceItemModelDao invoiceItemModelDao = invoiceItemSqlDao.getById(externalChargeId.toString(), context);
if (invoiceItemModelDao == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, externalChargeId.toString());
}
return invoiceItemModelDao;
}
});
}
@Override
public void notifyOfPayment(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoicePaymentSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class);
final List<InvoicePaymentModelDao> invoicePayments = transactional.getInvoicePayments(invoicePayment.getPaymentId().toString(), context);
final InvoicePaymentModelDao existingAttempt = Iterables.tryFind(invoicePayments, new Predicate<InvoicePaymentModelDao>() {
@Override
public boolean apply(final InvoicePaymentModelDao input) {
return input.getType() == InvoicePaymentType.ATTEMPT;
}
}).orNull();
if (existingAttempt == null) {
transactional.create(invoicePayment, context);
}
return null;
}
});
}
@Override
public InvoiceItemModelDao getCreditById(final UUID creditId, final InternalTenantContext context) throws InvoiceApiException {
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoiceItemModelDao>() {
@Override
public InvoiceItemModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceItemSqlDao invoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
final InvoiceItemModelDao invoiceItemModelDao = invoiceItemSqlDao.getById(creditId.toString(), context);
if (invoiceItemModelDao == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, creditId.toString());
}
return invoiceItemModelDao;
}
});
}
@Override
public void deleteCBA(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId, final InternalCallContext context) throws InvoiceApiException {
transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
// Retrieve the invoice and make sure it belongs to the right account
final InvoiceModelDao invoice = transactional.getById(invoiceId.toString(), context);
if (invoice == null || !invoice.getAccountId().equals(accountId)) {
throw new InvoiceApiException(ErrorCode.INVOICE_NOT_FOUND, invoiceId);
}
// Retrieve the invoice item and make sure it belongs to the right invoice
final InvoiceItemSqlDao invoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
final InvoiceItemModelDao cbaItem = invoiceItemSqlDao.getById(invoiceItemId.toString(), context);
if (cbaItem == null || !cbaItem.getInvoiceId().equals(invoice.getId())) {
throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, invoiceItemId);
}
// First, adjust the same invoice with the CBA amount to "delete"
final InvoiceItemModelDao cbaAdjItem = new InvoiceItemModelDao(context.getCreatedDate(), InvoiceItemType.CBA_ADJ, invoice.getId(), invoice.getAccountId(),
null, null, null, null, null, null, context.getCreatedDate().toLocalDate(),
null, cbaItem.getAmount().negate(), null, cbaItem.getCurrency(), cbaItem.getId());
createInvoiceItemFromTransaction(invoiceItemSqlDao, cbaAdjItem, context);
// Verify the final invoice balance is not negative
invoiceDaoHelper.populateChildren(invoice, entitySqlDaoWrapperFactory, context);
if (InvoiceModelDaoHelper.getBalance(invoice).compareTo(BigDecimal.ZERO) < 0) {
throw new InvoiceApiException(ErrorCode.INVOICE_WOULD_BE_NEGATIVE);
}
// If there is more account credit than CBA we adjusted, we're done.
// Otherwise, we need to find further invoices on which this credit was consumed
final BigDecimal accountCBA = cbaDao.getAccountCBAFromTransaction(accountId, entitySqlDaoWrapperFactory, context);
if (accountCBA.compareTo(BigDecimal.ZERO) < 0) {
if (accountCBA.compareTo(cbaItem.getAmount().negate()) < 0) {
throw new IllegalStateException("The account balance can't be lower than the amount adjusted");
}
final List<InvoiceModelDao> invoicesFollowing = getAllNonMigratedInvoicesByAccountAfterDate(transactional, invoice.getInvoiceDate(), context);
invoiceDaoHelper.populateChildren(invoicesFollowing, entitySqlDaoWrapperFactory, context);
// The remaining amount to adjust (i.e. the amount of credits used on following invoices)
// is the current account CBA balance (minus the sign)
BigDecimal positiveRemainderToAdjust = accountCBA.negate();
for (final InvoiceModelDao invoiceFollowing : invoicesFollowing) {
if (invoiceFollowing.getId().equals(invoice.getId())) {
continue;
}
// Add a single adjustment per invoice
BigDecimal positiveCBAAdjItemAmount = BigDecimal.ZERO;
for (final InvoiceItemModelDao cbaUsed : invoiceFollowing.getInvoiceItems()) {
// Ignore non CBA items or credits (CBA >= 0)
if (!InvoiceItemType.CBA_ADJ.equals(cbaUsed.getType()) ||
cbaUsed.getAmount().compareTo(BigDecimal.ZERO) >= 0) {
continue;
}
final BigDecimal positiveCBAUsedAmount = cbaUsed.getAmount().negate();
final BigDecimal positiveNextCBAAdjItemAmount;
if (positiveCBAUsedAmount.compareTo(positiveRemainderToAdjust) < 0) {
positiveNextCBAAdjItemAmount = positiveCBAUsedAmount;
positiveRemainderToAdjust = positiveRemainderToAdjust.subtract(positiveNextCBAAdjItemAmount);
} else {
positiveNextCBAAdjItemAmount = positiveRemainderToAdjust;
positiveRemainderToAdjust = BigDecimal.ZERO;
}
positiveCBAAdjItemAmount = positiveCBAAdjItemAmount.add(positiveNextCBAAdjItemAmount);
if (positiveRemainderToAdjust.compareTo(BigDecimal.ZERO) == 0) {
break;
}
}
// Add the adjustment on that invoice
final InvoiceItemModelDao nextCBAAdjItem = new InvoiceItemModelDao(context.getCreatedDate(), InvoiceItemType.CBA_ADJ, invoiceFollowing.getId(),
invoice.getAccountId(), null, null, null, null, null, null,
context.getCreatedDate().toLocalDate(), null,
positiveCBAAdjItemAmount, null, cbaItem.getCurrency(), cbaItem.getId());
createInvoiceItemFromTransaction(invoiceItemSqlDao, nextCBAAdjItem, context);
if (positiveRemainderToAdjust.compareTo(BigDecimal.ZERO) == 0) {
break;
}
}
}
return null;
}
});
}
public void consumeExstingCBAOnAccountWithUnpaidInvoices(final UUID accountId, final InternalCallContext context) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
// In theory we should only have to call useExistingCBAFromTransaction but just to be safe we also check for credit generation
cbaDao.addCBAComplexityFromTransaction(entitySqlDaoWrapperFactory, context);
return null;
}
});
}
private void notifyOfFutureBillingEvents(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
final Map<UUID, List<DateTime>> callbackDateTimePerSubscriptions, final InternalCallContext internalCallContext) {
final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule().getMillis();
final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
for (final UUID subscriptionId : callbackDateTimePerSubscriptions.keySet()) {
final List<DateTime> callbackDateTimeUTC = callbackDateTimePerSubscriptions.get(subscriptionId);
for (final DateTime cur : callbackDateTimeUTC) {
if (isInvoiceNotificationEnabled) {
final DateTime curDryRunNotificationTime = cur.minus(dryRunNotificationTime);
// Only schedule if the date is in the future
if (curDryRunNotificationTime.isAfter(clock.getUTCNow())) {
nextBillingDatePoster.insertNextBillingDryRunNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionId, curDryRunNotificationTime, cur, internalCallContext);
}
}
nextBillingDatePoster.insertNextBillingNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionId, cur, internalCallContext);
}
}
}
private void notifyBusOfInvoiceAdjustment(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID invoiceId, final UUID accountId,
final UUID userToken, final InternalCallContext context) {
try {
eventBus.postFromTransaction(new DefaultInvoiceAdjustmentEvent(invoiceId, accountId, context.getAccountRecordId(), context.getTenantRecordId(), userToken),
entitySqlDaoWrapperFactory.getHandle().getConnection());
} catch (final EventBusException e) {
log.warn("Failed to post adjustment event for invoice " + invoiceId, e);
}
}
private void createInvoiceItemFromTransaction(final InvoiceItemSqlDao invoiceItemSqlDao, final InvoiceItemModelDao invoiceItemModelDao, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
// There is no efficient way to retrieve an invoice item given an ID today (and invoice plugins can put item adjustments
// on a different invoice than the original item), so it's easier to do the check in the DAO rather than in the API layer
// See also https://github.com/killbill/killbill/issues/7
if (InvoiceItemType.ITEM_ADJ.equals(invoiceItemModelDao.getType())) {
validateInvoiceItemToBeAdjusted(invoiceItemSqlDao, invoiceItemModelDao, context);
}
invoiceItemSqlDao.create(invoiceItemModelDao, context);
}
private void validateInvoiceItemToBeAdjusted(final InvoiceItemSqlDao invoiceItemSqlDao, final InvoiceItemModelDao invoiceItemModelDao, final InternalCallContext context) throws InvoiceApiException {
Preconditions.checkNotNull(invoiceItemModelDao.getLinkedItemId(), "LinkedItemId cannot be null for ITEM_ADJ item: " + invoiceItemModelDao);
// Note: this assumes the linked item has already been created in or prior to the transaction, which should almost always be the case
// (unless some whacky plugin creates an out-of-order item adjustment on a subsequent external charge)
final InvoiceItemModelDao invoiceItemToBeAdjusted = invoiceItemSqlDao.getById(invoiceItemModelDao.getLinkedItemId().toString(), context);
if (!INVOICE_ITEM_TYPES_ADJUSTABLE.contains(invoiceItemToBeAdjusted.getType())) {
throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_ADJUSTMENT_ITEM_INVALID, invoiceItemToBeAdjusted.getId());
}
}
}