nl.strohalm.cyclos.webservices.payments.PaymentWebServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.webservices.payments.PaymentWebServiceImpl.java

Source

/*
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.webservices.payments;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import javax.jws.WebService;

import nl.strohalm.cyclos.entities.access.Channel;
import nl.strohalm.cyclos.entities.accounts.AccountOwner;
import nl.strohalm.cyclos.entities.accounts.AccountStatus;
import nl.strohalm.cyclos.entities.accounts.LockedAccountsOnPayments;
import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner;
import nl.strohalm.cyclos.entities.accounts.transactions.Payment;
import nl.strohalm.cyclos.entities.accounts.transactions.PaymentRequestTicket;
import nl.strohalm.cyclos.entities.accounts.transactions.Ticket;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferAuthorizationDTO;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferType;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomField;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomFieldValue;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.services.ServiceClient;
import nl.strohalm.cyclos.entities.services.ServiceOperation;
import nl.strohalm.cyclos.exceptions.PermissionDeniedException;
import nl.strohalm.cyclos.services.access.AccessServiceLocal;
import nl.strohalm.cyclos.services.access.exceptions.BlockedCredentialsException;
import nl.strohalm.cyclos.services.access.exceptions.InvalidCredentialsException;
import nl.strohalm.cyclos.services.accounts.AccountDTO;
import nl.strohalm.cyclos.services.accounts.AccountServiceLocal;
import nl.strohalm.cyclos.services.application.ApplicationServiceLocal;
import nl.strohalm.cyclos.services.customization.MemberCustomFieldServiceLocal;
import nl.strohalm.cyclos.services.elements.ElementServiceLocal;
import nl.strohalm.cyclos.services.services.ServiceClientServiceLocal;
import nl.strohalm.cyclos.services.transactions.BulkChargebackResult;
import nl.strohalm.cyclos.services.transactions.BulkPaymentResult;
import nl.strohalm.cyclos.services.transactions.DoPaymentDTO;
import nl.strohalm.cyclos.services.transactions.PaymentServiceLocal;
import nl.strohalm.cyclos.services.transactions.TicketServiceLocal;
import nl.strohalm.cyclos.services.transactions.TransferAuthorizationServiceLocal;
import nl.strohalm.cyclos.services.transactions.exceptions.InvalidChannelException;
import nl.strohalm.cyclos.utils.Pair;
import nl.strohalm.cyclos.webservices.WebServiceContext;
import nl.strohalm.cyclos.webservices.model.AccountHistoryTransferVO;
import nl.strohalm.cyclos.webservices.model.AccountStatusVO;
import nl.strohalm.cyclos.webservices.utils.AccountHelper;
import nl.strohalm.cyclos.webservices.utils.ChannelHelper;
import nl.strohalm.cyclos.webservices.utils.MemberHelper;
import nl.strohalm.cyclos.webservices.utils.PaymentHelper;
import nl.strohalm.cyclos.webservices.utils.WebServiceHelper;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.StringUtils;

/**
 * Implementation for payment web service
 * @author luis
 */
@WebService(name = "payment", serviceName = "payment")
public class PaymentWebServiceImpl implements PaymentWebService {

    private static class PrepareParametersResult {
        private final PaymentStatus status;
        private final AccountOwner from;
        private final AccountOwner to;
        private final Collection<MemberCustomField> fromRequiredFields;
        private final Collection<MemberCustomField> toRequiredFields;

        public PrepareParametersResult(final PaymentStatus status, final AccountOwner from, final AccountOwner to,
                final Collection<MemberCustomField> fromRequiredFields,
                final Collection<MemberCustomField> toRequiredFields) {
            this.status = status;
            this.from = from;
            this.to = to;
            this.fromRequiredFields = fromRequiredFields;
            this.toRequiredFields = toRequiredFields;
        }

        public AccountOwner getFrom() {
            return from;
        }

        public Collection<MemberCustomField> getFromRequiredFields() {
            return fromRequiredFields;
        }

        public PaymentStatus getStatus() {
            return status;
        }

        public AccountOwner getTo() {
            return to;
        }

        public Collection<MemberCustomField> getToRequiredFields() {
            return toRequiredFields;
        }
    }

    /**
     * Common interface used by chargeback / reverse
     * @author andres
     * @param <V>
     */
    private interface TransferLoader<V> {
        Transfer load(V v);
    }

    private AccountServiceLocal accountServiceLocal;
    private AccessServiceLocal accessServiceLocal;
    private ApplicationServiceLocal applicationServiceLocal;
    private PaymentServiceLocal paymentServiceLocal;
    private TicketServiceLocal ticketServiceLocal;
    private ElementServiceLocal elementServiceLocal;
    private MemberCustomFieldServiceLocal memberCustomFieldServiceLocal;
    private ServiceClientServiceLocal serviceClientServiceLocal;
    private TransferAuthorizationServiceLocal transferAuthorizationServiceLocal;
    private PaymentHelper paymentHelper;
    private MemberHelper memberHelper;
    private WebServiceHelper webServiceHelper;
    private AccountHelper accountHelper;
    private ChannelHelper channelHelper;

    @Override
    public ChargebackResult chargeback(final Long transferId) {
        return reverse(transferId, new TransferLoader<Long>() {
            @Override
            public Transfer load(final Long transferId) {
                return paymentServiceLocal.load(transferId);
            }
        });
    }

