/* 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()); } }