/* 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.math.BigDecimal; import java.math.MathContext; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; import nl.strohalm.cyclos.access.AdminMemberPermission; import nl.strohalm.cyclos.access.AdminSystemPermission; import nl.strohalm.cyclos.dao.accounts.loans.LoanDAO; import nl.strohalm.cyclos.dao.accounts.loans.LoanPaymentDAO; import nl.strohalm.cyclos.entities.Relationship; import nl.strohalm.cyclos.entities.accounts.Currency; import nl.strohalm.cyclos.entities.accounts.MemberAccount; import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner; import nl.strohalm.cyclos.entities.accounts.external.ExternalTransfer; import nl.strohalm.cyclos.entities.accounts.loans.Loan; import nl.strohalm.cyclos.entities.accounts.loans.LoanParameters; import nl.strohalm.cyclos.entities.accounts.loans.LoanPayment; import nl.strohalm.cyclos.entities.accounts.loans.LoanPayment.Status; import nl.strohalm.cyclos.entities.accounts.loans.LoanPaymentQuery; import nl.strohalm.cyclos.entities.accounts.loans.LoanQuery; import nl.strohalm.cyclos.entities.accounts.loans.LoanRepaymentAmountsDTO; import nl.strohalm.cyclos.entities.accounts.transactions.Payment; import nl.strohalm.cyclos.entities.accounts.transactions.Transfer; import nl.strohalm.cyclos.entities.accounts.transactions.TransferAuthorizationDTO; import nl.strohalm.cyclos.entities.accounts.transactions.TransferQuery; import nl.strohalm.cyclos.entities.accounts.transactions.TransferType; import nl.strohalm.cyclos.entities.alerts.MemberAlert; import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException; import nl.strohalm.cyclos.entities.groups.AdminGroup; 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.services.InitializingService; import nl.strohalm.cyclos.services.accounts.AccountDTO; import nl.strohalm.cyclos.services.accounts.AccountServiceLocal; import nl.strohalm.cyclos.services.accounts.rates.RateServiceLocal; import nl.strohalm.cyclos.services.alerts.AlertServiceLocal; import nl.strohalm.cyclos.services.customization.PaymentCustomFieldServiceLocal; 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.services.transactions.exceptions.AuthorizedPaymentInPastException; import nl.strohalm.cyclos.utils.Amount; import nl.strohalm.cyclos.utils.CacheCleaner; 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.Transactional; import nl.strohalm.cyclos.utils.access.LoggedUser; import nl.strohalm.cyclos.utils.conversion.CoercionHelper; import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler; 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.InvalidError; import nl.strohalm.cyclos.utils.validation.PositiveNonZeroValidation; import nl.strohalm.cyclos.utils.validation.PropertyValidation; import nl.strohalm.cyclos.utils.validation.RequiredError; import nl.strohalm.cyclos.utils.validation.RequiredValidation; import nl.strohalm.cyclos.utils.validation.ValidationError; import nl.strohalm.cyclos.utils.validation.ValidationException; import nl.strohalm.cyclos.utils.validation.Validator; import org.apache.commons.lang.mutable.MutableBoolean; import org.apache.commons.lang.time.DateUtils; import org.springframework.transaction.TransactionStatus; /** * Implementation for loan service * @author luis */ public class LoanServiceImpl implements LoanServiceLocal, InitializingService { /** * Validates a transfer type, forcing it to be a loan type * @author luis */ private class LoanTypeValidation implements PropertyValidation { private static final long serialVersionUID = -7166494808222423923L; public LoanTypeValidation() { } @Override public ValidationError validate(final Object object, final Object name, final Object value) { final TransferType transferType = (TransferType) value; if (transferType == null) { return null; } // Ensure transfer type is a loan if (!transferType.isLoanType()) { return new InvalidError(); } return null; } } /** * Validator for multi-payment loan's collection of loan payment * @author luis */ private final class MultiPaymentValidation implements PropertyValidation { private static final long serialVersionUID = -7905875152926109032L; @Override @SuppressWarnings("unchecked") public ValidationError validate(final Object object, final Object name, final Object value) { final GrantMultiPaymentLoanDTO dto = (GrantMultiPaymentLoanDTO) object; final Collection<LoanPayment> payments = (Collection<LoanPayment>) value; if (payments == null || payments.isEmpty()) { return null; } Calendar lastExpiration = DateHelper.truncate(Calendar.getInstance()); BigDecimal paymentAmounts = BigDecimal.ZERO; final BigDecimal totalAmount = dto.getAmount(); final boolean processAmount = totalAmount != null && totalAmount.compareTo(BigDecimal.ZERO) == 1; // Validate each payment for (final LoanPayment payment : payments) { // Check for required expiration date ValidationError error = RequiredValidation.instance().validate(object, name, payment.getExpirationDate()); // Check for required amount final BigDecimal paymentAmount = payment.getAmount(); if (error == null) { error = RequiredValidation.instance().validate(object, name, paymentAmount); } if (error == null) { error = PositiveNonZeroValidation.instance().validate(object, name, paymentAmount); } // Check if the current expiration date is after the last expiration date if (error == null && lastExpiration.after(payment.getExpirationDate())) { error = new ValidationError("loan.grant.error.unsortedPayments"); } // Return any error for this payment if (error != null) { return error; } // Sum an accumulator for total amount comparision if (processAmount) { paymentAmounts = paymentAmounts.add(paymentAmount); } lastExpiration = payment.getExpirationDate(); } // Check if the payment amount sum == total amount if ((paymentAmounts.subtract(totalAmount)).abs().floatValue() > PRECISION_DELTA) { return new ValidationError("loan.grant.error.invalidAmount"); } return null; } } private TransferAuthorizationServiceLocal transferAuthorizationService; private static final float PRECISION_DELTA = 0.0001F; private AccountServiceLocal accountService; private AlertServiceLocal alertService; private PaymentCustomFieldServiceLocal paymentCustomFieldService; private FetchServiceLocal fetchService; private LoanDAO loanDao; private LoanPaymentDAO loanPaymentDao; private PaymentServiceLocal paymentService; private RateServiceLocal rateService; private SettingsServiceLocal settingsService; private final Map<Loan.Type, LoanHandler> handlersByType = new EnumMap<Loan.Type, LoanHandler>(Loan.Type.class); private PermissionServiceLocal permissionService; private MemberNotificationHandler memberNotificationHandler; private TransactionHelper transactionHelper; @Override public void alertExpiredLoans(final Calendar time) { final Calendar deadline = DateHelper.truncate(time); deadline.add(Calendar.DATE, -1); final LoanPaymentQuery query = new LoanPaymentQuery(); query.setResultType(ResultType.ITERATOR); query.fetch(RelationshipHelper.nested(LoanPayment.Relationships.LOAN, Loan.Relationships.TRANSFER, Payment.Relationships.TO, MemberAccount.Relationships.MEMBER, Element.Relationships.GROUP)); query.setExpirationPeriod(Period.endingAt(deadline)); query.setStatus(LoanPayment.Status.OPEN); CacheCleaner cacheCleaner = new CacheCleaner(fetchService); final List<LoanPayment> payments = search(query); for (final LoanPayment payment : payments) { final Loan loan = payment.getLoan(); payment.setStatus(LoanPayment.Status.EXPIRED); loanPaymentDao.update(payment); // Create an alert final Member member = (Member) loan.getTransfer().getTo().getOwner(); alertService.create(member, MemberAlert.Alerts.EXPIRED_LOAN); // Notify the member memberNotificationHandler.expiredLoanNotification(payment); cacheCleaner.clearCache(); } } @Override public List<LoanPayment> calculatePaymentProjection(final ProjectionDTO params) { params.setTransferType(fetchService.fetch(params.getTransferType())); if (params.getDate() == null) { params.setDate(Calendar.getInstance()); } getProjectionValidator().validate(params); return handlersByType.get(params.getTransferType().getLoan().getType()).calculatePaymentProjection(params); } @Override public LoanPayment discard(final LoanPaymentDTO dto) { final LoanPaymentDTO dateDto = new LoanPaymentDTO(); dateDto.setLoan(dto.getLoan()); dateDto.setLoanPayment(dto.getLoanPayment()); return doDiscard(dateDto); } @Override public LoanPayment discardByExternalTransfer(final Loan loan, final ExternalTransfer externalTransfer) throws UnexpectedEntityException { final LoanPaymentDTO dto = new LoanPaymentDTO(); dto.setLoan(loan); final LoanPayment loanPayment = doDiscard(dto); loanPayment.setExternalTransfer(externalTransfer); return loanPaymentDao.update(loanPayment); } @Override public LoanRepaymentAmountsDTO getLoanPaymentAmount(final LoanPaymentDTO dto) { final LoanRepaymentAmountsDTO ret = new LoanRepaymentAmountsDTO(); Calendar date = dto.getDate(); if (date == null) { date = Calendar.getInstance(); } final Loan loan = fetchService.fetch(dto.getLoan(), Loan.Relationships.TRANSFER, Loan.Relationships.PAYMENTS); LoanPayment payment = fetchService.fetch(dto.getLoanPayment()); if (payment == null) { payment = loan.getFirstOpenPayment(); } ret.setLoanPayment(payment); // Update the dto with fetched values dto.setLoan(loan); dto.setLoanPayment(payment); if (payment != null) { payment = fetchService.fetch(payment, LoanPayment.Relationships.TRANSFERS); final BigDecimal paymentAmount = payment.getAmount(); BigDecimal remainingAmount = paymentAmount; Calendar expirationDate = payment.getExpirationDate(); Calendar lastPaymentDate = (Calendar) expirationDate.clone(); expirationDate = DateUtils.truncate(expirationDate, Calendar.DATE); final LoanParameters parameters = loan.getParameters(); Collection<Transfer> transfers = payment.getTransfers(); if (transfers == null) { transfers = Collections.emptyList(); } final BigDecimal expirationDailyInterest = CoercionHelper.coerce(BigDecimal.class, parameters.getExpirationDailyInterest()); final LocalSettings localSettings = settingsService.getLocalSettings(); final MathContext mathContext = localSettings.getMathContext(); for (final Transfer transfer : transfers) { Calendar trfDate = transfer.getDate(); trfDate = DateUtils.truncate(trfDate, Calendar.DATE); final BigDecimal trfAmount = transfer.getAmount(); BigDecimal actualAmount = trfAmount; final int diffDays = (int) ((trfDate.getTimeInMillis() - expirationDate.getTimeInMillis()) / DateUtils.MILLIS_PER_DAY); if (diffDays > 0 && expirationDailyInterest != null) { // Apply interest actualAmount = actualAmount.subtract(remainingAmount.multiply(new BigDecimal(diffDays)).multiply(expirationDailyInterest.divide(new BigDecimal(100), mathContext))); } remainingAmount = remainingAmount.subtract(actualAmount); lastPaymentDate = (Calendar) trfDate.clone(); } date = DateHelper.truncate(date); BigDecimal remainingAmountAtDate = remainingAmount; final int diffDays = (int) ((date.getTimeInMillis() - (expirationDate.before(lastPaymentDate) ? lastPaymentDate.getTimeInMillis() : expirationDate.getTimeInMillis())) / DateUtils.MILLIS_PER_DAY); if (diffDays > 0 && expirationDailyInterest != null) { // Apply interest remainingAmountAtDate = remainingAmountAtDate.add(remainingAmount.multiply(new BigDecimal(diffDays)).multiply(expirationDailyInterest.divide(new BigDecimal(100), mathContext))); } final Amount expirationFee = parameters.getExpirationFee(); if (expirationFee != null && (remainingAmountAtDate.compareTo(BigDecimal.ZERO) == 1) && expirationDate.before(date) && (expirationFee.getValue().compareTo(BigDecimal.ZERO) == 1)) { // Apply expiration fee remainingAmountAtDate = remainingAmountAtDate.add(expirationFee.apply(remainingAmount)); } // Round the result ret.setRemainingAmountAtExpirationDate(localSettings.round(remainingAmount)); ret.setRemainingAmountAtDate(localSettings.round(remainingAmountAtDate)); } return ret; } @Override public TransactionSummaryVO getOpenLoansSummary(final Currency currency) { final LoanQuery query = new LoanQuery(); query.setStatus(Loan.Status.OPEN); query.setCurrency(currency); if (LoggedUser.hasUser()) { AdminGroup adminGroup = LoggedUser.group(); adminGroup = fetchService.fetch(adminGroup, AdminGroup.Relationships.MANAGES_GROUPS); query.setGroups(adminGroup.getManagesGroups()); } return buildSummary(query); } @Override public Loan grant(final GrantLoanDTO params) { return doGrant(params, true); } @Override public Loan grantForGuarantee(final GrantLoanDTO params, final boolean automaticAuthorize) { final Loan loan = insert(params); if (automaticAuthorize && permissionService.hasPermission(AdminSystemPermission.PAYMENTS_AUTHORIZE) && (loan.getTransfer().getNextAuthorizationLevel() != null)) { final TransferAuthorizationDTO transferAuthorizationDto = new TransferAuthorizationDTO(); transferAuthorizationDto.setTransfer(loan.getTransfer()); transferAuthorizationService.authorize(transferAuthorizationDto, false); } return loan; } @Override public void initializeService() { alertExpiredLoans(Calendar.getInstance()); } @Override public Loan insert(final GrantLoanDTO params) { return doGrant(params, false); } @Override public Loan load(final Long id, final Relationship... fetch) { return loanDao.load(id, fetch); } @Override public TransactionSummaryVO loanSummary(final Member member) { final LoanQuery query = new LoanQuery(); query.setMember(member); query.setStatus(Loan.Status.OPEN); return buildSummary(query); } @Override public Loan markAsInProcess(final Loan loan) throws UnexpectedEntityException { return markAs(loan, LoanPayment.Status.EXPIRED, LoanPayment.Status.IN_PROCESS); } @Override public Loan markAsRecovered(final Loan loan) throws UnexpectedEntityException { return markAs(loan, LoanPayment.Status.IN_PROCESS, LoanPayment.Status.RECOVERED); } @Override public Loan markAsUnrecoverable(final Loan loan) throws UnexpectedEntityException { return markAs(loan, LoanPayment.Status.IN_PROCESS, LoanPayment.Status.UNRECOVERABLE); } @Override public TransactionSummaryVO paymentsSummary(final LoanPaymentQuery query) { final Status status = query.getStatus(); if (status == null) { throw new ValidationException("status", "loanPayment.status", new RequiredError()); } return loanPaymentDao.paymentsSummary(query); } @Override public Transfer repay(final RepayLoanDTO params) { 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 doRepay(params); } }); } @Override public List<LoanPayment> search(final LoanPaymentQuery query) { return loanPaymentDao.search(query); } @Override public List<Loan> search(final LoanQuery query) { if (query.getQueryStatus() == null) { query.setHideAuthorizationRelated(!permissionService.hasPermission(AdminMemberPermission.LOANS_VIEW_AUTHORIZED)); } return loanDao.search(query); } public void setAccountServiceLocal(final AccountServiceLocal accountService) { this.accountService = accountService; } public void setAlertServiceLocal(final AlertServiceLocal alertService) { this.alertService = alertService; } public void setFetchServiceLocal(final FetchServiceLocal fetchService) { this.fetchService = fetchService; } public void setLoanDao(final LoanDAO loanDao) { this.loanDao = loanDao; } public void setLoanPaymentDao(final LoanPaymentDAO loanPaymentDao) { this.loanPaymentDao = loanPaymentDao; } public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) { this.memberNotificationHandler = memberNotificationHandler; } public void setMultiPaymentHandler(final LoanHandler handler) { handlersByType.put(Loan.Type.MULTI_PAYMENT, handler); } public void setPaymentCustomFieldServiceLocal(final PaymentCustomFieldServiceLocal paymentCustomFieldService) { this.paymentCustomFieldService = paymentCustomFieldService; } 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 setSettingsServiceLocal(final SettingsServiceLocal settingsService) { this.settingsService = settingsService; } public void setSinglePaymentHandler(final LoanHandler handler) { handlersByType.put(Loan.Type.SINGLE_PAYMENT, handler); } public void setTransactionHelper(final TransactionHelper transactionHelper) { this.transactionHelper = transactionHelper; } public void setTransferAuthorizationServiceLocal(final TransferAuthorizationServiceLocal transferAuthorizationService) { this.transferAuthorizationService = transferAuthorizationService; } public void setWithInterestHandler(final LoanHandler handler) { handlersByType.put(Loan.Type.WITH_INTEREST, handler); } @Override public void validate(final GrantLoanDTO params) { final Validator validator = getValidator(params); if (validator == null) { throw new ValidationException("transferType", "loan.type", new InvalidError()); } validator.validate(params); } private TransactionSummaryVO buildSummary(final LoanQuery query) { BigDecimal amount = BigDecimal.ZERO; int count = 0; final List<Loan> loans = loanDao.search(query); for (Loan loan : loans) { count++; loan = fetchService.fetch(loan, Loan.Relationships.PAYMENTS); final List<LoanPayment> payments = loan.getPayments(); if (payments != null) { for (final LoanPayment payment : payments) { amount = amount.add(payment.getRemainingAmount()); } } } final TransactionSummaryVO ret = new TransactionSummaryVO(); ret.setCount(count); ret.setAmount(amount); return ret; } private LoanPayment doDiscard(final LoanPaymentDTO dto) { final Loan loan = fetchService.fetch(dto.getLoan(), Loan.Relationships.PAYMENTS); LoanPayment payment = fetchService.fetch(dto.getLoanPayment()); final Calendar date = dto.getDate() == null ? Calendar.getInstance() : dto.getDate(); if (payment == null) { payment = loan.getFirstOpenPayment(); } if (payment == null) { throw new UnexpectedEntityException(); } payment.setStatus(LoanPayment.Status.DISCARDED); payment.setRepaymentDate(date); return loanPaymentDao.update(payment); } private Loan doGrant(final GrantLoanDTO params, final boolean newTransaction) { validate(params); // Fetch and update the transfer type final TransferType transferType = fetchService.fetch(params.getTransferType()); params.setTransferType(transferType); // Insert the loan final Loan loan = handlersByType.get(params.getLoanType()).buildLoan(params); return transactionHelper.maybeRunInNewTransaction(new Transactional<Loan>() { @Override public Loan afterCommit(final Loan result) { return fetchService.fetch(result); } @Override public Loan doInTransaction(final TransactionStatus status) { return doGrant(loan, params); } }, newTransaction); } private Loan doGrant(Loan loan, final GrantLoanDTO params) { final LocalSettings localSettings = settingsService.getLocalSettings(); final TransferType transferType = params.getTransferType(); // Create the loan transfer final TransferDTO transferDto = new TransferDTO(); if (params.isAutomatic()) { transferDto.setContext(TransactionContext.AUTOMATIC_LOAN); } else { transferDto.setContext(TransactionContext.LOAN); } if (params.getDate() != null) { transferDto.setDate(params.getDate()); } transferDto.setToOwner(params.getMember()); transferDto.setFrom(accountService.getAccount(new AccountDTO(SystemAccountOwner.instance(), transferType.getFrom()))); transferDto.setTo(accountService.getAccount(new AccountDTO(transferDto.getToOwner(), transferType.getTo()))); transferDto.setAmount(params.getAmount()); transferDto.setDescription(params.getDescription()); transferDto.setTransferType(transferType); transferDto.setCustomValues(params.getCustomValues()); transferDto.setRates(rateService.applyLoan(transferDto, params)); final Transfer transfer = (Transfer) paymentService.insertWithoutNotification(transferDto); if (transfer.getProcessDate() == null && params.getDate() != null && DateHelper.daysBetween(params.getDate(), Calendar.getInstance()) != 0) { throw new AuthorizedPaymentInPastException(); } // Persist the loan loan.setTransfer(transfer); final List<LoanPayment> payments = loan.getPayments(); loan = loanDao.insert(loan); loan.setPayments(new ArrayList<LoanPayment>()); // Insert the installments int index = 0; BigDecimal total = BigDecimal.ZERO; for (final LoanPayment payment : payments) { payment.setLoan(loan); payment.setIndex(index++); BigDecimal amount = localSettings.round(payment.getAmount()); if (index == payments.size()) { // The last payment should round to total amount amount = localSettings.round(loan.getTotalAmount().subtract(total)); } else { total = total.add(amount); } payment.setAmount(amount); loan.getPayments().add(loanPaymentDao.insert(payment)); } // Notify memberNotificationHandler.grantedLoanNotification(loan); return loan; } private Transfer doRepay(final RepayLoanDTO params) { BigDecimal amount = params.getAmount(); // Check if the amount is valid if (amount.compareTo(paymentService.getMinimumPayment()) < 0) { throw new ValidationException("amount", "loan.amount", new InvalidError()); } // Get the loan payment to repay Calendar date = params.getDate(); if (date == null) { date = Calendar.getInstance(); params.setDate(date); } final LoanRepaymentAmountsDTO amountsDTO = getLoanPaymentAmount(params); final LoanPayment payment = amountsDTO.getLoanPayment(); if (payment == null) { throw new UnexpectedEntityException(); } // Validate the amount final BigDecimal remainingAmount = amountsDTO.getRemainingAmountAtDate(); final BigDecimal diff = remainingAmount.subtract(amount); final MutableBoolean totallyRepaid = new MutableBoolean(); // If the amount is on an acceptable delta, set the transfer value = parcel value if (diff.abs().floatValue() < PRECISION_DELTA) { amount = remainingAmount; totallyRepaid.setValue(true); } else if (diff.compareTo(BigDecimal.ZERO) < 0 || !params.getLoan().getTransfer().getType().getLoan().getType().allowsPartialRepayments()) { throw new ValidationException("amount", "loan.amount", new InvalidError()); } final LocalSettings localSettings = settingsService.getLocalSettings(); Loan loan = fetchService.fetch(params.getLoan(), Loan.Relationships.PAYMENTS, RelationshipHelper.nested(Loan.Relationships.TRANSFER, Payment.Relationships.TO, MemberAccount.Relationships.MEMBER), Loan.Relationships.TO_MEMBERS); // Build the transfers for repayment final List<TransferDTO> transfers = handlersByType.get(loan.getParameters().getType()).buildTransfersForRepayment(params, amountsDTO); Transfer root = null; BigDecimal totalAmount = BigDecimal.ZERO; for (final TransferDTO dto : transfers) { if (dto.getAmount().floatValue() < PRECISION_DELTA) { // If the root amount is zero, it means that the parent transfer should be the last transfer for this loan payment final TransferQuery tq = new TransferQuery(); tq.setLoanPayment(payment); tq.setReverseOrder(true); tq.setUniqueResult(); final List<Transfer> paymentTransfers = paymentService.search(tq); if (paymentTransfers.isEmpty()) { throw new IllegalStateException("The root transfer has amount 0 and there is no other transfers for this payment"); } root = paymentTransfers.iterator().next(); } else { totalAmount = totalAmount.add(dto.getAmount()); dto.setParent(root); dto.setLoanPayment(payment); final Transfer transfer = (Transfer) paymentService.insertWithoutNotification(dto); if (root == null) { // The first will be the root. All others are it's children root = transfer; } } } // Update the loan payment final BigDecimal totalRepaid = localSettings.round(payment.getRepaidAmount().add(totalAmount)); payment.setRepaidAmount(totalRepaid); if (totallyRepaid.booleanValue()) { // Mark the payment as repaid, if is the case payment.setStatus(LoanPayment.Status.REPAID); payment.setRepaymentDate(params.getDate()); } payment.setTransfers(null); // Avoid 2 representations of the transfers collection. It's inverse="true", no problem setting null loanPaymentDao.update(payment); // Return the generated root transfer return root; } private Validator getProjectionValidator() { final Validator projectionValidator = new Validator("loan"); projectionValidator.property("transferType").key("loan.type").required().add(new LoanTypeValidation()); projectionValidator.property("amount").required().positiveNonZero(); projectionValidator.property("firstExpirationDate").future().required(); projectionValidator.property("paymentCount").required().positiveNonZero(); return projectionValidator; } private Validator getValidator(final GrantLoanDTO params) { // The transfer type is implicitly validated by returning null on non-loan types final TransferType transferType = fetchService.fetch(params.getTransferType()); Loan.Type type; try { type = transferType.getLoan().getType(); } catch (final Exception e) { return null; } final Validator validator = new Validator("loan"); validator.property("amount").required().positiveNonZero(); final Currency currency = fetchService.fetch(transferType.getCurrency(), Currency.Relationships.A_RATE_PARAMETERS, Currency.Relationships.D_RATE_PARAMETERS); if (currency.isEnableARate() || currency.isEnableDRate()) { // if the date is not null, it might be a payment in past, which is not allowed with rates enabled. if (params.getDate() != null) { final Calendar now = Calendar.getInstance(); // make a few minutes earlier, because if the transfer's date has just before been set to Calendar.getInstance(), it may already be a // few milliseconds or even seconds later. now.add(Calendar.MINUTE, -4); if (params.getDate().before(now)) { validator.general(new GeneralValidation() { private static final long serialVersionUID = -7221645724425619586L; @Override public ValidationError validate(final Object object) { return new ValidationError("payment.error.pastDateWithRates"); } }); } } } else { validator.property("date").key("loan.grant.manualDate").pastOrToday(); } validator.property("description").required().maxLength(1000); validator.property("member").key("member.member").required(); switch (type) { case SINGLE_PAYMENT: validator.property("repaymentDate").required(); break; case MULTI_PAYMENT: validator.property("payments").required().add(new MultiPaymentValidation()); break; case WITH_INTEREST: validator.property("firstRepaymentDate").future().required(); validator.property("paymentCount").required().positiveNonZero(); } // Custom fields validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() { @Override public Validator getValidator() { return paymentCustomFieldService.getValueValidator(transferType); } })); return validator; } private Loan markAs(Loan loan, final LoanPayment.Status expectedStatus, final LoanPayment.Status newStatus) { loan = fetchService.fetch(loan, Loan.Relationships.PAYMENTS); for (final LoanPayment current : loan.getPayments()) { if (current.getStatus() == expectedStatus) { current.setStatus(newStatus); loanPaymentDao.update(current); } } return fetchService.reload(loan); } }