    @Override
    public PaymentResult confirmPayment(final ConfirmPaymentParameters params) {
        Exception errorException = null;
        AccountStatus fromMemberStatus = null;
        AccountStatus toMemberStatus = null;
        Member fromMember = null;
        Member toMember = null;

        // It's nonsense to use this if restricted to a member
        if (WebServiceContext.getMember() != null) {
            throw new PermissionDeniedException();
        }
        final Channel channel = WebServiceContext.getChannel();
        final String channelName = channel == null ? null : channel.getInternalName();

        PaymentStatus status = null;
        AccountHistoryTransferVO transferVO = null;

        // Get the ticket
        PaymentRequestTicket ticket = null;
        try {
            // Check that the ticket is valid
            final Ticket t = ticketServiceLocal.load(params.getTicket());
            fromMember = t.getFrom();
            toMember = t.getTo();

            if (!(t instanceof PaymentRequestTicket) || t.getStatus() != Ticket.Status.PENDING) {
                throw new Exception(
                        "Invalid ticket and/or status: " + t.getClass().getName() + ", status: " + t.getStatus());
            }
            // Check that the channel is the expected one
            ticket = (PaymentRequestTicket) t;
            if (!ticket.getToChannel().getInternalName().equals(channelName)) {
                throw new Exception("The ticket's destination channel is not the expected one (expected="
                        + channelName + "): " + ticket.getToChannel().getInternalName());
            }
        } catch (final Exception e) {
            errorException = e;
            status = PaymentStatus.INVALID_PARAMETERS;
        }

        // Validate the Channel and credentials
        Member member = null;
        if (status == null) {
            member = ticket.getFrom();
            if (!accessServiceLocal.isChannelEnabledForMember(channelName, member)) {
                status = PaymentStatus.INVALID_CHANNEL;
            }
            if (status == null && WebServiceContext.getClient().isCredentialsRequired()) {
                try {
                    checkCredentials(member, channel, params.getCredentials());
                } catch (final InvalidCredentialsException e) {
                    errorException = e;
                    status = PaymentStatus.INVALID_CREDENTIALS;
                } catch (final BlockedCredentialsException e) {
                    errorException = e;
                    status = PaymentStatus.BLOCKED_CREDENTIALS;
                }
            }
        }
        Transfer transfer = null;
        // Confirm the payment
        if (status == null) {
            try {
                transfer = paymentServiceLocal.confirmPayment(ticket.getTicket());
                status = paymentHelper.toStatus(transfer);
                transferVO = accountHelper.toVO(member, transfer, null);

                if (WebServiceContext.getClient().getPermissions().contains(ServiceOperation.ACCOUNT_DETAILS)) {
                    if (WebServiceContext.getMember() == null) {
                        fromMemberStatus = accountServiceLocal
                                .getCurrentStatus(new AccountDTO(fromMember, transfer.getFrom().getType()));
                        toMemberStatus = accountServiceLocal
                                .getCurrentStatus(new AccountDTO(toMember, transfer.getTo().getType()));
                    } else if (WebServiceContext.getMember().equals(fromMember)) {
                        fromMemberStatus = accountServiceLocal
                                .getCurrentStatus(new AccountDTO(fromMember, transfer.getFrom().getType()));
                    } else {
                        toMemberStatus = accountServiceLocal
                                .getCurrentStatus(new AccountDTO(toMember, transfer.getTo().getType()));
                    }
                }
            } catch (final Exception e) {
                errorException = e;
                if (applicationServiceLocal.getLockedAccountsOnPayments() == LockedAccountsOnPayments.NONE) {
                    // The payment is committed on the same transaction so it will be rolled back by this exception, then status is given by this
                    // exception.
                    status = paymentHelper.toStatus(e);
                } else if (status == null) {
                    /*
                     * The payment is committed on a new transaction. So this exception won't roll back the payment. The status sholuldn't be modified
                     * by this exception unless there isn't a status, in which case return this exception.
                     */
                    status = paymentHelper.toStatus(e);
                }
            }
        }

        if (!status.isSuccessful()) {
            if (errorException != null) {
                webServiceHelper.error(errorException);
            } else {
                webServiceHelper.error("Confirm payment status: " + status);
            }
        }
        // Build the result
        return new PaymentResult(status, transferVO, accountHelper.toVO(fromMemberStatus),
                accountHelper.toVO(toMemberStatus));
    }

    @Override
    public List<ChargebackResult> doBulkChargeback(final List<Long> transfersIds) {
        return doBulkChargeback(transfersIds, new TransferLoader<Long>() {
            @Override
            public Transfer load(final Long transfersId) {
                return paymentServiceLocal.load(transfersId);
            }
        });
    }

