/* 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.Calendar; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; 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.ScheduledPaymentDAO; import nl.strohalm.cyclos.dao.accounts.transactions.TransferDAO; import nl.strohalm.cyclos.entities.Relationship; import nl.strohalm.cyclos.entities.accounts.AccountOwner; import nl.strohalm.cyclos.entities.accounts.MemberAccountType; import nl.strohalm.cyclos.entities.accounts.transactions.Payment; import nl.strohalm.cyclos.entities.accounts.transactions.Payment.Status; import nl.strohalm.cyclos.entities.accounts.transactions.ScheduledPayment; import nl.strohalm.cyclos.entities.accounts.transactions.ScheduledPaymentQuery; import nl.strohalm.cyclos.entities.accounts.transactions.Transfer; import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomField; import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException; import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException; import nl.strohalm.cyclos.entities.members.Member; import nl.strohalm.cyclos.entities.settings.LocalSettings; import nl.strohalm.cyclos.exceptions.PermissionDeniedException; import nl.strohalm.cyclos.services.InitializingService; import nl.strohalm.cyclos.services.accounts.AccountServiceLocal; import nl.strohalm.cyclos.services.customization.PaymentCustomFieldService; 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.DateHelper; import nl.strohalm.cyclos.utils.Period; import nl.strohalm.cyclos.utils.RelationshipHelper; import nl.strohalm.cyclos.utils.TransactionHelper; import nl.strohalm.cyclos.utils.access.LoggedUser; import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler; import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData; import nl.strohalm.cyclos.utils.transaction.TransactionCommitListener; import nl.strohalm.cyclos.webservices.model.ScheduledPaymentVO; import nl.strohalm.cyclos.webservices.utils.AccountHelper; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.mutable.MutableBoolean; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; /** * Implementation for scheduled payment service * @author Jefferson Magno */ public class ScheduledPaymentServiceImpl implements ScheduledPaymentServiceLocal, InitializingService { private SettingsServiceLocal settingsService; private FetchServiceLocal fetchService; private ScheduledPaymentDAO scheduledPaymentDao; private TransferDAO transferDao; private PaymentServiceLocal paymentService; private PermissionServiceLocal permissionService; private AccountServiceLocal accountService; private InvoiceServiceLocal invoiceService; private TransferAuthorizationServiceLocal transferAuthorizationService; private AccountHelper accountHelper; private PaymentCustomFieldService paymentCustomFieldService; private MemberNotificationHandler memberNotificationHandler; private TransactionHelper transactionHelper; @Override public ScheduledPayment block(ScheduledPayment scheduledPayment) { scheduledPayment = fetchService.fetch(scheduledPayment, ScheduledPayment.Relationships.TRANSFERS); final AccountOwner owner = LoggedUser.accountOwner(); // Ensure that if the logged user is the from member, the transfer type allows blocking if (owner instanceof Member && owner.equals(scheduledPayment.getFromOwner())) { if (!scheduledPayment.getType().isAllowBlockScheduledPayments()) { throw new PermissionDeniedException(); } } final Status status = scheduledPayment.getStatus(); if (status == Status.PROCESSED || status == Status.BLOCKED || status == Status.CANCELED || status == Status.DENIED) { throw new UnexpectedEntityException(); } for (final Transfer transfer : scheduledPayment.getTransfers()) { final Status transferStatus = transfer.getStatus(); if (transferStatus == Status.SCHEDULED || transferStatus == Status.FAILED) { transferDao.updateStatus(transfer.getId(), Status.BLOCKED); } } return updateScheduledPaymentStatus(scheduledPayment); } @Override public boolean canBlock(final ScheduledPayment scheduledPayment) { // Can only block if there is at least one installment is scheduled boolean hasScheduledTransfer = false; for (Transfer transfer : scheduledPayment.getTransfers()) { if (transfer.getStatus() == Status.SCHEDULED) { hasScheduledTransfer = true; break; } } if (!hasScheduledTransfer) { return false; } return hasBlockPermission(scheduledPayment); } @Override public boolean canCancel(final ScheduledPayment scheduledPayment) { final Status status = scheduledPayment.getStatus(); // Depending on the initial state, it can no longer be cancelled if (status == Status.PROCESSED || status == Status.CANCELED || status == Status.DENIED) { return false; } // If logged as the from member (or his operators), it also depends on a TT setting to allow cancelling scheduled payments Member loggedMember = LoggedUser.member(); if (loggedMember != null) { if (loggedMember.equals(scheduledPayment.getFromOwner())) { final boolean allowUserToCancel = scheduledPayment.getType().isAllowCancelScheduledPayments(); if (!allowUserToCancel) { return false; } } } // When there are pending transfers, only allow canceling the scheduled payment if can also cancel a pending payment Transfer firstPendingTransfer = null; for (Transfer transfer : scheduledPayment.getTransfers()) { if (transfer.getStatus() == Status.PENDING) { firstPendingTransfer = transfer; break; } } // When there is an installment which is pending authorization, only allows cancelling the scheduling if can also cancel the authorization if (firstPendingTransfer != null && !transferAuthorizationService.canCancel(firstPendingTransfer)) { return false; } // Finally, check the permission for cancel scheduled payments if (scheduledPayment.isFromSystem()) { return permissionService.hasPermission(AdminSystemPermission.PAYMENTS_CANCEL_SCHEDULED); } else { Member fromMember = (Member) scheduledPayment.getFromOwner(); return permissionService.permission(fromMember) .admin(AdminMemberPermission.PAYMENTS_CANCEL_SCHEDULED_AS_MEMBER) .broker(BrokerPermission.MEMBER_PAYMENTS_CANCEL_SCHEDULED_AS_MEMBER) .member(MemberPermission.PAYMENTS_CANCEL_SCHEDULED) .operator(OperatorPermission.PAYMENTS_CANCEL_SCHEDULED) .hasPermission(); } } @Override public ScheduledPayment cancel(ScheduledPayment scheduledPayment) { scheduledPayment = fetchService.fetch(scheduledPayment, ScheduledPayment.Relationships.TRANSFERS); final AccountOwner owner = LoggedUser.accountOwner(); // Ensure that if the logged user is the from member, the transfer type allows canceling if (owner instanceof Member && owner.equals(scheduledPayment.getFromOwner())) { if (!scheduledPayment.getType().isAllowCancelScheduledPayments()) { throw new PermissionDeniedException(); } } final Status status = scheduledPayment.getStatus(); if (status == Status.PROCESSED || status == Status.CANCELED || status == Status.DENIED) { throw new UnexpectedEntityException(); } for (final Transfer transfer : scheduledPayment.getTransfers()) { final Status transferStatus = transfer.getStatus(); if (scheduledPayment.isReserveAmount() && (transferStatus == Status.SCHEDULED || transferStatus == Status.BLOCKED) || transferStatus == Status.PENDING) { // Ensure the amount is liberated accountService.returnReservationForInstallment(transfer); } if (transferStatus == Status.PENDING || transferStatus == Status.SCHEDULED || transferStatus == Status.FAILED || transferStatus == Status.BLOCKED) { transferDao.updateStatus(transfer.getId(), Status.CANCELED); } } scheduledPayment.setStatus(Status.CANCELED); return scheduledPaymentDao.update(scheduledPayment); } @Override public void cancelScheduledPaymentsAndNotify(final Member member, final Collection<MemberAccountType> accountTypes) { List<ScheduledPayment> scheduledPayments = scheduledPaymentDao.getUnrelatedPendingPayments(member, accountTypes); final Set<Member> membersToNotify = new HashSet<Member>(); final Set<MemberAccountType> removedAccounts = new HashSet<MemberAccountType>(); // this flag is true if the member was not removed and at least on of the incoming payment should notify the receiver (in this case the // member) // or is from an invoice or there is at least one outgoing payment (the member is the payer) final MutableBoolean notifyMember = new MutableBoolean(false); for (ScheduledPayment scheduledPayment : scheduledPayments) { cancel(scheduledPayment); boolean incoming = member.equals(scheduledPayment.getToOwner()); if (incoming) { // member is the receiver then notify the payer if (scheduledPayment.getFromOwner() instanceof Member) { // there is not notification for incoming system payments Member payer = (Member) scheduledPayment.getFromOwner(); membersToNotify.add(payer); if (!member.getGroup().isRemoved() && !notifyMember.booleanValue()) { notifyMember.setValue(scheduledPayment.isShowToReceiver() || isFromInvoice(scheduledPayment)); } removedAccounts.add((MemberAccountType) scheduledPayment.getTo().getType()); } } else { // outgoing (member is the payer) if (scheduledPayment.getToOwner() instanceof Member) { // there is not notification for outgoing system payments if (scheduledPayment.isShowToReceiver() || isFromInvoice(scheduledPayment)) { Member receiver = (Member) scheduledPayment.getToOwner(); membersToNotify.add(receiver); } removedAccounts.add((MemberAccountType) scheduledPayment.getFrom().getType()); } if (!member.getGroup().isRemoved()) { notifyMember.setValue(true); } } } if (!scheduledPayments.isEmpty()) { CurrentTransactionData.addTransactionCommitListener(new TransactionCommitListener() { @Override public void onTransactionCommit() { transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { memberNotificationHandler.scheduledPaymentsCancelledNotification(member, notifyMember.booleanValue(), membersToNotify, removedAccounts); } }); } }); } } @Override public boolean canPayNow(final Transfer transfer) { // Check, when part of scheduled payment, if can pay now boolean canPayNow = false; final ScheduledPayment scheduledPayment = transfer.getScheduledPayment(); if (scheduledPayment != null) { canPayNow = transfer.getStatus().canPayNow(); // Check if there's an expired payment if (canPayNow) { final List<Transfer> transfers = scheduledPayment.getTransfers(); final Calendar now = DateHelper.truncate(Calendar.getInstance()); for (final Transfer current : transfers) { // When there's an expired payment, only that one is payable if (current.getStatus().canPayNow() && current.getDate().before(now)) { canPayNow = transfer.equals(current); break; } } } // Check the allowed transfer types if (canPayNow) { canPayNow = paymentService.canMakePayment(transfer.getFromOwner(), transfer.getToOwner(), transfer.getType()); } // Check static permissions if (canPayNow) { if (LoggedUser.isOperator()) { canPayNow = permissionService.permission().operator(OperatorPermission.PAYMENTS_PAYMENT_TO_MEMBER) .hasPermission(); } } } return canPayNow; } @Override public boolean canUnblock(final ScheduledPayment scheduledPayment) { Transfer firstBlockedTransfer = null; for (Transfer transfer : scheduledPayment.getTransfers()) { if (transfer.getStatus() == Status.BLOCKED) { firstBlockedTransfer = transfer; break; } } if (firstBlockedTransfer == null) { // No blocked transfer - cannot unblock return false; } final Calendar now = Calendar.getInstance(); final Calendar date = firstBlockedTransfer.getDate(); if (now.after(date)) { return false; } return hasBlockPermission(scheduledPayment); } @Override public ScheduledPaymentVO getScheduledPaymentVO(final Long scheduledPaymentId) { ScheduledPayment scheduledPayment = load(scheduledPaymentId); List<PaymentCustomField> fields = paymentCustomFieldService.list(scheduledPayment.getType(), false); return accountHelper.toVO(LoggedUser.member(), scheduledPayment, fields); } @Override public void initializeService() { recoverScheduledPayments(); } @Override public ScheduledPayment load(final Long id, final Relationship... fetch) { return scheduledPaymentDao.<ScheduledPayment> load(id, fetch); } @Override public Transfer processTransfer(Transfer transfer) { transfer = fetchService.fetch(transfer, RelationshipHelper.nested(Transfer.Relationships.SCHEDULED_PAYMENT, ScheduledPayment.Relationships.TRANSFERS)); final ScheduledPayment scheduledPayment = transfer.getScheduledPayment(); if (scheduledPayment == null) { throw new UnexpectedEntityException(); } final Status scheduledPaymentStatus = scheduledPayment.getStatus(); if (scheduledPaymentStatus == Status.PROCESSED || scheduledPaymentStatus == Status.PENDING || scheduledPaymentStatus == Status.CANCELED || scheduledPaymentStatus == Status.DENIED) { throw new UnexpectedEntityException(); } final Calendar today = DateHelper.truncate(Calendar.getInstance()); final Transfer firstOpenTransfer = scheduledPayment.getFirstOpenTransfer(); if (firstOpenTransfer.getDate().before(today) && !firstOpenTransfer.equals(transfer)) { throw new UnexpectedEntityException(); } final Status firstOpenTransferStatus = firstOpenTransfer.getStatus(); if (firstOpenTransferStatus == Status.PROCESSED || firstOpenTransferStatus == Status.CANCELED || firstOpenTransferStatus == Status.DENIED || firstOpenTransferStatus == Status.PENDING) { throw new UnexpectedEntityException(); } return paymentService.processScheduled(transfer); } @Override public List<ScheduledPayment> search(final ScheduledPaymentQuery query) { return scheduledPaymentDao.search(query); } public void setAccountHelper(final AccountHelper accountHelper) { this.accountHelper = accountHelper; } public void setAccountServiceLocal(final AccountServiceLocal accountService) { this.accountService = accountService; } public void setFetchServiceLocal(final FetchServiceLocal fetchService) { this.fetchService = fetchService; } public void setInvoiceServiceLocal(final InvoiceServiceLocal invoiceService) { this.invoiceService = invoiceService; } public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) { this.memberNotificationHandler = memberNotificationHandler; } public void setPaymentCustomFieldService(final PaymentCustomFieldService paymentCustomFieldService) { this.paymentCustomFieldService = paymentCustomFieldService; } public void setPaymentServiceLocal(final PaymentServiceLocal paymentService) { this.paymentService = paymentService; } public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) { this.permissionService = permissionService; } public void setScheduledPaymentDao(final ScheduledPaymentDAO scheduledPaymentDao) { this.scheduledPaymentDao = scheduledPaymentDao; } public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) { this.settingsService = settingsService; } public void setTransactionHelper(final TransactionHelper transactionHelper) { this.transactionHelper = transactionHelper; } public void setTransferAuthorizationServiceLocal(final TransferAuthorizationServiceLocal transferAuthorizationService) { this.transferAuthorizationService = transferAuthorizationService; } public void setTransferDao(final TransferDAO transferDao) { this.transferDao = transferDao; } @Override public ScheduledPayment unblock(ScheduledPayment scheduledPayment) { scheduledPayment = fetchService.fetch(scheduledPayment, ScheduledPayment.Relationships.TRANSFERS); final Calendar today = DateHelper.truncate(Calendar.getInstance()); for (final Transfer transfer : scheduledPayment.getTransfers()) { final Status transferStatus = transfer.getStatus(); if (transferStatus == Status.BLOCKED) { if (transfer.getDate().before(today)) { throw new UnexpectedEntityException(); } else { transferDao.updateStatus(transfer.getId(), Status.SCHEDULED); } } } return updateScheduledPaymentStatus(scheduledPayment); } @Override public ScheduledPayment updateScheduledPaymentStatus(final ScheduledPayment scheduledPayment) { final Transfer firstOpenTransfer = scheduledPayment.getFirstOpenTransfer(); Payment.Status newStatus = null; if (firstOpenTransfer == null) { final List<Transfer> transfers = scheduledPayment.getTransfers(); if (CollectionUtils.isEmpty(transfers)) { newStatus = Payment.Status.PROCESSED; } else { newStatus = transfers.get(transfers.size() - 1).getStatus(); } } else { newStatus = firstOpenTransfer.getStatus(); } scheduledPayment.setStatus(newStatus); if (newStatus == Payment.Status.PROCESSED) { scheduledPayment.setProcessDate(Calendar.getInstance()); } return scheduledPaymentDao.update(scheduledPayment); } private boolean hasBlockPermission(final ScheduledPayment scheduledPayment) { if (scheduledPayment.isFromSystem()) { return permissionService.hasPermission(AdminSystemPermission.PAYMENTS_BLOCK_SCHEDULED); } else { Member fromMember = (Member) scheduledPayment.getFromOwner(); // When logged as the member, it doesn't suffice to have the permission, but there is a TT setting as well Member loggedMember = LoggedUser.member(); if (fromMember.equals(loggedMember)) { if (loggedMember.equals(scheduledPayment.getFromOwner())) { final boolean allowUserToBlock = scheduledPayment.getType().isAllowBlockScheduledPayments(); if (!allowUserToBlock) { return false; } } } // Check the permission itself return permissionService.permission(fromMember) .admin(AdminMemberPermission.PAYMENTS_BLOCK_SCHEDULED_AS_MEMBER) .broker(BrokerPermission.MEMBER_PAYMENTS_BLOCK_SCHEDULED_AS_MEMBER) .member(MemberPermission.PAYMENTS_BLOCK_SCHEDULED) .operator(OperatorPermission.PAYMENTS_BLOCK_SCHEDULED) .hasPermission(); } } /** * @return true if the scheduled payment was generated from an invoice */ private boolean isFromInvoice(final ScheduledPayment scheduledPayment) { try { invoiceService.loadByPayment(scheduledPayment); return true; } catch (final EntityNotFoundException e) { return false; } } private void recoverScheduledPayments() { // Find whether we are executing less than one hour before the scheduled tasks run. // If yes, consider today as deadline. Otherwise, today the scheduled task will yet run, // so, consider yesterday as deadline. final LocalSettings localSettings = settingsService.getLocalSettings(); final Calendar now = Calendar.getInstance(); final Calendar limit = DateHelper.truncate(now); limit.set(Calendar.HOUR_OF_DAY, localSettings.getSchedulingHour()); limit.add(Calendar.HOUR_OF_DAY, -1); limit.set(Calendar.MINUTE, localSettings.getSchedulingMinute()); Calendar time; if (now.before(limit)) { time = DateHelper.truncatePreviosDay(now); } else { time = DateHelper.truncate(now); } paymentService.processScheduled(Period.endingAt(time)); } }