/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.scheduling.polling;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee.InvoiceMode;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee.PaymentDirection;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFeeLog;
import nl.strohalm.cyclos.entities.accounts.fees.account.MemberAccountFeeLog;
import nl.strohalm.cyclos.entities.accounts.transactions.Invoice;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.alerts.SystemAlert;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.services.accountfees.AccountFeeServiceLocal;
import nl.strohalm.cyclos.services.alerts.AlertServiceLocal;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.services.transactions.InvoiceServiceLocal;
import nl.strohalm.cyclos.services.transactions.PaymentServiceLocal;
import nl.strohalm.cyclos.services.transactions.TransferDTO;
import nl.strohalm.cyclos.services.transactions.exceptions.NotEnoughCreditsException;
import nl.strohalm.cyclos.utils.Amount;
import nl.strohalm.cyclos.utils.MessageProcessingHelper;
import nl.strohalm.cyclos.utils.Period;
import nl.strohalm.cyclos.utils.TransactionHelper;
import nl.strohalm.cyclos.utils.conversion.AmountConverter;
import nl.strohalm.cyclos.utils.conversion.CalendarConverter;
import nl.strohalm.cyclos.utils.conversion.UnitsConverter;
import nl.strohalm.cyclos.utils.logging.LoggingHandler;
import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData;
import nl.strohalm.cyclos.utils.transaction.TransactionEndListener;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
/**
* A {@link PollingTask} which charges account fees
* @author luis
*/
public class ChargeAccountFeePollingTask extends PollingTask {
private Long logBeingCharged;
private LoggingHandler loggingHandler;
private AccountFeeServiceLocal accountFeeService;
private FetchServiceLocal fetchService;
private AlertServiceLocal alertService;
private InvoiceServiceLocal invoiceService;
private PaymentServiceLocal paymentService;
private SettingsServiceLocal settingsService;
private TransactionHelper transactionHelper;
public void setAccountFeeServiceLocal(final AccountFeeServiceLocal accountFeeService) {
this.accountFeeService = accountFeeService;
}
public void setAlertServiceLocal(final AlertServiceLocal alertService) {
this.alertService = alertService;
}
public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public void setInvoiceServiceLocal(final InvoiceServiceLocal invoiceService) {
this.invoiceService = invoiceService;
}
public void setLoggingHandler(final LoggingHandler loggingHandler) {
this.loggingHandler = loggingHandler;
}
public void setPaymentServiceLocal(final PaymentServiceLocal paymentService) {
this.paymentService = paymentService;
}
public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
public void setTransactionHelper(final TransactionHelper transactionHelper) {
this.transactionHelper = transactionHelper;
}
@Override
protected boolean runTask() {
if (logBeingCharged == null) {
AccountFeeLog feeLog = accountFeeService.nextLogToCharge();
logBeingCharged = feeLog == null ? null : feeLog.getId();
if (logBeingCharged == null) {
// No current log to charge. Force sleep
return false;
} else {
// Prepare the charge
boolean firstTime = accountFeeService.prepareCharge(getFeeLog());
// The first time this fee log is being charged, generate the alert
if (firstTime) {
alertService.create(SystemAlert.Alerts.ACCOUNT_FEE_RUNNING, feeLog.getAccountFee().getName());
}
// Log that the fee log is being charged
loggingHandler.logAccountFeeStarted(getFeeLog());
}
}
// Charge each member
AccountFeeLog feeLog = getFeeLog();
final List<Member> toCharge = accountFeeService.nextMembersToCharge(feeLog);
if (toCharge.isEmpty()) {
// No more members to charge. Set the finish date
feeLog.setFinishDate(Calendar.getInstance());
feeLog.setRechargingFailed(false);
accountFeeService.save(feeLog);
// Log that the fee has finished
loggingHandler.logAccountFeeFinished(feeLog);
int errors = feeLog.getFailedMembers();
if (errors == 0) {
alertService.create(SystemAlert.Alerts.ACCOUNT_FEE_FINISHED, feeLog.getAccountFee().getName());
} else {
alertService.create(SystemAlert.Alerts.ACCOUNT_FEE_FINISHED_WITH_ERRORS, feeLog.getAccountFee().getName(), errors);
}
// Mark the current account fee log as finished
logBeingCharged = null;
} else {
// Charge the next batch of members
charge(feeLog, toCharge);
}
// Keep processing
return true;
}
/**
* Runs the charging within a transaction, charging the next batch of members
*/
private void charge(final AccountFeeLog feeLog, final List<Member> toCharge) {
// Iterate each member
for (final Member member : toCharge) {
BigDecimal amount = null;
try {
// Calculate the charging amount
amount = accountFeeService.calculateAmount(feeLog, member);
if (amount == null) {
// Null amount means that the member shouldn't be charged, ie, moved to another group
// Just remove from pending and decrement the total members
accountFeeService.removeFromPending(feeLog, member);
feeLog.setTotalMembers(feeLog.getTotalMembers() - 1);
} else {
// Actually charge the member
doCharge(feeLog, amount, member);
}
} catch (Exception e) {
final BigDecimal theAmount = amount;
// When the transaction ends, make sure to record the error for this member
CurrentTransactionData.addTransactionEndListener(new TransactionEndListener() {
@Override
protected void onTransactionEnd(final boolean commit) {
transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(final TransactionStatus status) {
// Reload the account fee log
AccountFeeLog feeLog = getFeeLog();
// Increment the error counter if not recharging failures
if (!feeLog.isRechargingFailed()) {
feeLog.setFailedMembers(feeLog.getFailedMembers() + 1);
}
// Add the charging result for this member with no transfer / invoice, so it won't be taken again in case of failures
accountFeeService.setChargingError(feeLog, member, theAmount);
}
});
}
});
// Break the batch in case of exceptions
return;
}
}
}
private MemberAccountFeeLog doCharge(final AccountFeeLog feeLog, final BigDecimal amount, final Member member) {
Transfer transfer = null;
Invoice invoice = null;
// Charge the account fee if the amount is ok
if (amount.compareTo(BigDecimal.ZERO) > 0) {
AccountFee fee = feeLog.getAccountFee();
// Check who is paying: the member or the system
if (fee.getPaymentDirection() == PaymentDirection.TO_SYSTEM) {
// Member paying to system
if (fee.getInvoiceMode() == InvoiceMode.ALWAYS) {
invoice = sendInvoice(fee, feeLog, member, amount);
} else {
try {
transfer = insertTransfer(fee, feeLog, member, amount);
} catch (final NotEnoughCreditsException e) {
// Sends an Invoice to this member!
invoice = sendInvoice(fee, feeLog, member, amount);
}
}
} else {
// System paying to member
transfer = insertTransfer(fee, feeLog, member, amount);
}
}
// Compute the result
MemberAccountFeeLog result = accountFeeService.setChargingSuccess(feeLog, member, amount, transfer, invoice);
// When recharging failed members, decrement the error counter
if (feeLog.isRechargingFailed()) {
feeLog.setFailedMembers(feeLog.getFailedMembers() - 1);
}
return result;
}
private AccountFeeLog getFeeLog() {
return accountFeeService.loadLog(logBeingCharged);
}
/**
* Returns a description for a transfer
*/
private String getPaymentDescription(AccountFee fee, final AccountFeeLog feeLog, final Member member, final BigDecimal amount) {
final LocalSettings localSettings = settingsService.getLocalSettings();
final AmountConverter amountConverter = localSettings.getAmountConverter();
final UnitsConverter unitsConverter = localSettings.getUnitsConverter(fee.getAccountType().getCurrency().getPattern());
final CalendarConverter dateConverter = localSettings.getRawDateConverter();
final Map<String, Object> values = new HashMap<String, Object>();
final Amount amountValue = feeLog.getAmountValue();
if (amountValue.getType() == Amount.Type.PERCENTAGE) {
values.put("fee_amount", amountConverter.toString(amountValue));
} else {
values.put("fee_amount", unitsConverter.toString(amountValue.getValue()));
}
values.put("free_base", unitsConverter.toString(fee.getFreeBase()));
values.put("result", unitsConverter.toString(amount));
final Period period = feeLog.getPeriod();
values.put("begin_date", dateConverter.toString(period == null ? null : period.getBegin()));
values.put("end_date", dateConverter.toString(period == null ? null : period.getEnd()));
fee = fetchService.fetch(fee, AccountFee.Relationships.TRANSFER_TYPE);
return MessageProcessingHelper.processVariables(fee.getTransferType().getDescription(), values);
}
/**
* Insert the transfer that charges a fee
*/
private Transfer insertTransfer(final AccountFee fee, final AccountFeeLog feeLog, final Member member, final BigDecimal amount) {
final TransferDTO dto = new TransferDTO();
dto.setAutomatic(true);
// Determine who pays
if (fee.getPaymentDirection() == PaymentDirection.TO_SYSTEM) {
// Member paying to system
dto.setFromOwner(member);
dto.setToOwner(SystemAccountOwner.instance());
// We force the payment if either a volume fee or the invoice mode is never
dto.setForced(fee.getChargeMode().isVolume() || fee.getInvoiceMode() == InvoiceMode.NEVER);
} else {
// System paying to member
dto.setFromOwner(SystemAccountOwner.instance());
dto.setToOwner(member);
}
dto.setAmount(amount);
dto.setTransferType(fee.getTransferType());
dto.setDescription(getPaymentDescription(fee, feeLog, member, amount));
dto.setAccountFeeLog(feeLog);
final Transfer transfer = (Transfer) paymentService.insertWithNotification(dto);
loggingHandler.logAccountFeePayment(transfer);
return transfer;
}
/**
* Sends an invoice for a fee
*/
private Invoice sendInvoice(final AccountFee fee, final AccountFeeLog feeLog, final Member member, final BigDecimal amount) {
Invoice invoice = new Invoice();
invoice.setFromMember(null);
invoice.setFrom(SystemAccountOwner.instance());
invoice.setTo(member);
invoice.setAmount(amount);
invoice.setTransferType(fee.getTransferType());
invoice.setDescription(getPaymentDescription(fee, feeLog, member, amount));
invoice.setAccountFeeLog(feeLog);
invoice = invoiceService.sendAutomatically(invoice);
loggingHandler.logAccountFeeInvoice(invoice);
return invoice;
}
}