/* 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.elements; import java.util.Calendar; import java.util.Collection; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import nl.strohalm.cyclos.access.AdminMemberPermission; import nl.strohalm.cyclos.access.BrokerPermission; import nl.strohalm.cyclos.access.MemberPermission; import nl.strohalm.cyclos.access.OperatorPermission; import nl.strohalm.cyclos.dao.members.ReferenceDAO; import nl.strohalm.cyclos.dao.members.ReferenceHistoryDAO; import nl.strohalm.cyclos.entities.Relationship; import nl.strohalm.cyclos.entities.accounts.transactions.Payment; import nl.strohalm.cyclos.entities.accounts.transactions.PaymentAwaitingFeedbackDTO; import nl.strohalm.cyclos.entities.accounts.transactions.ScheduledPayment; import nl.strohalm.cyclos.entities.accounts.transactions.Transfer; import nl.strohalm.cyclos.entities.accounts.transactions.TransferType; import nl.strohalm.cyclos.entities.alerts.Alert; import nl.strohalm.cyclos.entities.alerts.AlertQuery; import nl.strohalm.cyclos.entities.alerts.MemberAlert; import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException; import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException; import nl.strohalm.cyclos.entities.groups.AdminGroup; import nl.strohalm.cyclos.entities.groups.Group; import nl.strohalm.cyclos.entities.groups.MemberGroup; import nl.strohalm.cyclos.entities.members.GeneralReference; import nl.strohalm.cyclos.entities.members.Member; import nl.strohalm.cyclos.entities.members.PaymentsAwaitingFeedbackQuery; import nl.strohalm.cyclos.entities.members.Reference; import nl.strohalm.cyclos.entities.members.Reference.Level; import nl.strohalm.cyclos.entities.members.Reference.Nature; import nl.strohalm.cyclos.entities.members.ReferenceHistoryLog; import nl.strohalm.cyclos.entities.members.ReferenceHistoryLogQuery; import nl.strohalm.cyclos.entities.members.ReferenceQuery; import nl.strohalm.cyclos.entities.members.TransactionFeedback; import nl.strohalm.cyclos.entities.settings.AlertSettings; import nl.strohalm.cyclos.exceptions.PermissionDeniedException; import nl.strohalm.cyclos.services.alerts.AlertServiceLocal; import nl.strohalm.cyclos.services.fetch.FetchServiceLocal; import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal; import nl.strohalm.cyclos.services.settings.SettingsServiceLocal; import nl.strohalm.cyclos.utils.CacheCleaner; import nl.strohalm.cyclos.utils.DateHelper; import nl.strohalm.cyclos.utils.EntityHelper; import nl.strohalm.cyclos.utils.Period; import nl.strohalm.cyclos.utils.access.LoggedUser; import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler; import nl.strohalm.cyclos.utils.query.PageHelper; import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType; import nl.strohalm.cyclos.utils.validation.GeneralValidation; import nl.strohalm.cyclos.utils.validation.InvalidError; import nl.strohalm.cyclos.utils.validation.PropertyValidation; import nl.strohalm.cyclos.utils.validation.RequiredError; import nl.strohalm.cyclos.utils.validation.ValidationError; import nl.strohalm.cyclos.utils.validation.ValidationException; import nl.strohalm.cyclos.utils.validation.Validator; import org.apache.commons.lang.StringUtils; /** * Implementation class for reference services * @author rafael * @author luis */ public class ReferenceServiceImpl implements ReferenceServiceLocal { private SettingsServiceLocal settingsService; private AlertServiceLocal alertService; private FetchServiceLocal fetchService; private PermissionServiceLocal permissionService; private ReferenceDAO referenceDao; private ReferenceHistoryDAO referenceHistoryDao; private MemberNotificationHandler memberNotificationHandler; @Override public boolean canGiveGeneralReference(final Member member) { return !LoggedUser.isAdministrator() && !LoggedUser.element().getAccountOwner().equals(member) && permissionService.permission().member(MemberPermission.REFERENCES_GIVE).operator(OperatorPermission.REFERENCES_MANAGE_MEMBER_REFERENCES).hasPermission() && permissionService.relatesTo(member); } @Override public boolean canManage(final Reference ref) { switch (ref.getNature()) { case GENERAL: return canManageGeneralReference(ref.getFrom()); case TRANSACTION: // If it's a new transaction feedback (no comments have been added) then if the logged user is a member or an operator, they must // manage the from of the transaction feedback. if (ref.isTransient() && (LoggedUser.isMember() || LoggedUser.isOperator()) && !permissionService.manages(ref.getFrom())) { return false; } // If it's a reply feedback (no reply comments have been added) then if the logged user is a member or an operator, he must manage the // to of the transaction feedback. if (!ref.isTransient() && (LoggedUser.isMember() || LoggedUser.isOperator()) && !permissionService.manages(ref.getTo())) { return false; } if (!(permissionService.manages(ref.getFrom()) || permissionService.manages(ref.getTo()))) { return false; // If the Logged user is a broker, then he can only manage his own transaction feedbacks. } else if (LoggedUser.isBroker() && !(LoggedUser.element().equals(ref.getFrom()) || LoggedUser.element().equals(ref.getTo()))) { return false; } return permissionService.permission() .admin(AdminMemberPermission.TRANSACTION_FEEDBACKS_MANAGE) .member() .operator(OperatorPermission.REFERENCES_MANAGE_MEMBER_TRANSACTION_FEEDBACKS).hasPermission(); } return false; } @Override public boolean canManageGeneralReference(final Member member) { return permissionService.permission(member) .admin(AdminMemberPermission.REFERENCES_MANAGE) .broker(BrokerPermission.REFERENCES_MANAGE) .member(MemberPermission.REFERENCES_GIVE) .operator(OperatorPermission.REFERENCES_MANAGE_MEMBER_REFERENCES).hasPermission(); } @Override public boolean canReplyFeedbackNow(final TransactionFeedback transactionFeedback) { Payment payment = transactionFeedback.getPayment(); Calendar date = transactionFeedback.getDate(); final Calendar replyLimit = payment.getType().getFeedbackReplyExpirationTime().add(date); return replyLimit.after(Calendar.getInstance()); } @Override public Map<Level, Integer> countGivenReferencesByLevel(final Reference.Nature nature) { Collection<MemberGroup> memberGroups = null; if (LoggedUser.hasUser()) { AdminGroup adminGroup = LoggedUser.group(); adminGroup = fetchService.fetch(adminGroup, AdminGroup.Relationships.MANAGES_GROUPS); memberGroups = adminGroup.getManagesGroups(); } return referenceDao.countGivenReferencesByLevel(nature, memberGroups); } @Override public Map<Level, Integer> countReferencesByLevel(final Reference.Nature nature, final Member member, final boolean received) { return normalizeCountByLevel(referenceDao.countReferencesByLevel(nature, null, member, received)); } @Override public Map<Level, Integer> countReferencesHistoryByLevel(final Reference.Nature nature, final Member member, final Period period, final boolean received) { Map<Level, Integer> countByLevel; if (nature == Reference.Nature.TRANSACTION) { countByLevel = referenceDao.countReferencesByLevel(nature, period, member, received); } else { countByLevel = referenceHistoryDao.countReferencesHistoryByLevel(member, period, received); } return normalizeCountByLevel(countByLevel); } @Override public Collection<Nature> getNaturesByGroup(MemberGroup group) { final Collection<Nature> natures = EnumSet.noneOf(Nature.class); // Check for transaction references group = fetchService.fetch(group, Group.Relationships.TRANSFER_TYPES); for (final TransferType transferType : group.getTransferTypes()) { if (transferType.isRequiresFeedback()) { natures.add(Nature.TRANSACTION); break; } } if (permissionService.hasPermission(group, MemberPermission.REFERENCES_GIVE)) { natures.add(Reference.Nature.GENERAL); } return natures; } @Override public TransactionFeedbackAction getPossibleAction(final TransactionFeedback transactionFeedback) { if (transactionFeedback.isTransient()) { if (canFeedbackNow(transactionFeedback)) { return TransactionFeedbackAction.COMMENTS; } else { return TransactionFeedbackAction.VIEW; } } else { // Check the current feedback final TransactionFeedback current = (TransactionFeedback) load(transactionFeedback.getId()); if (LoggedUser.isAdministrator()) { return TransactionFeedbackAction.ADMIN_EDIT; } else if ((LoggedUser.element().getAccountOwner().equals(current.getTo()) && StringUtils.isEmpty(current.getReplyComments())) && canReplyFeedbackNow(current)) { return TransactionFeedbackAction.REPLY_COMMENTS; } else { return TransactionFeedbackAction.VIEW; } } } @Override public Reference load(final Long id, final Relationship... fetch) { return referenceDao.load(id, fetch); } @Override @SuppressWarnings("unchecked") public GeneralReference loadGeneral(final Member from, final Member to, final Relationship... fetch) throws EntityNotFoundException { final ReferenceQuery query = new ReferenceQuery(); query.fetch(fetch); query.setNature(Reference.Nature.GENERAL); query.setFrom(from); query.setTo(to); final List<GeneralReference> list = (List<GeneralReference>) search(query); if (list.isEmpty()) { throw new EntityNotFoundException(GeneralReference.class); } return list.iterator().next(); } @Override @SuppressWarnings("unchecked") public TransactionFeedback loadTransactionFeedback(final Payment payment, final Relationship... fetch) throws EntityNotFoundException { final ReferenceQuery query = new ReferenceQuery(); query.setNature(Nature.TRANSACTION); if (payment instanceof ScheduledPayment) { query.setScheduledPayment((ScheduledPayment) payment); } else { query.setTransfer((Transfer) payment); } query.setUniqueResult(); final List<TransactionFeedback> refs = (List<TransactionFeedback>) search(query); if (refs.isEmpty()) { throw new EntityNotFoundException(TransactionFeedback.class); } return refs.iterator().next(); } @Override public int processExpiredFeedbacks(Calendar time) { time = DateHelper.truncate(time); final PaymentsAwaitingFeedbackQuery query = new PaymentsAwaitingFeedbackQuery(); query.setExpired(true); query.setResultType(ResultType.ITERATOR); final List<PaymentAwaitingFeedbackDTO> paymentsAwaitingFeedback = searchPaymentsAwaitingFeedback(query); int processed = 0; CacheCleaner cleaner = new CacheCleaner(fetchService); for (final PaymentAwaitingFeedbackDTO dto : paymentsAwaitingFeedback) { // Fetch the transfer type TransferType transferType = EntityHelper.reference(TransferType.class, dto.getTransferTypeId()); transferType = fetchService.fetch(transferType); // Fetch the payment Class<? extends Payment> paymentClass = dto.isScheduled() ? ScheduledPayment.class : Transfer.class; Payment payment = fetchService.fetch(EntityHelper.reference(paymentClass, dto.getId()), Payment.Relationships.FROM, Payment.Relationships.TO); // Insert the feedback with the default comments final TransactionFeedback feedback = new TransactionFeedback(); feedback.setDate(time); feedback.setComments(transferType.getDefaultFeedbackComments()); feedback.setLevel(transferType.getDefaultFeedbackLevel()); feedback.setFrom((Member) payment.getFromOwner()); feedback.setTo((Member) payment.getToOwner()); feedback.setPayment(payment); referenceDao.insert(feedback, false); cleaner.clearCache(); processed++; } return processed; } @Override public int remove(final Long... ids) { // Before remove the references, update their last reference history logs for (final Long id : ids) { final Reference reference = load(id, (Relationship[]) null); updatePreviousReferenceHistoryLog(reference); } return referenceDao.delete(ids); } @Override public GeneralReference save(final GeneralReference reference) { reference.setDate(Calendar.getInstance()); return (GeneralReference) doSave(reference); } @Override public TransactionFeedback save(final TransactionFeedback transactionFeedback) { switch (getPossibleAction(transactionFeedback)) { case ADMIN_EDIT: return saveTransactionFeedbackByAdmin(transactionFeedback); case COMMENTS: return saveTransactionFeedbackComments(transactionFeedback); case REPLY_COMMENTS: return saveTransactionFeedbackReplyComments(transactionFeedback); default: throw new PermissionDeniedException(); } } @Override public List<? extends Reference> search(final ReferenceQuery query) { return referenceDao.search(query); } @Override public List<PaymentAwaitingFeedbackDTO> searchPaymentsAwaitingFeedback(final PaymentsAwaitingFeedbackQuery query) { return referenceDao.searchPaymentsAwaitingFeedback(query); } public void setAlertServiceLocal(final AlertServiceLocal alertService) { this.alertService = alertService; } public void setFetchServiceLocal(final FetchServiceLocal fetchService) { this.fetchService = fetchService; } public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) { this.memberNotificationHandler = memberNotificationHandler; } public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) { this.permissionService = permissionService; } public void setReferenceDao(final ReferenceDAO referenceDAO) { referenceDao = referenceDAO; } public void setReferenceHistoryDao(final ReferenceHistoryDAO referenceHistoryDao) { this.referenceHistoryDao = referenceHistoryDao; } public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) { this.settingsService = settingsService; } @Override public void validate(final Reference reference) throws ValidationException { Validator validator; if (reference instanceof GeneralReference) { validator = getGeneralValidator(); } else { validator = getTransactionFeedbackValidator((TransactionFeedback) reference); } validator.validate(reference); } /** * Returns true if a member or an operator are in time to make the feedback. * @param transactionFeedback * @return */ private boolean canFeedbackNow(final TransactionFeedback transactionFeedback) { Payment payment = transactionFeedback.getPayment(); payment = fetchService.fetch(payment, Payment.Relationships.TYPE); Calendar paymentDate; if (payment instanceof ScheduledPayment) { paymentDate = payment.getDate(); } else { paymentDate = payment.getProcessDate(); } final Calendar commentLimit = payment.getType().getFeedbackExpirationTime().add(paymentDate); return commentLimit.after(Calendar.getInstance()); } private Validator createBasicValidator() { final Validator validator = new Validator("reference"); validator.property("level").required(); validator.property("from").required(); validator.property("to").required().add(new PropertyValidation() { private static final long serialVersionUID = 5881069152089528552L; @Override public ValidationError validate(final Object object, final Object name, final Object value) { final Reference reference = (Reference) object; // From and to cannot be the same final Member from = reference.getFrom(); if (from != null && from.equals(value)) { return new InvalidError(); } return null; } }); validator.property("comments").required().maxLength(1000); return validator; } /** * Create a reference history log of the reference */ private void createNewReferenceHistoryLog(final Reference reference) { final ReferenceHistoryLog log = new ReferenceHistoryLog(); log.setFrom(reference.getFrom()); log.setTo(reference.getTo()); log.setLevel(reference.getLevel()); log.setPeriod(Period.begginingAt(reference.getDate())); referenceHistoryDao.insert(log); } private Reference doSave(Reference reference) { validate(reference); boolean isInsert = reference.isTransient(); if (isInsert) { // Insert the reference reference = referenceDao.insert(reference); } else { // Update the current reference reference = referenceDao.update(reference); // Update the log of the previous reference if (reference instanceof GeneralReference) { updatePreviousReferenceHistoryLog(reference); } } // Specific operators for general references if (reference instanceof GeneralReference) { // Insert a log for the new reference createNewReferenceHistoryLog(reference); // Compute the given / received references to check if should create an alert final AlertSettings alertSettings = settingsService.getAlertSettings(); // Given final Member from = reference.getFrom(); if (alertSettings.getGivenVeryBadRefs() > 0) { final int givenVeryBad = referenceDao.countReferencesByLevel(reference.getNature(), null, from, false).get(Reference.Level.VERY_BAD); if (givenVeryBad >= alertSettings.getGivenVeryBadRefs()) { final AlertQuery query = new AlertQuery(); query.setType(Alert.Type.MEMBER); query.setMember(from); query.setKey(MemberAlert.Alerts.GIVEN_VERY_BAD_REFS.getValue()); query.setPageForCount(); final boolean hasAlert = PageHelper.getTotalCount(alertService.search(query)) > 0; if (!hasAlert) { alertService.create(from, MemberAlert.Alerts.GIVEN_VERY_BAD_REFS, givenVeryBad); } } } // Received final Member to = reference.getTo(); if (alertSettings.getReceivedVeryBadRefs() > 0) { final int receivedVeryBad = referenceDao.countReferencesByLevel(reference.getNature(), null, to, true).get(Reference.Level.VERY_BAD); if (receivedVeryBad >= alertSettings.getReceivedVeryBadRefs()) { final AlertQuery query = new AlertQuery(); query.setType(Alert.Type.MEMBER); query.setMember(from); query.setKey(MemberAlert.Alerts.RECEIVED_VERY_BAD_REFS.getValue()); query.setPageForCount(); final boolean hasAlert = PageHelper.getTotalCount(alertService.search(query)) > 0; if (!hasAlert) { alertService.create(to, MemberAlert.Alerts.RECEIVED_VERY_BAD_REFS, receivedVeryBad); } } } } if (isInsert && reference instanceof GeneralReference) { memberNotificationHandler.receivedReferenceNotification(reference); } return reference; } private Validator getGeneralValidator() { return createBasicValidator(); } private Validator getTransactionFeedbackValidator(final TransactionFeedback transactionFeedback) { final Validator transactionFeedbackValidator = createBasicValidator(); transactionFeedbackValidator.property("payment").required(); transactionFeedbackValidator.general(new GeneralValidation() { private static final long serialVersionUID = 1L; @Override public ValidationError validate(final Object object) { TransactionFeedback transactionFeedback = (TransactionFeedback) object; // Check if the time to make the feedback has expired. if (transactionFeedback.isTransient()) { boolean readyForFeedback = !LoggedUser.isAdministrator() && (LoggedUser.element().getAccountOwner().equals(transactionFeedback.getFrom())); if (readyForFeedback && !canFeedbackNow(transactionFeedback)) { return new ValidationError("reference.transactionFeedback.feedbackPeriodExpired"); } } // Check if the time to make the reply has expired. if (!transactionFeedback.isTransient()) { TransactionFeedback current = (TransactionFeedback) load(transactionFeedback.getId()); boolean readyForReply = (!LoggedUser.isAdministrator()) && (LoggedUser.element().getAccountOwner().equals(current.getTo()) && StringUtils.isEmpty(current.getReplyComments())); if (readyForReply && !canReplyFeedbackNow(current)) { return new ValidationError("reference.transactionFeedback.feedbackPeriodExpired"); } } return null; } }); return transactionFeedbackValidator; } private Map<Level, Integer> normalizeCountByLevel(final Map<Level, Integer> countMap) { final Map<Level, Integer> countByLevel = new LinkedHashMap<Level, Integer>(); for (final Level level : settingsService.getLocalSettings().getReferenceLevelList()) { final Integer count = countMap.get(level); countByLevel.put(level, count == null ? 0 : count); } return countByLevel; } private TransactionFeedback saveTransactionFeedbackByAdmin(TransactionFeedback transactionFeedback) { final Calendar now = Calendar.getInstance(); final Reference.Level level = transactionFeedback.getLevel(); final String comments = transactionFeedback.getComments(); final String replyComments = transactionFeedback.getReplyComments(); final String adminComments = transactionFeedback.getAdminComments(); transactionFeedback = referenceDao.load(transactionFeedback.getId()); transactionFeedback.setLevel(level); transactionFeedback.setComments(comments); if (StringUtils.isNotEmpty(replyComments)) { transactionFeedback.setReplyComments(replyComments); if (transactionFeedback.getReplyCommentsDate() == null) { transactionFeedback.setReplyCommentsDate(now); } } else { transactionFeedback.setReplyComments(null); } if (StringUtils.isNotEmpty(adminComments)) { transactionFeedback.setAdminComments(adminComments); transactionFeedback.setAdminCommentsDate(now); } else { transactionFeedback.setAdminComments(null); transactionFeedback.setAdminCommentsDate(null); } validate(transactionFeedback); transactionFeedback = referenceDao.update(transactionFeedback); memberNotificationHandler.transactionFeedBackAdminCommentsNotification(transactionFeedback); return transactionFeedback; } private TransactionFeedback saveTransactionFeedbackComments(TransactionFeedback transactionFeedback) { final Payment payment = fetchService.fetch(transactionFeedback.getPayment(), Payment.Relationships.TYPE, Payment.Relationships.FROM, Payment.Relationships.TO); final Reference.Level level = transactionFeedback.getLevel(); final String comments = transactionFeedback.getComments(); if (!payment.getType().isRequiresFeedback()) { throw new UnexpectedEntityException(); } // Check whether the feedback exists try { transactionFeedback = loadTransactionFeedback(payment); // It already exists. Return the current one } catch (final EntityNotFoundException e) { // Doesn't exist yet. Let's save it transactionFeedback = new TransactionFeedback(); transactionFeedback.setDate(Calendar.getInstance()); transactionFeedback.setPayment(payment); transactionFeedback.setFrom((Member) payment.getFromOwner()); transactionFeedback.setTo((Member) payment.getToOwner()); transactionFeedback.setLevel(level); transactionFeedback.setComments(comments); transactionFeedback = (TransactionFeedback) doSave(transactionFeedback); } memberNotificationHandler.transactionFeedBackReceivedNotification(transactionFeedback); return transactionFeedback; } private TransactionFeedback saveTransactionFeedbackReplyComments(TransactionFeedback transactionFeedback) { final String replyComments = transactionFeedback.getReplyComments(); transactionFeedback = referenceDao.load(transactionFeedback.getId()); // There was a reply already if (transactionFeedback.getReplyCommentsDate() != null) { return transactionFeedback; } // Validate if (StringUtils.isEmpty(replyComments)) { throw new ValidationException("replyComments", "reference.replyComments", new RequiredError()); } // Set the comments transactionFeedback.setReplyCommentsDate(Calendar.getInstance()); transactionFeedback.setReplyComments(replyComments); validate(transactionFeedback); transactionFeedback = referenceDao.update(transactionFeedback); memberNotificationHandler.transactionFeedBackReplyNotification(transactionFeedback); return transactionFeedback; } /* * Update the previous reference history log of the reference This methods set the final date of the log to the date of the current reference */ private void updatePreviousReferenceHistoryLog(final Reference reference) { final ReferenceHistoryLogQuery query = new ReferenceHistoryLogQuery(); query.setFrom(reference.getFrom()); query.setTo(reference.getTo()); final ReferenceHistoryLog previousLog = referenceHistoryDao.getOpenReferenceHistoryLog(query); if (previousLog != null) { final Period period = previousLog.getPeriod(); period.setEnd(reference.getDate()); previousLog.setPeriod(period); referenceHistoryDao.update(previousLog); } } }