/*
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.transactions;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;
import nl.strohalm.cyclos.access.AdminMemberPermission;
import nl.strohalm.cyclos.access.AdminSystemPermission;
import nl.strohalm.cyclos.access.BrokerPermission;
import nl.strohalm.cyclos.access.MemberPermission;
import nl.strohalm.cyclos.access.OperatorPermission;
import nl.strohalm.cyclos.dao.accounts.transactions.TransferAuthorizationDAO;
import nl.strohalm.cyclos.dao.accounts.transactions.TransferDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.accounts.LockedAccountsOnPayments;
import nl.strohalm.cyclos.entities.accounts.transactions.AuthorizationLevel;
import nl.strohalm.cyclos.entities.accounts.transactions.AuthorizationLevel.Authorizer;
import nl.strohalm.cyclos.entities.accounts.transactions.Payment;
import nl.strohalm.cyclos.entities.accounts.transactions.ScheduledPayment;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferAuthorization;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferAuthorizationDTO;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferAuthorizationQuery;
import nl.strohalm.cyclos.entities.accounts.transactions.TransfersAwaitingAuthorizationQuery;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.exceptions.PermissionDeniedException;
import nl.strohalm.cyclos.services.accounts.AccountServiceLocal;
import nl.strohalm.cyclos.services.accounts.rates.RateServiceLocal;
import nl.strohalm.cyclos.services.accounts.rates.RatesToSave;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal;
import nl.strohalm.cyclos.services.transactions.exceptions.AlreadyAuthorizedException;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.TransactionHelper;
import nl.strohalm.cyclos.utils.Transactional;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.lock.LockHandler;
import nl.strohalm.cyclos.utils.lock.LockHandlerFactory;
import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler;
import nl.strohalm.cyclos.utils.validation.RequiredError;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import org.apache.commons.lang.StringUtils;
import org.springframework.transaction.TransactionStatus;
public class TransferAuthorizationServiceImpl implements TransferAuthorizationServiceLocal {
private FetchServiceLocal fetchService;
private ScheduledPaymentServiceLocal scheduledPaymentService;
private TransferDAO transferDao;
private TransferAuthorizationDAO transferAuthorizationDao;
private AccountServiceLocal accountService;
private MemberNotificationHandler memberNotificationHandler;
private PermissionServiceLocal permissionService;
private RateServiceLocal rateService;
private LockHandlerFactory lockHandlerFactory;
private TransactionHelper transactionHelper;
private PaymentServiceLocal paymentService;
@Override
public Transfer authorize(final TransferAuthorizationDTO dto) throws AlreadyAuthorizedException, EntityNotFoundException, UnexpectedEntityException {
return authorize(dto, true);
}
@Override
public Transfer authorize(final TransferAuthorizationDTO dto, final boolean newTransaction) throws AlreadyAuthorizedException, EntityNotFoundException, UnexpectedEntityException {
return authorize(dto, newTransaction, false);
}
@Override
public Transfer authorizeOnInsert(final LockHandler lockHandler, Transfer transfer) {
// Only process when an user is performing a payment
if (LoggedUser.hasUser()) {
transfer = fetchService.fetch(transfer, Transfer.Relationships.PARENT, RelationshipHelper.nested(Transfer.Relationships.NEXT_AUTHORIZATION_LEVEL, AuthorizationLevel.Relationships.ADMIN_GROUPS));
final Transfer parent = transfer.getParent();
final AuthorizationLevel authorizationLevel = transfer.getNextAuthorizationLevel();
final Member fromMember = transfer.isFromSystem() ? null : (Member) transfer.getFromOwner();
// Only process root transfer from members and that require authorization
if (parent == null && authorizationLevel != null) {
boolean authorize = false;
switch (authorizationLevel.getAuthorizer()) {
case BROKER:
// Automatically authorize when logged as the member's broker and the authorization is made by broker, or as administrator
authorize = (LoggedUser.isBroker() && fromMember != null && LoggedUser.element().equals(fromMember.getBroker())) || (LoggedUser.isAdministrator() && authorizationLevel.getAdminGroups().contains(LoggedUser.group()));
break;
case ADMIN:
// Automatically authorize when logged as administrator
authorize = LoggedUser.isAdministrator() && authorizationLevel.getAdminGroups().contains(LoggedUser.group());
break;
}
// Perform the authorization
if (authorize) {
final TransferAuthorizationDTO dto = new TransferAuthorizationDTO();
dto.setTransfer(transfer);
transfer = doAuthorize(lockHandler, false, dto, true);
}
}
}
return transfer;
}
@Override
public boolean canAuthorizeOrDeny(Transfer transfer) {
transfer = fetchService.fetch(transfer, Payment.Relationships.FROM, Payment.Relationships.TO, RelationshipHelper.nested(Transfer.Relationships.NEXT_AUTHORIZATION_LEVEL, AuthorizationLevel.Relationships.ADMIN_GROUPS));
AuthorizationLevel level = transfer.getNextAuthorizationLevel();
if (level == null) {
return false;
}
try {
if (transfer.isFromSystem()) {
if (!transfer.isToSystem() && level.getAuthorizer() == Authorizer.RECEIVER) {
// A payment from system to member, having receiver as authorized
permissionService.permission((Member) transfer.getToOwner())
.member(MemberPermission.PAYMENTS_AUTHORIZE)
.operator(OperatorPermission.PAYMENTS_AUTHORIZE)
.check();
} else {
// Only admins can authorize
permissionService.permission()
.admin(AdminSystemPermission.PAYMENTS_AUTHORIZE)
.check();
}
} else {
// A payment from a member. Check according to the role
switch (level.getAuthorizer()) {
case PAYER:
permissionService.permission((Member) transfer.getFromOwner())
.member(MemberPermission.PAYMENTS_AUTHORIZE)
.operator(OperatorPermission.PAYMENTS_AUTHORIZE)
.check();
break;
case RECEIVER:
permissionService.permission((Member) transfer.getToOwner())
.member(MemberPermission.PAYMENTS_AUTHORIZE)
.operator(OperatorPermission.PAYMENTS_AUTHORIZE)
.check();
break;
case BROKER:
permissionService.permission((Member) transfer.getFromOwner())
.admin(AdminMemberPermission.PAYMENTS_AUTHORIZE)
.broker(BrokerPermission.MEMBER_PAYMENTS_AUTHORIZE)
.check();
break;
case ADMIN:
permissionService.permission((Member) transfer.getFromOwner())
.admin(AdminMemberPermission.PAYMENTS_AUTHORIZE)
.check();
break;
}
}
// Ensure that if the static permission is granted, and this level allows admins, the logged admin is in the allowed groups
if (LoggedUser.isAdministrator() && !level.getAdminGroups().contains(LoggedUser.group())) {
throw new PermissionDeniedException();
}
return true;
} catch (PermissionDeniedException e) {
return false;
}
}
@Override
public boolean canCancel(final Transfer transfer) {
if (transfer.isFromSystem()) {
return permissionService.permission()
.admin(AdminSystemPermission.PAYMENTS_CANCEL)
.hasPermission();
} else {
return permissionService.permission((Member) transfer.getFromOwner())
.admin(AdminMemberPermission.PAYMENTS_CANCEL_AUTHORIZED_AS_MEMBER)
.broker(BrokerPermission.MEMBER_PAYMENTS_CANCEL_AUTHORIZED_AS_MEMBER)
.member(MemberPermission.PAYMENTS_CANCEL_AUTHORIZED)
.operator(OperatorPermission.PAYMENTS_CANCEL_AUTHORIZED)
.hasPermission();
}
}
@Override
public Transfer cancel(final TransferAuthorizationDTO dto) throws EntityNotFoundException, UnexpectedEntityException {
Transfer transfer = fetchService.fetch(dto.getTransfer());
validateAuthorization(transfer);
// User can't cancel a transfer that is part of a scheduled payment
if (transfer.getScheduledPayment() != null) {
throw new UnexpectedEntityException();
}
return transactionHelper.maybeRunInNewTransaction(new Transactional<Transfer>() {
@Override
public Transfer afterCommit(final Transfer result) {
return fetchService.fetch(result);
}
@Override
public Transfer doInTransaction(final TransactionStatus status) {
return doCancel(dto);
}
});
}
@Override
public Transfer deny(final TransferAuthorizationDTO dto) throws EntityNotFoundException, UnexpectedEntityException {
if (StringUtils.isEmpty(dto.getComments())) {
// Deny requires comments
throw new ValidationException("comments", "transferAuthorization.comments", new RequiredError());
}
Transfer transfer = fetchService.fetch(dto.getTransfer());
validateAuthorization(transfer);
return transactionHelper.maybeRunInNewTransaction(new Transactional<Transfer>() {
@Override
public Transfer afterCommit(final Transfer result) {
return fetchService.fetch(result);
}
@Override
public Transfer doInTransaction(final TransactionStatus status) {
return doDeny(dto);
}
});
}
@Override
public boolean hasAlreadyAuthorized(Transfer transfer) {
if (!LoggedUser.hasUser()) {
return false;
}
transfer = fetchService.fetch(transfer, Transfer.Relationships.AUTHORIZATIONS);
Element logged = LoggedUser.element();
for (TransferAuthorization auth : transfer.getAuthorizations()) {
if (logged.equals(auth.getBy())) {
return true;
}
}
return false;
}
@Override
public Collection<TransferAuthorization> load(final Collection<Long> ids, final Relationship... fetch) {
return transferAuthorizationDao.load(ids, fetch);
}
@Override
public TransferAuthorization load(final Long id, final Relationship... fetch) throws EntityNotFoundException {
return transferAuthorizationDao.load(id, fetch);
}
@Override
public List<TransferAuthorization> searchAuthorizations(final TransferAuthorizationQuery query) {
final Element by = fetchService.fetch(query.getBy(), Member.Relationships.BROKER);
if (LoggedUser.isAdministrator()) {
// Administrators search by administration when 'by member' not specified
query.setByAdministration(query.getBy() == null);
} else if (LoggedUser.isBroker()) {
query.setByAdministration(false);
query.setBy(by == null ? LoggedUser.element() : by);
} else {
// Members can only search by themselves
final Member loggedMember = (Member) LoggedUser.accountOwner();
query.setByAdministration(false);
query.setBy(loggedMember);
}
return transferAuthorizationDao.search(query);
}
@Override
public List<Transfer> searchTransfersAwaitingAuthorization(final TransfersAwaitingAuthorizationQuery query) {
query.setAuthorizer(LoggedUser.element());
return transferDao.searchTransfersAwaitingAuthorization(query);
}
public void setAccountServiceLocal(final AccountServiceLocal accountService) {
this.accountService = accountService;
}
public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public void setLockHandlerFactory(final LockHandlerFactory lockHandlerFactory) {
this.lockHandlerFactory = lockHandlerFactory;
}
public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) {
this.memberNotificationHandler = memberNotificationHandler;
}
public void setPaymentServiceLocal(final PaymentServiceLocal paymentService) {
this.paymentService = paymentService;
}
public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) {
this.permissionService = permissionService;
}
public void setRateServiceLocal(final RateServiceLocal rateService) {
this.rateService = rateService;
}
public void setScheduledPaymentServiceLocal(final ScheduledPaymentServiceLocal scheduledPaymentService) {
this.scheduledPaymentService = scheduledPaymentService;
}
public void setTransactionHelper(final TransactionHelper transactionHelper) {
this.transactionHelper = transactionHelper;
}
public void setTransferAuthorizationDao(final TransferAuthorizationDAO transferAuthorizationDao) {
this.transferAuthorizationDao = transferAuthorizationDao;
}
public void setTransferDao(final TransferDAO transferDao) {
this.transferDao = transferDao;
}
private Transfer authorize(final TransferAuthorizationDTO dto, final boolean newTransaction, final boolean automaticallyAuthorize) {
Transfer transfer = fetchService.fetch(dto.getTransfer());
validateAuthorization(transfer);
return transactionHelper.maybeRunInNewTransaction(new Transactional<Transfer>() {
@Override
public Transfer afterCommit(final Transfer result) {
// Ensure the transfer is attached to the current transaction
return fetchService.fetch(result);
}
@Override
public Transfer doInTransaction(final TransactionStatus status) {
return doAuthorize(null, newTransaction, dto, automaticallyAuthorize);
}
}, newTransaction, LockedAccountsOnPayments.ALL);
}
/**
* Create a transfer authorization log
*/
private TransferAuthorization createAuthorization(final Transfer transfer, final TransferAuthorization.Action action, final AuthorizationLevel level, final String comments, final boolean showToMember) {
TransferAuthorization transferAuthorization = new TransferAuthorization();
transferAuthorization.setTransfer(transfer);
transferAuthorization.setLevel(level);
transferAuthorization.setBy(LoggedUser.element());
transferAuthorization.setDate(Calendar.getInstance());
transferAuthorization.setAction(action);
transferAuthorization.setComments(comments);
transferAuthorization.setShowToMember(showToMember);
transferAuthorization = transferAuthorizationDao.insert(transferAuthorization);
return transferAuthorization;
}
private Transfer doAuthorize(final LockHandler lockHandler, final boolean newTransaction, final TransferAuthorizationDTO dto, final boolean automaticallyAuthorize) {
final String comments = dto.getComments();
final boolean showToMember = dto.isShowToMember();
Transfer transfer = fetchService.reload(dto.getTransfer(), Transfer.Relationships.SCHEDULED_PAYMENT, Transfer.Relationships.AUTHORIZATIONS);
// Check if the authorizer has already authorized
final Element logged = LoggedUser.element();
for (final TransferAuthorization authorization : transfer.getAuthorizations()) {
if (logged.equals(authorization.getBy())) {
throw new AlreadyAuthorizedException();
}
}
// Update transfer
final AuthorizationLevel authorizationLevel = transfer.getNextAuthorizationLevel();
final AuthorizationLevel nextAuthorizationLevel = getNextAuthorizationLevel(transfer);
final boolean processed = nextAuthorizationLevel == null;
// Create the transfer authorization object
final TransferAuthorization authorization = createAuthorization(transfer, TransferAuthorization.Action.AUTHORIZE, authorizationLevel, comments, showToMember);
// Update the authorization data
if (lockHandler == null) {
// Invoke the method which creates and releases the locks
transfer = updateAuthorizationData(transfer, nextAuthorizationLevel, processed, authorization, newTransaction);
} else {
// If there is a lockHandler already, it is assumed to be in a transaction already
transfer = doUpdateAuthorizationData(lockHandler, transfer, nextAuthorizationLevel, processed, authorization);
}
transfer.getAuthorizations().add(authorization);
// If the transfer is part of a scheduled payment, update the scheduled payment status
if (transfer.getScheduledPayment() != null) {
scheduledPaymentService.updateScheduledPaymentStatus(transfer.getScheduledPayment());
}
// Notify
memberNotificationHandler.paymentAuthorizedOrDeniedNotification(transfer, !automaticallyAuthorize);
if (processed) {
paymentService.notifyTransferProcessed(transfer);
}
return transfer;
}
private Transfer doCancel(final TransferAuthorizationDTO dto) {
Transfer transfer = fetchService.fetch(dto.getTransfer());
final String comments = dto.getComments();
final AuthorizationLevel authorizationLevel = transfer.getNextAuthorizationLevel();
// Update the transfer
transfer = transferDao.updateAuthorizationData(transfer.getId(), Transfer.Status.CANCELED, null, null, null);
// Create the transfer authorization object
final TransferAuthorization authorization = createAuthorization(transfer, TransferAuthorization.Action.CANCEL, authorizationLevel, comments, true);
// Return the reserved amount
accountService.returnReservation(authorization, transfer);
// Update the child transfers
updateChildTransfers(transfer, authorization);
// Notify
memberNotificationHandler.paymentCancelledNotification(transfer);
return transfer;
}
private Transfer doDeny(final TransferAuthorizationDTO dto) {
Transfer transfer = fetchService.fetch(dto.getTransfer());
final String comments = dto.getComments();
final boolean showToMember = dto.isShowToMember();
// Update transfer
final AuthorizationLevel authorizationLevel = transfer.getNextAuthorizationLevel();
transfer = transferDao.updateAuthorizationData(transfer.getId(), Transfer.Status.DENIED, null, null, null);
// If the transfer is part of a scheduled payment, deny transfers of the payment with status PENDING, SCHEDULED or BLOCKED
if (transfer.getScheduledPayment() != null) {
final ScheduledPayment scheduledPayment = fetchService.fetch(transfer.getScheduledPayment(), ScheduledPayment.Relationships.TRANSFERS);
for (final Transfer currentTransfer : scheduledPayment.getTransfers()) {
final Transfer.Status currentTransferStatus = currentTransfer.getStatus();
if (currentTransferStatus == Transfer.Status.PENDING || currentTransferStatus == Transfer.Status.SCHEDULED || currentTransferStatus == Transfer.Status.BLOCKED) {
transferDao.updateAuthorizationData(currentTransfer.getId(), Transfer.Status.DENIED, null, null, null);
}
}
scheduledPaymentService.updateScheduledPaymentStatus(transfer.getScheduledPayment());
}
// Create the transfer authorization object
final TransferAuthorization authorization = createAuthorization(transfer, TransferAuthorization.Action.DENY, authorizationLevel, comments, showToMember);
// Return the reserved amount
accountService.returnReservation(authorization, transfer);
// Update child transfers
updateChildTransfers(transfer, authorization);
// Notify
memberNotificationHandler.paymentAuthorizedOrDeniedNotification(transfer, true);
return transfer;
}
private Transfer doUpdateAuthorizationData(final LockHandler lockHandler, Transfer transfer, final AuthorizationLevel nextAuthorizationLevel, final boolean processed, final TransferAuthorization authorization) {
if (processed) {
lockHandler.lock(transfer.getFrom(), transfer.getTo());
// apply rates
RatesToSave rates = rateService.applyTransfer(transfer);
/*
* set processDate AFTER applying rates, but before persisting them. This is important, because the transfer itself must not sum up for
* rates or balances when the rates are processed, and it does if processdate is already set. In that case, the transfer's processDate can
* equal the fromRates's date.
*/
Calendar processDate = (rates.getFromRates() == null) ? Calendar.getInstance() : rates.getFromRates().getDate();
transfer.setProcessDate(processDate);
rateService.persist(rates);
// Return the reserved amount
accountService.returnReservation(authorization, transfer);
// Remove any pending closed balances on the destination account. The returnReservation will do this for the source account
accountService.removeClosedBalancesAfter(transfer.getTo(), processDate);
// update the transfer information, and also the rates.
transfer = transferDao.updateAuthorizationData(transfer.getId(), Transfer.Status.PROCESSED, null, transfer.getProcessDate(), rates);
} else {
transfer = transferDao.updateAuthorizationData(transfer.getId(), Transfer.Status.PENDING, nextAuthorizationLevel, null, null);
}
// Update child transfers
for (final Transfer childTransfer : transfer.getChildren()) {
updateChildTransfer(lockHandler, transfer, childTransfer, authorization, processed);
}
return transfer;
}
private AuthorizationLevel getNextAuthorizationLevel(final Transfer transfer) {
final AuthorizationLevel authorizationLevel = transfer.getNextAuthorizationLevel();
final List<AuthorizationLevel> authorizationLevels = new ArrayList<AuthorizationLevel>(transfer.getType().getAuthorizationLevels());
final int index = authorizationLevels.indexOf(authorizationLevel);
if (index < 0 || index == authorizationLevels.size() - 1) {
// This level is the highest
return null;
}
final AuthorizationLevel wouldBeNext = authorizationLevels.get(index + 1);
// Amount is not enough for the next level
if (transfer.getAmount().compareTo(wouldBeNext.getAmount()) < 0) {
return null;
}
return wouldBeNext;
}
private Transfer updateAuthorizationData(final Transfer transfer, final AuthorizationLevel nextAuthorizationLevel, final boolean processed, final TransferAuthorization authorization, final boolean newTransaction) {
LockHandler lockHandler = lockHandlerFactory.getLockHandlerIfLockingAtLeast(LockedAccountsOnPayments.ALL);
try {
return doUpdateAuthorizationData(lockHandler, transfer, nextAuthorizationLevel, processed, authorization);
} finally {
if (lockHandler != null) {
lockHandler.release();
}
}
}
private void updateChildTransfer(final LockHandler lockHandler, final Transfer parent, Transfer child, final TransferAuthorization authorization, final boolean processed) {
child = fetchService.fetch(child, Transfer.Relationships.CHILDREN);
RatesToSave rates = null;
// Update the account status
if (processed) {
lockHandler.lock(child.getFrom(), child.getTo());
// apply rates
rates = rateService.applyTransfer(child);
child.setProcessDate(parent.getProcessDate());
rateService.persist(rates);
// Return the reserved amount
accountService.returnReservation(authorization, child);
// Remove any pending closed balances on the destination account. The returnReservation will do this for the source account
accountService.removeClosedBalancesAfter(child.getTo(), child.getProcessDate());
}
transferDao.updateAuthorizationData(child.getId(), parent.getStatus(), parent.getNextAuthorizationLevel(), parent.getProcessDate(), rates);
// Update child transfers
if (child.getChildren() != null) {
for (final Transfer childTransfer : child.getChildren()) {
updateChildTransfer(lockHandler, child, childTransfer, authorization, processed);
}
}
}
private void updateChildTransfers(final Transfer transfer, final TransferAuthorization authorization) {
LockHandler lockHandler = lockHandlerFactory.getLockHandlerIfLockingAtLeast(LockedAccountsOnPayments.ALL);
try {
for (final Transfer childTransfer : transfer.getChildren()) {
updateChildTransfer(lockHandler, transfer, childTransfer, authorization, true);
}
} finally {
if (lockHandler != null) {
lockHandler.release();
}
}
}
private void validateAuthorization(final Transfer transfer) {
// Can't cancel a nested transfer, and it must be pending authorization
if (!transfer.isRoot() || transfer.getStatus() != Transfer.Status.PENDING) {
throw new UnexpectedEntityException();
}
}
}