/* 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.controls.posweb; import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.Callable; import javax.servlet.http.HttpServletRequest; import nl.strohalm.cyclos.access.OperatorPermission; import nl.strohalm.cyclos.annotations.Inject; import nl.strohalm.cyclos.controls.ActionContext; import nl.strohalm.cyclos.controls.payments.SchedulingType; import nl.strohalm.cyclos.entities.access.Channel; import nl.strohalm.cyclos.entities.access.Channel.Credentials; import nl.strohalm.cyclos.entities.access.Channel.Principal; import nl.strohalm.cyclos.entities.access.MemberUser; import nl.strohalm.cyclos.entities.access.PrincipalType; import nl.strohalm.cyclos.entities.accounts.AccountOwner; import nl.strohalm.cyclos.entities.accounts.transactions.Payment; import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException; import nl.strohalm.cyclos.entities.members.Element; import nl.strohalm.cyclos.entities.members.Member; import nl.strohalm.cyclos.entities.settings.LocalSettings; import nl.strohalm.cyclos.services.access.ChannelService; import nl.strohalm.cyclos.services.access.exceptions.BlockedCredentialsException; import nl.strohalm.cyclos.services.access.exceptions.InvalidCardException; import nl.strohalm.cyclos.services.access.exceptions.InvalidCredentialsException; import nl.strohalm.cyclos.services.access.exceptions.InvalidUserForChannelException; import nl.strohalm.cyclos.services.transactions.DoPaymentDTO; import nl.strohalm.cyclos.services.transactions.ProjectionDTO; import nl.strohalm.cyclos.services.transactions.ScheduledPaymentDTO; import nl.strohalm.cyclos.services.transactions.TransactionContext; import nl.strohalm.cyclos.utils.RequestHelper; import nl.strohalm.cyclos.utils.access.LoggedUser; import nl.strohalm.cyclos.utils.binding.BeanBinder; import nl.strohalm.cyclos.utils.binding.DataBinder; import nl.strohalm.cyclos.utils.conversion.CoercionHelper; import nl.strohalm.cyclos.utils.validation.InvalidError; import nl.strohalm.cyclos.utils.validation.PositiveNonZeroValidation; import nl.strohalm.cyclos.utils.validation.RequiredError; import nl.strohalm.cyclos.utils.validation.TodayValidation; import nl.strohalm.cyclos.utils.validation.ValidationError; import nl.strohalm.cyclos.utils.validation.ValidationException; import org.apache.commons.lang.StringUtils; /** * Action for the posweb member to receive a payment * @author luis */ public class ReceivePaymentAction extends BasePosWebPaymentAction { private ChannelService channelService; @Inject public void setChannelService(final ChannelService channelService) { this.channelService = channelService; } @Override protected Payment doPayment(final ActionContext context, final DoPaymentDTO dto) { final Member from; try { // Load the member that is making the payment from = elementService.load(((Member) dto.getFrom()).getId(), Element.Relationships.USER); if (from == null) { throw new Exception(); } } catch (final Exception e) { throw new ValidationException(); } // run as the member, to ensure the model layer will test permissions as that member return LoggedUser.runAs(from.getUser(), new Callable<Payment>() { @Override public Payment call() throws Exception { // Check if the member has access to the "posweb" channel; if (!accessService.isChannelEnabledForMember(Channel.POSWEB, from)) { throw new InvalidUserForChannelException(from.getUsername()); } // Check the paying member credentials final ValidationException validation = checkCredentials(context, from); validation.throwIfHasErrors(); // Ensure that the scheduled payment, if any, is shown to the user dto.setShowScheduledToReceiver(true); // Perform the payment itself return getPaymentService().doPayment(dto); } }); } @Override protected OperatorPermission getPermission() { return OperatorPermission.PAYMENTS_POSWEB_RECEIVE_PAYMENT; } @Override protected DataBinder<DoPaymentDTO> initDataBinder() { final BeanBinder<DoPaymentDTO> binder = (BeanBinder<DoPaymentDTO>) super.initDataBinder(); // As we will handle from member by principal, remove from the regular binder binder.getMappings().remove("from"); return binder; } @Override protected void prepareForm(final ActionContext context) throws Exception { super.prepareForm(context); final ReceivePaymentForm form = context.getForm(); final HttpServletRequest request = context.getRequest(); final Channel channel = posWebChannel(); final Credentials credentials = channel.getCredentials(); boolean numericCredentials = false; boolean uppercasedCredentials = false; switch (credentials) { case PIN: case CARD_SECURITY_CODE: numericCredentials = true; break; case LOGIN_PASSWORD: numericCredentials = settingsService.getAccessSettings().isNumericPassword(); break; case TRANSACTION_PASSWORD: numericCredentials = StringUtils.containsOnly(settingsService.getAccessSettings().getTransactionPasswordChars(), "0123456789"); uppercasedCredentials = true; break; } final PrincipalType selectedPrincipalType = channelService.resolvePrincipalType(channel.getInternalName(), form.getPrincipalType()); form.setPrincipalType(selectedPrincipalType.toString()); final Map<String, PrincipalType> principalTypes = new TreeMap<String, PrincipalType>(); for (final PrincipalType principalType : channel.getPrincipalTypes()) { principalTypes.put(principalTypeLabel(principalType), principalType); } request.setAttribute("principalTypes", principalTypes); request.setAttribute("selectedPrincipalType", selectedPrincipalType); request.setAttribute("selectedPrincipalLabel", principalTypeLabel(selectedPrincipalType)); request.setAttribute("credentials", credentials); request.setAttribute("numericCredentials", numericCredentials); request.setAttribute("uppercasedCredentials", uppercasedCredentials); request.setAttribute("credentialsKey", getCredentialsKey()); final LocalSettings localSettings = settingsService.getLocalSettings(); request.setAttribute("today", localSettings.getRawDateConverter().toString(Calendar.getInstance())); RequestHelper.storeEnum(request, SchedulingType.class, "schedulingTypes"); } @Override protected DoPaymentDTO resolvePaymentDTO(final ActionContext context) { final Member member = (Member) context.getAccountOwner(); final ReceivePaymentForm form = context.getForm(); final DoPaymentDTO payment = getDataBinder().readFromString(form); try { final Member fromMember = CoercionHelper.coerce(Member.class, form.getFrom()); payment.setFrom(fromMember); } catch (final EntityNotFoundException e) { // Leave null - will fail validation } payment.setChannel(Channel.POSWEB); payment.setContext(TransactionContext.PAYMENT); payment.setTo(member); if (context.isOperator()) { payment.setReceiver(context.getElement()); } final LocalSettings localSettings = settingsService.getLocalSettings(); // Handle scheduling SchedulingType schedulingType = CoercionHelper.coerce(SchedulingType.class, form.getSchedulingType()); if (schedulingType == null) { schedulingType = SchedulingType.IMMEDIATELY; } List<ScheduledPaymentDTO> installments = null; switch (schedulingType) { case SINGLE_FUTURE: final ScheduledPaymentDTO installment = new ScheduledPaymentDTO(); installment.setAmount(payment.getAmount()); installment.setDate(localSettings.getRawDateConverter().valueOf(form.getScheduledFor())); installments = Collections.singletonList(installment); break; case MULTIPLE_FUTURE: final int paymentCount = CoercionHelper.coerce(int.class, form.getPaymentCount()); final Calendar firstPaymentDate = localSettings.getRawDateConverter().valueOf(form.getFirstPaymentDate()); if (paymentCount > 0 && firstPaymentDate != null) { final ProjectionDTO projection = new ProjectionDTO(); projection.setTransferType(payment.getTransferType()); projection.setAmount(payment.getAmount()); projection.setFirstExpirationDate(firstPaymentDate); projection.setPaymentCount(paymentCount); installments = getPaymentService().calculatePaymentProjection(projection); } break; } payment.setPayments(installments); return payment; } @Override protected void validateForm(final ActionContext context) { final DoPaymentDTO dto = resolvePaymentDTO(context); dto.setPayments(null); ValidationException validation = null; try { getPaymentService().validate(dto); validation = new ValidationException(); } catch (final ValidationException e) { validation = e; } final ReceivePaymentForm form = context.getForm(); final LocalSettings localSettings = settingsService.getLocalSettings(); final AccountOwner from = dto.getFrom(); if (from instanceof Member) { validation.setPropertyKey("paymentCount", "transfer.paymentCount"); validation.setPropertyKey("firstPaymentDate", "transfer.firstPaymentDate"); validation.setPropertyKey("scheduledFor", "transfer.scheduledFor"); validation.setPropertyKey("_credentials", getCredentialsKey()); SchedulingType schedulingType = CoercionHelper.coerce(SchedulingType.class, form.getSchedulingType()); if (schedulingType == null) { schedulingType = SchedulingType.IMMEDIATELY; } switch (schedulingType) { case SINGLE_FUTURE: // Validate the scheduled for date Calendar scheduledFor = null; try { scheduledFor = localSettings.getRawDateConverter().valueOf(form.getScheduledFor()); ValidationError error = null; if (scheduledFor == null) { error = new RequiredError(); } else { error = TodayValidation.future().validate(null, null, scheduledFor); } if (error != null) { validation.addPropertyError("scheduledFor", error); } } catch (final Exception e) { validation.addPropertyError("scheduledFor", new InvalidError()); } break; case MULTIPLE_FUTURE: // Validate the payment count Integer paymentCount; try { paymentCount = CoercionHelper.coerce(Integer.class, form.getPaymentCount()); ValidationError error = null; if (paymentCount == null) { error = new RequiredError(); } else { error = PositiveNonZeroValidation.instance().validate(null, null, paymentCount); } if (error != null) { validation.addPropertyError("paymentCount", new InvalidError()); } } catch (final Exception e) { validation.addPropertyError("paymentCount", new InvalidError()); } // Validate the first payment date Calendar firstPaymentDate = null; try { firstPaymentDate = localSettings.getRawDateConverter().valueOf(form.getFirstPaymentDate()); ValidationError error = null; if (firstPaymentDate == null) { error = new RequiredError(); } else { error = TodayValidation.futureOrToday().validate(null, null, firstPaymentDate); } if (error != null) { validation.addPropertyError("firstPaymentDate", error); } } catch (final Exception e) { validation.addPropertyError("firstPaymentDate", new InvalidError()); } break; } // Validate the credentials if (StringUtils.isEmpty(form.getCredentials())) { validation.addPropertyError("_credentials", new RequiredError()); } else { if (!validation.hasErrors()) { final Member member = elementService.load(((Member) from).getId()); validation = checkCredentials(context, member); } } } validation.throwIfHasErrors(); } private ValidationException checkCredentials(final ActionContext context, final Member member) { final String cardFormProperty = "_card"; final String credentialsFormProperty = "_credentials"; final ValidationException validation = new ValidationException(); validation.setPropertyKey(cardFormProperty, "posweb.client.card"); validation.setPropertyKey(credentialsFormProperty, getCredentialsKey()); final ReceivePaymentForm form = context.getForm(); final String credentials = form.getCredentials(); // Validate the credentials if (StringUtils.isEmpty(credentials)) { // Missing validation.addPropertyError(credentialsFormProperty, new RequiredError()); } else { // Check it final Member relatedMember = (Member) context.getAccountOwner(); try { final MemberUser payer = CoercionHelper.coerce(MemberUser.class, form.getFrom()); accessService.checkCredentials(posWebChannel(), payer, credentials, context.getRequest().getRemoteAddr(), relatedMember); } catch (final InvalidCardException e) { validation.addPropertyError(cardFormProperty, new InvalidError()); } catch (final InvalidCredentialsException e) { validation.addPropertyError(credentialsFormProperty, new InvalidError()); } catch (final BlockedCredentialsException e) { String key; switch (e.getCredentialsType()) { case TRANSACTION_PASSWORD: key = "transactionPassword.error.blockedByTrials"; break; case PIN: key = "pin.error.blocked"; break; case CARD_SECURITY_CODE: key = "cardSecurityCode.error.blocked"; break; default: key = "login.error.blocked"; break; } validation.addGeneralError(new ValidationError(key)); } } return validation; } private String getCredentialsKey() { final Channel channel = posWebChannel(); String credentialsKey; switch (channel.getCredentials()) { case PIN: credentialsKey = "posweb.client.pin"; break; case LOGIN_PASSWORD: credentialsKey = "posweb.client.loginPassword"; break; case TRANSACTION_PASSWORD: credentialsKey = "posweb.client.transactionPassword"; break; case CARD_SECURITY_CODE: credentialsKey = "posweb.client.cardSecurityCode"; break; default: throw new IllegalStateException("Cannot use credentials type " + channel.getCredentials() + " for PosWeb"); } return credentialsKey; } private Channel posWebChannel() { final Channel channel = channelService.loadByInternalName(Channel.POSWEB); return channel; } private String principalTypeLabel(final PrincipalType principalType) { final Principal principal = principalType.getPrincipal(); switch (principal) { case USER: return messageHelper.message("posweb.client.username"); case CUSTOM_FIELD: return principalType.getCustomField().getName(); default: return messageHelper.message(principal.getKey()); } } }