/*
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.services.transfertypes;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import nl.strohalm.cyclos.dao.accounts.fee.transaction.TransactionFeeDAO;
import nl.strohalm.cyclos.dao.members.brokerings.BrokeringCommissionStatusDAO;
import nl.strohalm.cyclos.dao.members.brokerings.DefaultBrokerCommissionDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.accounts.Account;
import nl.strohalm.cyclos.entities.accounts.AccountOwner;
import nl.strohalm.cyclos.entities.accounts.AccountType;
import nl.strohalm.cyclos.entities.accounts.Currency;
import nl.strohalm.cyclos.entities.accounts.MemberAccount;
import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.BrokerCommission;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.BrokerCommission.When;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.BrokerCommission.WhichBroker;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.SimpleTransactionFee;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.SimpleTransactionFee.ARateRelation;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.TransactionFee;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.TransactionFee.ChargeType;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.TransactionFee.Nature;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.TransactionFee.Subject;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.TransactionFeeQuery;
import nl.strohalm.cyclos.entities.accounts.transactions.Invoice;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferType;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferTypeQuery;
import nl.strohalm.cyclos.entities.exceptions.DaoException;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.BrokerGroup;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.members.brokerings.BrokerCommissionContract;
import nl.strohalm.cyclos.entities.members.brokerings.Brokering;
import nl.strohalm.cyclos.entities.members.brokerings.BrokeringCommissionStatus;
import nl.strohalm.cyclos.entities.members.brokerings.BrokeringCommissionStatusQuery;
import nl.strohalm.cyclos.entities.members.brokerings.DefaultBrokerCommission;
import nl.strohalm.cyclos.entities.members.brokerings.DefaultBrokerCommissionQuery;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.services.accounts.AccountDTO;
import nl.strohalm.cyclos.services.accounts.AccountServiceLocal;
import nl.strohalm.cyclos.services.accounts.rates.ARatedFeeDTO;
import nl.strohalm.cyclos.services.accounts.rates.RateServiceLocal;
import nl.strohalm.cyclos.services.accounts.rates.RatesDTO;
import nl.strohalm.cyclos.services.accounts.rates.RatesPreviewDTO;
import nl.strohalm.cyclos.services.accounts.rates.RatesResultDTO;
import nl.strohalm.cyclos.services.elements.BrokeringServiceLocal;
import nl.strohalm.cyclos.services.elements.CommissionServiceLocal;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.services.transactions.TransactionContext;
import nl.strohalm.cyclos.utils.Amount;
import nl.strohalm.cyclos.utils.MessageProcessingHelper;
import nl.strohalm.cyclos.utils.MessageResolver;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.TimePeriod.Field;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.conversion.NumberConverter;
import nl.strohalm.cyclos.utils.conversion.UnitsConverter;
import nl.strohalm.cyclos.utils.query.PageHelper;
import nl.strohalm.cyclos.utils.validation.GeneralValidation;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.RequiredValidation;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.Validator;
import nl.strohalm.cyclos.utils.validation.Validator.Property;
import nl.strohalm.cyclos.webservices.model.TransactionFeeVO;
import nl.strohalm.cyclos.webservices.utils.PaymentHelper;
/**
* Implementation class for transaction fee service
* @author Jefferson Magno
* @author rafael
* @author Rinke (everything with rates)
*/
public class TransactionFeeServiceImpl implements TransactionFeeServiceLocal {
/**
* Count is required when "When" property is COUNT or DAYS
* @author Jefferson Magno
*/
public class CountValidation implements PropertyValidation {
private static final long serialVersionUID = -8075223146818925038L;
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final BrokerCommission commission = (BrokerCommission) object;
final When when = commission.getWhen();
if (when == When.COUNT || when == When.DAYS) {
return RequiredValidation.instance().validate(object, property, value);
}
return null;
}
}
/**
* Validates the generated transfer type
* @author Jefferson Magno
*/
public class GeneratedTransferTypeValidation implements GeneralValidation {
private static final long serialVersionUID = 1616929350799341483L;
@Override
public ValidationError validate(final Object object) {
final TransactionFee fee = (TransactionFee) object;
final TransferType generatedType = fetchService.fetch(fee.getGeneratedTransferType(), TransferType.Relationships.FROM);
fee.setGeneratedTransferType(generatedType);
// This validation is necessary only when updating a transaction fee
if (fee.isTransient()) {
return null;
}
// Retrieve saved fee and it's generated transfer type
final TransactionFee savedFee = load(fee.getId(), TransactionFee.Relationships.GENERATED_TRANSFER_TYPE);
final TransferType savedGeneratedType = savedFee.getGeneratedTransferType();
// If it's a broker commission paid by member, the generated type cannot be changed
if (savedFee.getNature() == Nature.BROKER && !savedFee.isFromSystem()) {
if (!generatedType.equals(savedGeneratedType)) {
return new ValidationError("transactionFee.error.cannotChangeGeneratedType");
}
}
// It's not allowed to change a "from system" generated type to a "from member" generated type
if (savedFee.isFromSystem() && !fee.isFromSystem()) {
return new ValidationError("transactionFee.erro.fromSystemGeneratedTypeRequired");
}
return null;
}
}
private static class PayerAndReceiverValidation implements GeneralValidation {
private static final long serialVersionUID = -1969165853079125625L;
@Override
public ValidationError validate(final Object object) {
final SimpleTransactionFee fee = (SimpleTransactionFee) object;
final Subject payer = fee.getPayer();
final Subject receiver = fee.getReceiver();
if ((payer == Subject.SOURCE || payer == Subject.DESTINATION) && payer == receiver) {
return new ValidationError("transactionFee.error.samePayerAndReceiver");
}
return null;
}
}
private MessageResolver messageResolver;
private AccountServiceLocal accountService;
private RateServiceLocal rateService;
private BrokeringServiceLocal brokeringService;
private CommissionServiceLocal commissionService;
private FetchServiceLocal fetchService;
private SettingsServiceLocal settingsService;
private TransferTypeServiceLocal transferTypeService;
private BrokeringCommissionStatusDAO brokeringCommissionStatusDao;
private DefaultBrokerCommissionDAO defaultBrokerCommissionDao;
private TransactionFeeDAO transactionFeeDao;
private PaymentHelper paymentHelper;
@Override
public Transfer buildTransfer(final BuildTransferWithFeesDTO params) {
final Calendar date = params.getDate();
final Account from = params.getFrom();
final Account to = params.getTo();
final BigDecimal transferAmount = params.getTransferAmount();
TransactionFee fee = params.getFee();
final boolean simulation = params.isSimulation();
final Calendar rawARateParam = params.getEmissionDate();
final Calendar rawDRateParam = params.getExpirationDate();
if (fee.isTransient()) {
throw new UnexpectedEntityException();
}
fee = fetchService.fetch(fee, TransactionFee.Relationships.ORIGINAL_TRANSFER_TYPE, RelationshipHelper.nested(TransactionFee.Relationships.GENERATED_TRANSFER_TYPE, TransferType.Relationships.FROM, TransferType.Relationships.TO, AccountType.Relationships.CURRENCY), TransactionFee.Relationships.FROM_GROUPS, TransactionFee.Relationships.TO_GROUPS, TransactionFee.Relationships.FROM_FIXED_MEMBER, SimpleTransactionFee.Relationships.TO_FIXED_MEMBER);
if (!doTests(fee, transferAmount, from, to)) {
return null;
}
BrokerCommissionContract commissionContract = null;
ChargeType feeChargeType = fee.getChargeType();
BigDecimal feeValue = fee.getValue();
final TransferType generated = fee.getGeneratedTransferType();
final AccountOwner fromOwner = getOwner(fee.getPayer(), fee.getFromFixedMember(), from, to);
// Get the to owner
AccountOwner toOwner = null;
if (fee.getNature() == Nature.SIMPLE) {
SimpleTransactionFee simpleFee = (SimpleTransactionFee) fee;
toOwner = getOwner(simpleFee.getReceiver(), simpleFee.getToFixedMember(), from, to);
} else { // It´s a broker commission
// TODO this is an extremely long method. What about putting this broker part in a separate method?
final BrokerCommission brokerCommission = (BrokerCommission) fee;
// Check if the broker is the source´s broker or is destination´s broker
Account relatedAccount = null;
switch (brokerCommission.getWhichBroker()) {
case SOURCE:
relatedAccount = from;
break;
case DESTINATION:
relatedAccount = to;
break;
}
// When source member pays destination´s broker or when destination member pays source´s broker, it´s a cross payment
boolean crossPayment = false;
if ((fee.getPayer() == TransactionFee.Subject.SOURCE && brokerCommission.getWhichBroker() == BrokerCommission.WhichBroker.DESTINATION) || (fee.getPayer() == TransactionFee.Subject.DESTINATION && brokerCommission.getWhichBroker() == BrokerCommission.WhichBroker.SOURCE)) {
crossPayment = true;
}
// The toOwner is a member's broker
if (relatedAccount instanceof MemberAccount) {
final Member member = ((MemberAccount) relatedAccount).getMember();
toOwner = member.getBroker();
// For a broker commission, first, get the brokering
final Brokering brokering = brokeringService.getActiveBrokering(member);
// There is no active brokering, return null
if (brokering == null) {
return null;
}
final Member broker = brokering.getBroker();
// Check broker groups
if (!brokerCommission.isAllBrokerGroups()) {
final BrokerGroup brokerGroup = (BrokerGroup) fetchService.fetch(brokering.getBroker().getGroup());
if (!brokerCommission.getBrokerGroups().contains(brokerGroup)) {
return null;
}
}
// When the broker commission status is created, it's data is gotten from the default broker commission (commissions
// paid by member) or from the broker commission (commissions paid by system)
BrokeringCommissionStatus brokeringCommissionStatus = commissionService.getBrokeringCommissionStatus(brokering, brokerCommission);
boolean testBrokeringCommissionStatus = false;
When when = null;
int maxCount = 0;
Amount feeAmount;
// Member paying commission to one broker
final boolean fromMember = generated.isFromMember();
if (fromMember) {
if (crossPayment) {
// On cross payments, the commission is always charged and the fee is retrieved from the commission it self
// The brokering commission status conditions are never tested
feeAmount = brokerCommission.getAmount();
} else {
commissionContract = commissionService.getActiveBrokerCommissionContract(brokering, brokerCommission);
if (commissionContract != null) {
// There is an active broker contract, the fee is retrieved from the contract
feeAmount = commissionContract.getAmount();
} else if (brokeringCommissionStatus != null) {
// There is a default broker commission, the fee is retrieved from the brokering commission status
if (brokeringCommissionStatus.getPeriod().getEnd() != null) {
return null;
}
// Parameters for testing broker commission status conditions
testBrokeringCommissionStatus = true;
when = brokeringCommissionStatus.getWhen();
if (when != BrokerCommission.When.ALWAYS) {
maxCount = brokeringCommissionStatus.getMaxCount();
}
feeAmount = brokeringCommissionStatus.getAmount();
} else {
// If there is not a contract and there is not a default broker commission, no commission will be charged
return null;
}
}
} else {
// System paying a commission to a broker
// Parameters for testing brokering commission status conditions
testBrokeringCommissionStatus = true;
when = brokerCommission.getWhen();
if (when != BrokerCommission.When.ALWAYS) {
maxCount = brokerCommission.getCount();
}
feeAmount = brokeringCommissionStatus.getAmount();
}
// Set the charge data
feeChargeType = ChargeType.from(feeAmount.getType());
feeValue = feeAmount.getValue();
// Test brokering commission status conditions (suspended, number of transactions or period of validity)
if (testBrokeringCommissionStatus) {
// When member's pay, a DefaultBrokerCommission is required
if (fromMember) {
// The broker commission is suspended, don´t charge the commission
final DefaultBrokerCommission defaultBrokerCommission = commissionService.getDefaultBrokerCommission(broker, brokerCommission);
if (defaultBrokerCommission != null && defaultBrokerCommission.isSuspended()) {
return null;
}
}
// The brokering commission status is closed, don't charge commission anymore
if (brokeringCommissionStatus.getPeriod().getEnd() != null) {
return null;
}
if (when == BrokerCommission.When.COUNT) {
final int count = brokeringCommissionStatus.getTotal().getCount();
// Number of transactions exceeded, don't charge commission anymore
if (count >= maxCount) {
return null;
}
// This is the last transaction that generates the fee, so it's necessary to close the brokering commission status
if (count == (maxCount - 1) && !simulation) {
brokeringCommissionStatus = commissionService.closeBrokeringCommissionStatus(brokeringCommissionStatus);
}
}
if (when == BrokerCommission.When.DAYS) {
// Pays if the current day is not beyond max. day stored in the fee or default broker commission
final Calendar begin = brokeringCommissionStatus.getPeriod().getBegin();
final Calendar maxDay = new TimePeriod(maxCount, Field.DAYS).add(begin);
// Period expired, dont charge commission anymore
if (Calendar.getInstance().after(maxDay)) {
return null;
}
}
}
if (!simulation && !crossPayment && commissionContract == null) {
// Update total count and total amount of the brokering commission status
brokeringCommissionStatus.setTotal(brokeringCommissionStatus.getTotal().add(transferAmount));
commissionService.updateBrokeringCommissionStatus(brokeringCommissionStatus);
}
}
}// END OF BROKER PART
// Check if we found the from and to owner
if (fromOwner == null || toOwner == null) {
return null;
}
if (fromOwner instanceof Member && fromOwner.equals(toOwner)) {
// If the combination resulted in the same member as from and to, don't generate the fee
return null;
}
final LocalSettings localSettings = settingsService.getLocalSettings();
// Get the accounts
Account fromAccount;
Account toAccount;
try {
fromAccount = accountService.getAccount(new AccountDTO(fromOwner, generated.getFrom()));
toAccount = accountService.getAccount(new AccountDTO(toOwner, generated.getTo()));
} catch (EntityNotFoundException e) {
// If any of the expected accounts are not found, the fee is not generated
return null;
}
if (fromAccount.equals(toAccount)) {
// Same accounts? Don't charge the fee
return null;
}
// Calculate the fee amount
BigDecimal amount = BigDecimal.ZERO;
final Currency currency = fromAccount.getType().getCurrency();
switch (feeChargeType) {
case FIXED:
amount = feeValue;
break;
case PERCENTAGE:
amount = Amount.percentage(feeValue).apply(transferAmount);
break;
case A_RATE:
case MIXED_A_D_RATES:
if (rawARateParam != null) {
feeValue = rateService.getARatedFeePercentage(fee, rawARateParam, rawDRateParam, date);
fee.setAmountForRates(Amount.percentage(feeValue));
amount = Amount.percentage(feeValue).apply(transferAmount);
}
break;
case D_RATE:
if (rawDRateParam != null) {
final RatesDTO setOfUnits = RatesDTO.createSetOfUnitsForMerge(transferAmount, rawDRateParam, currency);
setOfUnits.setDate(params.getDate());
final BigDecimal result = rateService.getDRateConversionResult(setOfUnits);
amount = transferAmount.subtract(result);
final MathContext mc = new MathContext(LocalSettings.BIG_DECIMAL_DIVISION_PRECISION);
feeValue = amount.multiply(new BigDecimal(100)).divide(transferAmount, mc);
fee.setAmountForRates(Amount.percentage(feeValue));
}
break;
}
// A very small fee which was rounded to zero
amount = localSettings.round(amount);
if (!params.isShowZeroFees() && amount.compareTo(BigDecimal.ZERO) <= 0) {
return null;
}
// Get the description variables
final String description = fee.getGeneratedTransferType().getDescription();
final UnitsConverter unitsConverter = localSettings.getUnitsConverter(generated.getFrom().getCurrency().getPattern());
final NumberConverter<BigDecimal> numberConverter = localSettings.getNumberConverter();
final Map<String, Object> values = new HashMap<String, Object>();
values.put("amount", unitsConverter.toString(amount));
values.put("transfer", unitsConverter.toString(transferAmount));
values.put("original_amount", values.get("transfer")); // Aliasing to keep old transfer
switch (feeChargeType) {
case FIXED:
values.put("fee", unitsConverter.toString(feeValue));
break;
case PERCENTAGE:
values.put("fee", numberConverter.toString(feeValue) + "%");
break;
case A_RATE:
values.put("fee", numberConverter.toString(feeValue) + "%");
BigDecimal aRate = getRoundedARate(date, rawARateParam, localSettings);
values.put("a_rate", aRate);
break;
case MIXED_A_D_RATES:
values.put("fee", numberConverter.toString(feeValue) + "%");
BigDecimal aRate1 = getRoundedARate(date, rawARateParam, localSettings);
values.put("a_rate", aRate1);
// break was left out deliberately
case D_RATE:
RatesDTO ratesDTO = new RatesDTO();
ratesDTO.setExpirationDate(rawDRateParam);
ratesDTO.setDate(date);
ratesDTO.setCurrency(currency);
BigDecimal dRate = rateService.dateToRate(ratesDTO).getdRate();
dRate = localSettings.round(dRate);
values.put("d_rate", dRate);
break;
}
values.put("fee_amount", values.get("fee")); // Aliasing to keep old transfer
values.put("member", fromAccount.getOwnerName());
// Build the transfer
final Transfer feeTransfer = new Transfer();
feeTransfer.setFrom(fromAccount);
feeTransfer.setTo(toAccount);
feeTransfer.setAmount(amount);
feeTransfer.setDescription(MessageProcessingHelper.processVariables(description, values));
feeTransfer.setType(generated);
feeTransfer.setTransactionFee(fee);
if (commissionContract != null) {
feeTransfer.setBrokerCommissionContract(commissionContract);
}
return feeTransfer;
}
@Override
public Collection<ChargeType> getPossibleChargeType(final TransferType originalTransferType, final Nature feeNature) {
Collection<ChargeType> chargeTypes = Arrays.asList(ChargeType.FIXED, ChargeType.PERCENTAGE);
TransferType transferType = fetchService.fetch(originalTransferType, TransferType.Relationships.FROM, TransferType.Relationships.TO);
if (feeNature == TransactionFee.Nature.SIMPLE) {
// Rates are allowed only in payments from member to unlimited system accounts
if (transferType.isFromMember() && transferType.isToSystem() && !transferType.getTo().isLimited()) {
final Currency currency = transferType.getCurrency();
final boolean allowARate = currency.isEnableARate();
final boolean allowDRate = currency.isEnableDRate();
chargeTypes = EnumSet.allOf(ChargeType.class);
if (!allowARate) {
chargeTypes.remove(ChargeType.A_RATE);
chargeTypes.remove(ChargeType.MIXED_A_D_RATES);
}
if (!allowDRate) {
chargeTypes.remove(ChargeType.D_RATE);
}
}
}
return chargeTypes;
}
@Override
public Collection<Subject> getPossibleSubjects(TransferType originalTransferType, final Nature nature) {
originalTransferType = fetchService.fetch(originalTransferType, TransferType.Relationships.FROM, TransferType.Relationships.TO);
switch (nature) {
case SIMPLE:
if (originalTransferType.isFromSystem()) {
if (originalTransferType.isToSystem()) {
// System to system may only have system as payer or receiver
return Collections.singleton(Subject.SYSTEM);
} else {
// System to member may have system or destination as payer or receiver
return EnumSet.of(Subject.SYSTEM, Subject.DESTINATION, Subject.DESTINATION_BROKER, Subject.FIXED_MEMBER);
}
} else {
if (originalTransferType.isToSystem()) {
// Member to member may have source or system as payer or receiver
return EnumSet.of(Subject.SYSTEM, Subject.SOURCE, Subject.SOURCE_BROKER, Subject.FIXED_MEMBER);
} else {
// Member to member may be any
return EnumSet.allOf(Subject.class);
}
}
case BROKER:
// BrokerCommissions can't be fixed member
return EnumSet.of(Subject.SYSTEM, Subject.SOURCE, Subject.DESTINATION);
default:
throw new IllegalArgumentException("Unexpected transaction fee nature: " + nature);
}
}
@Override
public List<TransactionFeeVO> getTransactionFeeVOs(final TransactionFeePreviewDTO preview) {
Map<TransactionFee, BigDecimal> fees = preview.getFees();
return paymentHelper.toTransactionFeeVOs(fees);
}
@Override
public TransactionFee load(final Long id, final Relationship... fetch) {
return transactionFeeDao.load(id, fetch);
}
@Override
public TransactionFeePreviewDTO preview(final AccountOwner from, final AccountOwner to, TransferType transferType, final BigDecimal amount) {
RatesResultDTO rates = new RatesResultDTO();
transferType = fetchService.fetch(transferType);
// if the TT has rated fees, the rates MUST be provided.
if (transferType.isHavingRatedFees()) {
final Account fromAccount = accountService.getAccount(new AccountDTO(from, transferType.getFrom()));
rates = rateService.getRatesForTransferFrom(fromAccount, amount, null);
}
return preview(from, to, transferType, amount, rates);
}
@Override
public TransactionFeePreviewDTO preview(AccountOwner from, final AccountOwner to, TransferType transferType, final BigDecimal amount, final RatesResultDTO rates) {
Calendar rawARate = rates.getEmissionDate();
Calendar rawDRate = rates.getExpirationDate();
final LocalSettings localSettings = settingsService.getLocalSettings();
if (from == null) {
from = LoggedUser.accountOwner();
}
boolean showZeroFees = (rates instanceof RatesPreviewDTO && ((RatesPreviewDTO) rates).isGraph());
final Account fromAccount = accountService.getAccount(new AccountDTO(from, transferType.getFrom()));
final Account toAccount = accountService.getAccount(new AccountDTO(to, transferType.getTo()));
BigDecimal finalAmount = amount;
final Map<TransactionFee, BigDecimal> map = new LinkedHashMap<TransactionFee, BigDecimal>();
transferType = fetchService.fetch(transferType, TransferType.Relationships.TRANSACTION_FEES);
final Collection<? extends TransactionFee> fees = transferType.getTransactionFees();
final Calendar processDate = (rates != null && rates.getDate() != null) ? rates.getDate() : Calendar.getInstance();
if (fees != null && !fees.isEmpty()) {
// Search for enabled fees that the source pays
for (final TransactionFee fee : fees) {
// We just want fees the source member would pay
if (!shouldPreviewFee(from, to, amount, fee)) {
continue;
}
// The buildTransfer() method returns a transfer if the fee should be applied or null
final BuildTransferWithFeesDTO buildParams = new BuildTransferWithFeesDTO(processDate, fromAccount, toAccount, amount, fee, true);
buildParams.setEmissionDate(rawARate);
buildParams.setExpirationDate(rawDRate);
buildParams.setShowZeroFees(showZeroFees);
final Transfer generatedTransfer = buildTransfer(buildParams);
if (generatedTransfer != null) {
final BigDecimal feeAmount = generatedTransfer.getAmount();
map.put(fee, feeAmount);
// Check deducted amount
if (fee.isDeductAmount()) {
finalAmount = finalAmount.subtract(feeAmount);
}
}
}
}
final TransactionFeePreviewForRatesDTO result = new TransactionFeePreviewForRatesDTO();
result.setFees(map);
result.setFinalAmount(localSettings.round(finalAmount));
result.setAmount(localSettings.round(amount));
result.setARate(rates.getaRate());
result.setDRate(rates.getdRate());
return result;
}
@Override
public TransactionFeePreviewDTO preview(final Invoice invoice) {
TransferType transferType = invoice.getTransferType();
if (transferType == null) {
return null;
}
return preview(invoice.getTo(), invoice.getFrom(), transferType, invoice.getAmount());
}
@Override
public int remove(final Long... ids) {
for (final Long id : ids) {
final TransactionFee transactionFee = load(id);
if (transactionFee instanceof BrokerCommission) {
// Before removing the broker commission, check if it was already charged ...
final BrokerCommission brokerCommission = (BrokerCommission) transactionFee;
BrokeringCommissionStatusQuery query = new BrokeringCommissionStatusQuery();
query.setBrokerCommission(brokerCommission);
query.setAlreadyCharged(true);
query.setPageForCount();
List<BrokeringCommissionStatus> brokeringCommissionStatusList = brokeringCommissionStatusDao.search(query);
// ... if it was already charged, it´s not possible to remove the broker commission, so throw an exception
if (PageHelper.getTotalCount(brokeringCommissionStatusList) > 0) {
throw new DaoException();
}
// ... if it was not charged yet, remove the brokering commission status objects related to it
query = new BrokeringCommissionStatusQuery();
query.setBrokerCommission(brokerCommission);
brokeringCommissionStatusList = brokeringCommissionStatusDao.search(query);
for (final BrokeringCommissionStatus brokeringCommissionStatus : brokeringCommissionStatusList) {
brokeringCommissionStatusDao.delete(brokeringCommissionStatus.getId());
}
// Check if the commission was already customized by the broker ...
DefaultBrokerCommissionQuery defaultBrokerCommissionQuery = new DefaultBrokerCommissionQuery();
defaultBrokerCommissionQuery.setBrokerCommission(brokerCommission);
defaultBrokerCommissionQuery.setSetByBroker(true);
defaultBrokerCommissionQuery.setPageForCount();
List<DefaultBrokerCommission> defaultBrokerCommissions = defaultBrokerCommissionDao.search(defaultBrokerCommissionQuery);
// ... if it was already customized, it´s not possible to remove the broker commission, so throw an exception
if (PageHelper.getTotalCount(defaultBrokerCommissions) > 0) {
throw new DaoException();
}
// ... if it was not customized yet, remove the default broker commissions related to it
defaultBrokerCommissionQuery = new DefaultBrokerCommissionQuery();
defaultBrokerCommissionQuery.setBrokerCommission(brokerCommission);
defaultBrokerCommissions = defaultBrokerCommissionDao.search(defaultBrokerCommissionQuery);
for (final DefaultBrokerCommission defaultBrokerCommission : defaultBrokerCommissions) {
defaultBrokerCommissionDao.delete(defaultBrokerCommission.getId());
}
}
}
return transactionFeeDao.delete(ids);
}
@Override
public BrokerCommission save(BrokerCommission brokerCommission) {
preSave(brokerCommission);
if (brokerCommission.isAllBrokerGroups()) {
brokerCommission.setBrokerGroups(null);
}
validate(brokerCommission);
if (brokerCommission.isTransient()) {
brokerCommission = transactionFeeDao.insert(brokerCommission);
if (brokerCommission.isFromMember()) {
commissionService.createDefaultBrokerCommissions(brokerCommission);
} else { // Broker commission paid by system
commissionService.createBrokeringCommissionStatus(brokerCommission);
}
} else {
final BrokerCommission savedBrokerCommission = transactionFeeDao.load(brokerCommission.getId(), BrokerCommission.Relationships.BROKER_GROUPS);
fetchService.removeFromCache(savedBrokerCommission);
brokerCommission = transactionFeeDao.update(brokerCommission);
if (brokerCommission.isFromMember()) {
commissionService.updateDefaultBrokerCommissions(brokerCommission, savedBrokerCommission);
} else { // Broker commission paid by system
commissionService.updateBrokeringCommissionStatus(brokerCommission, savedBrokerCommission);
}
}
return brokerCommission;
}
@Override
public SimpleTransactionFee save(SimpleTransactionFee fee, final ARateRelation aRateRelation) {
preSave(fee);
// Ensure deduct amount is set for rates
if (Arrays.asList(ChargeType.A_RATE, ChargeType.D_RATE, ChargeType.MIXED_A_D_RATES).contains(fee.getChargeType())) {
fee.setDeductAmount(true);
}
validate(fee, aRateRelation);
if (fee.getReceiver() != Subject.FIXED_MEMBER) {
fee.setToFixedMember(null);
}
if (fee.isTransient()) {
fee = transactionFeeDao.insert(fee);
} else {
fee = transactionFeeDao.update(fee);
}
return fee;
}
@Override
public List<TransactionFee> search(final TransactionFeeQuery query) {
return transactionFeeDao.search(query);
}
@Override
public List<TransferType> searchGeneratedTransferTypes(final TransactionFee fee, final boolean allowAnyAccount, final boolean onlyFromSystem) {
TransferTypeQuery generatedQuery = buildGeneratedTypeQuery(fee, allowAnyAccount);
if (onlyFromSystem) {
generatedQuery.setFromNature(AccountType.Nature.SYSTEM);
}
return transferTypeService.search(generatedQuery);
}
public void setAccountServiceLocal(final AccountServiceLocal accountService) {
this.accountService = accountService;
}
public void setBrokeringCommissionStatusDao(final BrokeringCommissionStatusDAO brokeringCommissionStatusDao) {
this.brokeringCommissionStatusDao = brokeringCommissionStatusDao;
}
public void setBrokeringServiceLocal(final BrokeringServiceLocal brokeringService) {
this.brokeringService = brokeringService;
}
public void setCommissionServiceLocal(final CommissionServiceLocal commissionService) {
this.commissionService = commissionService;
}
public void setDefaultBrokerCommissionDao(final DefaultBrokerCommissionDAO defaultBrokerCommissionDao) {
this.defaultBrokerCommissionDao = defaultBrokerCommissionDao;
}
public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public void setMessageResolver(final MessageResolver messageResolver) {
this.messageResolver = messageResolver;
}
public void setPaymentHelper(final PaymentHelper paymentHelper) {
this.paymentHelper = paymentHelper;
}
public void setRateServiceLocal(final RateServiceLocal rateService) {
this.rateService = rateService;
}
public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
public void setTransactionFeeDao(final TransactionFeeDAO dao) {
transactionFeeDao = dao;
}
public void setTransferTypeServiceLocal(final TransferTypeServiceLocal transferTypeService) {
this.transferTypeService = transferTypeService;
}
@Override
public void validate(final BrokerCommission brokerCommission) {
getValidator(brokerCommission).validate(brokerCommission);
}
@Override
public void validate(final SimpleTransactionFee transactionFee, final ARateRelation aRateRelation) {
getValidator(transactionFee, aRateRelation).validate(transactionFee);
}
/**
* Checks whether the given fee will be shown on the preview
*/
protected boolean shouldPreviewFee(final AccountOwner from, final AccountOwner to, final BigDecimal amount, final TransactionFee fee) {
if (fee.getPayer() == Subject.SOURCE || from.equals(fee.getFromFixedMember())) {
return true;
} else if (fee.getPayer() == Subject.SYSTEM && (from instanceof SystemAccountOwner)) {
return true;
}
return false;
}
private Member brokerOf(final AccountOwner owner) {
if (owner instanceof Member) {
return ((Member) owner).getBroker();
}
return null;
}
private TransferTypeQuery buildGeneratedTypeQuery(final TransactionFee fee, final boolean allowAnyAccount) {
final TransferType originalTransferType = fetchService.fetch(fee.getOriginalTransferType(), TransferType.Relationships.FROM, TransferType.Relationships.TO);
final AccountType fromAccountType = originalTransferType.getFrom();
final AccountType toAccountType = originalTransferType.getTo();
final TransferTypeQuery generatedQuery = new TransferTypeQuery();
generatedQuery.setContext(TransactionContext.ANY);
// Get the from account
switch (fee.getPayer()) {
case SYSTEM:
generatedQuery.setFromNature(AccountType.Nature.SYSTEM);
break;
case SOURCE:
if (allowAnyAccount) {
generatedQuery.setFromNature(fromAccountType.getNature());
} else {
generatedQuery.setFromAccountType(fromAccountType);
}
break;
case DESTINATION:
if (allowAnyAccount) {
generatedQuery.setFromNature(toAccountType.getNature());
} else {
generatedQuery.setFromAccountType(toAccountType);
}
break;
case FIXED_MEMBER:
if (allowAnyAccount) {
generatedQuery.setFromNature(AccountType.Nature.MEMBER);
} else {
generatedQuery.setFromAccountType(fromAccountType);
}
break;
case SOURCE_BROKER:
case DESTINATION_BROKER:
// There's no way to know the accounts of the broker
generatedQuery.setFromNature(AccountType.Nature.MEMBER);
break;
}
// The to account depends on the nature
switch (fee.getNature()) {
case SIMPLE:
switch (((SimpleTransactionFee) fee).getReceiver()) {
case SYSTEM:
generatedQuery.setToNature(AccountType.Nature.SYSTEM);
break;
case SOURCE:
if (allowAnyAccount) {
generatedQuery.setToNature(fromAccountType.getNature());
} else {
generatedQuery.setToAccountType(fromAccountType);
}
break;
case DESTINATION:
if (allowAnyAccount) {
generatedQuery.setToNature(toAccountType.getNature());
} else {
generatedQuery.setToAccountType(toAccountType);
}
break;
case FIXED_MEMBER:
if (allowAnyAccount) {
generatedQuery.setToNature(AccountType.Nature.MEMBER);
} else {
generatedQuery.setToAccountType(toAccountType);
}
break;
case SOURCE_BROKER:
case DESTINATION_BROKER:
// There's no way to know the accounts of the broker
generatedQuery.setToNature(AccountType.Nature.MEMBER);
break;
}
break;
case BROKER:
generatedQuery.setToNature(AccountType.Nature.MEMBER);
break;
}
return generatedQuery;
}
/**
* performs all initial tests for buildTransfer method
* @return false when tests not passed.
*/
private boolean doTests(final TransactionFee fee, final BigDecimal transferAmount, final Account from, final Account to) {
// If fee is not enabled, does not generate the fee transfer
if (!fee.isEnabled()) {
return false;
}
// Test the amount range
final BigDecimal initialAmount = fee.getInitialAmount();
if (initialAmount != null && transferAmount.compareTo(initialAmount) < 0) {
return false;
}
final BigDecimal finalAmount = fee.getFinalAmount();
if (finalAmount != null && transferAmount.compareTo(finalAmount) > 0) {
return false;
}
// Test from group
final AccountOwner originalFromAccountOwner = from.getOwner();
if (originalFromAccountOwner instanceof Member && !fee.isFromAllGroups()) {
final Member fromMember = fetchService.fetch((Member) originalFromAccountOwner, Element.Relationships.GROUP);
final MemberGroup fromGroup = fromMember.getMemberGroup();
if (!fee.getFromGroups().contains(fromGroup)) {
return false;
}
}
// Test to group
final AccountOwner originalToAccountOwner = to.getOwner();
if (originalToAccountOwner instanceof Member && !fee.isToAllGroups()) {
final Member toMember = fetchService.fetch((Member) originalToAccountOwner, Element.Relationships.GROUP);
final MemberGroup toGroup = toMember.getMemberGroup();
if (!fee.getToGroups().contains(toGroup)) {
return false;
}
}
return true;
}
private Validator getBasicValidator(final TransactionFee fee) {
final Validator validator = new Validator("transactionFee");
validator.property("name").required().maxLength(100);
validator.property("description").maxLength(1000);
validator.property("chargeType").required();
final Property value = validator.property("value");
if (fee.getChargeType() != null) {
switch (fee.getChargeType()) {
case PERCENTAGE:
value.required().positiveNonZero().lessEquals(100);
break;
case FIXED:
value.required().positiveNonZero();
break;
}
}
validator.property("originalTransferType").required();
validator.property("generatedTransferType").required();
validator.property("payer").required();
if (fee.getPayer() == Subject.FIXED_MEMBER) {
validator.property("fromFixedMember").key("transactionFee.fromFixedMember.name").required();
}
return validator;
}
private AccountOwner getOwner(final Subject subject, final Member fixedMember, final Account from, final Account to) {
switch (subject) {
case SYSTEM:
return SystemAccountOwner.instance();
case SOURCE:
return from.getOwner();
case DESTINATION:
return to.getOwner();
case FIXED_MEMBER:
return fixedMember;
case SOURCE_BROKER:
return brokerOf(from.getOwner());
case DESTINATION_BROKER:
return brokerOf(to.getOwner());
}
return null;
}
private BigDecimal getRoundedARate(final Calendar date, final Calendar rawARateParam, final LocalSettings localSettings) {
RatesDTO ratesDTO = new RatesDTO();
ratesDTO.setEmissionDate(rawARateParam);
ratesDTO.setDate(date);
BigDecimal aRate = rateService.dateToRate(ratesDTO).getaRate();
aRate = localSettings.round(aRate);
return aRate;
}
private Validator getValidator(final BrokerCommission commission) {
final Validator validator = getBasicValidator(commission);
validator.property("maxFixedValue").positiveNonZero();
validator.property("maxPercentageValue").positiveNonZero();
validator.property("when").required();
validator.property("count").add(new CountValidation());
validator.property("chargeType").anyOf(ChargeType.FIXED, ChargeType.PERCENTAGE);
validator.property("payer").anyOf(Subject.SYSTEM, Subject.SOURCE, Subject.DESTINATION);
Subject payer = commission.getPayer();
if (payer == Subject.SOURCE) {
validator.property("whichBroker").anyOf(WhichBroker.SOURCE);
} else if (payer == Subject.DESTINATION) {
validator.property("whichBroker").anyOf(WhichBroker.DESTINATION);
}
if (commission.getInitialAmount() != null && commission.getFinalAmount() != null) {
final Property initialAmount = validator.property("initialAmount");
final BigDecimal finalAmount = commission.getFinalAmount();
initialAmount.comparable(finalAmount, "<=", new ValidationError("errors.greaterThan", messageResolver.message("transactionFee.finalAmount")));
}
return validator;
}
private Validator getValidator(final SimpleTransactionFee fee, final ARateRelation arateRelation) {
final TransferType originalTransferType = fetchService.fetch(fee.getOriginalTransferType(), RelationshipHelper.nested(TransferType.Relationships.FROM, AccountType.Relationships.CURRENCY));
Validator validator = getBasicValidator(fee);
validator.property("receiver").required();
if (fee.getReceiver() == Subject.FIXED_MEMBER) {
validator.property("toFixedMember").key("transactionFee.toFixedMember.name").required();
}
validator.general(new PayerAndReceiverValidation());
final Collection<ChargeType> allowedChargeTypes = EnumSet.allOf(ChargeType.class);
final Currency currency = originalTransferType.getCurrency();
if (!currency.isEnableARate()) {
allowedChargeTypes.remove(ChargeType.A_RATE);
allowedChargeTypes.remove(ChargeType.MIXED_A_D_RATES);
}
if (!currency.isEnableDRate()) {
allowedChargeTypes.remove(ChargeType.D_RATE);
allowedChargeTypes.remove(ChargeType.MIXED_A_D_RATES);
}
final ARatedFeeDTO dto = new ARatedFeeDTO(fee);
validator = rateService.applyARateFieldsValidation(validator, dto, arateRelation);
validator.property("chargeType").anyOf(allowedChargeTypes);
if (fee.getInitialAmount() != null && fee.getFinalAmount() != null) {
final Property initialAmount = validator.property("initialAmount");
final BigDecimal finalAmount = fee.getFinalAmount();
initialAmount.comparable(finalAmount, "<=", new ValidationError("errors.greaterThan", messageResolver.message("transactionFee.finalAmount")));
}
return validator;
}
private void preSave(final TransactionFee transactionFee) {
if (transactionFee.isFromAllGroups()) {
transactionFee.setFromGroups(null);
}
if (transactionFee.isToAllGroups()) {
transactionFee.setToGroups(null);
}
if (transactionFee.getPayer() != Subject.FIXED_MEMBER) {
transactionFee.setFromFixedMember(null);
}
}
}