/*
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.accounts.guarantees;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import nl.strohalm.cyclos.access.AdminMemberPermission;
import nl.strohalm.cyclos.access.MemberPermission;
import nl.strohalm.cyclos.access.OperatorPermission;
import nl.strohalm.cyclos.access.Permission;
import nl.strohalm.cyclos.dao.accounts.guarantees.GuaranteeDAO;
import nl.strohalm.cyclos.dao.accounts.guarantees.GuaranteeLogDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.accounts.AccountOwner;
import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner;
import nl.strohalm.cyclos.entities.accounts.guarantees.Certification;
import nl.strohalm.cyclos.entities.accounts.guarantees.Guarantee;
import nl.strohalm.cyclos.entities.accounts.guarantees.Guarantee.Status;
import nl.strohalm.cyclos.entities.accounts.guarantees.GuaranteeLog;
import nl.strohalm.cyclos.entities.accounts.guarantees.GuaranteeQuery;
import nl.strohalm.cyclos.entities.accounts.guarantees.GuaranteeType;
import nl.strohalm.cyclos.entities.accounts.guarantees.GuaranteeType.FeeType;
import nl.strohalm.cyclos.entities.accounts.guarantees.PaymentObligation;
import nl.strohalm.cyclos.entities.accounts.guarantees.PaymentObligationLog;
import nl.strohalm.cyclos.entities.accounts.loans.Loan;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferType;
import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomField;
import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomFieldValue;
import nl.strohalm.cyclos.entities.groups.Group;
import nl.strohalm.cyclos.entities.groups.GroupQuery;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.members.Element;
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.guarantees.exceptions.ActiveCertificationNotFoundException;
import nl.strohalm.cyclos.services.accounts.guarantees.exceptions.CertificationAmountExceededException;
import nl.strohalm.cyclos.services.accounts.guarantees.exceptions.CertificationValidityExceededException;
import nl.strohalm.cyclos.services.accounts.guarantees.exceptions.GuaranteeStatusChangeException;
import nl.strohalm.cyclos.services.customization.PaymentCustomFieldServiceLocal;
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.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.services.transactions.GrantSinglePaymentLoanDTO;
import nl.strohalm.cyclos.services.transactions.LoanServiceLocal;
import nl.strohalm.cyclos.services.transactions.PaymentServiceLocal;
import nl.strohalm.cyclos.services.transactions.TransactionContext;
import nl.strohalm.cyclos.services.transactions.TransferDTO;
import nl.strohalm.cyclos.utils.CustomFieldHelper;
import nl.strohalm.cyclos.utils.CustomFieldsContainer;
import nl.strohalm.cyclos.utils.DateHelper;
import nl.strohalm.cyclos.utils.MessageProcessingHelper;
import nl.strohalm.cyclos.utils.MessageResolver;
import nl.strohalm.cyclos.utils.Period;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.TransactionHelper;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.conversion.NumberConverter;
import nl.strohalm.cyclos.utils.guarantees.GuaranteesHelper;
import nl.strohalm.cyclos.utils.notifications.AdminNotificationHandler;
import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler;
import nl.strohalm.cyclos.utils.query.PageParameters;
import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType;
import nl.strohalm.cyclos.utils.validation.DelegatingValidator;
import nl.strohalm.cyclos.utils.validation.GeneralValidation;
import nl.strohalm.cyclos.utils.validation.PeriodValidation;
import nl.strohalm.cyclos.utils.validation.PeriodValidation.ValidationType;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.Validator;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.functors.AndPredicate;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
public class GuaranteeServiceImpl implements GuaranteeServiceLocal, InitializingService, ApplicationContextAware {
/**
* Validation to avoid register/accept a guarantee with fees' amount greater than guarantee's amount
* @author ameyer
*/
private class GuaranteeFeeValidation implements GeneralValidation {
private static final long serialVersionUID = 840449718151754491L;
@Override
public ValidationError validate(final Object object) {
final Guarantee guarantee = (Guarantee) object;
BigDecimal fees = null;
fees = guarantee.getCreditFee() != null ? guarantee.getCreditFee() : new BigDecimal(0);
fees = guarantee.getIssueFee() != null ? fees.add(guarantee.getIssueFee()) : fees;
if (fees.compareTo(guarantee.getAmount()) == 1) {
return new ValidationError("guarantee.error.invalidGuarantee");
}
return null;
}
}
private GuaranteeDAO guaranteeDao;
private GuaranteeLogDAO guaranteeLogDao;
private PermissionServiceLocal permissionService;
private GuaranteeTypeServiceLocal guaranteeTypeService;
private GroupServiceLocal groupService;
private PaymentObligationServiceLocal paymentObligationService;
private CertificationServiceLocal certificationService;
private FetchServiceLocal fetchService;
private LoanServiceLocal loanService;
private PaymentServiceLocal paymentService;
private SettingsServiceLocal settingsService;
private PaymentCustomFieldServiceLocal paymentCustomFieldService;
private ApplicationContext applicationContext;
private MessageResolver messageResolver = new MessageResolver.NoOpMessageResolver();
private MemberNotificationHandler memberNotificationHandler;
private AdminNotificationHandler adminNotificationHandler;
private TransactionHelper transactionHelper;
private CustomFieldHelper customFieldHelper;
@Override
public Guarantee acceptGuarantee(Guarantee guarantee, final boolean automaticLoanAuthorization) {
validate(guarantee, LoggedUser.isAdministrator() ? automaticLoanAuthorization : false);
if (LoggedUser.isAdministrator()) {
guarantee = doChangeStatus(guarantee, Guarantee.Status.ACCEPTED, automaticLoanAuthorization);
memberNotificationHandler.guaranteeAcceptedNotification(guarantee);
} else {
Status currentStatus = load(guarantee.getId()).getStatus();
guarantee = doChangeStatus(guarantee, Guarantee.Status.ACCEPTED, false);
memberNotificationHandler.guaranteeStatusChangedNotification(guarantee, currentStatus);
adminNotificationHandler.notifyPendingGuarantee(guarantee);
}
return guarantee;
}
@Override
public BigDecimal calculateFee(final GuaranteeFeeCalculationDTO dto) {
return GuaranteesHelper.calculateFee(dto.getValidity(), dto.getAmount(), dto.getFeeSpec());
}
@Override
public boolean canChangeStatus(final Guarantee guarantee, final Status newStatus) {
switch (newStatus) {
case ACCEPTED:
case REJECTED:
if (isInSomeStatus(guarantee, Status.PENDING_ADMIN)) {
Permission permission = Guarantee.Status.ACCEPTED == newStatus ? AdminMemberPermission.GUARANTEES_ACCEPT_GUARANTEES_AS_MEMBER : AdminMemberPermission.GUARANTEES_CANCEL_GUARANTEES_AS_MEMBER;
final boolean hasPermission = permissionService.hasPermission(permission);
return hasPermission;
} else if (isInSomeStatus(guarantee, Status.PENDING_ISSUER)) {
return isIssuer() && guarantee.getIssuer().equals(LoggedUser.accountOwner());
} else {
return false;
}
case CANCELLED:
final boolean hasPermission = permissionService.hasPermission(AdminMemberPermission.GUARANTEES_CANCEL_GUARANTEES_AS_MEMBER);
return hasPermission && isInSomeStatus(guarantee, Status.ACCEPTED, Status.PENDING_ADMIN, Status.PENDING_ISSUER) && guarantee.getLoan() == null;
default:
throw new GuaranteeStatusChangeException(newStatus, "Can't change guarantee's status, unsupported target status: " + newStatus);
}
}
/**
* Only an administrator can delete a just registered (pending by administrator) guarantee registered by himself
* @param guarantee
*/
@Override
public boolean canRemoveGuarantee(Guarantee guarantee) {
guarantee = fetchService.fetch(guarantee, Guarantee.Relationships.LOGS, Guarantee.Relationships.GUARANTEE_TYPE);
final boolean currentStatusIsPendingByAdmin = guarantee.getStatus() == Status.PENDING_ADMIN;
final GuaranteeLog log = guarantee.getLogs().iterator().next();
final boolean isOnlyPendingByAdmin = guarantee.getLogs().size() == 1 && log.getStatus() == Status.PENDING_ADMIN;
return LoggedUser.isAdministrator() && currentStatusIsPendingByAdmin && isOnlyPendingByAdmin && log.getBy().equals(LoggedUser.element()) && guarantee.getGuaranteeType().getModel() != GuaranteeType.Model.WITH_PAYMENT_OBLIGATION;
}
@Override
public Guarantee changeStatus(final Long guaranteeId, final Status newStatus) {
Guarantee current = load(guaranteeId);
Status currentStatus = current.getStatus();
current = doChangeStatus(current, newStatus, false);
switch (newStatus) {
case CANCELLED:
memberNotificationHandler.guaranteeCancelledNotification(current);
break;
case REJECTED:
memberNotificationHandler.guaranteeDeniedNotification(current);
break;
default:
memberNotificationHandler.guaranteeStatusChangedNotification(current, currentStatus);
adminNotificationHandler.notifyPendingGuarantee(current);
}
return current;
}
@Override
public Collection<? extends MemberGroup> getBuyers() {
if (LoggedUser.isAdministrator()) {
return filterBuyers();
} else if (isIssuer()) {
final MemberGroup group = fetchService.fetch((MemberGroup) ((Member) LoggedUser.accountOwner()).getGroup(), MemberGroup.Relationships.CAN_ISSUE_CERTIFICATION_TO_GROUPS);
return group.getCanIssueCertificationToGroups();
} else { // is a seller
return guaranteeDao.getBuyers(((Member) LoggedUser.accountOwner()).getGroup());
}
}
@Override
public List<Guarantee> getGuarantees(final Certification certification, final PageParameters pageParameters, final List<Status> statusList) {
final GuaranteeQuery guaranteeQuery = new GuaranteeQuery();
guaranteeQuery.setResultType(ResultType.PAGE);
guaranteeQuery.setPageParameters(pageParameters);
guaranteeQuery.setCertification(certification);
guaranteeQuery.setStatusList(statusList);
guaranteeQuery.fetch(RelationshipHelper.nested(Guarantee.Relationships.LOAN, Loan.Relationships.PAYMENTS));
return guaranteeDao.search(guaranteeQuery);
}
@Override
public Collection<? extends MemberGroup> getIssuers() {
return filterIssuers();
}
@Override
public Collection<? extends MemberGroup> getIssuers(final GuaranteeType guaranteeType) {
final Collection<? extends Group> groups = guaranteeDao.getIssuers(guaranteeType);
// we must filter the list because it might contains System or removed (issuers) groups
return filterMemberGroups(null, groups);
}
public MessageResolver getMessageResolver(final MessageResolver messageResolver) {
return messageResolver;
}
@Override
public Collection<GuaranteeType.Model> getRelatedGuaranteeModels() {
return guaranteeDao.getRelatedGuaranteeModels((Member) LoggedUser.accountOwner());
}
@Override
public Collection<? extends MemberGroup> getSellers() {
if (LoggedUser.isAdministrator()) {
return filterSellers();
} else {
if (isBuyer()) {
final MemberGroup group = fetchService.fetch((MemberGroup) ((Member) LoggedUser.accountOwner()).getGroup(), MemberGroup.Relationships.CAN_BUY_WITH_PAYMENT_OBLIGATIONS_FROM_GROUPS);
return group.getCanBuyWithPaymentObligationsFromGroups();
} else { // is an issuer
return guaranteeDao.getSellers(((Member) LoggedUser.accountOwner()).getGroup());
}
}
}
@Override
public List<Guarantee> guaranteesToProcess(Calendar time) {
time = DateHelper.truncate(time);
final GuaranteeQuery query = new GuaranteeQuery();
query.setResultType(ResultType.ITERATOR);
final Set<Relationship> fetch = new HashSet<Relationship>();
fetch.add(Guarantee.Relationships.GUARANTEE_TYPE);
fetch.add(Guarantee.Relationships.LOGS);
query.setFetch(fetch);
query.setStatusList(Arrays.asList(Guarantee.Status.PENDING_ADMIN, Guarantee.Status.PENDING_ISSUER));
final List<Guarantee> result = new ArrayList<Guarantee>();
final List<Guarantee> guarantees = guaranteeDao.search(query);
for (final Guarantee guarantee : guarantees) {
final TimePeriod period = guarantee.getGuaranteeType().getPendingGuaranteeExpiration();
final Calendar lowerBound = period.remove(time);
final Calendar registrationDate = DateHelper.truncate(guarantee.getRegistrationDate());
if (registrationDate.before(lowerBound)) {
result.add(guarantee);
}
}
return result;
}
@Override
public void initializeService() {
processGuaranteeLoans(Calendar.getInstance());
// We need a proxy here in order to run AOP
GuaranteeServiceLocal proxy = applicationContext.getBean(GuaranteeServiceLocal.class);
final Calendar time = Calendar.getInstance();
final List<Guarantee> guarantees = guaranteesToProcess(time);
for (final Guarantee guarantee : guarantees) {
proxy.processGuarantee(guarantee, time);
}
}
@Override
public boolean isBuyer() {
return permissionService.permission()
.member(MemberPermission.GUARANTEES_BUY_WITH_PAYMENT_OBLIGATIONS)
.operator(OperatorPermission.GUARANTEES_BUY_WITH_PAYMENT_OBLIGATIONS)
.hasPermission();
}
@Override
public boolean isIssuer() {
return permissionService.permission()
.member(MemberPermission.GUARANTEES_ISSUE_GUARANTEES)
.operator(OperatorPermission.GUARANTEES_ISSUE_GUARANTEES)
.hasPermission();
}
@Override
public boolean isSeller() {
return permissionService.permission()
.member(MemberPermission.GUARANTEES_SELL_WITH_PAYMENT_OBLIGATIONS)
.operator(OperatorPermission.GUARANTEES_SELL_WITH_PAYMENT_OBLIGATIONS)
.hasPermission();
}
@Override
public Guarantee load(final Long id, final Relationship... fetch) {
Guarantee guarantee = guaranteeDao.load(id, fetch);
guarantee = fetchService.fetch(guarantee, Guarantee.Relationships.GUARANTEE_TYPE, Guarantee.Relationships.ISSUER, RelationshipHelper.nested(Guarantee.Relationships.BUYER, Element.Relationships.GROUP), RelationshipHelper.nested(Guarantee.Relationships.SELLER, Element.Relationships.GROUP));
return guarantee;
}
@Override
public Guarantee loadFromTransfer(final Transfer transfer) {
Transfer root = transfer.getRootTransfer();
return guaranteeDao.loadFromTransfer(root);
}
@Override
public Guarantee processGuarantee(final Guarantee guarantee, final Calendar time) {
guarantee.setStatus(Guarantee.Status.WITHOUT_ACTION);
final GuaranteeLog log = guarantee.changeStatus(Guarantee.Status.WITHOUT_ACTION, null);
saveLog(log);
save(guarantee, true);
return guarantee;
}
/**
* Generates a new loan for each guarantee if its status is ACCEPTED and the begin period is before or equals to the time parameter
* @param time the times used as the current time
*/
@Override
public void processGuaranteeLoans(final Calendar time) {
final GuaranteeQuery query = new GuaranteeQuery();
query.fetch(Guarantee.Relationships.GUARANTEE_TYPE, Guarantee.Relationships.SELLER, Guarantee.Relationships.BUYER);
query.setStatusList(Collections.singletonList(Guarantee.Status.ACCEPTED));
query.setStartIn(Period.endingAt(time));
query.setLoanFilter(GuaranteeQuery.LoanFilter.WITHOUT_LOAN);
final List<Guarantee> guarantees = guaranteeDao.search(query);
for (final Guarantee guarantee : guarantees) {
grantLoan(time, guarantee, false);
save(guarantee, true);
}
}
@Override
public Guarantee registerGuarantee(Guarantee guarantee) {
validate(guarantee, false);
guarantee = save(guarantee, true);
grantLoan(Calendar.getInstance(), guarantee, false);
memberNotificationHandler.guaranteePendingIssuerNotification(guarantee);
adminNotificationHandler.notifyPendingGuarantee(guarantee);
return guarantee;
}
@Override
public int remove(final Long guaranteeId) {
return guaranteeDao.delete(guaranteeId);
}
@Override
public Guarantee requestGuarantee(final PaymentObligationPackDTO pack) {
final Long[] poIds = pack.getPaymentObligations();
// verifies the pack validity
if (pack.getIssuer() == null) {
throw new IllegalArgumentException("Invalid guarantee request: Issuer is null");
} else if (poIds == null || poIds.length == 0) {
throw new IllegalArgumentException("Invalid guarantee request: payment obligations pack is empty");
}
// take the first payment obligation to get the currency and buyer (all belong to the same buyer and have the same currency)
final PaymentObligation firstPaymentObligation = paymentObligationService.load(poIds[0]);
final Member buyer = firstPaymentObligation.getBuyer();
final Member seller = firstPaymentObligation.getSeller();
final Member issuer = pack.getIssuer();
final AccountOwner accOwner = LoggedUser.accountOwner();
if (!accOwner.equals(seller)) {
throw new PermissionDeniedException();
}
final Certification certification = certificationService.getActiveCertification(firstPaymentObligation.getCurrency(), buyer, issuer);
// verifies if there is an active certification
if (certification == null) {
throw new ActiveCertificationNotFoundException(buyer, issuer, firstPaymentObligation.getCurrency());
}
// calculates the guarantee's amount and expirationDate
BigDecimal amount = firstPaymentObligation.getAmount();
final ArrayList<PaymentObligation> paymentObligations = new ArrayList<PaymentObligation>();
paymentObligations.add(firstPaymentObligation);
Calendar lastExpirationdate = firstPaymentObligation.getExpirationDate();
for (int i = 1; i < poIds.length; i++) {
final PaymentObligation po = paymentObligationService.load(poIds[i]);
if (!accOwner.equals(po.getSeller())) {
throw new PermissionDeniedException();
}
amount = amount.add(po.getAmount());
if (po.getExpirationDate().after(lastExpirationdate)) {
lastExpirationdate = po.getExpirationDate();
}
paymentObligations.add(po);
}
// verify that the certificatin's amount is not exceeded
final BigDecimal usedCertificationAmount = certificationService.getUsedAmount(certification, true);
final BigDecimal remainingAmount = certification.getAmount().subtract(usedCertificationAmount);
if (amount.compareTo(remainingAmount) > 0) {
throw new CertificationAmountExceededException(certification, remainingAmount, amount);
}
// verify that the certificatin's validity is not exceeded
if (lastExpirationdate.after(certification.getValidity().getEnd())) {
throw new CertificationValidityExceededException(certification);
}
final GuaranteeType guaranteeType = certification.getGuaranteeType();
Guarantee guarantee = new Guarantee();
guarantee.setBuyer(buyer);
guarantee.setSeller(seller);
guarantee.setIssuer(pack.getIssuer());
guarantee.setCertification(certification);
guarantee.setGuaranteeType(guaranteeType);
guarantee.setAmount(amount);
guarantee.setValidity(new Period(null, lastExpirationdate));
guarantee.setPaymentObligations(paymentObligations);
guarantee.setCreditFeeSpec((GuaranteeFeeVO) guaranteeType.getCreditFee().clone());
guarantee.setIssueFeeSpec((GuaranteeFeeVO) guaranteeType.getIssueFee().clone());
guarantee = save(guarantee, false);
for (int i = 0; i < poIds.length; i++) {
final PaymentObligation po = paymentObligations.get(i);
po.setGuarantee(guarantee);
paymentObligationService.changeStatus(po.getId(), PaymentObligation.Status.ACCEPTED);
}
memberNotificationHandler.guaranteePendingIssuerNotification(guarantee);
return guarantee;
}
public GuaranteeLog saveLog(final GuaranteeLog guaranteeLog) {
if (guaranteeLog.isTransient()) {
return guaranteeLogDao.insert(guaranteeLog);
} else {
return guaranteeLogDao.update(guaranteeLog);
}
}
@Override
public List<Guarantee> search(final GuaranteeQuery queryParameters) {
return guaranteeDao.search(queryParameters);
}
public void setAdminNotificationHandler(final AdminNotificationHandler adminNotificationHandler) {
this.adminNotificationHandler = adminNotificationHandler;
}
@Override
public void setApplicationContext(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public void setCertificationServiceLocal(final CertificationServiceLocal certificationService) {
this.certificationService = certificationService;
}
public void setCustomFieldHelper(final CustomFieldHelper customFieldHelper) {
this.customFieldHelper = customFieldHelper;
}
public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public void setGroupServiceLocal(final GroupServiceLocal groupService) {
this.groupService = groupService;
}
public void setGuaranteeDao(final GuaranteeDAO guaranteeDao) {
this.guaranteeDao = guaranteeDao;
}
public void setGuaranteeLogDao(final GuaranteeLogDAO guaranteeLogDao) {
this.guaranteeLogDao = guaranteeLogDao;
}
public void setGuaranteeTypeServiceLocal(final GuaranteeTypeServiceLocal guaranteeTypeService) {
this.guaranteeTypeService = guaranteeTypeService;
}
public void setLoanServiceLocal(final LoanServiceLocal loanService) {
this.loanService = loanService;
}
public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) {
this.memberNotificationHandler = memberNotificationHandler;
}
public void setMessageResolver(final MessageResolver messageResolver) {
this.messageResolver = messageResolver;
}
public void setPaymentCustomFieldServiceLocal(final PaymentCustomFieldServiceLocal paymentCustomFieldService) {
this.paymentCustomFieldService = paymentCustomFieldService;
}
public void setPaymentObligationServiceLocal(final PaymentObligationServiceLocal paymentObligationService) {
this.paymentObligationService = paymentObligationService;
}
public void setPaymentServiceLocal(final PaymentServiceLocal paymentService) {
this.paymentService = paymentService;
}
public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) {
this.permissionService = permissionService;
}
public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
public void setTransactionHelper(final TransactionHelper transactionHelper) {
this.transactionHelper = transactionHelper;
}
@Override
public void validate(final Guarantee guarantee, final boolean isAuthorization) {
final Validator validator = isAuthorization ? getValidatorForAuthorization(guarantee) : getValidator(guarantee);
validator.validate(guarantee);
}
/**
* Adds the fee's amount to the loan's amount only if the fee payer is the buyer and the model is different from WITH_BUYER_ONLY
* @param loanAmount
* @param feePayer
* @param guarantee
*/
private BigDecimal addFeeToLoanAmount(BigDecimal loanAmount, final AccountOwner feePayer, final BigDecimal fee, final Guarantee guarantee) {
final LocalSettings localSettings = settingsService.getLocalSettings();
final boolean hasFee = BigDecimal.ZERO.compareTo(localSettings.round(fee)) == -1;
final GuaranteeType.Model model = guarantee.getGuaranteeType().getModel();
if (model != GuaranteeType.Model.WITH_BUYER_ONLY && feePayer == guarantee.getBuyer().getAccountOwner() && hasFee) {
loanAmount = loanAmount.add(localSettings.round(fee));
}
return loanAmount;
}
/**
* Calculates the initial guarantee's status according to the guarantee's type
* @param guarantee
*/
private Status calcInitialStatus(final Guarantee guarantee) {
final GuaranteeType guaranteeType = guaranteeTypeService.load(guarantee.getGuaranteeType().getId());
if (guaranteeType.getModel() == GuaranteeType.Model.WITH_PAYMENT_OBLIGATION) {
return Status.PENDING_ISSUER;
} else {
switch (guaranteeType.getAuthorizedBy()) {
case ISSUER:
case BOTH:
return Status.PENDING_ISSUER;
case ADMIN:
return Status.PENDING_ADMIN;
case NONE:
return Status.ACCEPTED;
default:
throw new IllegalArgumentException("Unsupported authorizer value: " + guaranteeType.getAuthorizedBy());
}
}
}
private String convertFee(final boolean isCreditFee, final Guarantee guarantee) {
final LocalSettings localSettings = settingsService.getLocalSettings();
NumberConverter<BigDecimal> numberConverter;
final GuaranteeType guaranteeType = guarantee.getGuaranteeType();
final GuaranteeFeeVO feeSpec = isCreditFee ? guarantee.getCreditFeeSpec() : guarantee.getIssueFeeSpec();
if (feeSpec.getType() == FeeType.FIXED) {
numberConverter = localSettings.getUnitsConverter(guaranteeType.getCurrency().getPattern());
return numberConverter.toString(feeSpec.getFee());
} else {
numberConverter = localSettings.getNumberConverter();
return numberConverter.toString(feeSpec.getFee()) + " " + messageResolver.message("guaranteeType.feeType." + feeSpec.getType());
}
}
private Guarantee doChangeStatus(Guarantee guarantee, Status newStatus, final boolean automaticLoanAuthorization) {
// this is necessary to ensure an instance of (the entity) Guarantee
guarantee = fetchService.fetch(guarantee, Guarantee.Relationships.PAYMENT_OBLIGATIONS);
final boolean changeAllowed = canChangeStatus(guarantee, newStatus);
if (!changeAllowed) {
throw new GuaranteeStatusChangeException(newStatus);
} else {
// Force the status to PENDING_ADMIN if the condition is true
if (newStatus == Guarantee.Status.ACCEPTED && isInSomeStatus(guarantee, Status.PENDING_ISSUER) && guarantee.getGuaranteeType().getAuthorizedBy() == GuaranteeType.AuthorizedBy.BOTH) {
newStatus = Status.PENDING_ADMIN;
}
// Create log of the status changing
final GuaranteeLog log = guarantee.changeStatus(newStatus, LoggedUser.user().getElement());
saveLog(log);
// Generates a new loan if the status of guarantee is ACCEPTED and the begin period is now or before.
grantLoan(Calendar.getInstance(), guarantee, automaticLoanAuthorization);
// Save guarantee
save(guarantee, true);
// If the guarantee was cancelled, change the status of associated payment obligations
if (newStatus == Status.CANCELLED) {
updateAssociatedPaymentObligations(guarantee);
}
}
return guarantee; // the return is used in the aspects
}
/**
* Generates a new loan only if the guarantee' status is ACCEPTED and the begin period is now or before.
* @param guarantee
* @param time the times used as the current time
*/
private void doGrantLoan(final Calendar time, final Guarantee guarantee, final boolean automaticLoanAuthorization) {
if (guarantee.getStatus() != Guarantee.Status.ACCEPTED || guarantee.getValidity().getBegin().after(time)) {
return;
}
transactionHelper.maybeRunInNewTransaction(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(final TransactionStatus status) {
performGrantLoan(guarantee, automaticLoanAuthorization);
}
});
}
private Collection<? extends MemberGroup> filterBuyers() {
return filterMemberGroups(new Predicate() {
@Override
public boolean evaluate(final Object object) {
return isBuyerMember((Group) object);
}
});
}
private Collection<? extends MemberGroup> filterIssuers() {
return filterMemberGroups(new Predicate() {
@Override
public boolean evaluate(final Object object) {
return isIssuerMember((Group) object);
}
});
}
private Collection<? extends MemberGroup> filterMemberGroups(final Predicate predicate) {
return filterMemberGroups(predicate, null);
}
@SuppressWarnings("unchecked")
private Collection<? extends MemberGroup> filterMemberGroups(final Predicate predicate, Collection<? extends Group> groups) {
Predicate predicateToApply = predicate;
if (groups == null) { // search for not removed member and broker groups
final GroupQuery query = new GroupQuery();
query.setStatus(Group.Status.NORMAL);
query.setNatures(Group.Nature.MEMBER, Group.Nature.BROKER);
groups = groupService.search(query);
} else if (groups.isEmpty()) { // if the group list is empty then return the same (empty) list
return (Collection<? extends MemberGroup>) groups;
} else { // it creates a predicate to filter not removed member and broker groups
final Predicate memberGroupPredicate = new Predicate() {
@Override
public boolean evaluate(final Object object) {
final Group grp = (Group) object;
return Group.Status.NORMAL == grp.getStatus() && (Group.Nature.MEMBER == grp.getNature() || Group.Nature.BROKER == grp.getNature());
}
};
predicateToApply = predicate == null ? memberGroupPredicate : new AndPredicate(memberGroupPredicate, predicate);
}
CollectionUtils.filter(groups, predicateToApply);
return (Collection<? extends MemberGroup>) groups;
}
private Collection<? extends MemberGroup> filterSellers() {
return filterMemberGroups(new Predicate() {
@Override
public boolean evaluate(final Object object) {
return isSellerMember((Group) object);
}
});
}
/**
*
* @param isCreditFee if false it is issue fee payer
* @param guaranteeType
* @param guarantee
* @return the guarantee's fee payer according to GuaranteeType
*/
private AccountOwner getFeePayer(final boolean isCreditFee, final Guarantee guarantee) {
Member payer = null;
final GuaranteeType guaranteeType = guarantee.getGuaranteeType();
if (isCreditFee) {
payer = guaranteeType.getCreditFeePayer() == GuaranteeType.FeePayer.BUYER ? guarantee.getBuyer() : guarantee.getSeller();
} else {
payer = guaranteeType.getIssueFeePayer() == GuaranteeType.FeePayer.BUYER ? guarantee.getBuyer() : guarantee.getSeller();
}
return payer.getAccountOwner();
}
private Validator getValidator(final Guarantee guarantee) {
final Validator validator = new Validator("guarantee");
validator.property("guaranteeType").required();
final GuaranteeType guaranteeType = fetchService.fetch(guarantee.getGuaranteeType());
if (guaranteeType != null) {
if (guaranteeType.getModel() != GuaranteeType.Model.WITH_BUYER_ONLY) {
validator.property("seller").required().key("guarantee.sellerUsername");
}
// Custom fields
validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() {
@Override
public Validator getValidator() {
final TransferType transferType = guaranteeType.getLoanTransferType();
return paymentCustomFieldService.getValueValidator(transferType);
}
}));
}
validator.property("buyer").required().key("guarantee.buyerUsername");
validator.property("issuer").required().key("guarantee.issuerUsername");
validator.property("amount").required().positiveNonZero();
validator.property("validity").add(new PeriodValidation(ValidationType.BOTH_REQUIRED_AND_NOT_EXPIRED)).key("guarantee.validity");
return validator;
}
private Validator getValidatorForAuthorization(final Guarantee guarantee) {
final Guarantee loaded = load(guarantee.getId(), Guarantee.Relationships.GUARANTEE_TYPE);
final Validator validator = new Validator("guarantee");
final GuaranteeFeeValidation guaranteeFeValidation = new GuaranteeFeeValidation();
validator.general(guaranteeFeValidation);
validator.property("validity").add(new PeriodValidation(ValidationType.BOTH_REQUIRED_AND_NOT_EXPIRED)).key("guarantee.validity");
// Custom fields
validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() {
@Override
public Validator getValidator() {
final TransferType transferType = loaded.getGuaranteeType().getLoanTransferType();
return paymentCustomFieldService.getValueValidator(transferType);
}
}));
return validator;
}
/**
* Generates a new (running as System) loan only if the guarantee' status is ACCEPTED and the begin period is now or before.
* @param guarantee
* @param time the times used as the current time
*/
private void grantLoan(final Calendar time, final Guarantee guarantee, final boolean automaticLoanAuthorization) {
// automaticLoanAuthorization can be true only for an Administrator
if (automaticLoanAuthorization) {
// in this case we don't run as System because the authorization code doesn't support that (there must be a logged user)
doGrantLoan(time, guarantee, automaticLoanAuthorization);
} else { // this could be an Administrator or an Issuer accepting a guarantee
// we run as System to support an issuer accepting a guarantee without authorizers
LoggedUser.runAsSystem(new Callable<Void>() {
@Override
public Void call() throws Exception {
doGrantLoan(time, guarantee, automaticLoanAuthorization);
return null;
}
});
}
}
/**
* Calculates the initial Guarantee Status according to the associated GuaranteeType
* @param guarantee
*/
private void initialize(final Guarantee guarantee) {
final Status status = calcInitialStatus(guarantee);
guarantee.setStatus(status);
guarantee.setRegistrationDate(Calendar.getInstance());
final GuaranteeType guaranteeType = fetchService.fetch(guarantee.getGuaranteeType());
if (guaranteeType.getCreditFee().isReadonly()) {
guarantee.setCreditFeeSpec((GuaranteeFeeVO) guaranteeType.getCreditFee().clone());
}
if (guaranteeType.getIssueFee().isReadonly()) {
guarantee.setIssueFeeSpec((GuaranteeFeeVO) guaranteeType.getIssueFee().clone());
}
}
private boolean isBuyerMember(final Group group) {
return permissionService.hasPermission(group, MemberPermission.GUARANTEES_BUY_WITH_PAYMENT_OBLIGATIONS);
}
private boolean isInSomeStatus(final Guarantee guarantee, final Status... status) {
for (final Status s : status) {
if (guarantee.getStatus() == s) {
return true;
}
}
return false;
}
private boolean isIssuerMember(final Group group) {
return permissionService.hasPermission(group, MemberPermission.GUARANTEES_ISSUE_GUARANTEES);
}
private boolean isSellerMember(final Group group) {
return permissionService.hasPermission(group, MemberPermission.GUARANTEES_SELL_WITH_PAYMENT_OBLIGATIONS);
}
private void performGrantLoan(final Guarantee guarantee, final boolean automaticLoanAuthorization) {
final GuaranteeType guaranteeType = guarantee.getGuaranteeType();
final LocalSettings localSettings = settingsService.getLocalSettings();
final BigDecimal creditFee = guarantee.getCreditFee();
final BigDecimal issueFee = guarantee.getIssueFee();
AccountOwner creditFeePayer = null;
AccountOwner issueFeePayer = null;
/* obtains the fees payers */
if (guaranteeType.getModel() != GuaranteeType.Model.WITH_BUYER_ONLY) {
creditFeePayer = getFeePayer(true, guarantee);
issueFeePayer = getFeePayer(false, guarantee);
} else { /* in this case we only have set the buyer in the guarantee */
creditFeePayer = guarantee.getBuyer().getAccountOwner();
issueFeePayer = creditFeePayer;
}
/* add the credit fee amount to the loan's amount if the fee payer is the buyer */
BigDecimal loanAmount = addFeeToLoanAmount(guarantee.getAmount(), creditFeePayer, creditFee, guarantee);
/* add the issue fee amount to the loan's amount if the fee payer is the buyer */
loanAmount = addFeeToLoanAmount(loanAmount, issueFeePayer, issueFee, guarantee);
/* grants loan to buyer */
final GrantSinglePaymentLoanDTO loanDto = new GrantSinglePaymentLoanDTO();
loanDto.setAutomatic(true);
loanDto.setMember(guarantee.getBuyer());
loanDto.setAmount(loanAmount);
loanDto.setDescription(guaranteeType.getLoanTransferType().getDescription());
loanDto.setTransferType(guaranteeType.getLoanTransferType());
loanDto.setRepaymentDate(guarantee.getValidity().getEnd());
customFieldHelper.cloneFieldValues(guarantee, new CustomFieldsContainer<PaymentCustomField, PaymentCustomFieldValue>() {
@Override
public Class<PaymentCustomField> getCustomFieldClass() {
return loanDto.getCustomFieldClass();
}
@Override
public Class<PaymentCustomFieldValue> getCustomFieldValueClass() {
return loanDto.getCustomFieldValueClass();
}
@Override
public Collection<PaymentCustomFieldValue> getCustomValues() {
return loanDto.getCustomValues();
}
@Override
public void setCustomValues(final Collection<PaymentCustomFieldValue> values) {
loanDto.setCustomValues(values);
}
}, true);
final Loan loan = loanService.grantForGuarantee(loanDto, automaticLoanAuthorization);
TransferDTO transferDto = null;
/* only in this case there is a seller to forward to the loan */
if (guaranteeType.getModel() != GuaranteeType.Model.WITH_BUYER_ONLY) {
/* forwards loan's amount from Buyer to Seller */
transferDto = new TransferDTO();
transferDto.setForced(true);
transferDto.setContext(TransactionContext.AUTOMATIC);
transferDto.setFromOwner(guarantee.getBuyer().getAccountOwner());
transferDto.setToOwner(guarantee.getSeller().getAccountOwner());
transferDto.setTransferType(guaranteeType.getForwardTransferType());
transferDto.setAmount(guarantee.getAmount());
transferDto.setDescription(guaranteeType.getForwardTransferType().getDescription());
transferDto.setParent(loan.getTransfer());
paymentService.insertWithoutNotification(transferDto);
}
/* credit fee payment from Buyer/Seller (according to GuaranteeType) to system account */
if (BigDecimal.ZERO.compareTo(localSettings.round(creditFee)) == -1) {
final Map<String, String> valuesMap = new HashMap<String, String>();
valuesMap.put("creditFee", convertFee(true, guarantee));
transferDto = new TransferDTO();
transferDto.setForced(true);
transferDto.setFromOwner(creditFeePayer);
transferDto.setToOwner(SystemAccountOwner.instance());
transferDto.setTransferType(guaranteeType.getCreditFeeTransferType());
transferDto.setAmount(creditFee);
transferDto.setContext(TransactionContext.AUTOMATIC);
transferDto.setDescription(MessageProcessingHelper.processVariables(guaranteeType.getCreditFeeTransferType().getDescription(), valuesMap));
transferDto.setParent(loan.getTransfer());
paymentService.insertWithoutNotification(transferDto);
}
/* issue fee payment from Buyer/Seller (according to GuaranteeType) to Issuer */
if (BigDecimal.ZERO.compareTo(localSettings.round(issueFee)) == -1) {
final Map<String, String> valuesMap = new HashMap<String, String>();
valuesMap.put("emissionFee", convertFee(false, guarantee));
transferDto = new TransferDTO();
transferDto.setForced(true);
transferDto.setFromOwner(issueFeePayer);
transferDto.setToOwner(guarantee.getIssuer().getAccountOwner());
transferDto.setTransferType(guaranteeType.getIssueFeeTransferType());
transferDto.setAmount(issueFee);
transferDto.setContext(TransactionContext.AUTOMATIC);
transferDto.setDescription(MessageProcessingHelper.processVariables(guaranteeType.getIssueFeeTransferType().getDescription(), valuesMap));
transferDto.setParent(loan.getTransfer());
paymentService.insertWithoutNotification(transferDto);
}
/* update guarantee with the generated loan */
guarantee.setLoan(loan);
}
/**
* It saves the Guarantee taking into account if it is new or an already created guarantee (update)
*/
private Guarantee save(Guarantee guarantee, final boolean validateCustomFields) {
final Collection<PaymentCustomFieldValue> customValues = guarantee.getCustomValues();
if (guarantee.isTransient()) {
initialize(guarantee);
guarantee = guaranteeDao.insert(guarantee);
final GuaranteeLog log = guarantee.getNewLog(guarantee.getStatus(), LoggedUser.user().getElement());
saveLog(log);
} else {
guarantee = guaranteeDao.update(guarantee);
}
guarantee.setCustomValues(customValues);
paymentCustomFieldService.saveValues(guarantee, validateCustomFields);
return guarantee;
}
private void updateAssociatedPaymentObligations(final Guarantee guarantee) {
final Calendar today = DateHelper.truncateNextDay(Calendar.getInstance());
for (final PaymentObligation paymentObligation : guarantee.getPaymentObligations()) {
final Calendar expirationDate = paymentObligation.getExpirationDate();
final Calendar maxPublishDate = paymentObligation.getMaxPublishDate();
final Element by = LoggedUser.element();
PaymentObligationLog log = null;
if (today.after(expirationDate)) {
log = paymentObligation.changeStatus(PaymentObligation.Status.EXPIRED, by);
} else if (today.after(maxPublishDate)) {
log = paymentObligation.changeStatus(PaymentObligation.Status.REGISTERED, by);
paymentObligation.setMaxPublishDate(null);
} else {
log = paymentObligation.changeStatus(PaymentObligation.Status.PUBLISHED, by);
}
paymentObligation.setGuarantee(null);
paymentObligationService.saveLog(log);
paymentObligationService.save(paymentObligation, false);
}
}
}