    @Override
    public List<PaymentResult> doBulkPayment(final List<PaymentParameters> params) {
        final int size = params == null ? 0 : params.size();
        final List<PaymentResult> results = new ArrayList<PaymentResult>(size);
        final DoPaymentDTO[] dtos = new DoPaymentDTO[size];
        final PrepareParametersResult[] loadedPrepareParameters = new PrepareParametersResult[size];
        if (size > 0) {
            // We should lock at once all from accounts for all payments, but only if all accounts are passed ok
            boolean hasError = false;
            final List<AccountDTO> allAccounts = new ArrayList<AccountDTO>();
            for (int i = 0; i < params.size(); i++) {
                final PaymentParameters param = params.get(i);
                final PrepareParametersResult result = prepareParameters(param);

                if (result.getStatus() == null) {
                    try {
                        final DoPaymentDTO dto = paymentHelper.toExternalPaymentDTO(param, result.getFrom(),
                                result.getTo());
                        if (!validateTransferType(dto)) {
                            results.add(new PaymentResult(PaymentStatus.INVALID_PARAMETERS, null));
                            webServiceHelper
                                    .error("The specified transfer type is invalid: " + dto.getTransferType());
                            hasError = true;
                        } else {
                            allAccounts.add(new AccountDTO(result.getFrom(), dto.getTransferType().getFrom()));
                            results.add(new PaymentResult(PaymentStatus.NOT_PERFORMED, null));
                        }
                        loadedPrepareParameters[i] = result;
                        dtos[i] = dto;
                    } catch (final Exception e) {
                        webServiceHelper.error(e);
                        hasError = true;
                        results.add(new PaymentResult(paymentHelper.toStatus(e), null));
                    }
                } else {
                    hasError = true;
                    results.add(new PaymentResult(result.getStatus(), null));
                    webServiceHelper.error("Bulk payment validation status [" + i + "]: " + result.getStatus());
                }
            }
            if (!hasError) {
                // No static validation error. Perform all payments
                final List<BulkPaymentResult> bulkResults = paymentServiceLocal.doBulkPayment(Arrays.asList(dtos));
                for (int i = 0; i < params.size(); i++) {
                    final PaymentParameters param = params.get(i);
                    BulkPaymentResult bulkResult;
                    try {
                        bulkResult = bulkResults.get(i);
                    } catch (final IndexOutOfBoundsException e) {
                        bulkResult = null;
                    }
                    PaymentResult result;
                    if (hasError || bulkResult == null) {
                        result = new PaymentResult(PaymentStatus.NOT_PERFORMED, null);
                    } else {
                        result = new PaymentResult();
                        final Transfer transfer = (Transfer) bulkResult.getPayment();
                        PaymentStatus status = null;

                        try {
                            if (transfer == null) {
                                status = paymentHelper.toStatus(bulkResult.getException());
                            } else {
                                status = paymentHelper.toStatus(transfer);
                                result.setTransfer(accountHelper.toVO(WebServiceContext.getMember(), transfer, null,
                                        loadedPrepareParameters[i].getFromRequiredFields(),
                                        loadedPrepareParameters[i].getToRequiredFields()));
                            }
                            if (!status.isSuccessful()) {
                                hasError = true;
                            }

                            // Set the account status, as requested
                            final AccountStatusVO[] statuses = getAccountStatusesForPayment(param, transfer);
                            result.setFromAccountStatus(statuses[0]);
                            result.setToAccountStatus(statuses[1]);
                        } catch (Exception e) {
                            webServiceHelper.error(e);
                            if (status == null) {
                                /*
                                 * doBulkPayment always opens a new transaction to perform the payments. So this exception won't roll back the
                                 * payments. The status sholuldn't be modified by this exception unless there isn't a status, in which case return
                                 * this exception.
                                 */

                                status = paymentHelper.toStatus(e);
                            }
                        }
                        result.setStatus(status);

                    }
                    results.set(i, result);
                }
            }
        }
        return results;
    }

    @Override
    public List<ChargebackResult> doBulkReverse(final List<String> traces) {
        return doBulkChargeback(traces, new TransferLoader<String>() {
            @Override
            public Transfer load(final String traceNumber) {
                return paymentServiceLocal.loadTransferForReverse(traceNumber);
            }
        });
    }

    @Override
    public PaymentResult doPayment(final PaymentParameters params) {
        AccountHistoryTransferVO transferVO = null;
        Transfer transfer = null;
        PaymentStatus status = null;
        AccountStatusVO fromMemberStatus = null;
        AccountStatusVO toMemberStatus = null;
        try {
            final PrepareParametersResult result = prepareParameters(params);
            status = result.getStatus();

            if (status == null) {
                // Status null means no "pre-payment" errors (like validation, pin, channel...)
                // Perform the payment
                final DoPaymentDTO dto = paymentHelper.toExternalPaymentDTO(params, result.getFrom(),
                        result.getTo());

                // Validate the transfer type
                if (!validateTransferType(dto)) {
                    status = PaymentStatus.INVALID_PARAMETERS;
                    webServiceHelper.trace(status
                            + ". Reason: The service client doesn't have permission to the specified transfer type: "
                            + dto.getTransferType());
                } else {
                    transfer = (Transfer) paymentServiceLocal.doPayment(dto);
                    status = paymentHelper.toStatus(transfer);
                    transferVO = accountHelper.toVO(WebServiceContext.getMember(), transfer, null,
                            result.getFromRequiredFields(), result.getToRequiredFields());
                    final AccountStatusVO[] statuses = getAccountStatusesForPayment(params, transfer);
                    fromMemberStatus = statuses[0];
                    toMemberStatus = statuses[1];
                }
            }
        } catch (final Exception e) {
            webServiceHelper.error(e);
            if (applicationServiceLocal.getLockedAccountsOnPayments() == LockedAccountsOnPayments.NONE) {
                // The payment is committed on the same transaction so it will be rolled back by this exception, then status is given by this
                // exception.
                status = paymentHelper.toStatus(e);
            } else if (status == null) {
                /*
                 * The payment is committed on a new transaction. So this exception won't roll back the payment. The status sholuldn't be modified by
                 * this exception unless there isn't a status, in which case return this exception.
                 */
                status = paymentHelper.toStatus(e);
            }
        }

        if (!status.isSuccessful()) {
            webServiceHelper.error("Payment error status: " + status);
        }

        return new PaymentResult(status, transferVO, fromMemberStatus, toMemberStatus);
    }

