/* This file is part of Cyclos (www.cyclos.org). A project of the Social Trade Organisation (www.socialtrade.org). Cyclos is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Cyclos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Cyclos; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package nl.strohalm.cyclos.services.transfertypes; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import nl.strohalm.cyclos.access.AdminSystemPermission; import nl.strohalm.cyclos.dao.accounts.transactions.AuthorizationLevelDAO; import nl.strohalm.cyclos.dao.accounts.transactions.TransferTypeDAO; import nl.strohalm.cyclos.entities.Relationship; import nl.strohalm.cyclos.entities.accounts.Account; import nl.strohalm.cyclos.entities.accounts.AccountType; import nl.strohalm.cyclos.entities.accounts.AccountType.Nature; import nl.strohalm.cyclos.entities.accounts.Currency; import nl.strohalm.cyclos.entities.accounts.MemberAccountType; import nl.strohalm.cyclos.entities.accounts.MemberGroupAccountSettings; import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee; import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee.PaymentDirection; import nl.strohalm.cyclos.entities.accounts.loans.Loan; import nl.strohalm.cyclos.entities.accounts.loans.LoanParameters; import nl.strohalm.cyclos.entities.accounts.transactions.AuthorizationLevel; import nl.strohalm.cyclos.entities.accounts.transactions.Payment; import nl.strohalm.cyclos.entities.accounts.transactions.PaymentFilter; import nl.strohalm.cyclos.entities.accounts.transactions.TransferListener; import nl.strohalm.cyclos.entities.accounts.transactions.TransferQuery; import nl.strohalm.cyclos.entities.accounts.transactions.TransferType; import nl.strohalm.cyclos.entities.accounts.transactions.TransferType.Context; import nl.strohalm.cyclos.entities.accounts.transactions.TransferTypeQuery; import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomField; import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException; import nl.strohalm.cyclos.entities.groups.AdminGroup; import nl.strohalm.cyclos.entities.groups.BrokerGroup; import nl.strohalm.cyclos.entities.groups.Group; import nl.strohalm.cyclos.entities.groups.MemberGroup; import nl.strohalm.cyclos.entities.groups.OperatorGroup; import nl.strohalm.cyclos.entities.members.Administrator; import nl.strohalm.cyclos.entities.members.Element; import nl.strohalm.cyclos.entities.members.Member; import nl.strohalm.cyclos.services.accounts.AccountServiceLocal; import nl.strohalm.cyclos.services.fetch.FetchServiceLocal; import nl.strohalm.cyclos.services.groups.GroupServiceLocal; import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal; import nl.strohalm.cyclos.services.transactions.PaymentServiceLocal; import nl.strohalm.cyclos.services.transactions.TransactionContext; import nl.strohalm.cyclos.services.transfertypes.exceptions.HasPendingPaymentsException; import nl.strohalm.cyclos.utils.Amount; import nl.strohalm.cyclos.utils.PropertyHelper; import nl.strohalm.cyclos.utils.RelationshipHelper; import nl.strohalm.cyclos.utils.access.LoggedUser; import nl.strohalm.cyclos.utils.query.PageHelper; import nl.strohalm.cyclos.utils.validation.GeneralValidation; import nl.strohalm.cyclos.utils.validation.InvalidError; import nl.strohalm.cyclos.utils.validation.PositiveNonZeroValidation; import nl.strohalm.cyclos.utils.validation.PropertyValidation; import nl.strohalm.cyclos.utils.validation.RequiredValidation; import nl.strohalm.cyclos.utils.validation.ValidationError; import nl.strohalm.cyclos.utils.validation.Validator; import nl.strohalm.cyclos.webservices.model.TransferTypeVO; import nl.strohalm.cyclos.webservices.utils.AccountHelper; import org.apache.commons.collections.CollectionUtils; /** * Implementation class for transfer types * @author rafael * @author Rinke (conversion methods) */ public class TransferTypeServiceImpl implements TransferTypeServiceLocal { private final class DestinationAccountTypeValidator implements PropertyValidation { private static final long serialVersionUID = -1068050406929695757L; @Override public ValidationError validate(final Object object, final Object property, final Object value) { final TransferType transferType = (TransferType) object; // Get source and destination account types final AccountType from = fetchService.fetch(transferType.getFrom(), AccountType.Relationships.CURRENCY); AccountType to = (AccountType) value; // Validate if the currency of the source account type is the same currency of the destination account type final Currency sourceAccountTypeCurrency = from.getCurrency(); to = fetchService.fetch(to, AccountType.Relationships.CURRENCY); final Currency destinationAccountTypeCurrency = to.getCurrency(); if (!sourceAccountTypeCurrency.equals(destinationAccountTypeCurrency)) { return new ValidationError("transferType.error.invalidDestinationType"); } if (from != null && to != null) { if (from.equals(to) && from.getNature() == AccountType.Nature.SYSTEM) { // Cannot be from and to the same account if system return new InvalidError(); } if (transferType.isLoanType() && ((from.getNature() == AccountType.Nature.MEMBER) || (to.getNature() == AccountType.Nature.SYSTEM))) { // When is a loan, can only be from system to member return new InvalidError(); } } return null; } } /** * A property validator that is only used when transaction feedback is required * @author luis */ private class FeedbackValidator implements PropertyValidation { private static final long serialVersionUID = 2435741054912450932L; private final PropertyValidation validation; public FeedbackValidator(final PropertyValidation validation) { this.validation = validation; } @Override public ValidationError validate(final Object object, final Object property, final Object value) { final TransferType tt = (TransferType) object; if (tt.isRequiresFeedback()) { return validation.validate(object, property, value); } return null; } } /** * Validates a repayment type to be required if it's associated component is present * @author luis */ private final class LoanWithInterestRepaymentTypeValidator implements PropertyValidation { private static final long serialVersionUID = -3441031471188004677L; private final String property; private final boolean toSystem; public LoanWithInterestRepaymentTypeValidator(final String property, final boolean toSystem) { this.property = property; this.toSystem = toSystem; } @Override public ValidationError validate(final Object object, final Object name, final Object value) { final LoanParameters loan = (LoanParameters) object; final TransferType repayment = fetchService.fetch((TransferType) value, TransferType.Relationships.FROM, TransferType.Relationships.TO); ValidationError error = null; if (loan.getType() == Loan.Type.WITH_INTEREST) { final Object related = PropertyHelper.get(loan, property); boolean required = false; if (related instanceof Amount) { final Amount amount = ((Amount) related); required = amount != null && amount.getValue() != null && amount.getValue().compareTo(BigDecimal.ZERO) == 1; } else if (related instanceof BigDecimal) { final BigDecimal f = ((BigDecimal) related); required = (f != null) && (f.compareTo(BigDecimal.ZERO) == 1); } if (required) { // There must be a repayment type for this value error = RequiredValidation.instance().validate(object, name, value); if (error == null) { // Validates the TT direction if (toSystem && !(!repayment.isFromSystem() && repayment.isToSystem())) { // From system to member error = new InvalidError(); } else if (!toSystem && !(repayment.isFromSystem() && !repayment.isToSystem())) { // From member to system error = new InvalidError(); } } } } return error; } } private final class MaxMinAmountValidator implements GeneralValidation { private static final long serialVersionUID = -7872725176651230479L; @Override public ValidationError validate(final Object object) { TransferType tt = (TransferType) object; BigDecimal minAmount = tt.getMinAmount(); BigDecimal maxAmountPerDay = tt.getMaxAmountPerDay(); if (minAmount != null && maxAmountPerDay != null) { if (minAmount.compareTo(maxAmountPerDay) > 0) { return new ValidationError("transferType.error.minMaxPerDayAmount"); } } return null; } } private static final float PRECISION_DELTA = 0.0001F; private AuthorizationLevelDAO authorizationLevelDao; private FetchServiceLocal fetchService; private TransferTypeDAO transferTypeDao; private PaymentServiceLocal paymentService; private AccountServiceLocal accountService; private GroupServiceLocal groupService; private PermissionServiceLocal permissionService; private AccountHelper accountHelper; @Override public Collection<TransferType> getAllowedTTs(Element element) { element = fetchService.fetch(element, RelationshipHelper.nested(Element.Relationships.GROUP, Group.Relationships.TRANSFER_TYPES), RelationshipHelper.nested(Element.Relationships.GROUP, AdminGroup.Relationships.TRANSFER_TYPES_AS_MEMBER), RelationshipHelper.nested(Element.Relationships.GROUP, Group.Relationships.CONVERSION_SIMULATION_TTS)); Group group = element.getGroup(); Set<TransferType> allowed = new HashSet<TransferType>(); CollectionUtils.addAll(allowed, group.getConversionSimulationTTs().iterator()); // Add the TTs with permission as member if (group instanceof AdminGroup) { CollectionUtils.addAll(allowed, ((AdminGroup) group).getTransferTypesAsMember().iterator()); } else if (group instanceof BrokerGroup) { CollectionUtils.addAll(allowed, ((BrokerGroup) group).getTransferTypesAsMember().iterator()); } // Add the TTs with permission if (group instanceof OperatorGroup) { // an operator doesn't have its own TT it inherits from member OperatorGroup operatorGroup = (OperatorGroup) group; group = fetchService.fetch(operatorGroup.getMember().getGroup(), Group.Relationships.TRANSFER_TYPES); } CollectionUtils.addAll(allowed, group.getTransferTypes().iterator()); // All TTs used to pay account fees should be accessible if (group instanceof MemberGroup) { MemberGroup memberGroup = (MemberGroup) group; Collection<AccountFee> accountFees = memberGroup.getAccountFees(); for (AccountFee accountFee : accountFees) { allowed.add(accountFee.getTransferType()); } } return allowed; } @Override public List<TransferType> getAuthorizableTTs() { final TransferTypeQuery query = new TransferTypeQuery(); query.setAuthorizable(true); if (!permissionService.hasPermission(AdminSystemPermission.ACCOUNTS_VIEW)) { query.setPossibleTransferTypes(getAllowedTTs(LoggedUser.element())); } return search(query); } @Override public List<TransferType> getConversionTTs() { final TransferTypeQuery ttQuery = makeConversionTransferTypeQuery(); return search(ttQuery); } @Override public List<TransferType> getConversionTTs(final AccountType fromAccountType) { final TransferTypeQuery ttQuery = makeConversionTransferTypeQuery(); final List<AccountType> accountTypes = new ArrayList<AccountType>(1); accountTypes.add(fromAccountType); ttQuery.setFromAccountTypes(accountTypes); return search(ttQuery); } @Override public List<TransferType> getConversionTTs(final Currency currency) { final TransferTypeQuery ttQuery = makeConversionTransferTypeQuery(); ttQuery.setCurrency(currency); return search(ttQuery); } @Override public List<TransferType> getPaymentAndSelfPaymentTTs() { if (!(LoggedUser.element() instanceof Administrator)) { throw new IllegalArgumentException("Expected an administrator logged user"); } AdminGroup group = (AdminGroup) fetchService.fetch(LoggedUser.element().getGroup(), AdminGroup.Relationships.VIEW_INFORMATION_OF); final TransferTypeQuery transferTypeQuery = new TransferTypeQuery(); transferTypeQuery.setFromOrToAccountTypes(group.getViewInformationOf()); if (!permissionService.hasPermission(AdminSystemPermission.ACCOUNTS_VIEW)) { transferTypeQuery.setPossibleTransferTypes(getAllowedTTs(LoggedUser.element())); } final List<TransferType> transferTypes = search(transferTypeQuery); for (final Iterator<TransferType> iterator = transferTypes.iterator(); iterator.hasNext();) { // Remove those transfer types which does not allow direct payment or self payment final TransferType tt = iterator.next(); final Context ttContext = tt.getContext(); if (!ttContext.isPayment() && !ttContext.isSelfPayment()) { iterator.remove(); } } return transferTypes; } @Override public List<TransferType> getPosibleTTsForAccountFee(final MemberAccountType accountType, final PaymentDirection paymentDirection) { final TransferTypeQuery ttQuery = new TransferTypeQuery(); ttQuery.setContext(TransactionContext.ANY); switch (paymentDirection) { case TO_MEMBER: ttQuery.setFromNature(AccountType.Nature.SYSTEM); ttQuery.setToAccountType(accountType); break; case TO_SYSTEM: ttQuery.setFromAccountType(accountType); ttQuery.setToNature(AccountType.Nature.SYSTEM); break; default: throw new IllegalArgumentException(); } return search(ttQuery); } @Override public TransferTypeVO getTransferTypeVO(final Long transferTypeId, final boolean extended) { if (transferTypeId == null) { return null; } TransferType tt = load(transferTypeId); if (extended) { return accountHelper.toDetailedVO(tt); } else { return accountHelper.toVO(tt); } } /** * gets transferTypes possibly being a conversion, and having an A-Rated TransferFee. */ @Override public List<TransferType> listARatedTTs() { final List<TransferType> conversionTTs = getConversionTTs(); final List<TransferType> result = new ArrayList<TransferType>(conversionTTs.size()); for (final TransferType tt : conversionTTs) { if (tt.isHavingAratedFees()) { result.add(tt); } } return result; } @Override public TransferType load(final Long id, final Relationship... fetch) { return transferTypeDao.load(id, fetch); } @Override public int remove(final Long... ids) { return transferTypeDao.delete(ids); } @Override public TransferType save(TransferType transferType) { // Validates the transfer type, if validation fails, // a Validation exception will be thrown validate(transferType); if (transferType.getContext().isSelfPayment() && transferType.getFrom().getClass() == Nature.SYSTEM.getType() && transferType.getTo().getClass() == Nature.MEMBER.getType()) { final TransferType.Context context = new TransferType.Context(); context.setPayment(true); transferType.setContext(context); } else if (transferType.getContext().isPayment() && transferType.getFrom().getClass() == Nature.SYSTEM.getType() && transferType.getTo().getClass() == Nature.SYSTEM.getType()) { final TransferType.Context context = new TransferType.Context(); context.setSelfPayment(true); transferType.setContext(context); } // Clear all loan parameters if they are not valid if (!transferType.isLoanType()) { transferType.setLoan(null); } if (transferType.isTransient()) { if (transferType.isRequiresFeedback()) { transferType.setFeedbackEnabledSince(Calendar.getInstance()); } return transferTypeDao.insert(transferType); } else { // We must keep the many-to-many relationships final TransferType current = load(transferType.getId(), TransferType.Relationships.PAYMENT_FILTERS, TransferType.Relationships.LINKED_CUSTOM_FIELDS); transferType.setPaymentFilters(new ArrayList<PaymentFilter>(current.getPaymentFilters())); transferType.setLinkedCustomFields(new ArrayList<PaymentCustomField>(current.getLinkedCustomFields())); if (current.isRequiresAuthorization() && !transferType.isRequiresAuthorization()) { // Authorization has been disabled. Raise an error if there are any pending payments final TransferQuery query = new TransferQuery(); query.setPageForCount(); query.setTransferType(transferType); query.setRequiresAuthorization(true); query.setStatus(Payment.Status.PENDING); final int payments = PageHelper.getTotalCount(paymentService.search(query)); if (payments > 0) { throw new HasPendingPaymentsException(); } } // Update the feedbackEnabledSince if (current.isRequiresFeedback() && !transferType.isRequiresFeedback()) { // It was enabled but is no longer - remove the enabled since transferType.setFeedbackEnabledSince(null); } else if (!current.isRequiresFeedback() && transferType.isRequiresFeedback()) { // It was not enabled but is now - set the enabled since transferType.setFeedbackEnabledSince(Calendar.getInstance()); } else { // Keep it as is transferType.setFeedbackEnabledSince(current.getFeedbackEnabledSince()); } transferType = transferTypeDao.update(transferType); // If transfer type does not require authorization, clean authorization levels final Collection<AuthorizationLevel> authorizationLevels = transferType.getAuthorizationLevels(); if (!transferType.isRequiresAuthorization() && !CollectionUtils.isEmpty(authorizationLevels)) { for (final AuthorizationLevel authorizationLevel : authorizationLevels) { authorizationLevelDao.delete(authorizationLevel.getId()); } } return transferType; } } @Override public List<TransferType> search(final TransferTypeQuery query) { // If searching for an operator group permissions, the transfer types are the same as his member's. So we have to actually check by his // member's permissions final Group group = fetchService.fetch(query.getGroup(), RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP)); if (group instanceof OperatorGroup) { final OperatorGroup operatorGroup = (OperatorGroup) group; query.setGroup(operatorGroup.getMember().getGroup()); } final TransferTypeQuery finalQuery = (TransferTypeQuery) query.clone(); finalQuery.setUsePriority(false); if (query.isUsePriority()) { query.setPriority(true); query.setPageForCount(); final int totalCount = PageHelper.getTotalCount(transferTypeDao.search(query)); finalQuery.setPriority(totalCount > 0); } // When fromOwner is a member, ensure disabled accounts (when credit limit is zero) are not used if (query.getFromOwner() instanceof Member) { final Member member = fetchService.fetch((Member) query.getFromOwner(), RelationshipHelper.nested(Element.Relationships.GROUP, MemberGroup.Relationships.ACCOUNT_SETTINGS)); final List<AccountType> accountTypes = new ArrayList<AccountType>(); final List<? extends Account> accounts = accountService.getAccounts(member); for (final Account account : accounts) { boolean hidden = false; try { final MemberGroupAccountSettings accountSettings = groupService.loadAccountSettings(member.getGroup().getId(), account.getType().getId()); if (accountSettings.isHideWhenNoCreditLimit() && Math.abs(account.getCreditLimit().floatValue()) < PRECISION_DELTA) { hidden = true; } } catch (final EntityNotFoundException e) { continue; } if (!hidden) { accountTypes.add(account.getType()); } } if (CollectionUtils.isNotEmpty(finalQuery.getFromAccountTypes())) { // When there were already from account types set, retain them only accountTypes.retainAll(finalQuery.getFromAccountTypes()); } // Finally, ensure that only transfer types from visible accounts are used finalQuery.setFromAccountTypes(accountTypes); } return transferTypeDao.search(finalQuery); } public void setAccountHelper(final AccountHelper accountHelper) { this.accountHelper = accountHelper; } public void setAccountServiceLocal(final AccountServiceLocal accountService) { this.accountService = accountService; } public void setAuthorizationLevelDao(final AuthorizationLevelDAO authorizationLevelDao) { this.authorizationLevelDao = authorizationLevelDao; } public void setFetchServiceLocal(final FetchServiceLocal fetchService) { this.fetchService = fetchService; } public void setGroupServiceLocal(final GroupServiceLocal groupService) { this.groupService = groupService; } public void setPaymentServiceLocal(final PaymentServiceLocal paymentService) { this.paymentService = paymentService; } public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) { this.permissionService = permissionService; } public void setTransferTypeDao(final TransferTypeDAO dao) { transferTypeDao = dao; } @Override public void validate(final TransferType transferType) { if (transferType.isLoanType()) { getLoanValidator().validate(transferType); } else { getValidator().validate(transferType); } } private Validator createValidator() { final Validator validator = new Validator("transferType"); validator.property("name").required().maxLength(100); validator.property("description").required().maxLength(1000); validator.property("confirmationMessage").maxLength(4000); validator.property("from").required(); validator.property("to").required().add(new DestinationAccountTypeValidator()); validator.property("maxAmountPerDay").positiveNonZero(); validator.property("minAmount").positiveNonZero(); validator.property("feedbackExpirationTime.number").key("transferType.feedbackExpirationTime").add(new FeedbackValidator(RequiredValidation.instance())).add(new FeedbackValidator(PositiveNonZeroValidation.instance())); validator.property("feedbackExpirationTime.field").key("transferType.feedbackExpirationTime").add(new FeedbackValidator(RequiredValidation.instance())); validator.property("feedbackReplyExpirationTime.number").key("transferType.feedbackReplyExpirationTime").add(new FeedbackValidator(RequiredValidation.instance())).add(new FeedbackValidator(PositiveNonZeroValidation.instance())); validator.property("feedbackReplyExpirationTime.field").key("transferType.feedbackReplyExpirationTime").add(new FeedbackValidator(RequiredValidation.instance())); validator.property("defaultFeedbackComments").add(new FeedbackValidator(RequiredValidation.instance())); validator.property("defaultFeedbackLevel").add(new FeedbackValidator(RequiredValidation.instance())); validator.property("transferListenerClass").instanceOf(TransferListener.class); validator.general(new MaxMinAmountValidator()); return validator; } private Validator getLoanValidator() { final Validator loanValidator = createValidator(); // Chain the loan parameters validator final Validator nestedValidator = new Validator("loan", "loan"); loanValidator.chained(nestedValidator); nestedValidator.property("repaymentType").add(new PropertyValidation() { private static final long serialVersionUID = -3441031471188004677L; @Override public ValidationError validate(final Object object, final Object name, final Object value) { final LoanParameters lp = ((LoanParameters) object); ValidationError error = null; if (lp != null && lp.getType() != null) { // There must be a repayment type on loan types error = RequiredValidation.instance().validate(object, name, value); if (error == null) { // Validate the repayment type as being from member to system final TransferType repayment = fetchService.fetch((TransferType) value, TransferType.Relationships.FROM, TransferType.Relationships.TO); if (!(!repayment.isFromSystem() && repayment.isToSystem())) { // Must be from member to system error = new InvalidError(); } } } return error; } }); nestedValidator.property("repaymentDays").positiveNonZero().add(new PropertyValidation() { private static final long serialVersionUID = -3665200579172755969L; @Override public ValidationError validate(final Object object, final Object name, final Object value) { final LoanParameters lp = ((LoanParameters) object); if (lp != null && lp.getType() == Loan.Type.SINGLE_PAYMENT) { // RepaymentDays is required on single payment type return RequiredValidation.instance().validate(object, name, value); } return null; } }); nestedValidator.property("grantFee").positiveNonZero(); nestedValidator.property("grantFeeRepaymentType").add(new LoanWithInterestRepaymentTypeValidator("grantFee", true)); nestedValidator.property("monthlyInterest").positiveNonZero(); nestedValidator.property("monthlyInterestRepaymentType").add(new LoanWithInterestRepaymentTypeValidator("monthlyInterest", true)); nestedValidator.property("expirationFee").positiveNonZero(); nestedValidator.property("expirationFeeRepaymentType").add(new LoanWithInterestRepaymentTypeValidator("expirationFee", true)); nestedValidator.property("expirationDailyInterest").positiveNonZero(); nestedValidator.property("expirationDailyInterestRepaymentType").add(new LoanWithInterestRepaymentTypeValidator("expirationDailyInterest", true)); return loanValidator; } private Validator getValidator() { return createValidator(); } private TransferTypeQuery makeConversionTransferTypeQuery() { final TransferTypeQuery ttQuery = new TransferTypeQuery(); ttQuery.setContext(TransactionContext.PAYMENT); ttQuery.setFromNature(AccountType.Nature.MEMBER); ttQuery.setToNature(AccountType.Nature.SYSTEM); ttQuery.setToLimitType(AccountType.LimitType.UNLIMITED); return ttQuery; } }