/* 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.transactions; import java.awt.Color; import java.math.BigDecimal; import java.math.BigInteger; import java.math.MathContext; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import nl.strohalm.cyclos.access.AdminMemberPermission; import nl.strohalm.cyclos.access.AdminSystemPermission; import nl.strohalm.cyclos.access.BrokerPermission; import nl.strohalm.cyclos.access.MemberPermission; import nl.strohalm.cyclos.access.OperatorPermission; import nl.strohalm.cyclos.dao.accounts.transactions.ScheduledPaymentDAO; import nl.strohalm.cyclos.dao.accounts.transactions.TraceNumberDAO; import nl.strohalm.cyclos.dao.accounts.transactions.TransferDAO; import nl.strohalm.cyclos.entities.Relationship; import nl.strohalm.cyclos.entities.access.Channel; import nl.strohalm.cyclos.entities.accounts.Account; import nl.strohalm.cyclos.entities.accounts.AccountOwner; import nl.strohalm.cyclos.entities.accounts.AccountStatus; import nl.strohalm.cyclos.entities.accounts.AccountType; import nl.strohalm.cyclos.entities.accounts.Currency; import nl.strohalm.cyclos.entities.accounts.LockedAccountsOnPayments; import nl.strohalm.cyclos.entities.accounts.MemberAccount; import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner; import nl.strohalm.cyclos.entities.accounts.SystemAccountType; import nl.strohalm.cyclos.entities.accounts.external.ExternalTransfer; import nl.strohalm.cyclos.entities.accounts.fees.transaction.BrokerCommission; import nl.strohalm.cyclos.entities.accounts.fees.transaction.SimpleTransactionFee; 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.TransactionFeeQuery; import nl.strohalm.cyclos.entities.accounts.transactions.AuthorizationLevel; import nl.strohalm.cyclos.entities.accounts.transactions.Invoice; import nl.strohalm.cyclos.entities.accounts.transactions.Payment; import nl.strohalm.cyclos.entities.accounts.transactions.PaymentRequestTicket; import nl.strohalm.cyclos.entities.accounts.transactions.ScheduledPayment; import nl.strohalm.cyclos.entities.accounts.transactions.Ticket; import nl.strohalm.cyclos.entities.accounts.transactions.TraceNumber; import nl.strohalm.cyclos.entities.accounts.transactions.Transfer; import nl.strohalm.cyclos.entities.accounts.transactions.TransferListener; import nl.strohalm.cyclos.entities.accounts.transactions.TransferQuery; import nl.strohalm.cyclos.entities.accounts.transactions.TransferType; import nl.strohalm.cyclos.entities.accounts.transactions.TransferType.Relationships; import nl.strohalm.cyclos.entities.accounts.transactions.TransferTypeQuery; import nl.strohalm.cyclos.entities.alerts.MemberAlert; import nl.strohalm.cyclos.entities.customization.fields.CustomField; import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomField; import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomFieldValue; 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.AdminGroup; import nl.strohalm.cyclos.entities.groups.BrokerGroup; import nl.strohalm.cyclos.entities.groups.Group; import nl.strohalm.cyclos.entities.groups.MemberGroup; import nl.strohalm.cyclos.entities.groups.OperatorGroup; import nl.strohalm.cyclos.entities.members.Element; import nl.strohalm.cyclos.entities.members.Member; import nl.strohalm.cyclos.entities.members.Operator; import nl.strohalm.cyclos.entities.members.brokerings.BrokerCommissionContract; import nl.strohalm.cyclos.entities.members.brokerings.BrokerCommissionContractQuery; import nl.strohalm.cyclos.entities.reports.StatisticalNumber; import nl.strohalm.cyclos.entities.services.ServiceClient; import nl.strohalm.cyclos.entities.settings.LocalSettings; import nl.strohalm.cyclos.entities.settings.LocalSettings.TransactionNumber; import nl.strohalm.cyclos.exceptions.ApplicationException; import nl.strohalm.cyclos.exceptions.PermissionDeniedException; import nl.strohalm.cyclos.services.accounts.AccountDTO; import nl.strohalm.cyclos.services.accounts.AccountDateDTO; import nl.strohalm.cyclos.services.accounts.AccountServiceLocal; import nl.strohalm.cyclos.services.accounts.rates.ConversionSimulationDTO; import nl.strohalm.cyclos.services.accounts.rates.RateServiceLocal; import nl.strohalm.cyclos.services.accounts.rates.RatesPreviewDTO; import nl.strohalm.cyclos.services.accounts.rates.RatesResultDTO; import nl.strohalm.cyclos.services.accounts.rates.RatesToSave; import nl.strohalm.cyclos.services.alerts.AlertServiceLocal; import nl.strohalm.cyclos.services.application.ApplicationServiceLocal; import nl.strohalm.cyclos.services.customization.PaymentCustomFieldServiceLocal; import nl.strohalm.cyclos.services.elements.CommissionServiceLocal; import nl.strohalm.cyclos.services.fetch.FetchServiceLocal; import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal; import nl.strohalm.cyclos.services.settings.SettingsServiceLocal; import nl.strohalm.cyclos.services.stats.StatisticalResultDTO; import nl.strohalm.cyclos.services.transactions.exceptions.AuthorizedPaymentInPastException; import nl.strohalm.cyclos.services.transactions.exceptions.MaxAmountPerDayExceededException; import nl.strohalm.cyclos.services.transactions.exceptions.NotEnoughCreditsException; import nl.strohalm.cyclos.services.transactions.exceptions.TransferMinimumPaymentException; import nl.strohalm.cyclos.services.transactions.exceptions.UpperCreditLimitReachedException; import nl.strohalm.cyclos.services.transfertypes.BuildTransferWithFeesDTO; import nl.strohalm.cyclos.services.transfertypes.TransactionFeePreviewDTO; import nl.strohalm.cyclos.services.transfertypes.TransactionFeePreviewForRatesDTO; import nl.strohalm.cyclos.services.transfertypes.TransactionFeeServiceLocal; import nl.strohalm.cyclos.services.transfertypes.TransferTypeServiceLocal; import nl.strohalm.cyclos.utils.BaseTransactional; import nl.strohalm.cyclos.utils.BigDecimalHelper; import nl.strohalm.cyclos.utils.CacheCleaner; import nl.strohalm.cyclos.utils.CustomObjectHandler; import nl.strohalm.cyclos.utils.DataIteratorHelper; import nl.strohalm.cyclos.utils.DateHelper; import nl.strohalm.cyclos.utils.MessageProcessingHelper; import nl.strohalm.cyclos.utils.MessageResolver; import nl.strohalm.cyclos.utils.Period; import nl.strohalm.cyclos.utils.RelationshipHelper; import nl.strohalm.cyclos.utils.TimePeriod; import nl.strohalm.cyclos.utils.TransactionHelper; import nl.strohalm.cyclos.utils.Transactional; import nl.strohalm.cyclos.utils.access.LoggedUser; import nl.strohalm.cyclos.utils.conversion.CalendarConverter; import nl.strohalm.cyclos.utils.conversion.CoercionHelper; import nl.strohalm.cyclos.utils.lock.LockHandler; import nl.strohalm.cyclos.utils.lock.LockHandlerFactory; import nl.strohalm.cyclos.utils.logging.LoggingHandler; import nl.strohalm.cyclos.utils.notifications.AdminNotificationHandler; import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler; import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType; import nl.strohalm.cyclos.utils.statistics.GraphHelper; import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData; import nl.strohalm.cyclos.utils.transaction.TransactionCommitListener; import nl.strohalm.cyclos.utils.validation.CompareToValidation; import nl.strohalm.cyclos.utils.validation.DelegatingValidator; import nl.strohalm.cyclos.utils.validation.GeneralValidation; import nl.strohalm.cyclos.utils.validation.InvalidError; import nl.strohalm.cyclos.utils.validation.PropertyValidation; import nl.strohalm.cyclos.utils.validation.RequiredError; import nl.strohalm.cyclos.utils.validation.UniqueError; import nl.strohalm.cyclos.utils.validation.ValidationError; import nl.strohalm.cyclos.utils.validation.ValidationException; import nl.strohalm.cyclos.utils.validation.Validator; import nl.strohalm.cyclos.utils.validation.Validator.Property; import nl.strohalm.cyclos.webservices.accounts.AccountHistoryResultPage; import nl.strohalm.cyclos.webservices.model.AccountHistoryTransferVO; import nl.strohalm.cyclos.webservices.model.AccountStatusVO; import nl.strohalm.cyclos.webservices.payments.AccountHistoryParams; import nl.strohalm.cyclos.webservices.utils.AccountHelper; import nl.strohalm.cyclos.webservices.utils.PaymentHelper; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.commons.lang.time.DateUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jfree.chart.plot.CategoryMarker; import org.jfree.chart.plot.Marker; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallbackWithoutResult; /** * Implementation for payment service * @author luis * @author rinke (rates stuff) */ public class PaymentServiceImpl implements PaymentServiceLocal { /** * A key to monitor which fees have been charged, in order to detect loops * @author luis */ private static class ChargedFee { private final TransactionFee fee; private final Account from; private final Account to; private ChargedFee(final TransactionFee fee, final Account from, final Account to) { this.fee = fee; this.from = from; this.to = to; } @Override public boolean equals(final Object obj) { if (!(obj instanceof ChargedFee)) { return false; } final ChargedFee f = (ChargedFee) obj; return new EqualsBuilder().append(fee, f.fee).append(from, f.from).append(to, f.to).isEquals(); } @Override public int hashCode() { return new HashCodeBuilder().append(fee).append(from).append(to).toHashCode(); } } /** * validator always returning a validationError. To be called if the final amount of a payment (after applying all fees) is negative * @author rinke */ private final class FinalAmountValidator implements GeneralValidation { private static final long serialVersionUID = -2789145696000017181L; @Override public ValidationError validate(final Object object) { return new ValidationError("payment.error.negativeFinalAmount"); } } /** * validator which always returns a validationError. To be called if a past date on a transfer is combined with rates. * @author Rinke * */ private static final class NoPastDateWithRatesValidator implements GeneralValidation { private static final long serialVersionUID = -6914314732478889087L; @Override public ValidationError validate(final Object object) { return new ValidationError("payment.error.pastDateWithRates"); } } private final class PendingContractValidator implements GeneralValidation { private static final long serialVersionUID = 5608258953479316287L; @Override @SuppressWarnings("unchecked") public ValidationError validate(final Object object) { // Validate the scheduled payments final DoPaymentDTO payment = (DoPaymentDTO) object; Member fromMember = (Member) (payment.getFrom() instanceof Member ? payment.getFrom() : payment.getFrom() == null ? LoggedUser.member() : null); if (fromMember != null) { fromMember = fetchService.fetch(fromMember, Element.Relationships.GROUP); // Validate if there is a fee (broker commission) with a pending contract if (payment.getTo() != null && payment.getTo() instanceof Member && payment.getTransferType() != null) { final TransferType transferType = fetchService.fetch(payment.getTransferType(), TransferType.Relationships.TRANSACTION_FEES); final Collection<TransactionFee> transactionFees = (Collection<TransactionFee>) fetchService.fetch(transferType.getTransactionFees(), TransactionFee.Relationships.GENERATED_TRANSFER_TYPE); for (final TransactionFee transactionFee : transactionFees) { if (transactionFee instanceof BrokerCommission && transactionFee.isFromMember()) { final BrokerCommission brokerCommission = (BrokerCommission) transactionFee; final BrokerCommissionContractQuery contractsQuery = new BrokerCommissionContractQuery(); contractsQuery.setBrokerCommission(brokerCommission); contractsQuery.setStatus(BrokerCommissionContract.Status.PENDING); switch (brokerCommission.getWhichBroker()) { case SOURCE: contractsQuery.setMember(fromMember); break; case DESTINATION: contractsQuery.setMember((Member) payment.getTo()); break; } final List<BrokerCommissionContract> commissionContracts = commissionService.searchBrokerCommissionContracts(contractsQuery); if (CollectionUtils.isNotEmpty(commissionContracts)) { return new ValidationError("payment.error.pendingCommissionContract", brokerCommission.getName()); } } } } } return null; } } private final class SchedulingValidator implements GeneralValidation { private static final long serialVersionUID = 4085922259108191939L; @Override @SuppressWarnings("unchecked") public ValidationError validate(final Object object) { // Validate the scheduled payments final DoPaymentDTO payment = (DoPaymentDTO) object; final List<ScheduledPaymentDTO> payments = payment.getPayments(); if (CollectionUtils.isEmpty(payments)) { return null; } final TransferType transferType = fetchService.fetch(payment.getTransferType(), TransferType.Relationships.TRANSACTION_FEES); // It is assumed that the validation where this Validator is used, checks the requirement of the transferType. // So it's safe to return, cause the validation will fail. if (transferType == null) { return null; } // Validate the from member Member fromMember = null; if (payment.getFrom() instanceof Member) { fromMember = fetchService.fetch((Member) payment.getFrom(), Element.Relationships.GROUP); } else if (LoggedUser.hasUser() && LoggedUser.isMember()) { fromMember = LoggedUser.element(); } Calendar maxPaymentDate = null; if (fromMember != null) { final MemberGroup group = fromMember.getMemberGroup(); // Validate the max payments final int maxSchedulingPayments = transferType.isAllowsScheduledPayments() ? group.getMemberSettings().getMaxSchedulingPayments() : 0; if (payments.size() > maxSchedulingPayments) { return new ValidationError("errors.greaterEquals", messageResolver.message("transfer.paymentCount"), maxSchedulingPayments); } // Get the maximum payment date final TimePeriod maxSchedulingPeriod = group.getMemberSettings().getMaxSchedulingPeriod(); if (maxSchedulingPeriod != null) { maxPaymentDate = maxSchedulingPeriod.add(DateHelper.truncate(Calendar.getInstance())); } // Validate if there is a fee with a pending contract if (payment.getTo() != null && payment.getTo() instanceof Member) { final Collection<TransactionFee> transactionFees = (Collection<TransactionFee>) fetchService.fetch(transferType.getTransactionFees(), TransactionFee.Relationships.GENERATED_TRANSFER_TYPE); for (final TransactionFee transactionFee : transactionFees) { if (transactionFee instanceof BrokerCommission && transactionFee.isFromMember()) { final BrokerCommission brokerCommission = (BrokerCommission) transactionFee; final BrokerCommissionContractQuery contractsQuery = new BrokerCommissionContractQuery(); contractsQuery.setBrokerCommission(brokerCommission); contractsQuery.setStatus(BrokerCommissionContract.Status.PENDING); switch (brokerCommission.getWhichBroker()) { case SOURCE: contractsQuery.setMember(fromMember); break; case DESTINATION: contractsQuery.setMember((Member) payment.getTo()); break; } final List<BrokerCommissionContract> commissionContracts = commissionService.searchBrokerCommissionContracts(contractsQuery); if (CollectionUtils.isNotEmpty(commissionContracts)) { return new ValidationError("payment.error.pendingCommissionContract", brokerCommission.getName()); } } } } } // Validate the total payment amount and dates final BigDecimal paymentAmount = payment.getAmount(); final BigDecimal minimumPayment = getMinimumPayment(); BigDecimal totalAmount = BigDecimal.ZERO; Calendar lastDate = DateHelper.truncatePreviosDay(Calendar.getInstance()); for (final ScheduledPaymentDTO dto : payments) { final Calendar date = dto.getDate(); // Validate the max payment date if (maxPaymentDate != null && date.after(maxPaymentDate)) { final LocalSettings localSettings = settingsService.getLocalSettings(); final CalendarConverter dateConverter = localSettings.getRawDateConverter(); return new ValidationError("payment.invalid.schedulingDate", dateConverter.toString(maxPaymentDate)); } final BigDecimal amount = dto.getAmount(); if (amount == null || amount.compareTo(minimumPayment) < 0) { return new RequiredError(messageResolver.message("transfer.amount")); } BigDecimal minAmount = transferType.getMinAmount(); if (minAmount != null) { if (amount.compareTo(minAmount) < 0) { return new ValidationError("errors.greaterEquals", amount, minAmount); } } if (date == null) { return new RequiredError(messageResolver.message("transfer.date")); } else if (date.before(lastDate) || DateUtils.isSameDay(date, lastDate)) { return new ValidationError("payment.invalid.paymentDates"); } totalAmount = totalAmount.add(amount); lastDate = date; } // Validate the total payment amount if (paymentAmount != null && totalAmount.compareTo(paymentAmount) != 0) { return new ValidationError("payment.invalid.paymentAmount"); } return null; } } private class TicketValidation implements PropertyValidation { private static final long serialVersionUID = 1L; @Override public ValidationError validate(final Object object, final Object property, final Object value) { if (value != null) { DoPaymentDTO dto = (DoPaymentDTO) object; Ticket ticket = (Ticket) value; if (ticket != null && dto.getChannel() != Channel.WEBSHOP) { return new InvalidError(); } else { try { ticket = fetchService.fetch(ticket); if (ticket != null && ticket.getStatus() != Ticket.Status.PENDING) { throw new EntityNotFoundException(Ticket.class); } } catch (EntityNotFoundException e) { return new InvalidError(); } } } return null; } } private class TraceNumberValidation implements PropertyValidation { private static final long serialVersionUID = 2424106851078796317L; @Override public ValidationError validate(final Object object, final Object property, final Object value) { final TransferDTO dto = (TransferDTO) object; final Long clientId = dto.getClientId(); final String traceNumber = dto.getTraceNumber(); if (clientId == null || StringUtils.isEmpty(traceNumber)) { return null; } try { transferDao.loadTransferByTraceNumber(traceNumber, clientId); traceNumberDao.load(clientId, traceNumber); // Invalid, as if it reaches here, there is at least one other transfer with the given trace number return new UniqueError(traceNumber); } catch (final EntityNotFoundException e) { // Is valid, as there are no other transfer using that trace number for that client id return null; } } } private static final float PRECISION_DELTA = 0.0001F; private static final Relationship[] CONCILIATION_FETCH = { Transfer.Relationships.EXTERNAL_TRANSFER, RelationshipHelper.nested(Payment.Relationships.FROM, MemberAccount.Relationships.MEMBER) }; private AccountServiceLocal accountService; private CommissionServiceLocal commissionService; private SettingsServiceLocal settingsService; private TransferAuthorizationServiceLocal transferAuthorizationService; private TicketServiceLocal ticketService; private TransactionFeeServiceLocal transactionFeeService; private TransferDAO transferDao; private TraceNumberDAO traceNumberDao; private ScheduledPaymentDAO scheduledPaymentDao; private TransferTypeServiceLocal transferTypeService; private FetchServiceLocal fetchService; private LoggingHandler loggingHandler; private PermissionServiceLocal permissionService; private AlertServiceLocal alertService; private MessageResolver messageResolver; private PaymentCustomFieldServiceLocal paymentCustomFieldService; private RateServiceLocal rateService; private MemberNotificationHandler memberNotificationHandler; private AdminNotificationHandler adminNotificationHandler; private TransactionHelper transactionHelper; private ApplicationServiceLocal applicationService; private LockHandlerFactory lockHandlerFactory; private PaymentHelper paymentHelper; private AccountHelper accountHelper; private CustomObjectHandler customObjectHandler; private static final Log LOG = LogFactory.getLog(PaymentServiceImpl.class); @Override public List<BulkChargebackResult> bulkChargeback(final List<Transfer> transfers) { return transactionHelper.runInNewTransaction(new Transactional<List<BulkChargebackResult>>() { @Override public List<BulkChargebackResult> afterCommit(final List<BulkChargebackResult> result) { // Make sure all transfers are attached to the current session for (BulkChargebackResult bulkChargebackResult : result) { bulkChargebackResult.setTransfer(fetchService.fetch(bulkChargebackResult.getTransfer())); } return result; } @Override public List<BulkChargebackResult> doInTransaction(final TransactionStatus status) { List<BulkChargebackResult> results = new ArrayList<BulkChargebackResult>(transfers.size()); try { for (Transfer transfer : transfers) { if (transfer == null) { results.add(null); } else { Transfer chargeback = insertChargeback(transfer, false); results.add(new BulkChargebackResult(chargeback)); } } } catch (ApplicationException e) { results.add(new BulkChargebackResult(e)); status.setRollbackOnly(); } return results; } }); } @Override public List<ScheduledPaymentDTO> calculatePaymentProjection(final ProjectionDTO params) { getProjectionValidator().validate(params); final LocalSettings localSettings = settingsService.getLocalSettings(); final int paymentCount = params.getPaymentCount(); final TimePeriod recurrence = params.getRecurrence(); final BigDecimal totalAmount = params.getAmount(); final BigDecimal paymentAmount = localSettings.round(totalAmount.divide(CoercionHelper.coerce(BigDecimal.class, paymentCount), localSettings.getMathContext())); BigDecimal accumulatedAmount = BigDecimal.ZERO; Calendar currentDate = DateHelper.truncate(params.getFirstExpirationDate()); final List<ScheduledPaymentDTO> payments = new ArrayList<ScheduledPaymentDTO>(paymentCount); for (int i = 0; i < paymentCount; i++) { final ScheduledPaymentDTO dto = new ScheduledPaymentDTO(); dto.setDate(currentDate); dto.setAmount(i == paymentCount - 1 ? totalAmount.subtract(accumulatedAmount) : paymentAmount); payments.add(dto); accumulatedAmount = accumulatedAmount.add(dto.getAmount(), localSettings.getMathContext()); currentDate = recurrence.add(currentDate); } return payments; } @Override public boolean canChargeback(Transfer transfer, final boolean ignorePendingPayment) { transfer = fetchService.fetch(transfer, Payment.Relationships.FROM, Payment.Relationships.TO, Transfer.Relationships.PARENT, Transfer.Relationships.CHARGEBACK_OF, Transfer.Relationships.CHILDREN); if (transfer == null) { return false; } // Pending payments cannot be charged back final Calendar processDate = transfer.getProcessDate(); if (!ignorePendingPayment && processDate == null) { return false; } // Check the max chargeback time final LocalSettings localSettings = settingsService.getLocalSettings(); final TimePeriod maxChargebackTime = localSettings.getMaxChargebackTime(); final Calendar maxDate = maxChargebackTime.add(processDate); if (Calendar.getInstance().after(maxDate)) { return false; } // Nested transfers cannot be charged back if (transfer.getParent() != null) { return false; } // Payments which has already been charged back cannot be charged back again if (transfer.getChargedBackBy() != null) { return false; } // Payments which are chargebacks cannot be charged back if (transfer.getChargebackOf() != null) { return false; } // Cannot chargeback if from owner is removed if (!transfer.isFromSystem()) { final Member fromOwner = (Member) transfer.getFromOwner(); if (fromOwner.getGroup().getStatus() == Group.Status.REMOVED) { return false; } } // Cannot chargeback if to owner is removed if (!transfer.isToSystem()) { final Member toOwner = (Member) transfer.getToOwner(); if (toOwner.getGroup().getStatus() == Group.Status.REMOVED) { return false; } } return true; } /** * To perform a payment, the logged user must either manage the from owner or to owner and must be allowed<br> * to use the specified transfer type (if any). */ @Override public boolean canMakePayment(AccountOwner from, final AccountOwner to, final TransferType transferType) { if (LoggedUser.isSystem()) { return true; } boolean checkToMember = false; boolean hasPermission = false; if (from == null) { from = LoggedUser.accountOwner(); } if (from instanceof SystemAccountOwner) { if (to instanceof SystemAccountOwner) { // System to system payment hasPermission = permissionService.permission().admin(AdminSystemPermission.PAYMENTS_PAYMENT).hasPermission(); } else { // System to member payment if (transferType == null) { // No information about the TT. Can be either loan or payment hasPermission = permissionService.permission().admin(AdminMemberPermission.PAYMENTS_PAYMENT, AdminMemberPermission.LOANS_GRANT).hasPermission(); } else { // We know whether is a loan type or payment type: check the specific permission AdminMemberPermission permission = transferType.isLoanType() ? AdminMemberPermission.LOANS_GRANT : AdminMemberPermission.PAYMENTS_PAYMENT; hasPermission = permissionService.permission().admin(permission).hasPermission(); } checkToMember = true; } } else { Member member = (Member) from; if (from.equals(to)) { // Member self payment hasPermission = permissionService.permission(member) .admin(AdminMemberPermission.PAYMENTS_PAYMENT_AS_MEMBER_TO_SELF) .broker(BrokerPermission.MEMBER_PAYMENTS_PAYMENT_AS_MEMBER_TO_SELF) .member(MemberPermission.PAYMENTS_PAYMENT_TO_SELF) .operator(OperatorPermission.PAYMENTS_PAYMENT_TO_SELF) .hasPermission(); } else if (to instanceof SystemAccountOwner) { // Member to system hasPermission = permissionService.permission(member) .admin(AdminMemberPermission.PAYMENTS_PAYMENT_AS_MEMBER_TO_SYSTEM) .broker(BrokerPermission.MEMBER_PAYMENTS_PAYMENT_AS_MEMBER_TO_SYSTEM) .member(MemberPermission.PAYMENTS_PAYMENT_TO_SYSTEM) .operator(OperatorPermission.PAYMENTS_PAYMENT_TO_SYSTEM) .hasPermission(); } else { // Member to member hasPermission = permissionService.permission(member) .admin(AdminMemberPermission.PAYMENTS_PAYMENT_AS_MEMBER_TO_MEMBER) .broker(BrokerPermission.MEMBER_PAYMENTS_PAYMENT_AS_MEMBER_TO_MEMBER) .member(MemberPermission.PAYMENTS_PAYMENT_TO_MEMBER) .operator(OperatorPermission.PAYMENTS_PAYMENT_TO_MEMBER, OperatorPermission.PAYMENTS_POSWEB_MAKE_PAYMENT) .hasPermission(); checkToMember = true; } } if (hasPermission && transferType != null) { Collection<TransferType> allowedTypes = Collections.emptyList(); // checks if the specified TT can be used if (LoggedUser.accountOwner().equals(from)) { Group group = fetchService.fetch(LoggedUser.isOperator() ? LoggedUser.member().getGroup() : LoggedUser.group(), Group.Relationships.TRANSFER_TYPES); allowedTypes = group.getTransferTypes(); } else if (LoggedUser.isBroker()) { BrokerGroup brokerGroup = fetchService.fetch((BrokerGroup) LoggedUser.group(), BrokerGroup.Relationships.TRANSFER_TYPES_AS_MEMBER); allowedTypes = brokerGroup.getTransferTypesAsMember(); } else if (LoggedUser.isAdministrator()) { AdminGroup admGroup = fetchService.fetch((AdminGroup) LoggedUser.group(), AdminGroup.Relationships.TRANSFER_TYPES_AS_MEMBER); allowedTypes = admGroup.getTransferTypesAsMember(); } hasPermission = allowedTypes.contains(transferType); } // Besides, if the payment is to a member, ensure he is visible if (hasPermission && checkToMember) { return permissionService.relatesTo((Member) to); } else { return hasPermission; } } @Override public Transfer chargeback(final Transfer transfer) throws UnexpectedEntityException { if (!canChargeback(transfer, false)) { throw new UnexpectedEntityException(); } // Insert the chargeback return insertChargeback(transfer, true); } @Override public Transfer conciliate(Transfer transfer, final ExternalTransfer externalTransfer) { transfer = fetchService.fetch(transfer, CONCILIATION_FETCH); if (transfer != null && transfer.getExternalTransfer() != null) { // If the transfer is already conciliated, ignore it transfer = null; } if (transfer != null) { final Account from = transfer.getFrom(); final AccountOwner owner = from.getOwner(); if (!owner.equals(externalTransfer.getMember())) { // The account does not belong to the expected member, ignore it transfer = null; } } if (transfer == null) { throw new UnexpectedEntityException(); } return transferDao.updateExternalTransfer(transfer.getId(), externalTransfer); } @Override public Transfer confirmPayment(final String ticketStr) throws NotEnoughCreditsException, MaxAmountPerDayExceededException, EntityNotFoundException, UpperCreditLimitReachedException, AuthorizedPaymentInPastException { // Get and validate the ticket final PaymentRequestTicket ticket = ticketService.loadPendingPaymentRequest(ticketStr); final Member fromMember = ticket.getFrom(); final Member toMember = ticket.getTo(); final String channel = ticket.getToChannel().getInternalName(); final String description = ticket.getDescription(); // Create the payment final TransferDTO dto = new TransferDTO(); dto.setFromOwner(fromMember); dto.setToOwner(toMember); dto.setTransferType(ticket.getTransferType()); dto.setAmount(ticket.getAmount()); dto.setChannel(channel); dto.setTicket(ticket); dto.setDescription(description); final Transfer transfer = (Transfer) insert(dto, true, false); // Update the ticket ticket.setStatus(Ticket.Status.OK); ticket.setTransfer(transfer); // Notify memberNotificationHandler.externalChannelPaymentConfirmed(ticket); return transfer; } @Override public List<BulkPaymentResult> doBulkPayment(final List<DoPaymentDTO> dtos) { return transactionHelper.runInNewTransaction(new Transactional<List<BulkPaymentResult>>() { @Override public List<BulkPaymentResult> afterCommit(final List<BulkPaymentResult> result) { // Make sure all transfers are attached to the current session for (BulkPaymentResult bulkPaymentResult : result) { bulkPaymentResult.setPayment(fetchService.fetch(bulkPaymentResult.getPayment())); } return result; } @Override public List<BulkPaymentResult> doInTransaction(final TransactionStatus status) { List<BulkPaymentResult> results = new ArrayList<BulkPaymentResult>(dtos.size()); try { for (DoPaymentDTO dto : dtos) { Payment payment = doPayment(dto, false, true, false); results.add(new BulkPaymentResult(payment)); } } catch (ApplicationException e) { results.add(new BulkPaymentResult(e)); status.setRollbackOnly(); } return results; } }); } @Override public Payment doPayment(final DoPaymentDTO params) { return doPayment(params, true, true, false); } @Override public AccountHistoryResultPage getAccountHistoryResultPage(final AccountHistoryParams params) { TransferQuery query = paymentHelper.toTransferQuery(params); List<Transfer> transfers = search(query); AccountHistoryResultPage result = accountHelper.toAccountHistoryResultPage(query.getOwner(), transfers); // Get the account status if requested if (params.getShowStatus()) { AccountStatusVO statusVO = accountService.getCurrentAccountStatusVO(new AccountDTO(query.getOwner(), query.getType())); result.setAccountStatus(statusVO); } return result; } @Override public AccountHistoryTransferVO getAccountHistoryTransferVO(final Long id) { Transfer transfer = load(id); List<PaymentCustomField> fields = paymentCustomFieldService.list(transfer.getType(), false); return accountHelper.toVO(LoggedUser.member(), transfer, fields, null, null); } @Override public ConversionSimulationDTO getDefaultConversionDTO(MemberAccount account, final List<TransferType> transferTypes) { account = fetchService.fetch(account, Account.Relationships.TYPE, MemberAccount.Relationships.MEMBER); // Get the current account status final AccountStatus status = accountService.getRatedStatus(account, null); final ConversionSimulationDTO dto = new ConversionSimulationDTO(); dto.setAccount(account); // Find the default amount: the balance of the current account BigDecimal defaultAmount = status.getAvailableBalanceWithoutCreditLimit(); if (BigDecimal.ZERO.compareTo(defaultAmount) > 0) { defaultAmount = BigDecimal.ZERO; } dto.setAmount(defaultAmount); // find the first rated TT, and choose this. for (final TransferType currentTT : transferTypes) { if (currentTT.isHavingRatedFees()) { dto.setTransferType(currentTT); break; } } // If not any rated TT, just choose the first if (dto.getTransferType() == null) { dto.setTransferType(transferTypes.get(0)); } dto.setDate(Calendar.getInstance()); // erase any present content dto.setArate(null); dto.setDrate(null); // rates on a zero balance are meaningless, so... if (dto.getTransferType().isHavingRatedFees() && BigDecimal.ZERO.compareTo(defaultAmount) < 0) { if (dto.getTransferType().isHavingAratedFees()) { final BigDecimal aRate = status.getaRate(); dto.setArate(aRate); } if (dto.getTransferType().isHavingDratedFees()) { final BigDecimal dRate = status.getdRate(); dto.setDrate(dRate); } } return dto; } @Override public BigDecimal getMinimumPayment() { final LocalSettings localSettings = settingsService.getLocalSettings(); final int precision = localSettings.getPrecision().getValue(); final BigDecimal minimumPayment = new BigDecimal(new BigInteger("1"), precision); return minimumPayment; } @Override public StatisticalResultDTO getSimulateConversionGraph(final ConversionSimulationDTO input) { final LocalSettings localSettings = settingsService.getLocalSettings(); final byte precision = (byte) localSettings.getPrecision().getValue(); // get series final TransactionFeePreviewForRatesDTO temp = simulateConversion(input); final int series = temp.getFees().size(); // get range of points, but without values for A < 0 BigDecimal initialARate = null; RatesResultDTO rates = new RatesResultDTO(); if (input.isUseActualRates()) { rates = rateService.getRatesForTransferFrom(input.getAccount(), input.getAmount(), null); rates.setDate(input.getDate()); initialARate = rates.getaRate(); } else { initialARate = input.getArate(); } // lowerlimit takes care that values for A < 0 are left out of the graph final Double lowerLimit = (initialARate == null) ? null : initialARate.negate().doubleValue(); final Number[] xRange = GraphHelper.getOptimalRangeAround(0, 33, 0, 0.8, lowerLimit); // Data structure to build the table final Number[][] tableCells = new Number[xRange.length][series]; // initialize series names and x labels final String[] seriesNames = new String[series]; final byte[] seriesOrder = new byte[series]; final Calendar[] xPointDates = new Calendar[xRange.length]; final Calendar now = Calendar.getInstance(); BigDecimal inputARate = temp.getARate(); BigDecimal inputDRate = temp.getDRate(); // assign data for (int i = 0; i < xRange.length; i++) { final ConversionSimulationDTO inputPointX = (ConversionSimulationDTO) input.clone(); final Calendar date = (Calendar) ((input.isUseActualRates()) ? input.getDate().clone() : now.clone()); date.add(Calendar.DAY_OF_YEAR, xRange[i].intValue()); xPointDates[i] = date; // Set useActualRates for this input to false, otherwise simulateConversion will use the account's the balance and rates of that date, and // we don't want that. inputPointX.setUseActualRates(false); if (inputARate != null) { final BigDecimal aRate = inputARate.add(new BigDecimal(xRange[i].doubleValue())); inputPointX.setArate(aRate); } if (inputDRate != null) { final BigDecimal dRate = inputDRate.subtract(new BigDecimal(xRange[i].doubleValue())); inputPointX.setDrate(dRate); } final TransactionFeePreviewDTO tempResult = simulateConversion(inputPointX); int j = 0; for (final TransactionFee fee : tempResult.getFees().keySet()) { tableCells[i][j] = new StatisticalNumber(tempResult.getFees().get(fee).doubleValue(), precision); byte index; switch (fee.getChargeType()) { case D_RATE: index = 2; break; case A_RATE: case MIXED_A_D_RATES: index = 3; break; default: index = 1; break; } seriesOrder[j] = index; seriesNames[j++] = fee.getName(); } } // create the graph object final StatisticalResultDTO result = new StatisticalResultDTO(tableCells); result.setBaseKey("conversionSimulation.result.graph"); result.setHelpFile("account_management"); // date labels along x-axis final String[] rowKeys = new String[xRange.length]; Arrays.fill(rowKeys, ""); result.setRowKeys(rowKeys); for (int i = 0; i < rowKeys.length; i++) { final String rowHeader = localSettings.getDateConverterForGraphs().toString(xPointDates[i]); result.setRowHeader(rowHeader, i); } // mark the actual date upon which the x-axis is based as a vertical line final Calendar baseDate = (input.isUseActualRates()) ? (Calendar) input.getDate().clone() : now; final String baseDateString = localSettings.getDateConverterForGraphs().toString(baseDate); final Marker[] markers = new Marker[1]; markers[0] = new CategoryMarker(baseDateString); markers[0].setPaint(Color.ORANGE); final String todayString = localSettings.getDateConverterForGraphs().toString(now); if (todayString.equals(baseDateString)) { markers[0].setLabel("global.today"); } result.setDomainMarkers(markers); // Series labels indicate fee names final String[] columnKeys = new String[series]; Arrays.fill(columnKeys, ""); result.setColumnKeys(columnKeys); for (int j = 0; j < columnKeys.length; j++) { result.setColumnHeader(seriesNames[j], j); } // order the series result.orderSeries(seriesOrder); final TransferType tt = fetchService.fetch(input.getTransferType(), RelationshipHelper.nested(TransferType.Relationships.FROM, AccountType.Relationships.CURRENCY)); result.setYAxisUnits(tt.getCurrency().getSymbol()); result.setShowTable(false); result.setGraphType(StatisticalResultDTO.GraphType.STACKED_AREA); return result; } @Override public boolean hasPermissionsToChargeback(Transfer transfer) { transfer = fetchService.fetch(transfer, RelationshipHelper.nested(Payment.Relationships.TO, MemberAccount.Relationships.MEMBER)); // If it's a payment from member.. check it's related boolean isFromMember = !transfer.isFromSystem(); if (isFromMember) { if (!permissionService.relatesTo((Member) transfer.getFromOwner())) { return false; } } if (transfer.isToSystem()) { // Payment to system return permissionService.permission() .adminFor(AdminSystemPermission.PAYMENTS_CHARGEBACK, transfer.getType()) .hasPermission(); } else { // Payment to member return permissionService.permission((Member) transfer.getToOwner()) .adminFor(AdminMemberPermission.PAYMENTS_CHARGEBACK, transfer.getType()) .memberFor(MemberPermission.PAYMENTS_CHARGEBACK, transfer.getType()) .hasPermission(); } } @Override public Payment insertWithNotification(final TransferDTO dto) throws NotEnoughCreditsException, MaxAmountPerDayExceededException, UnexpectedEntityException, UpperCreditLimitReachedException { Payment payment = insert(dto, false, false); if (payment instanceof Transfer) { memberNotificationHandler.automaticPaymentReceivedNotification((Transfer) payment, dto); } return payment; } @Override public Payment insertWithoutNotification(final TransferDTO dto) throws NotEnoughCreditsException, MaxAmountPerDayExceededException, UnexpectedEntityException, UpperCreditLimitReachedException { return insert(dto, false, false); } @Override public boolean isVisible(Payment payment) { if (LoggedUser.isSystem()) { return true; } if (payment instanceof Transfer && LoggedUser.isOperator()) { // An operator should be able to view transfers he has received Transfer transfer = (Transfer) payment; if (LoggedUser.element().equals(transfer.getReceiver()) || LoggedUser.element().equals(transfer.getBy())) { return true; } } if (payment instanceof Transfer) { payment = fetchService.fetch((Transfer) payment, Payment.Relationships.FROM, Payment.Relationships.TO); } else { payment = fetchService.fetch((ScheduledPayment) payment, Payment.Relationships.FROM, Payment.Relationships.TO); } return accountService.canView(payment.getFrom()) || accountService.canView(payment.getTo()); } @Override public Transfer load(final Long id, final Relationship... fetch) { return transferDao.<Transfer> load(id, fetch); } @Override public Transfer loadTransferForReverse(final String traceNumber, final Relationship... fetch) throws EntityNotFoundException { long clientId = LoggedUser.serviceClient().getId(); Transfer transfer = transferDao.loadTransferByTraceNumber(traceNumber, clientId); if (transfer == null) { if (!insertTN(LoggedUser.serviceClient().getId(), traceNumber)) { // the TN already exists transfer = transferDao.loadTransferByTraceNumber(traceNumber, clientId); } if (transfer == null) { throw new EntityNotFoundException(Transfer.class, null, String.format("TraceNumber and client id used to load: <%1$s, %2$s>", traceNumber, clientId)); } } return transfer; } @Override public void notifyTransferProcessed(final Transfer transfer) { if (transfer.getProcessDate() == null) { // Transfer is not processed return; } final Collection<TransferListener> listeners = getTransferListeners(transfer); if (CollectionUtils.isNotEmpty(listeners)) { CurrentTransactionData.addTransactionCommitListener(new TransactionCommitListener() { @Override public void onTransactionCommit() { transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { Transfer fetchedTransfer = fetchService.fetch(transfer, Payment.Relationships.FROM, Payment.Relationships.TO); for (TransferListener listener : listeners) { try { listener.onTransferProcessed(fetchedTransfer); } catch (Exception e) { LOG.warn("Error running TransferListener " + listener, e); } } } }); } }); } } @Override public void processScheduled(final Period period) { // Process each transfer final TransferQuery query = new TransferQuery(); query.setResultType(ResultType.ITERATOR); query.setPeriod(period); query.setStatus(Payment.Status.SCHEDULED); query.setUnordered(true); CacheCleaner cacheCleaner = new CacheCleaner(fetchService); final List<Transfer> transfers = transferDao.search(query); try { for (final Transfer transfer : transfers) { processScheduledTransfer(transfer, true, true, true); cacheCleaner.clearCache(); } } finally { DataIteratorHelper.close(transfers); } } @Override public Transfer processScheduled(final Transfer transfer) { return processScheduledTransfer(transfer, false, false, true); } @Override public void purgeOldTraceNumbers(final Calendar time) { Calendar c = (Calendar) time.clone(); c.add(Calendar.DAY_OF_MONTH, -1); traceNumberDao.delete(c); } @Override public List<Transfer> search(final TransferQuery query) { return transferDao.search(query); } public void setAccountHelper(final AccountHelper accountHelper) { this.accountHelper = accountHelper; } public void setAccountServiceLocal(final AccountServiceLocal accountService) { this.accountService = accountService; } public void setAdminNotificationHandler(final AdminNotificationHandler adminNotificationHandler) { this.adminNotificationHandler = adminNotificationHandler; } public void setAlertServiceLocal(final AlertServiceLocal alertService) { this.alertService = alertService; } public void setApplicationServiceLocal(final ApplicationServiceLocal applicationService) { this.applicationService = applicationService; } public void setCommissionServiceLocal(final CommissionServiceLocal commissionService) { this.commissionService = commissionService; } public void setCustomObjectHandler(final CustomObjectHandler customObjectHandler) { this.customObjectHandler = customObjectHandler; } public void setFetchServiceLocal(final FetchServiceLocal fetchService) { this.fetchService = fetchService; } public void setLockHandlerFactory(final LockHandlerFactory lockHandlerFactory) { this.lockHandlerFactory = lockHandlerFactory; } public void setLoggingHandler(final LoggingHandler loggingHandler) { this.loggingHandler = loggingHandler; } public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) { this.memberNotificationHandler = memberNotificationHandler; } public void setMessageResolver(final MessageResolver messageResolver) { this.messageResolver = messageResolver; } public void setPaymentCustomFieldServiceLocal(final PaymentCustomFieldServiceLocal paymentCustomFieldService) { this.paymentCustomFieldService = paymentCustomFieldService; } public void setPaymentHelper(final PaymentHelper paymentHelper) { this.paymentHelper = paymentHelper; } public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) { this.permissionService = permissionService; } public void setRateServiceLocal(final RateServiceLocal rateService) { this.rateService = rateService; } public void setScheduledPaymentDao(final ScheduledPaymentDAO scheduledPaymentDao) { this.scheduledPaymentDao = scheduledPaymentDao; } public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) { this.settingsService = settingsService; } public void setTicketServiceLocal(final TicketServiceLocal ticketService) { this.ticketService = ticketService; } public void setTraceNumberDao(final TraceNumberDAO traceNumberDao) { this.traceNumberDao = traceNumberDao; } public void setTransactionFeeServiceLocal(final TransactionFeeServiceLocal transactionFeeService) { this.transactionFeeService = transactionFeeService; } public void setTransactionHelper(final TransactionHelper transactionHelper) { this.transactionHelper = transactionHelper; } public void setTransferAuthorizationServiceLocal(final TransferAuthorizationServiceLocal transferAuthorizationService) { this.transferAuthorizationService = transferAuthorizationService; } public void setTransferDao(final TransferDAO transferDao) { this.transferDao = transferDao; } public void setTransferTypeServiceLocal(final TransferTypeServiceLocal transferTypeService) { this.transferTypeService = transferTypeService; } @Override public TransactionFeePreviewForRatesDTO simulateConversion(final ConversionSimulationDTO params) { TransferType transferType = params.getTransferType(); transferType = fetchService.fetch(transferType, TransferType.Relationships.TO, TransferType.Relationships.TRANSACTION_FEES, RelationshipHelper.nested(SystemAccountType.Relationships.ACCOUNT)); final MemberAccount account = fetchService.fetch(params.getAccount(), Account.Relationships.TYPE, MemberAccount.Relationships.MEMBER); RatesPreviewDTO rates = new RatesPreviewDTO(); if (params.isUseActualRates()) { rates = new RatesPreviewDTO(rateService.getRatesForTransferFrom(account, params.getAmount(), params.getDate())); } else { if (transferType.isHavingAratedFees()) { rates.setaRate(params.getArate()); } if (transferType.isHavingDratedFees()) { rates.setdRate(params.getDrate()); } rates.setDate(params.getDate()); rates = new RatesPreviewDTO(rateService.rateToDate(rates)); } rates.setGraph(params.isGraph()); final Member from = account.getMember(); final BigDecimal amount = params.getAmount(); final SystemAccountOwner to = SystemAccountOwner.instance(); final TransactionFeePreviewForRatesDTO preview = (TransactionFeePreviewForRatesDTO) transactionFeeService.preview(from, to, transferType, amount, rates); return preview; } @Override public Payment simulatePayment(final DoPaymentDTO params) throws NotEnoughCreditsException, MaxAmountPerDayExceededException, UnexpectedEntityException, UpperCreditLimitReachedException, AuthorizedPaymentInPastException { return transactionHelper.runInNewTransaction(new BaseTransactional<Payment>() { @Override public Payment doInTransaction(final TransactionStatus status) { status.setRollbackOnly(); return doPayment(params, false, false, true); } }); } @Override public void validate(final ConversionSimulationDTO dto) { final Validator validator = new Validator(""); validator.property("amount").key("conversionSimulation.amount").required().positiveNonZero(); if (dto.isUseActualRates()) { validator.property("date").key("conversionSimulation.date").required(); } else { final Account account = fetchService.fetch(dto.getAccount(), RelationshipHelper.nested(Account.Relationships.TYPE, AccountType.Relationships.CURRENCY)); final TransferType transferType = fetchService.fetch(dto.getTransferType(), TransferType.Relationships.TRANSACTION_FEES); final Currency currency = account.getType().getCurrency(); if (currency.isEnableARate() && transferType.isHavingAratedFees()) { validator.property("arate").key("conversionSimulation.aRate.targeted").required().positive(); } if (currency.isEnableDRate() && transferType.isHavingDratedFees()) { validator.property("drate").key("conversionSimulation.dRate.targeted").required(); } } validator.validate(dto); } @Override public void validate(final DoPaymentDTO payment) { getPaymentValidator(payment).validate(payment); } /** * Validates the max amount per day */ @Override public void validateMaxAmountAtDate(final Calendar date, final Account account, final TransferType transferType, BigDecimal maxAmountPerDay, final BigDecimal amount) { // Test the max amount per day maxAmountPerDay = maxAmountPerDay == null ? transferType.getMaxAmountPerDay() : maxAmountPerDay; if (maxAmountPerDay != null && maxAmountPerDay.floatValue() > PRECISION_DELTA) { // Get the amount on today BigDecimal amountOnDay = transferDao.getTransactionedAmountAt(date, account, transferType); // Validate if (amountOnDay.add(amount).compareTo(maxAmountPerDay) > 0) { throw new MaxAmountPerDayExceededException(date, transferType, account, amount); } } // Test the operator max amount per day if (LoggedUser.hasUser() && LoggedUser.isOperator()) { final Operator operator = LoggedUser.element(); OperatorGroup group = operator.getOperatorGroup(); group = fetchService.fetch(group, OperatorGroup.Relationships.MAX_AMOUNT_PER_DAY_BY_TRANSFER_TYPE); final BigDecimal maxAmount = group.getMaxAmountPerDayByTransferType().get(transferType); if (maxAmount != null && maxAmount.floatValue() > PRECISION_DELTA) { // Get the amount on today BigDecimal amountOnDay = transferDao.getTransactionedAmountAt(date, operator, account, transferType); // Validate if (amountOnDay.add(amount).compareTo(maxAmount) == 1) { throw new MaxAmountPerDayExceededException(date, transferType, account, amount); } } } } @Override public boolean wouldRequireAuthorization(final DoPaymentDTO params) { AuthorizationLevel firstAuthorizationLevel = null; // Scheduled payments shouldn't be authorized, only it's payments if (CollectionUtils.isEmpty(params.getPayments())) { firstAuthorizationLevel = firstAuthorizationLevel(params.getTransferType(), params.getAmount(), params.getFrom()); } return firstAuthorizationLevel != null; } @Override public boolean wouldRequireAuthorization(final Invoice invoice) { final DoPaymentDTO payment = new DoPaymentDTO(); payment.setFrom(invoice.getTo()); payment.setTo(invoice.getFrom()); payment.setTransferType(invoice.getTransferType()); payment.setAmount(invoice.getAmount()); return wouldRequireAuthorization(payment); } @Override public boolean wouldRequireAuthorization(final Transfer transfer) { return firstAuthorizationLevel(transfer) != null; } @Override public boolean wouldRequireAuthorization(final TransferType transferType, final BigDecimal amount, final AccountOwner from) { return firstAuthorizationLevel(transferType, amount, from) != null; } private void addAmountValidator(final Validator validator, final TransferType tt) { Property amountProperty = validator.property("amount").required().positiveNonZero(); // Max amount & min amount if (tt != null && tt.getMinAmount() != null) { amountProperty.greaterEquals(tt.getMinAmount()); } } /** * Runs the {@link #performInsert(TransferDTO, AuthorizationLevel)} method optionally in a new transaction. Does this while deadlocks occur. Other * errors are just rethrown. */ private Payment doInsert(final TransferDTO dto, final boolean newTransaction, final boolean simulation) { return transactionHelper.maybeRunInNewTransaction(new Transactional<Payment>() { @Override public Payment afterCommit(final Payment payment) { return fetchService.fetch(payment); } @Override public Payment doInTransaction(final TransactionStatus status) { return performInsert(dto, simulation); } }, newTransaction); } private Payment doPayment(final DoPaymentDTO params, final boolean newTransaction, final boolean notify, final boolean simulation) { // Check permission to pay with date if (params.getDate() != null && !permissionService.hasPermission(AdminMemberPermission.PAYMENTS_PAYMENT_WITH_DATE)) { throw new PermissionDeniedException(); } // Validate dto validate(params); // Insert the transfer final TransferDTO dto = verify(params); final Payment payment = doInsert(dto, newTransaction, simulation); // Notify if (notify) { if (LoggedUser.isWebService()) { memberNotificationHandler.externalChannelPaymentPerformed(params, payment); } else { memberNotificationHandler.paymentReceivedNotification(payment); } if (payment instanceof Transfer) { Transfer transfer = (Transfer) payment; if (payment.getProcessDate() == null) { adminNotificationHandler.notifyNewPendingPayment(transfer); } else { adminNotificationHandler.notifyPayment(transfer); } } } return payment; } private Transfer doProcessScheduledTransfer(final LockHandler lockHandler, Transfer transfer, final boolean failOnError, final boolean notifyPayer, final boolean notifyReceiver) { transfer = fetchService.fetch(transfer, Transfer.Relationships.SCHEDULED_PAYMENT); ScheduledPayment scheduledPayment = transfer.getScheduledPayment(); if (scheduledPayment == null || !transfer.getStatus().canPayNow()) { throw new UnexpectedEntityException(); } final Account from = transfer.getFrom(); final Account to = transfer.getTo(); // Lock the required accounts LockedAccountsOnPayments lockedAccountsOnPayments = applicationService.getLockedAccountsOnPayments(); if (lockedAccountsOnPayments == LockedAccountsOnPayments.ORIGIN) { lockHandler.lock(from); } else if (lockedAccountsOnPayments == LockedAccountsOnPayments.ALL) { lockHandler.lock(from, to); } // We have to refresh the transfer after locking, to make sure it's still possible to pay now transfer = fetchService.reload(transfer, Transfer.Relationships.SCHEDULED_PAYMENT); scheduledPayment = transfer.getScheduledPayment(); if (!transfer.getStatus().canPayNow()) { throw new UnexpectedEntityException(); } final BigDecimal amount = transfer.getAmount(); final TransferType transferType = transfer.getType(); final AuthorizationLevel firstAuthorizationLevel = firstAuthorizationLevel(transfer); try { Account fromAccountToValidate = from; if (scheduledPayment.isReserveAmount()) { // When the scheduled payment has reserved the amount, we don't need to validate the from amount, because it's guaranteed to have a // reserved amount, so, we pass fromAccount = null fromAccountToValidate = null; } Collection<TransferListener> listeners = getTransferListeners(transfer); // Notify the listeners before the amount is validated for (TransferListener listener : listeners) { listener.onBeforeValidateBalance(transfer); } validateAmount(amount, fromAccountToValidate, to, transfer); final TransactionFeePreviewDTO preview = transactionFeeService.preview(from.getOwner(), to.getOwner(), transferType, amount); transfer.setAmount(preview.getFinalAmount()); if (LoggedUser.hasUser()) { transfer.setBy(LoggedUser.element()); } boolean shouldLiberateAmount = false; if (firstAuthorizationLevel != null) { transfer.setStatus(Transfer.Status.PENDING); transfer.setNextAuthorizationLevel(firstAuthorizationLevel); // Insert an amount reservation for this pending transfer, unless the scheduled payment has already reserved it if (!scheduledPayment.isReserveAmount()) { accountService.reservePending(transfer); } } else { // apply rates RatesToSave rates = rateService.applyTransfer(transfer); /* * set processDate AFTER applying rates, but before persisting them. This is important, because the transfer itself must not sum up * for rates or balances when the rates are processed, and it does if processdate is already set. In that case, the transfer's * processDate can equal the fromRates's date. */ Calendar processDate = (rates.getFromRates() == null) ? Calendar.getInstance() : rates.getFromRates().getDate(); transfer.setStatus(Transfer.Status.PROCESSED); transfer.setProcessDate(processDate); rateService.persist(rates); transfer.setEmissionDate(rates.getEmissionDate()); transfer.setExpirationDate(rates.getExpirationDate()); transfer.setiRate(rates.getiRate()); shouldLiberateAmount = scheduledPayment.isReserveAmount(); // Generate the transaction number final TransactionNumber transactionNumber = settingsService.getLocalSettings().getTransactionNumber(); if (transactionNumber != null && transactionNumber.isValid()) { final String generated = transactionNumber.generate(transfer.getId(), transfer.getProcessDate()); transfer.setTransactionNumber(generated); } } // Notify the listeners before the payment is updated (may be seen as an insert) for (TransferListener listener : listeners) { listener.onTransferInserted(transfer); } transferDao.update(transfer); // Make sure no closed balances exist on the future, to fix eventual future closed balances accountService.removeClosedBalancesAfter(transfer.getFrom(), transfer.getProcessDate()); accountService.removeClosedBalancesAfter(transfer.getTo(), transfer.getProcessDate()); // Notify listeners - Should be before inserting fees to ensure the correct notification order notifyTransferProcessed(transfer); // Insert fees insertFees(lockHandler, transfer, false, amount, false, new HashSet<ChargedFee>()); // Insert the corresponding amount reservation if the scheduled payment had reserved the total amount if (shouldLiberateAmount) { accountService.returnReservationForInstallment(transfer); } // Update scheduled payment status updateScheduledPaymentStatus(scheduledPayment); memberNotificationHandler.scheduledPaymentProcessingNotification(transfer, notifyPayer, notifyReceiver); // Notify admins if (transfer.getProcessDate() == null) { adminNotificationHandler.notifyNewPendingPayment(transfer); } } catch (final RuntimeException e) { if (failOnError) { transferDao.updateStatus(transfer.getId(), Payment.Status.FAILED); updateScheduledPaymentStatus(scheduledPayment); // Ensure the amount is liberated if (scheduledPayment.isReserveAmount()) { accountService.returnReservationForInstallment(transfer); } memberNotificationHandler.scheduledPaymentProcessingNotification(transfer, notifyPayer, notifyReceiver); // Generate an alert when it's from system if (transfer.isFromSystem()) { final Member member = (Member) transfer.getToOwner(); final LocalSettings settings = settingsService.getLocalSettings(); final Object[] arguments = { settings.getUnitsConverter(transfer.getType().getFrom().getCurrency().getPattern()).toString(transfer.getAmount()), transfer.getType().getName() }; alertService.create(member, MemberAlert.Alerts.SCHEDULED_PAYMENT_FAILED, arguments); } } else { throw e; } } return transfer; } private Transfer doProcessScheduledTransfer(final Transfer transfer, final boolean failOnError, final boolean notifyPayer, final boolean notifyReceiver) { LockHandler lockHandler = lockHandlerFactory.getLockHandlerIfLockingAccounts(); try { return doProcessScheduledTransfer(lockHandler, transfer, failOnError, notifyPayer, notifyReceiver); } finally { if (lockHandler != null) { lockHandler.release(); } } } /** * Resolve the first authorization level for the given payment, if any. When the payment wouldn't be authorizable, return null */ private AuthorizationLevel firstAuthorizationLevel(final Transfer transfer) { // If the transfer is an installment of a scheduled payment, when exists another installment which was already authorized, no new auth is // needed if (transfer.getScheduledPayment() != null) { for (Transfer installment : transfer.getScheduledPayment().getTransfers()) { if (installment.getProcessDate() != null) { // A processed installment. No further authorization needed return null; } } } return firstAuthorizationLevel(transfer.getType(), transfer.getAmount(), transfer.getFromOwner()); } /** * Resolve the first authorization level for the given payment, if any. When the payment wouldn't be authorizable, return null */ private AuthorizationLevel firstAuthorizationLevel(TransferType transferType, final BigDecimal amount, AccountOwner from) { transferType = fetchService.fetch(transferType, TransferType.Relationships.AUTHORIZATION_LEVELS); if (transferType.isRequiresAuthorization() && CollectionUtils.isNotEmpty(transferType.getAuthorizationLevels())) { if (from == null) { from = LoggedUser.accountOwner(); } final Account account = accountService.getAccount(new AccountDTO(from, transferType.getFrom())); BigDecimal amountSoFarToday = transferDao.getTransactionedAmountAt(null, account, transferType); final AuthorizationLevel authorization = transferType.getAuthorizationLevels().iterator().next(); // When the amount is greater than the authorization, return true final BigDecimal amountToTest = amountSoFarToday.add(amount); if (amountToTest.compareTo(authorization.getAmount()) >= 0) { return transferType.getAuthorizationLevels().iterator().next(); } } return null; } private Validator getPaymentValidator(final DoPaymentDTO payment) { final Validator validator = new Validator("transfer"); Collection<TransactionContext> possibleContexts = new ArrayList<TransactionContext>(); possibleContexts.add(TransactionContext.PAYMENT); if (LoggedUser.isWebService() || LoggedUser.isSystem()) { possibleContexts.add(TransactionContext.AUTOMATIC); } else { possibleContexts.add(TransactionContext.SELF_PAYMENT); } validator.property("context").required().anyOf(possibleContexts); validator.property("to").required().key("payment.recipient"); // as currency is maybe not set on the DTO, we get it from the TT in stead of directly from the DTO final TransferType tt = fetchService.fetch(payment.getTransferType(), Relationships.TRANSACTION_FEES, RelationshipHelper.nested(TransferType.Relationships.FROM, TransferType.Relationships.TO, AccountType.Relationships.CURRENCY, Currency.Relationships.A_RATE_PARAMETERS), RelationshipHelper.nested(TransferType.Relationships.FROM, TransferType.Relationships.TO, AccountType.Relationships.CURRENCY, Currency.Relationships.D_RATE_PARAMETERS)); final Currency currency = tt == null ? null : tt.getCurrency(); if (currency != null && (currency.isEnableARate() || currency.isEnableDRate())) { // if the date is not null at this moment, it is in the past, which is not allowed with rates. if (payment.getDate() != null) { validator.general(new NoPastDateWithRatesValidator()); } } else { validator.property("date").key("payment.manualDate").past(); } validator.property("ticket").add(new TicketValidation()); addAmountValidator(validator, tt); validator.property("transferType").key("transfer.type").required(); validator.property("description").maxLength(1000); validator.general(new SchedulingValidator()); validator.general(new PendingContractValidator()); if (payment.getTransferType() != null && payment.getTo() != null && payment.getAmount() != null) { /* * For user validation, we need to check if the transaction amount is high enough to cover all fees. This depends on all fees, but only in * case of fixed fees it makes sense to increase the transaction amount. The formula for this is: given transactionamount > (sum of fixed * fees )/ (1 minus sum of percentage fees expressed as fractions). This of course only applies for fees with deductAmount; fees which are * not deducted are excluded from this calculation. */ final TransactionFeePreviewDTO preview = transactionFeeService.preview(payment.getFrom(), payment.getTo(), tt, payment.getAmount()); final Property amount = validator.property("amount"); final Collection<? extends TransactionFee> fees = preview.getFees().keySet(); BigDecimal sumOfFixedFees = BigDecimal.ZERO; BigDecimal sumOfPercentageFees = BigDecimal.ZERO; for (final TransactionFee fee : fees) { if (fee.isDeductAmount()) { if (fee.getChargeType() == ChargeType.FIXED) { sumOfFixedFees = sumOfFixedFees.add(preview.getFees().get(fee)); } else { sumOfPercentageFees = sumOfPercentageFees.add(preview.getFees().get(fee)); } } } // Show a warning if there are fixed fees and if the amount is not enough to cover them if (sumOfFixedFees.signum() == 1) { final int scale = LocalSettings.MAX_PRECISION; final MathContext mc = new MathContext(scale); final BigDecimal sumOfPercentages = sumOfPercentageFees.divide(payment.getAmount(), mc); final BigDecimal minimalAmount = sumOfFixedFees.divide((BigDecimal.ONE.subtract(sumOfPercentages)), mc); amount.comparable(minimalAmount, ">", new ValidationError("errors.greaterThan", messageResolver.message("transactionFee.invalidChargeValue", minimalAmount))); } else if (preview.getFinalAmount().signum() == -1) { validator.general(new FinalAmountValidator()); } // Custom fields validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() { @Override public Validator getValidator() { return paymentCustomFieldService.getValueValidator(payment.getTransferType()); } })); } return validator; } private Validator getProjectionValidator() { final Validator projectionValidator = new Validator("transfer"); projectionValidator.property("paymentCount").required().positiveNonZero().add(new PropertyValidation() { private static final long serialVersionUID = 5022911381764849941L; @Override public ValidationError validate(final Object object, final Object property, final Object value) { final Integer paymentCount = (Integer) value; if (paymentCount == null) { return null; } final ProjectionDTO dto = (ProjectionDTO) object; final AccountOwner from = dto.getFrom(); if (from instanceof Member) { final Member member = fetchService.fetch((Member) from, Element.Relationships.GROUP); final int maxSchedulingPayments = member.getMemberGroup().getMemberSettings().getMaxSchedulingPayments(); return CompareToValidation.lessEquals(maxSchedulingPayments).validate(object, property, value); } return null; } }); projectionValidator.property("amount").required().positiveNonZero(); projectionValidator.property("firstExpirationDate").key("transfer.firstPaymentDate").required().add(new PropertyValidation() { private static final long serialVersionUID = -3612786027250751763L; @Override public ValidationError validate(final Object object, final Object property, final Object value) { final Calendar firstDate = CoercionHelper.coerce(Calendar.class, value); if (firstDate == null) { return null; } if (firstDate.before(DateHelper.truncate(Calendar.getInstance()))) { return new InvalidError(); } return null; } }); projectionValidator.property("recurrence.number").key("transfer.paymentEvery").required().between(1, 100); projectionValidator.property("recurrence.field").key("transfer.paymentEvery").required().anyOf(TimePeriod.Field.DAYS, TimePeriod.Field.WEEKS, TimePeriod.Field.MONTHS); return projectionValidator; } /** * gets the topmost parent of the transfer */ private Transfer getTopMost(final Transfer transfer) { Transfer topMost = transfer; while (topMost.getParent() != null) { topMost = topMost.getParent(); } return topMost; } private Collection<TransferListener> getTransferListeners(final Transfer transfer) { TransferType type = transfer.getType(); Collection<TransferListener> result = new ArrayList<TransferListener>(2); // Get the listener from transfer type, if any if (StringUtils.isNotEmpty(type.getTransferListenerClass())) { TransferListener listener = customObjectHandler.get(type.getTransferListenerClass()); result.add(listener); } // Get the listener from settings, if any LocalSettings settings = settingsService.getLocalSettings(); if (StringUtils.isNotEmpty(settings.getTransferListenerClass())) { TransferListener listener = customObjectHandler.get(settings.getTransferListenerClass()); result.add(listener); } return result; } private Validator getTransferValidator(final TransferDTO transfer) { final Validator validator = new Validator("transfer"); // as currency is sometimes not set on the DTO, we get it from the TT in stead of directly from the DTO final TransferType tt = fetchService.fetch(transfer.getTransferType(), RelationshipHelper.nested(TransferType.Relationships.FROM, AccountType.Relationships.CURRENCY, Currency.Relationships.A_RATE_PARAMETERS, Currency.Relationships.D_RATE_PARAMETERS)); final Currency currency = tt.getCurrency(); // if rates enabled, it is not allowed to have a date in the past. if (currency.isEnableARate() || currency.isEnableDRate()) { final Calendar now = Calendar.getInstance(); // make a few minutes earlier, because if the transfer's date has just before been set to Calendar.getInstance(), it may already be a // few milliseconds or even seconds later. now.add(Calendar.MINUTE, -4); final Calendar date = transfer.getDate(); if (date != null && date.before(now)) { validator.general(new NoPastDateWithRatesValidator()); } } else { validator.property("date").key("payment.manualDate").pastOrToday(); } validator.property("fromOwner").required(); validator.property("toOwner").required(); addAmountValidator(validator, tt); validator.property("transferType").key("transfer.type").required(); validator.property("description").maxLength(1000); validator.property("traceNumber").add(new TraceNumberValidation()); if (transfer.getTransferType() != null) { // Custom fields validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() { @Override public Validator getValidator() { return paymentCustomFieldService.getValueValidator(transfer.getTransferType()); } })); } return validator; } private Payment insert(final TransferDTO dto, final boolean newTransaction, final boolean simulation) { // Verify the parameters verify(dto); return doInsert(dto, newTransaction, simulation); } private Transfer insertChargeback(final Transfer transfer, final boolean newTransaction) { return transactionHelper.maybeRunInNewTransaction(new Transactional<Transfer>() { @Override public Transfer afterCommit(final Transfer result) { // Ensure the transfer is attached to the current transaction return fetchService.fetch(result); } @Override public Transfer doInTransaction(final TransactionStatus status) { return performChargeback(transfer); } }, newTransaction); } private void insertFees(final LockHandler lockHandler, final Transfer transfer, final boolean forced, final BigDecimal originalAmount, final boolean simulation, final Set<ChargedFee> chargedFees) { final TransferType transferType = transfer.getType(); final Account from = transfer.getFrom(); final Account to = transfer.getTo(); final TransactionFeeQuery query = new TransactionFeeQuery(); query.setTransferType(transferType); final List<? extends TransactionFee> fees = transactionFeeService.search(query); BigDecimal totalPercentage = BigDecimal.ZERO; BigDecimal feeTotalAmount = BigDecimal.ZERO; Transfer topMost = getTopMost(transfer); final Calendar date = topMost.getDate(); transfer.setChildren(new ArrayList<Transfer>()); for (final TransactionFee fee : fees) { final Account fromAccount = fetchService.fetch(from, Account.Relationships.TYPE, MemberAccount.Relationships.MEMBER); final Account toAccount = fetchService.fetch(to, Account.Relationships.TYPE, MemberAccount.Relationships.MEMBER); final ChargedFee key = new ChargedFee(fee, fromAccount, toAccount); if (chargedFees.contains(key)) { throw new ValidationException("payment.error.circularFees"); } chargedFees.add(key); // Build the fee transfer final BuildTransferWithFeesDTO params = new BuildTransferWithFeesDTO(date, fromAccount, toAccount, originalAmount, fee, false); // rate stuff; buildTransfer MUST have these set. params.setEmissionDate(transfer.getEmissionDate()); params.setExpirationDate(transfer.getExpirationDate()); final Transfer feeTransfer = transactionFeeService.buildTransfer(params); // If the fee transfer is null, the fee should not be applied if (feeTransfer == null) { continue; } // Ensure the last fee when 100% will be the exact amount left if (fee instanceof SimpleTransactionFee && fee.getAmount().isPercentage()) { final BigDecimal feeValue = fee.getAmount().getValue(); // Only when it's not a single fee if (!(totalPercentage.equals(BigDecimal.ZERO) && feeValue.doubleValue() == 100.0)) { totalPercentage = totalPercentage.add(feeValue); // TODO: shouldn't this be >= 0 in stead of == 0 (Rinke) ? if (totalPercentage.compareTo(new BigDecimal(100)) == 0 && feeTransfer != null) { feeTransfer.setAmount(originalAmount.subtract(feeTotalAmount)); } } } // Insert the fee transfer if (feeTransfer != null && feeTransfer.getAmount().floatValue() > PRECISION_DELTA) { feeTotalAmount = feeTotalAmount.add(feeTransfer.getAmount()); feeTransfer.setParent(transfer); feeTransfer.setDate(transfer.getDate()); feeTransfer.setStatus(transfer.getStatus()); feeTransfer.setNextAuthorizationLevel(transfer.getNextAuthorizationLevel()); feeTransfer.setProcessDate(transfer.getProcessDate()); feeTransfer.setExternalTransfer(transfer.getExternalTransfer()); feeTransfer.setBy(transfer.getBy()); // Copy custom values of common custom fields from the parent to the fee transfer final List<PaymentCustomField> customFields = paymentCustomFieldService.list(feeTransfer.getType(), false); if (!CollectionUtils.isEmpty(transfer.getCustomValues())) { final Collection<PaymentCustomFieldValue> feeTransferCustomValues = new ArrayList<PaymentCustomFieldValue>(); for (final PaymentCustomFieldValue fieldValue : transfer.getCustomValues()) { final CustomField field = fieldValue.getField(); if (customFields.contains(field)) { final PaymentCustomFieldValue newFieldValue = new PaymentCustomFieldValue(); newFieldValue.setField(field); newFieldValue.setValue(fieldValue.getValue()); feeTransferCustomValues.add(newFieldValue); } } feeTransfer.setCustomValues(feeTransferCustomValues); } insertTransferAndPayFees(lockHandler, feeTransfer, forced, simulation, chargedFees); transfer.getChildren().add(feeTransfer); } } } /** * Inserts a TN for a transfer with the specified trace number, for the current service client * @return true if the TN was inserted */ private boolean insertTN(final Long clientId, final String traceNumber) { return transactionHelper.runInNewTransaction(new TransactionCallback<Boolean>() { @Override public Boolean doInTransaction(final TransactionStatus status) { final TraceNumber tn = new TraceNumber(); tn.setDate(Calendar.getInstance()); tn.setClientId(clientId); tn.setTraceNumber(traceNumber); try { traceNumberDao.insert(tn); return true; } catch (DaoException e) { status.setRollbackOnly(); if (ExceptionUtils.indexOfThrowable(e, DataIntegrityViolationException.class) != -1) { // the unique constraint was violated - It means the trace number was already stored by a payment or by other reverse. // If it was inserted by a payment then we must reverse it. // If was inserted by other reverse then just ignore it. return false; } else { throw e; } } } }); } /** * Insert a transfer and it's generated fees * @param simulation */ private Transfer insertTransferAndPayFees(final LockHandler lockHandler, Transfer transfer, final boolean forced, final boolean simulation, final Set<ChargedFee> chargedFees) { final TransferType transferType = transfer.getType(); final Collection<PaymentCustomFieldValue> customValues = transfer.getCustomValues(); final Account fromAccount = transfer.getFrom(); final Account toAccount = transfer.getTo(); if (fromAccount.equals(toAccount)) { throw new ValidationException("payment.error.sameFromAntToInFee"); } if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ALL) { lockHandler.lock(fromAccount, toAccount); } final AccountOwner from = fromAccount.getOwner(); final AccountOwner to = toAccount.getOwner(); // Preview fees to determine the deducted amount final BigDecimal originalAmount = transfer.getAmount(); final TransactionFeePreviewDTO preview = transactionFeeService.preview(from, to, transferType, transfer.getAmount()); transfer.setAmount(preview.getFinalAmount()); final Collection<TransferListener> listeners = getTransferListeners(transfer); // validate parent amount if (!forced) { // Notify any registered listener before validating the amount if (!simulation) { for (final TransferListener listener : listeners) { listener.onBeforeValidateBalance(transfer); } } validateAmount(transfer.getAmount(), fromAccount, toAccount, transfer); } transfer.setCustomValues(null); // apply rates, but NOT on inserting authorized payments RatesToSave rates = new RatesToSave(); if (transfer.getProcessDate() != null) { rates = rateService.applyTransfer(transfer); transfer.setEmissionDate(rates.getEmissionDate()); transfer.setExpirationDate(rates.getExpirationDate()); transfer.setiRate(rates.getiRate()); } // insert transfer transfer = transferDao.insert(transfer); // now we have the tranfers' id, we can persist rate info: rateService.persist(rates); final TransactionNumber transactionNumber = settingsService.getLocalSettings().getTransactionNumber(); if (transactionNumber != null && transactionNumber.isValid()) { final String generated = transactionNumber.generate(transfer.getId(), transfer.getDate()); transferDao.updateTransactionNumber(transfer.getId(), generated); } transfer.setCustomValues(customValues); paymentCustomFieldService.saveValues(transfer); if (transfer.getProcessDate() == null) { // Reserve the amount if pending authorization accountService.reservePending(transfer); } else { // Make sure no closed balances exist after the payment. This works both for payments in past and to fix eventual future closed balances accountService.removeClosedBalancesAfter(transfer.getFrom(), transfer.getProcessDate()); accountService.removeClosedBalancesAfter(transfer.getTo(), transfer.getProcessDate()); } // Notify any registered listener after inserting the transfer if (!simulation) { for (TransferListener listener : listeners) { listener.onTransferInserted(transfer); } } // Log this transfer if the transaction succeeds final Transfer toLog = transfer; CurrentTransactionData.addTransactionCommitListener(new TransactionCommitListener() { @Override public void onTransactionCommit() { loggingHandler.logTransfer(toLog); // Notify the registered listeners if the transfer is processed if (!simulation && toLog.getProcessDate() != null && !listeners.isEmpty()) { transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { Transfer fetchedTransfer = fetchService.fetch(toLog, Payment.Relationships.FROM, Payment.Relationships.TO); for (TransferListener listener : listeners) { try { listener.onTransferProcessed(fetchedTransfer); } catch (Exception e) { LOG.warn("Error running TransferListener " + listener, e); } } } }); } } }); insertFees(lockHandler, transfer, forced, originalAmount, simulation, chargedFees); return transfer; } private Transfer performChargeback(final LockHandler lockHandler, Transfer transfer, final Transfer parentChargeback) { transfer = fetchService.fetch(transfer, Transfer.Relationships.CHILDREN); if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ALL) { lockHandler.lock(transfer.getFrom(), transfer.getTo()); } // Validate the amount validateAmount(transfer.getAmount(), transfer.getTo(), transfer.getFrom(), transfer); // Duplicate the transfer, setting relevant properties on the charge-back Transfer chargeback = transferDao.duplicate(transfer); chargeback.setTraceNumber(null); ServiceClient serviceClient = LoggedUser.serviceClient(); if (serviceClient != null) { chargeback.setClientId(serviceClient.getId()); } chargeback.setChargebackOf(transfer); chargeback.setParent(parentChargeback); chargeback.setAmount(chargeback.getAmount().negate()); final Calendar now = Calendar.getInstance(); chargeback.setDate(now); chargeback.setProcessDate(now); chargeback.setStatus(Payment.Status.PROCESSED); if (LoggedUser.hasUser()) { chargeback.setBy(LoggedUser.element()); } chargeback.setReceiver(null); chargeback.setScheduledPayment(null); // Build the description according to settings final LocalSettings localSettings = settingsService.getLocalSettings(); final Map<String, Object> variables = new HashMap<String, Object>(); variables.put("description", transfer.getDescription()); variables.put("date", localSettings.getDateConverter().toString(transfer.getDate())); chargeback.setDescription(MessageProcessingHelper.processVariables(localSettings.getChargebackDescription(), variables)); // Insert the chargeback chargeback = transferDao.insert(chargeback, false); // Copy the custom values from the original transfer if (CollectionUtils.isNotEmpty(transfer.getCustomValues())) { final Collection<PaymentCustomFieldValue> customValues = new ArrayList<PaymentCustomFieldValue>(); if (transfer.getCustomValues() != null) { for (final PaymentCustomFieldValue original : transfer.getCustomValues()) { final PaymentCustomFieldValue newValue = new PaymentCustomFieldValue(); newValue.setTransfer(chargeback); newValue.setField(original.getField()); newValue.setStringValue(original.getStringValue()); newValue.setPossibleValue(original.getPossibleValue()); customValues.add(newValue); } } chargeback.setCustomValues(customValues); paymentCustomFieldService.saveValues(chargeback); } // Update the original transfer transfer = transferDao.updateChargeBack(transfer, chargeback); // Assign the transaction number final TransactionNumber transactionNumber = settingsService.getLocalSettings().getTransactionNumber(); if (transactionNumber != null && transactionNumber.isValid()) { final String generated = transactionNumber.generate(chargeback.getId(), chargeback.getDate()); transferDao.updateTransactionNumber(chargeback.getId(), generated); } // Make sure no closed balances exist on the future, to fix eventual future closed balances accountService.removeClosedBalancesAfter(chargeback.getFrom(), chargeback.getProcessDate()); accountService.removeClosedBalancesAfter(chargeback.getTo(), chargeback.getProcessDate()); // Correct the rates, if available rateService.chargeback(transfer, chargeback); // Insert children chargebacks for (final Transfer child : transfer.getChildren()) { performChargeback(lockHandler, child, chargeback); } return chargeback; } private Transfer performChargeback(final Transfer transfer) { LockHandler lockHandler = lockHandlerFactory.getLockHandlerIfLockingAccounts(); try { // If only the source account needs to be locked, lock it here if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ORIGIN && transfer.getTo().getCreditLimit() != null) { lockHandler.lock(transfer.getTo()); } return performChargeback(lockHandler, transfer, null); } finally { if (lockHandler != null) { lockHandler.release(); } } } /** * Locks the accounts which need to be locked, and perform the insert */ private Payment performInsert(final LockHandler lockHandler, final TransferDTO dto, final boolean simulation) { final TransferType transferType = dto.getTransferType(); final Account fromAccount = fetchService.fetch(dto.getFrom(), RelationshipHelper.nested(Account.Relationships.TYPE, AccountType.Relationships.CURRENCY)); final Account toAccount = fetchService.fetch(dto.getTo(), MemberAccount.Relationships.MEMBER); if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ALL) { lockHandler.lock(fromAccount, toAccount); } // Get the feedback deadline Calendar feedbackDeadline = null; if (transferType.isRequiresFeedback()) { feedbackDeadline = transferType.getFeedbackExpirationTime().add(Calendar.getInstance()); } boolean hasMaxAmountPerDay = BigDecimalHelper.nvl(transferType.getMaxAmountPerDay()).compareTo(BigDecimal.ZERO) > 0; Payment payment; // Check scheduling final Calendar now = Calendar.getInstance(); if (CollectionUtils.isEmpty(dto.getPayments())) { // Not scheduled - build a transfer final String traceNumber = dto.getTraceNumber(); final Long clientId = dto.getClientId(); Transfer transfer = new Transfer(); transfer.setFrom(fromAccount); transfer.setTo(toAccount); transfer.setBy(dto.getBy()); transfer.setDate(now); transfer.setAmount(dto.getAmount()); transfer.setType(transferType); transfer.setDescription(dto.getDescription()); transfer.setAccountFeeLog(dto.getAccountFeeLog()); transfer.setLoanPayment(dto.getLoanPayment()); transfer.setParent(dto.getParent()); transfer.setReceiver(dto.getReceiver()); transfer.setExternalTransfer(dto.getExternalTransfer()); transfer.setCustomValues(dto.getCustomValues()); transfer.setTraceNumber(traceNumber); transfer.setClientId(clientId); transfer.setTraceData(dto.getTraceData()); transfer.setTransactionFeedbackDeadline(feedbackDeadline); if (transferType.isLoanType()) { transfer.setEmissionDate(dto.getEmissionDate()); transfer.setExpirationDate(dto.getExpirationDate()); transfer.setiRate(dto.getiRate()); } // Lock the accounts if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ORIGIN && (fromAccount.getCreditLimit() != null || hasMaxAmountPerDay)) { lockHandler.lock(fromAccount); } // Tests whether there is a valid ticket to be used final Ticket ticket = fetchService.reload(dto.getTicket()); if (ticket != null) { if (ticket.getStatus() != Ticket.Status.PENDING) { throw new EntityNotFoundException(Ticket.class); } // Force the ticket parameters on the payment if (ticket.getAmount() != null && !ticket.getAmount().equals(transfer.getAmount())) { // TODO add a translation key throw new ValidationException("The payment amount is not the expected one according to the ticket"); } if (!ticket.getTo().equals(transfer.getToOwner())) { // TODO add a translation key throw new ValidationException("The payment destination member is not the expected one according to the ticket"); } if (StringUtils.isNotEmpty(ticket.getDescription()) && StringUtils.isEmpty(transfer.getDescription())) { transfer.setDescription(ticket.getDescription()); } } // Determine whether the transfer is authorized AuthorizationLevel firstAuthorizationLevel; final Transfer parent = fetchService.fetch(dto.getParent(), Transfer.Relationships.NEXT_AUTHORIZATION_LEVEL); if (parent != null && parent.getNextAuthorizationLevel() != null) { firstAuthorizationLevel = parent.getNextAuthorizationLevel(); } else { firstAuthorizationLevel = firstAuthorizationLevel(transferType, transfer.getAmount(), transfer.getFromOwner()); } // Authorized payments are not allowed in past date if (firstAuthorizationLevel != null) { if (dto.getDate() != null && !DateUtils.isSameDay(dto.getDate(), Calendar.getInstance())) { throw new AuthorizedPaymentInPastException(); } } // Set the status according to the authorization level if (firstAuthorizationLevel == null) { transfer.setProcessDate(dto.getDate() == null ? now : dto.getDate()); transfer.setStatus(Transfer.Status.PROCESSED); } else { transfer.setStatus(Transfer.Status.PENDING); transfer.setNextAuthorizationLevel(firstAuthorizationLevel); } // Within the critical session, we must check the trace number again, as another thread could have inserted a reverse on it if (clientId != null && StringUtils.isNotEmpty(traceNumber)) { // If the TN was not inserted then this payment was already reversed, and should fail if (!insertTN(clientId, traceNumber)) { throw new ValidationException("traceNumber", "transfer.traceNumber", new UniqueError(traceNumber)); } } // Validate the max amount today if (!dto.isForced()) { validateMaxAmountAtDate(null, fromAccount, transferType, null, transfer.getAmount()); } // Insert the transfer and pay fees transfer = insertTransferAndPayFees(lockHandler, transfer, dto.isForced(), simulation, new HashSet<ChargedFee>()); // Process the authorization automatically when the authorizer is performing a payment as member payment = transferAuthorizationService.authorizeOnInsert(lockHandler, transfer); // Complete the ticket, if it exists if (ticket != null) { ticket.setAmount(payment.getAmount()); ticket.setDescription(payment.getDescription()); if (payment.getFrom().getOwner() instanceof Member) { ticket.setFrom((Member) payment.getFrom().getOwner()); } else { ticket.setFrom(null); } ticket.setTo((Member) payment.getTo().getOwner()); ticket.setStatus(Ticket.Status.OK); ticket.setTransfer((Transfer) payment); } } else { // Scheduled payment final boolean reserveTotalAmount = transferType.isReserveTotalAmountOnScheduling(); if (!dto.isForced() && (reserveTotalAmount || hasMaxAmountPerDay)) { // Ensure the from account is locked, to prevent concurrent access to available balance (which could allow an account pass the limit) lockHandler.lock(fromAccount); // Validate the account has balance for the total amount if (reserveTotalAmount) { validateAmount(dto.getAmount(), fromAccount, null, null); } // Validate the max amount today if (hasMaxAmountPerDay) { for (final ScheduledPaymentDTO current : dto.getPayments()) { validateMaxAmountAtDate(current.getDate(), fromAccount, transferType, null, current.getAmount()); } // TODO now we're controlling the total amount, but should control by installment } } final Collection<PaymentCustomFieldValue> customValues = dto.getCustomValues(); ScheduledPayment scheduledPayment = new ScheduledPayment(); scheduledPayment.setFrom(fromAccount); scheduledPayment.setTo(toAccount); scheduledPayment.setBy(dto.getBy()); scheduledPayment.setDate(now); scheduledPayment.setAmount(dto.getAmount()); scheduledPayment.setType(transferType); scheduledPayment.setDescription(dto.getDescription()); scheduledPayment.setStatus(Payment.Status.SCHEDULED); scheduledPayment.setReserveAmount(reserveTotalAmount); scheduledPayment.setShowToReceiver(transferType.isShowScheduledPaymentsToDestination() || dto.isShowScheduledToReceiver()); scheduledPayment.setTransactionFeedbackDeadline(feedbackDeadline); scheduledPayment = scheduledPaymentDao.insert(scheduledPayment); scheduledPayment.setCustomValues(new ArrayList<PaymentCustomFieldValue>(customValues)); paymentCustomFieldService.saveValues(scheduledPayment); final List<Transfer> scheduledTransfers = new ArrayList<Transfer>(); Transfer transferToProcess = null; for (final ScheduledPaymentDTO current : dto.getPayments()) { final TransferDTO currentDTO = (TransferDTO) dto.clone(); currentDTO.setDate(current.getDate()); currentDTO.setAmount(current.getAmount()); currentDTO.setScheduledPayment(scheduledPayment); Transfer transfer = new Transfer(); transfer.setFrom(fromAccount); transfer.setTo(dto.getTo()); transfer.setBy(dto.getBy()); transfer.setDate(current.getDate()); transfer.setAmount(current.getAmount()); transfer.setType(transferType); transfer.setDescription(dto.getDescription()); transfer.setStatus(Transfer.Status.SCHEDULED); transfer.setScheduledPayment(scheduledPayment); // When the payment is scheduled for today, process it now if (DateUtils.isSameDay(now, transfer.getDate())) { transferToProcess = transfer; transfer.setDate(now); } transfer = transferDao.insert(transfer); transfer.setCustomValues(new ArrayList<PaymentCustomFieldValue>()); if (customValues != null) { for (final PaymentCustomFieldValue fieldValue : customValues) { final PaymentCustomFieldValue newValue = new PaymentCustomFieldValue(); newValue.setField(fieldValue.getField()); newValue.setStringValue(fieldValue.getStringValue()); newValue.setPossibleValue(fieldValue.getPossibleValue()); transfer.getCustomValues().add(newValue); } } paymentCustomFieldService.saveValues(transfer); scheduledTransfers.add(transfer); } scheduledPayment.setTransfers(scheduledTransfers); // When the scheduled payment is set to reserve the amount, add the corresponding amount reservation if (scheduledPayment.isReserveAmount()) { accountService.reserve(scheduledPayment); } // When the first transfer should already by processed, do it now if (transferToProcess != null) { doProcessScheduledTransfer(lockHandler, transferToProcess, true, false, true); } payment = scheduledPayment; } // Return the transfer object return payment; } /** * Locks the accounts which need to be locked, and perform the insert */ private Payment performInsert(final TransferDTO dto, final boolean simulation) { LockHandler lockHandler = lockHandlerFactory.getLockHandlerIfLockingAccounts(); try { return performInsert(lockHandler, dto, simulation); } finally { if (lockHandler != null) { lockHandler.release(); } } } private Transfer processScheduledTransfer(final Transfer transfer, final boolean failOnError, final boolean notifyPayer, final boolean notifyReceiver) { return transactionHelper.maybeRunInNewTransaction(new Transactional<Transfer>() { @Override public Transfer afterCommit(final Transfer result) { // Ensure the transfer is attached to the current transaction return fetchService.fetch(result); } @Override public Transfer doInTransaction(final TransactionStatus status) { return doProcessScheduledTransfer(transfer, failOnError, notifyPayer, notifyReceiver); }; }); } private ScheduledPayment updateScheduledPaymentStatus(ScheduledPayment scheduledPayment) { scheduledPayment = fetchService.fetch(scheduledPayment, ScheduledPayment.Relationships.TRANSFERS); scheduledPayment.setStatus(Payment.Status.PROCESSED); for (final Transfer transfer : scheduledPayment.getTransfers()) { if (transfer.getProcessDate() == null) { scheduledPayment.setStatus(transfer.getStatus()); break; } } return scheduledPaymentDao.update(scheduledPayment); } private void validate(final TransferDTO params) { getTransferValidator(params).validate(params); } /** * Validates the given amount */ private void validateAmount(final BigDecimal amount, Account fromAccount, final Account toAccount, final Transfer transfer) { // Validate the from account credit limit ... final LocalSettings localSettings = settingsService.getLocalSettings(); if (fromAccount != null) { final BigDecimal creditLimit = fromAccount.getCreditLimit(); if (creditLimit != null) { // ... only if not unlimited final AccountStatus fromStatus = accountService.getCurrentStatus(new AccountDTO(fromAccount)); if (creditLimit.abs().floatValue() > -PRECISION_DELTA) { final BigDecimal available = localSettings.round(fromStatus.getAvailableBalance()); if (available.subtract(amount).floatValue() < -PRECISION_DELTA) { final boolean isOriginalAccount = transfer == null ? true : fromAccount.equals(transfer.getRootTransfer().getFrom()); fromAccount = fetchService.fetch(fromAccount, Account.Relationships.TYPE); throw new NotEnoughCreditsException(fromAccount, amount, isOriginalAccount); } } } } // Validate the to account upper credit limit if (toAccount != null) { final BigDecimal upperCreditLimit = toAccount.getUpperCreditLimit(); if (upperCreditLimit != null && upperCreditLimit.floatValue() > PRECISION_DELTA) { final BigDecimal balance = accountService.getBalance(new AccountDateDTO(toAccount)); if (upperCreditLimit.subtract(balance).subtract(amount).floatValue() < -PRECISION_DELTA) { throw new UpperCreditLimitReachedException(localSettings.getUnitsConverter(toAccount.getType().getCurrency().getPattern()).toString(toAccount.getUpperCreditLimit()), toAccount, amount); } } } } /** * Validates if a given transfer type is valid */ private TransferType validateTransferType(final TransferDTO params) { final TransferType transferType = transferTypeService.load(params.getTransferType().getId(), TransferType.Relationships.FROM, TransferType.Relationships.TO); final TransferTypeQuery ttQuery = new TransferTypeQuery(); ttQuery.setChannel(params.getChannel()); if (params.isAutomatic()) { ttQuery.setContext(transferType.isLoanType() ? TransactionContext.AUTOMATIC_LOAN : TransactionContext.AUTOMATIC); } else { ttQuery.setContext(params.getContext()); } final TransactionContext context = ttQuery.getContext(); if (context != TransactionContext.AUTOMATIC && context != TransactionContext.AUTOMATIC_LOAN) { ttQuery.setUsePriority(true); } ttQuery.setCurrency(params.getCurrency()); ttQuery.setFromAccountType(transferType.getFrom()); ttQuery.setToAccountType(transferType.getTo()); final AccountOwner fromOwner = params.getFromOwner(); // For non-automatic payments, ensure there is permission for the TransferType if (context != TransactionContext.AUTOMATIC && context != TransactionContext.AUTOMATIC_LOAN) { if (params.getBy() != null && fromOwner != null && !params.getBy().getAccountOwner().equals(fromOwner)) { // Set by when performing a payment in behalf of someone ttQuery.setBy(params.getBy()); } else { // Test the permission for the payment if (fromOwner instanceof Member) { ttQuery.setGroup(((Member) fromOwner).getGroup()); } else if (LoggedUser.hasUser()) { ttQuery.setGroup(LoggedUser.group()); } } } ttQuery.setFromOwner(fromOwner); ttQuery.setToOwner(params.getToOwner()); final List<TransferType> possibleTypes = transferTypeService.search(ttQuery); if (possibleTypes == null || !possibleTypes.contains(transferType)) { throw new UnexpectedEntityException("Transfer type not found for query"); } return transferType; } private TransferDTO verify(final DoPaymentDTO params) { // Build and verify the DTO final TransferDTO dto = new TransferDTO(); dto.setAmount(params.getAmount()); dto.setCurrency(params.getCurrency()); dto.setChannel(params.getChannel()); dto.setContext(params.getContext()); if (params.getDate() != null) { dto.setDate(params.getDate()); } dto.setDescription(params.getDescription()); dto.setFromOwner(params.getFrom() == null ? LoggedUser.accountOwner() : params.getFrom()); if (LoggedUser.hasUser() && !LoggedUser.isWebService()) { dto.setBy(LoggedUser.element()); } dto.setToOwner(params.getTo()); dto.setTicket(params.getTicket()); dto.setTransferType(params.getTransferType()); dto.setReceiver(params.getReceiver()); dto.setPayments(params.getPayments()); dto.setCustomValues(params.getCustomValues()); dto.setTraceData(params.getTraceData()); dto.setShowScheduledToReceiver(params.isShowScheduledToReceiver()); ServiceClient serviceClient = LoggedUser.serviceClient(); if (serviceClient != null && params.getTraceNumber() != null) { dto.setTraceNumber(params.getTraceNumber()); dto.setClientId(serviceClient.getId()); } verify(dto); return dto; } private void verify(final TransferDTO params) { if (params.getFrom() != null) { final Account from = fetchService.fetch(params.getFrom(), MemberAccount.Relationships.MEMBER); params.setFromOwner(from.getOwner()); } if (params.getTo() != null) { final Account to = fetchService.fetch(params.getTo(), MemberAccount.Relationships.MEMBER); params.setToOwner(to.getOwner()); } validate(params); final AccountOwner fromOwner = params.getFromOwner(); final AccountOwner toOwner = params.getToOwner(); // Validate the transfer type final TransferType transferType = validateTransferType(params); // Retrieve the from and to accounts final Account fromAccount = accountService.getAccount(new AccountDTO(fromOwner, transferType.getFrom())); final Account toAccount = accountService.getAccount(new AccountDTO(toOwner, transferType.getTo())); if (fromAccount.equals(toAccount)) { throw new ValidationException(new ValidationError("payment.error.sameAccount")); } // Retrieve the amount final BigDecimal amount = params.getAmount(); // Check the minimum payment if (amount.compareTo(getMinimumPayment()) == -1) { final LocalSettings localSettings = settingsService.getLocalSettings(); throw new TransferMinimumPaymentException(localSettings.getUnitsConverter(fromAccount.getType().getCurrency().getPattern()).toString(getMinimumPayment()), fromAccount, amount); } // Update some retrieved parameters on the DTO params.setTransferType(transferType); params.setFrom(fromAccount); params.setTo(toAccount); if (StringUtils.isBlank(params.getDescription())) { params.setDescription(transferType.getDescription()); } } }