    @Override
    public boolean expireTicket(final String ticketStr) {
        try {
            final PaymentRequestTicket ticket = (PaymentRequestTicket) ticketServiceLocal.load(ticketStr,
                    PaymentRequestTicket.Relationships.FROM_CHANNEL);
            // Check the member restriction
            final Member restricted = WebServiceContext.getMember();
            if (restricted != null && !restricted.equals(ticket.getTo())) {
                throw new Exception();
            }

            // Check the from channel
            final Channel resolvedChannel = WebServiceContext.getChannel();
            final Channel fromChannel = ticket.getFromChannel();
            final Channel toChannel = ticket.getToChannel();
            if ((fromChannel == null || !fromChannel.equals(resolvedChannel))
                    && (toChannel == null || !toChannel.equals(resolvedChannel))) {
                throw new Exception();
            }

            // Check by status
            if (ticket.getStatus() == Ticket.Status.PENDING) {
                ticketServiceLocal.expirePaymentRequestTicket(ticket);
                return true;
            }
        } catch (final Exception e) {
            webServiceHelper.error(e);
            // Ignore exceptions
        }
        return false;
    }

    @Override
    public RequestPaymentResult requestPaymentConfirmation(final RequestPaymentParameters params) {
        Exception errorException = null;
        PaymentRequestStatus status = null;
        // Get the to member
        Member toMember = null;
        final Member restricted = WebServiceContext.getMember();
        if (restricted != null) {
            // When restricted to a member, he is always the to
            toMember = restricted;
        } else {
            try {
                toMember = paymentHelper.resolveToMember(params);
            } catch (final EntityNotFoundException e) {
                status = PaymentRequestStatus.TO_NOT_FOUND;
            }
            // When not restricted to a member, check the channel access of the payment receiver
            if (status == null && !memberHelper.isChannelEnabledForMember(toMember)) {
                status = PaymentRequestStatus.TO_INVALID_CHANNEL;
            }
        }
        // Get the from member
        Member fromMember = null;
        if (status == null) {
            try {
                fromMember = paymentHelper.resolveFromMember(params);
            } catch (final EntityNotFoundException e) {
                status = PaymentRequestStatus.FROM_NOT_FOUND;
            }
        }

        // Generate the ticket if no error so far
        PaymentRequestTicket ticket = null;
        if (status == null) {
            try {
                ticket = paymentHelper.toTicket(params, null);
                ticket.setFrom(fromMember);
                ticket.setTo(toMember);
                ticket = ticketServiceLocal.generate(ticket);
                status = PaymentRequestStatus.REQUEST_RECEIVED;
            } catch (final InvalidChannelException e) {
                status = PaymentRequestStatus.FROM_INVALID_CHANNEL;
            } catch (final Exception e) {
                errorException = e;
                final PaymentStatus paymentStatus = paymentHelper.toStatus(e);
                try {
                    // Probably it's a payment status also present on payment request status
                    status = PaymentRequestStatus.valueOf(paymentStatus.name());
                } catch (final Exception e1) {
                    e1.initCause(e);
                    errorException = e1;
                    status = PaymentRequestStatus.UNKNOWN_ERROR;
                }
            }
        }

        if (!status.isSuccessful()) {
            if (errorException != null) {
                webServiceHelper.error(errorException);
            } else {
                webServiceHelper.error("Request payment confirmation status: " + status);
            }
        }

        // Build a result
        final RequestPaymentResult result = new RequestPaymentResult();
        result.setStatus(status);
        if (ticket != null) {
            result.setTicket(ticket.getTicket());
        }
        return result;
    }

    @Override
    public ChargebackResult reverse(final String traceNumber) {
        return reverse(traceNumber, new TransferLoader<String>() {
            @Override
            public Transfer load(final String traceNumber) {
                return paymentServiceLocal.loadTransferForReverse(traceNumber);
            }
        });
    }

    public void setAccessServiceLocal(final AccessServiceLocal accessService) {
        accessServiceLocal = accessService;
    }

    public void setAccountHelper(final AccountHelper accountHelper) {
        this.accountHelper = accountHelper;
    }

    public void setAccountServiceLocal(final AccountServiceLocal accountService) {
        accountServiceLocal = accountService;
    }

    public void setApplicationServiceLocal(final ApplicationServiceLocal applicationServiceLocal) {
        this.applicationServiceLocal = applicationServiceLocal;
    }

    public void setChannelHelper(final ChannelHelper channelHelper) {
        this.channelHelper = channelHelper;
    }

    public void setElementServiceLocal(final ElementServiceLocal elementService) {
        elementServiceLocal = elementService;
    }

    public void setMemberCustomFieldServiceLocal(final MemberCustomFieldServiceLocal memberCustomFieldService) {
        memberCustomFieldServiceLocal = memberCustomFieldService;
    }

    public void setMemberHelper(final MemberHelper memberHelper) {
        this.memberHelper = memberHelper;
    }

