/*
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.HashSet;
import java.util.List;
import java.util.Set;
import nl.strohalm.cyclos.dao.members.ElementDAO;
import nl.strohalm.cyclos.dao.members.RemarkDAO;
import nl.strohalm.cyclos.dao.members.brokerings.BrokerCommissionContractDAO;
import nl.strohalm.cyclos.dao.members.brokerings.BrokeringCommissionStatusDAO;
import nl.strohalm.cyclos.dao.members.brokerings.BrokeringDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.BrokerCommission;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.TransactionFeeQuery;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.BrokerGroup;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.members.BrokeringQuery;
import nl.strohalm.cyclos.entities.members.BrokeringQuery.Status;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.FullTextMemberQuery;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.members.brokerings.BrokerCommissionContract;
import nl.strohalm.cyclos.entities.members.brokerings.BrokerCommissionContractQuery;
import nl.strohalm.cyclos.entities.members.brokerings.Brokering;
import nl.strohalm.cyclos.entities.members.brokerings.BrokeringCommissionStatus;
import nl.strohalm.cyclos.entities.members.brokerings.BrokeringCommissionStatusQuery;
import nl.strohalm.cyclos.entities.members.remarks.BrokerRemark;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.services.InitializingService;
import nl.strohalm.cyclos.services.elements.exceptions.MemberAlreadyInBrokeringsException;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.services.transfertypes.TransactionFeeServiceLocal;
import nl.strohalm.cyclos.utils.CacheCleaner;
import nl.strohalm.cyclos.utils.DataIteratorHelper;
import nl.strohalm.cyclos.utils.DateHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler;
import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType;
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.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
/**
* Implementation for brokering service
* @author luis
*/
public class BrokeringServiceImpl implements BrokeringServiceLocal, InitializingService {
/**
* Validates a property as being a broker
* @author luis
*/
public final class BrokerValidation implements PropertyValidation {
private static final long serialVersionUID = 580603314020373024L;
@Override
public ValidationError validate(final Object object, final Object name, final Object value) {
final ChangeBrokerDTO dto = (ChangeBrokerDTO) object;
final Member member = fetchService.fetch((Member) value, Element.Relationships.GROUP);
if (value == null) {
return null;
}
if (member != null && !member.getMemberGroup().isBroker()) {
return new InvalidError();
}
final MemberGroup viewerGroup = (MemberGroup) member.getGroup();
final Member brokeredMember = fetchService.fetch(dto.getMember());
if (!viewerGroup.getCanViewProfileOfGroups().contains(brokeredMember.getGroup())) {
throw new ValidationException();
}
return null;
}
}
private BrokerCommissionContractDAO brokerCommissionContractDao;
private BrokeringCommissionStatusDAO brokeringCommissionStatusDao;
private BrokeringDAO brokeringDao;
private CommissionServiceLocal commissionService;
private ElementServiceLocal elementService;
private ElementDAO elementDao;
private FetchServiceLocal fetchService;
private RemarkDAO remarkDao;
private SettingsServiceLocal settingsService;
private TransactionFeeServiceLocal transactionFeeService;
private MemberNotificationHandler memberNotificationHandler;
@Override
@SuppressWarnings("unchecked")
public BulkMemberActionResultVO bulkChangeMemberBroker(final FullTextMemberQuery query, Member newBroker, final boolean suspendCommission, final String comments) {
if (newBroker == null || newBroker.isTransient()) {
throw new ValidationException();
}
if (StringUtils.isEmpty(comments)) {
throw new ValidationException();
}
newBroker = fetchService.fetch(newBroker);
int changed = 0;
int unchanged = 0;
// force the result type to ITERATOR to avoid load all members in memory
query.setIterateAll();
final List<Member> members = (List<Member>) elementService.fullTextSearch(query);
CacheCleaner cacheCleaner = new CacheCleaner(fetchService);
for (final Member member : members) {
if (newBroker.equals(member.getBroker())) {
unchanged++;
} else {
final ChangeBrokerDTO dto = new ChangeBrokerDTO();
dto.setMember(member);
dto.setNewBroker(newBroker);
dto.setSuspendCommission(suspendCommission);
dto.setComments(comments);
changeBroker(dto);
changed++;
cacheCleaner.clearCache();
}
}
return new BulkMemberActionResultVO(changed, unchanged);
}
@Override
public Brokering changeBroker(final ChangeBrokerDTO dto) {
memberNotificationHandler.removedBrokeringNotification(dto);
return doChangeBroker(dto);
}
/**
* Creates a new brokering
*/
@Override
public Brokering create(final Member broker, final Member brokered) {
Brokering brokering = new Brokering();
brokering.setBroker(broker);
brokering.setBrokered(brokered);
brokering.setStartDate(Calendar.getInstance());
brokering = brokeringDao.insert(brokering);
// Create brokering commission status
createBrokeringCommissionStatus(brokering);
return brokering;
}
@Override
public Brokering getActiveBrokering(Member member) {
member = fetchService.fetch(member, Member.Relationships.BROKER);
if (member.getBroker() == null) {
return null;
}
// Verifies if there is an active broker for the subject
final BrokeringQuery query = new BrokeringQuery();
query.fetch(Brokering.Relationships.BROKER);
query.setBroker(member.getBroker());
query.setBrokered(member);
query.setStatus(BrokeringQuery.Status.ACTIVE);
query.setUniqueResult();
final List<Brokering> list = search(query);
if (list != null && !list.isEmpty()) {
return list.get(0);
}
return null;
}
@Override
public Brokering getBrokering(final Member broker, final Member member) throws ValidationException {
final BrokeringQuery brokeringQuery = new BrokeringQuery();
brokeringQuery.setBroker(broker);
brokeringQuery.setBrokered(member);
brokeringQuery.setStatus(BrokeringQuery.Status.ACTIVE);
final List<Brokering> brokerings = search(brokeringQuery);
if (CollectionUtils.isEmpty(brokerings)) {
throw new ValidationException();
}
final Brokering brokering = brokerings.iterator().next();
return brokering;
}
public ElementDAO getElementDao() {
return elementDao;
}
@Override
public void initializeService() {
removeExpiredBrokerings(Calendar.getInstance());
}
@Override
public Collection<Status> listPossibleStatuses(BrokerGroup brokerGroup) {
brokerGroup = fetchService.fetch(brokerGroup, BrokerGroup.Relationships.POSSIBLE_INITIAL_GROUPS);
boolean hasInactive = false;
Collection<MemberGroup> possibleInitialGroups = brokerGroup.getPossibleInitialGroups();
for (MemberGroup group : possibleInitialGroups) {
if (!group.isActive()) {
hasInactive = true;
break;
}
}
return hasInactive ? EnumSet.allOf(BrokeringQuery.Status.class) : EnumSet.complementOf(EnumSet.of(BrokeringQuery.Status.PENDING));
}
@Override
public Brokering load(final Long id, final Relationship... fetch) {
return brokeringDao.load(id, fetch);
}
@Override
public Brokering remove(final Brokering brokering, final String remark) throws UnexpectedEntityException {
if (brokering == null || brokering.isTransient() || brokering.getEndDate() != null) {
throw new UnexpectedEntityException();
}
final ChangeBrokerDTO dto = new ChangeBrokerDTO();
dto.setComments(remark);
dto.setMember(brokering.getBrokered());
dto.setNewBroker(null);
return doChangeBroker(dto);
}
@Override
public void removeExpiredBrokerings(final Calendar time) {
final LocalSettings localSettings = settingsService.getLocalSettings();
final TimePeriod brokeringExpirationPeriod = localSettings.getBrokeringExpirationPeriod();
if (brokeringExpirationPeriod == null || brokeringExpirationPeriod.getNumber() <= 0) {
return;
}
final Calendar startDate = brokeringExpirationPeriod.remove(DateHelper.truncate(time));
final BrokeringQuery query = new BrokeringQuery();
query.setResultType(ResultType.ITERATOR);
query.setStatus(BrokeringQuery.Status.ACTIVE);
query.setStartExpirationDate(startDate);
CacheCleaner cleaner = new CacheCleaner(fetchService);
final List<Brokering> expired = search(query);
try {
for (final Brokering brokering : expired) {
// Update the brokering, expiring it
brokering.setEndDate(time);
brokeringDao.update(brokering);
memberNotificationHandler.expiredBrokeringNotification(brokering);
cleaner.clearCache();
}
} finally {
DataIteratorHelper.close(expired);
}
}
@Override
public List<Brokering> search(final BrokeringQuery query) {
return brokeringDao.search(query);
}
public void setBrokerCommissionContractDao(final BrokerCommissionContractDAO brokerCommissionContractDao) {
this.brokerCommissionContractDao = brokerCommissionContractDao;
}
public void setBrokeringCommissionStatusDao(final BrokeringCommissionStatusDAO brokeringCommissionStatusDao) {
this.brokeringCommissionStatusDao = brokeringCommissionStatusDao;
}
public void setBrokeringDao(final BrokeringDAO brokeringDAO) {
brokeringDao = brokeringDAO;
}
public void setCommissionServiceLocal(final CommissionServiceLocal commissionService) {
this.commissionService = commissionService;
}
public void setElementDao(final ElementDAO elementDao) {
this.elementDao = elementDao;
}
public void setElementServiceLocal(final ElementServiceLocal elementService) {
this.elementService = elementService;
}
public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) {
this.memberNotificationHandler = memberNotificationHandler;
}
public void setRemarkDao(final RemarkDAO remarkDao) {
this.remarkDao = remarkDao;
}
public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
public void setTransactionFeeServiceLocal(final TransactionFeeServiceLocal transactionFeeService) {
this.transactionFeeService = transactionFeeService;
}
@Override
public void validate(final Brokering brokering) throws ValidationException {
getBrokeringValidator().validate(brokering);
}
@Override
public void validate(final ChangeBrokerDTO dto) throws ValidationException {
getDtoValidator().validate(dto);
}
private void closeBrokerContractsAndBrokeringStatus(final Brokering brokering) {
// Close active broker commission contracts
final BrokerCommissionContractQuery bccQuery = new BrokerCommissionContractQuery();
bccQuery.setBroker(brokering.getBroker());
bccQuery.setMember(brokering.getBrokered());
bccQuery.setStatus(BrokerCommissionContract.Status.ACTIVE);
final List<BrokerCommissionContract> brokerCommissionContracts = brokerCommissionContractDao.search(bccQuery);
for (final BrokerCommissionContract brokerCommissionContract : brokerCommissionContracts) {
brokerCommissionContract.setStatus(BrokerCommissionContract.Status.CLOSED);
brokerCommissionContractDao.update(brokerCommissionContract);
}
// Close active brokering commission status
final Calendar now = Calendar.getInstance();
final BrokeringCommissionStatusQuery bcsQuery = new BrokeringCommissionStatusQuery();
bcsQuery.setBrokering(brokering);
bcsQuery.setOnlyActive(true);
final List<BrokeringCommissionStatus> brokeringCommissionStatusList = brokeringCommissionStatusDao.search(bcsQuery);
for (final BrokeringCommissionStatus brokeringCommissionStatus : brokeringCommissionStatusList) {
brokeringCommissionStatus.getPeriod().setEnd(now);
brokeringCommissionStatusDao.update(brokeringCommissionStatus);
}
}
/*
* Creates a new brokering commission status for each broker commission and the specified brokering. This method is called by doChangeBroker()
*/
@SuppressWarnings("unchecked")
private void createBrokeringCommissionStatus(final Brokering brokering) {
final Member broker = fetchService.fetch(brokering.getBroker(), Element.Relationships.GROUP);
final BrokerGroup brokerGroup = (BrokerGroup) broker.getGroup();
// Create a brokering commission status for each broker commission applicable to the broker
final TransactionFeeQuery query = new TransactionFeeQuery();
query.setEntityType(BrokerCommission.class);
query.setBrokerGroup(brokerGroup);
query.setReturnDisabled(true);
final List<BrokerCommission> brokerCommissions = (List<BrokerCommission>) transactionFeeService.search(query);
for (final BrokerCommission brokerCommission : brokerCommissions) {
commissionService.createBrokeringCommissionStatus(brokering, brokerCommission);
}
}
private Brokering doChangeBroker(final ChangeBrokerDTO dto) {
validate(dto);
final Calendar now = Calendar.getInstance();
final Member member = fetchService.reload(dto.getMember(), Element.Relationships.USER);
final Brokering oldBrokering = getActiveBrokering(member);
final Member oldBroker = (oldBrokering == null) ? null : oldBrokering.getBroker();
final Member newBroker = fetchService.fetch(dto.getNewBroker(), Member.Relationships.BROKER);
// Check if it's just to suspend commission
final boolean justSuspendCommission = (oldBroker != null && oldBroker.equals(newBroker) && dto.isSuspendCommission());
if (!justSuspendCommission) {
// Check for circular brokerings
if (member.equals(newBroker)) {
throw new ValidationException("brokering.error.circularBrokering");
}
Member current = newBroker;
final Set<Member> visited = new HashSet<Member>();
while (current != null) {
if (visited.contains(current)) {
throw new ValidationException("brokering.error.circularBrokering");
}
visited.add(current);
current = fetchService.fetch(current, Member.Relationships.BROKER).getBroker();
}
// Check if the member is already on the brokering list
if (newBroker != null) {
final BrokeringQuery query = new BrokeringQuery();
query.setBroker(newBroker);
query.setBrokered(member);
query.setStatus(BrokeringQuery.Status.ACTIVE);
if (!search(query).isEmpty()) {
throw new MemberAlreadyInBrokeringsException();
}
}
// Update the brokered member
member.setBroker(newBroker);
elementDao.update(member, false);
// Reindex the element
elementDao.addToIndex(member);
}
// Update the active brokering
if (oldBrokering != null) {
if (!justSuspendCommission) {
oldBrokering.setEndDate(now);
}
brokeringDao.update(oldBrokering, false);
// Close broker commission contracts and brokering commission status
closeBrokerContractsAndBrokeringStatus(oldBrokering);
}
// Build a brokering to return
Brokering brokering;
if (justSuspendCommission) {
// If just suspending commission, the result will be the same brokering
brokering = oldBrokering;
} else if (newBroker != null) {
// Create a new brokering if not just suspending commission
brokering = new Brokering();
brokering.setBroker(newBroker);
brokering.setBrokered(member);
brokering.setStartDate(now);
brokering = brokeringDao.insert(brokering, false);
createBrokeringCommissionStatus(brokering);
} else {
// No new broker - return null
brokering = null;
}
// Create the broker remark
final BrokerRemark remark = new BrokerRemark();
remark.setWriter(LoggedUser.element());
remark.setSubject(member);
remark.setDate(now);
remark.setComments(dto.getComments());
remark.setOldBroker(oldBroker);
remark.setNewBroker(newBroker);
remark.setSuspendCommission(dto.isSuspendCommission());
remarkDao.insert(remark);
return brokering;
}
private Validator getBrokeringValidator() {
final Validator brokeringValidator = new Validator();
brokeringValidator.property("broker").key("member.broker").required().add(new BrokerValidation());
brokeringValidator.property("brokered").key("member.member").required();
brokeringValidator.property("notes").key("brokering.notes").maxLength(1000);
return brokeringValidator;
}
private Validator getDtoValidator() {
final Validator dtoValidator = new Validator();
dtoValidator.property("member").required();
dtoValidator.property("newBroker").key("changeBroker.new").add(new BrokerValidation()).add(new PropertyValidation() {
private static final long serialVersionUID = 580603314020373024L;
@Override
public ValidationError validate(final Object object, final Object name, final Object value) {
final ChangeBrokerDTO dto = (ChangeBrokerDTO) object;
final Member newBroker = (Member) value;
if (dto.isSuspendCommission()) {
// When commission is suspended, there must be a new broker
if (newBroker == null || newBroker.isTransient()) {
return new RequiredError();
}
} else {
// The old and new brokers can be the same only when suspending commission
final Member member = fetchService.fetch(dto.getMember());
if (member != null && newBroker != null && newBroker.equals(member.getBroker())) {
return new InvalidError();
}
}
return null;
}
});
dtoValidator.property("comments").key("remark.comments").required().maxLength(4000);
return dtoValidator;
}
}