    public void setPaymentHelper(final PaymentHelper paymentHelper) {
        this.paymentHelper = paymentHelper;
    }

    public void setPaymentServiceLocal(final PaymentServiceLocal paymentService) {
        paymentServiceLocal = paymentService;
    }

    public void setServiceClientServiceLocal(final ServiceClientServiceLocal serviceClientService) {
        serviceClientServiceLocal = serviceClientService;
    }

    public void setTicketServiceLocal(final TicketServiceLocal ticketService) {
        ticketServiceLocal = ticketService;
    }

    public void setTransferAuthorizationServiceLocal(
            final TransferAuthorizationServiceLocal transferAuthorizationService) {
        transferAuthorizationServiceLocal = transferAuthorizationService;
    }

    public void setWebServiceHelper(final WebServiceHelper webServiceHelper) {
        this.webServiceHelper = webServiceHelper;
    }

    @Override
    public PaymentStatus simulatePayment(final PaymentParameters params) {

        PaymentStatus status = null;

        try {
            final PrepareParametersResult result = prepareParameters(params);
            status = result.getStatus();

            if (status == null) {
                final DoPaymentDTO dto = paymentHelper.toExternalPaymentDTO(params, result.getFrom(),
                        result.getTo());
                if (!validateTransferType(dto)) {
                    webServiceHelper.trace(PaymentStatus.INVALID_PARAMETERS
                            + ". Reason: The service client doesn't have permission to the specified transfer type: "
                            + dto.getTransferType());
                    return PaymentStatus.INVALID_PARAMETERS;
                } else {
                    // Simulate the payment
                    final Transfer transfer = (Transfer) paymentServiceLocal.simulatePayment(dto);
                    return paymentHelper.toStatus(transfer);
                }
            }
        } catch (final Exception e) {
            webServiceHelper.error(e);
            return paymentHelper.toStatus(e);
        }

        if (!status.isSuccessful()) {
            webServiceHelper.error("Simulate payment status: " + status);
        }
        return status;
    }

    /**
     * Checks the given member's pin
     */
    private void checkCredentials(Member member, final Channel channel, final String credentials) {
        if (member == null) {
            return;
        }
        final ServiceClient client = WebServiceContext.getClient();
        final Member restrictedMember = client.getMember();
        if (restrictedMember == null) {
            // Non-restricted clients use the flag credentials required
            if (!client.isCredentialsRequired()) {
                // No credentials should be checked
                throw new InvalidCredentialsException();
            }
        } else {
            // Restricted clients don't need check if is the same member
            if (restrictedMember.equals(member)) {
                throw new InvalidCredentialsException();
            }
        }
        if (StringUtils.isEmpty(credentials)) {
            throw new InvalidCredentialsException();
        }
        member = elementServiceLocal.load(member.getId(), Element.Relationships.USER);
        accessServiceLocal.checkCredentials(channel, member.getMemberUser(), credentials,
                WebServiceContext.getRequest().getRemoteAddr(), WebServiceContext.getMember());
    }

    /**
     * Performs some static checks and loads the transfers to be chargedback by the bulk reverse operation.
     * @param ids: Could be trace numbers or transfer ids.
     * @param loader: The object responsible for loading the transfer by trace number or transferId.
     */
    private <V> List<ChargebackResult> doBulkChargeback(final List<V> ids, final TransferLoader<V> loader) {
        final List<Transfer> transfers = new LinkedList<Transfer>();
        final List<Transfer> actualTransfers = new LinkedList<Transfer>();
        final Member member = WebServiceContext.getMember();
        final List<ChargebackResult> results = new LinkedList<ChargebackResult>();
        boolean hasError = false;

        // Load each transfer
        for (final V id : ids) {
            Transfer transfer = null;
            try {
                transfer = loader.load(id);
                transfers.add(transfer);
                if (member != null && !transfer.getToOwner().equals(member)) {
                    throw new EntityNotFoundException();
                }

                // Preprocess the chargeback
                final Pair<ChargebackStatus, Transfer> preprocessResult = preprocessChargeback(transfer);
                final ChargebackStatus status = preprocessResult.getFirst();
                final Transfer preprocessTransfer = preprocessResult.getSecond();

                if (status == ChargebackStatus.SUCCESS) {
                    // This transfer was considered succes by other means (for example, by canceling a pending payment). Don't pass it to
                    // bulkChargeback
                    actualTransfers.add(null);
                    final AccountHistoryTransferVO chargebackVO = accountHelper.toVO(WebServiceContext.getMember(),
                            preprocessTransfer, null);
                    final AccountHistoryTransferVO transferVO = accountHelper.toVO(WebServiceContext.getMember(),
                            transfer, null);
                    results.add(new ChargebackResult(status, chargebackVO, transferVO));
                } else {
                    // The transfer could have already failed if status != null or ok if status == null;
                    actualTransfers.add(transfer);
                    results.add(new ChargebackResult(status == null ? ChargebackStatus.NOT_PERFORMED : status, null,
                            null));
                    if (status != null) {
                        hasError = true;
                    }
                }
            } catch (final EntityNotFoundException e) {
                hasError = true;
                results.add(new ChargebackResult(ChargebackStatus.TRANSFER_NOT_FOUND, null, null));
                webServiceHelper.error(
                        new Exception("Bulk status [Id=" + id + "]: " + ChargebackStatus.TRANSFER_NOT_FOUND, e));
            }
        }

        if (hasError) {
            // No need to go on with bulkChargeback as we already know something has failed
            return results;
        }

        // Do the bulk chargeback
        final List<BulkChargebackResult> bulkResults = paymentServiceLocal.bulkChargeback(actualTransfers);
        for (int i = 0; i < actualTransfers.size(); i++) {
            final Transfer actualTransfer = actualTransfers.get(i);
            if (actualTransfer == null) {
                // It was not passed to bulkChargeback, as we already knew the status. Skip this one
                continue;
            }
            BulkChargebackResult bulkResult;
            try {
                bulkResult = bulkResults.get(i);
            } catch (final IndexOutOfBoundsException e) {
                bulkResult = null;
            }

            ChargebackResult result;
            if (hasError || bulkResult == null) {
                result = new ChargebackResult(ChargebackStatus.NOT_PERFORMED, null, null);
            } else {
                result = new ChargebackResult();
                final Transfer chargeback = bulkResult.getTransfer();
                ChargebackStatus status = null;
                if (chargeback == null) {
                    status = ChargebackStatus.TRANSFER_CANNOT_BE_CHARGEDBACK;
                    hasError = true;
                } else {
                    status = ChargebackStatus.SUCCESS;
                    try {
                        result.setChargebackTransfer(
                                accountHelper.toVO(WebServiceContext.getMember(), chargeback, null));
                        result.setOriginalTransfer(
                                accountHelper.toVO(WebServiceContext.getMember(), actualTransfer, null));
                    } catch (Exception e) {
                        webServiceHelper.error(e);
                        if (status == null) {
                            /*
                             * doBulkChargeback always opens a new transaction to perform the payments. So this exception won't roll back the
                             * chargebacks. The status sholuldn't be modified by this exception unless there isn't a status, in which case return this
                             * exception.
                             */

                            status = ChargebackStatus.TRANSFER_CANNOT_BE_CHARGEDBACK;
                        }
                    }
                }
                result.setStatus(status);
            }
            results.set(i, result);
        }
        return results;
    }

    private ChargebackResult doChargeback(final Transfer transfer) {

        final Pair<ChargebackStatus, Transfer> preprocessResult = preprocessChargeback(transfer);
        ChargebackStatus status = preprocessResult.getFirst();
        Transfer chargebackTransfer = preprocessResult.getSecond();

        // Do the chargeback
        if (status == null) {
            chargebackTransfer = paymentServiceLocal.chargeback(transfer);
            status = ChargebackStatus.SUCCESS;
        }

        if (!status.isSuccessful()) {
            webServiceHelper.error("Chargeback result: " + status);
        }

        Member member = WebServiceContext.getMember();
        // Build the result
        if (status == ChargebackStatus.SUCCESS || status == ChargebackStatus.TRANSFER_ALREADY_CHARGEDBACK) {
            AccountHistoryTransferVO originalVO = null;
            AccountHistoryTransferVO chargebackVO = null;
            try {
                final AccountOwner owner = member == null ? transfer.getToOwner() : member;
                originalVO = accountHelper.toVO(owner, transfer, null);
                chargebackVO = accountHelper.toVO(owner, chargebackTransfer, null);
            } catch (Exception e) {
                webServiceHelper.error(e);
                if (applicationServiceLocal.getLockedAccountsOnPayments() == LockedAccountsOnPayments.NONE) {
                    // The chargeback is committed on the same transaction so it will be rolled back by this exception, then status is given by this
                    // exception.
                    status = ChargebackStatus.TRANSFER_CANNOT_BE_CHARGEDBACK;
                }
                // When the locking method is not NONE, the chargeback is committed on a new transaction so we'll preserve the status.
            }
            return new ChargebackResult(status, originalVO, chargebackVO);
        } else {
            return new ChargebackResult(status, null, null);
        }
    }

    /**
     * Returns the account status for the from account and to account, if the given {@link PaymentParameters#isReturnStatus()} is true and according
     * to the current invocation permissions
     */
    private AccountStatusVO[] getAccountStatusesForPayment(final PaymentParameters params,
            final Transfer transfer) {
        AccountStatus fromMemberStatus = null;
        AccountStatus toMemberStatus = null;
        if (WebServiceContext.getClient().getPermissions().contains(ServiceOperation.ACCOUNT_DETAILS)
                && params.getReturnStatus()) {
            if (WebServiceContext.getMember() == null) {
                fromMemberStatus = transfer.isFromSystem() ? null
                        : accountServiceLocal.getCurrentStatus(new AccountDTO(transfer.getFrom()));
                toMemberStatus = transfer.isToSystem() ? null
                        : accountServiceLocal.getCurrentStatus(new AccountDTO(transfer.getTo()));
            } else if (WebServiceContext.getMember().equals(paymentHelper.resolveFromMember(params))) {
                fromMemberStatus = transfer.isFromSystem() ? null
                        : accountServiceLocal.getCurrentStatus(new AccountDTO(transfer.getFrom()));
            } else {
                toMemberStatus = transfer.isToSystem() ? null
                        : accountServiceLocal.getCurrentStatus(new AccountDTO(transfer.getTo()));
            }
        }
        return new AccountStatusVO[] { accountHelper.toVO(fromMemberStatus), accountHelper.toVO(toMemberStatus) };
    }

    private Collection<MemberCustomField> getMemberCustomFields(final Member member,
            final List<String> fieldInternalNames) {
        Collection<MemberCustomField> fields = new HashSet<MemberCustomField>();

        for (final String internalName : fieldInternalNames) {
            MemberCustomFieldValue mcfv = (MemberCustomFieldValue) CollectionUtils.find(member.getCustomValues(),
                    new Predicate() {
                        @Override
                        public boolean evaluate(final Object object) {
                            MemberCustomFieldValue mcfv = (MemberCustomFieldValue) object;
                            return mcfv.getField().getInternalName().equals(internalName);
                        }
                    });
            if (mcfv == null) {
                webServiceHelper.trace(
                        String.format("Required field '%1$s' was not found for member %2$s", internalName, member));
                return null;
            } else {
                fields.add(memberCustomFieldServiceLocal.load(mcfv.getField().getId()));
            }
        }

        return fields;
    }

    /**
     * Prepares the parameters for a payment. The resulting status is null when no problem found
     */
    private PrepareParametersResult prepareParameters(final PaymentParameters params) {

        final Member restricted = WebServiceContext.getMember();
        final boolean fromSystem = params.getFromSystem();
        final boolean toSystem = params.getToSystem();
        PaymentStatus status = null;
        Member fromMember = null;
        Member toMember = null;
        // Load the from member
        if (!fromSystem) {
            try {
                fromMember = paymentHelper.resolveFromMember(params);
            } catch (final EntityNotFoundException e) {
                webServiceHelper.error(e);
                status = PaymentStatus.FROM_NOT_FOUND;
            }
        }
        // Load the to member
        if (status == null && !toSystem) {
            try {
                toMember = paymentHelper.resolveToMember(params);
            } catch (final EntityNotFoundException e) {
                webServiceHelper.error(e);
                status = PaymentStatus.TO_NOT_FOUND;
            }
        }

        if (status == null) {
            if (restricted == null) {
                // Ensure has the do payment permission
                if (!WebServiceContext.hasPermission(ServiceOperation.DO_PAYMENT)) {
                    throw new PermissionDeniedException("The service client doesn't have the following permission: "
                            + ServiceOperation.DO_PAYMENT);
                }
                // Check the channel immediately, as needed by SMS controller
                if (fromMember != null
                        && !accessServiceLocal.isChannelEnabledForMember(channelHelper.restricted(), fromMember)) {
                    status = PaymentStatus.INVALID_CHANNEL;
                }
            } else {
                // Enforce the restricted to member parameters
                if (fromSystem) {
                    // Restricted to member can't perform payment from system
                    status = PaymentStatus.FROM_NOT_FOUND;
                } else {
                    if (fromMember == null) {
                        fromMember = restricted;
                    } else if (toMember == null && !toSystem) {
                        toMember = restricted;
                    }
                }
                if (status == null) {
                    // Check make / receive payment permissions
                    if (fromMember.equals(restricted)) {
                        if (!WebServiceContext.hasPermission(ServiceOperation.DO_PAYMENT)) {
                            throw new PermissionDeniedException(
                                    "The service client doesn't have the following permission: "
                                            + ServiceOperation.DO_PAYMENT);
                        }
                    } else {
                        if (!WebServiceContext.hasPermission(ServiceOperation.RECEIVE_PAYMENT)) {
                            throw new PermissionDeniedException(
                                    "The service client doesn't have the following permission: "
                                            + ServiceOperation.RECEIVE_PAYMENT);
                        }
                    }
                    // Ensure that either from or to member is the restricted one
                    if (!fromMember.equals(restricted) && !toMember.equals(restricted)) {
                        status = PaymentStatus.INVALID_PARAMETERS;
                        webServiceHelper.trace(status
                                + ". Reason: Neither the origin nor the destination members are equal to the restricted: "
                                + restricted);
                    }
                }
                if (status == null) {
                    // Enforce the permissions
                    if (restricted.equals(fromMember)
                            && !WebServiceContext.hasPermission(ServiceOperation.DO_PAYMENT)) {
                        throw new PermissionDeniedException(
                                "The service client doesn't have the following permission: "
                                        + ServiceOperation.DO_PAYMENT);
                    } else if (restricted.equals(toMember)
                            && !WebServiceContext.hasPermission(ServiceOperation.RECEIVE_PAYMENT)) {
                        throw new PermissionDeniedException(
                                "The service client doesn't have the following permission: "
                                        + ServiceOperation.RECEIVE_PAYMENT);
                    }
                }
            }
        }

        // Ensure both from and to member are present
        if (status == null) {
            if (fromMember == null && !fromSystem) {
                status = PaymentStatus.FROM_NOT_FOUND;
            } else if (toMember == null && !toSystem) {
                status = PaymentStatus.TO_NOT_FOUND;
            } else if (fromMember != null && toMember != null) {
                // Ensure the to member is visible by the from member
                final Collection<MemberGroup> visibleGroups = fromMember.getMemberGroup()
                        .getCanViewProfileOfGroups();
                if (CollectionUtils.isEmpty(visibleGroups) || !visibleGroups.contains(toMember.getGroup())) {
                    status = PaymentStatus.TO_NOT_FOUND;
                }
            }
        }

        // Ensure required CF are present ONLY for unrestricted client
        Collection<MemberCustomField> fromMemberfields = null, toMemberfields = null;
        if (status == null) {
            boolean hasFromRequired = CollectionUtils.isNotEmpty(params.getFromMemberFieldsToReturn());
            boolean hasToRequired = CollectionUtils.isNotEmpty(params.getToMemberFieldsToReturn());
            if (restricted != null && (hasFromRequired || hasToRequired) || hasFromRequired && fromSystem
                    || hasToRequired && toSystem) {
                webServiceHelper.trace(restricted != null
                        ? "Restricted web service clients are not allowed to require member custom field values"
                        : "Can't require custom field values for a system payment");
                status = PaymentStatus.INVALID_PARAMETERS;
            }
            if (status == null && hasFromRequired) {
                fromMemberfields = getMemberCustomFields(fromMember, params.getFromMemberFieldsToReturn());
                status = fromMemberfields == null ? PaymentStatus.INVALID_PARAMETERS : null;
            }

            if (status == null && hasToRequired) {
                toMemberfields = getMemberCustomFields(toMember, params.getToMemberFieldsToReturn());
                status = toMemberfields == null ? PaymentStatus.INVALID_PARAMETERS : null;
            }
        }

        if (status == null) {
            // Check the channel
            if (fromMember != null
                    && !accessServiceLocal.isChannelEnabledForMember(channelHelper.restricted(), fromMember)) {
                status = PaymentStatus.INVALID_CHANNEL;
            }
        }
        if (status == null) {
            // Check the credentials
            boolean checkCredentials;
            if (restricted != null) {
                checkCredentials = !fromMember.equals(restricted);
            } else {
                checkCredentials = !fromSystem && WebServiceContext.getClient().isCredentialsRequired();
            }
            if (checkCredentials) {
                try {
                    checkCredentials(fromMember, WebServiceContext.getChannel(), params.getCredentials());
                } catch (final InvalidCredentialsException e) {
                    webServiceHelper.error(e);
                    status = PaymentStatus.INVALID_CREDENTIALS;
                } catch (final BlockedCredentialsException e) {
                    webServiceHelper.error(e);
                    status = PaymentStatus.BLOCKED_CREDENTIALS;
                }
            }
        }

        // No error
        final AccountOwner fromOwner = fromSystem ? SystemAccountOwner.instance() : fromMember;
        final AccountOwner toOwner = toSystem ? SystemAccountOwner.instance() : toMember;
        return new PrepareParametersResult(status, fromOwner, toOwner, fromMemberfields, toMemberfields);
    }

    private Pair<ChargebackStatus, Transfer> preprocessChargeback(final Transfer transfer) {
        ChargebackStatus status = null;
        Transfer chargebackTransfer = null;

        // Check if the transfer can be charged back
        if (!paymentServiceLocal.canChargeback(transfer, false)) {
            if (transfer.getChargedBackBy() != null) {
                chargebackTransfer = transfer.getChargedBackBy();
                status = ChargebackStatus.TRANSFER_ALREADY_CHARGEDBACK;
            } else {
                if (transfer.getStatus() == Payment.Status.PENDING) {
                    final TransferAuthorizationDTO transferAuthorizationDto = new TransferAuthorizationDTO();
                    transferAuthorizationDto.setTransfer(transfer);
                    transferAuthorizationDto.setShowToMember(false);
                    chargebackTransfer = transferAuthorizationServiceLocal.cancel(transferAuthorizationDto);
                    status = ChargebackStatus.SUCCESS;
                } else {
                    status = ChargebackStatus.TRANSFER_CANNOT_BE_CHARGEDBACK;
                }
            }
        }

        return new Pair<ChargebackStatus, Transfer>(status, chargebackTransfer);
    }

    private <V> ChargebackResult reverse(final V transferId, final TransferLoader<V> loader) {
        Exception errorException = null;
        ChargebackStatus status = null;
        Transfer transfer = null;

        try {
            transfer = loader.load(transferId);
            // Ensure the member is the one who received the payment
            final Member member = WebServiceContext.getMember();
            if (member != null && !transfer.getToOwner().equals(member)) {
                throw new EntityNotFoundException();
            } else {
                final Collection<TransferType> possibleTypes = serviceClientServiceLocal
                        .load(WebServiceContext.getClient().getId(),
                                ServiceClient.Relationships.CHARGEBACK_PAYMENT_TYPES)
                        .getChargebackPaymentTypes();
                if (!possibleTypes.contains(transfer.getType())) {
                    throw new EntityNotFoundException();
                }
            }
        } catch (final EntityNotFoundException e) {
            errorException = e;
            status = ChargebackStatus.TRANSFER_NOT_FOUND;
        }

        if (status == null) {
            try {
                return doChargeback(transfer);
            } catch (final Exception e) {
                webServiceHelper.error(e);
                return new ChargebackResult(ChargebackStatus.TRANSFER_CANNOT_BE_CHARGEDBACK, null, null);
            }
        } else {
            if (!status.isSuccessful()) {
                if (errorException != null) {
                    webServiceHelper.error(errorException);
                } else {
                    webServiceHelper.error("Chargeback status: " + status);
                }
            }
            return new ChargebackResult(status, null, null);
        }
    }

    private boolean validateTransferType(final DoPaymentDTO dto) {
        final Collection<TransferType> possibleTypes = paymentHelper.listPossibleTypes(dto);
        return possibleTypes != null && possibleTypes.contains(dto.getTransferType());
    }
}