add provider project source.

This commit is contained in:
Keith Woodlock
2012-04-20 11:50:23 +01:00
parent ceb30e8012
commit c1e5474f1e
252 changed files with 20100 additions and 0 deletions
@@ -0,0 +1,15 @@
package org.mifosng.oauth;
import org.springframework.security.oauth.provider.token.OAuthProviderToken;
import org.springframework.security.oauth.provider.token.OAuthProviderTokenServices;
/**
* This is just a temporary hack, should pass oauth access token, access secret, consumer key, consumer secret to get access to authenticated user permissions
*/
public interface CustomOAuthProviderTokenServices extends OAuthProviderTokenServices {
OAuthProviderToken getTokenByNonEncodedKey(String oauthToken);
void removeTokenByNonEncodedKey(String oauthToken);
}
@@ -0,0 +1,154 @@
package org.mifosng.oauth;
import java.io.IOException;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth.common.OAuthConsumerParameter;
import org.springframework.security.oauth.common.OAuthException;
import org.springframework.security.oauth.common.signature.OAuthSignatureMethod;
import org.springframework.security.oauth.common.signature.SignatureSecret;
import org.springframework.security.oauth.common.signature.UnsupportedSignatureMethodException;
import org.springframework.security.oauth.provider.ConsumerAuthentication;
import org.springframework.security.oauth.provider.ConsumerCredentials;
import org.springframework.security.oauth.provider.ConsumerDetails;
import org.springframework.security.oauth.provider.InvalidOAuthParametersException;
import org.springframework.security.oauth.provider.filter.ProtectedResourceProcessingFilter;
import org.springframework.security.oauth.provider.token.OAuthProviderToken;
public class CustomProtectedResourceProcessingFilter extends
ProtectedResourceProcessingFilter {
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (!skipProcessing(request)) {
if (requiresAuthentication(request, response, chain)) {
if (!allowMethod(request.getMethod().toUpperCase())) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return;
}
try {
Map<String, String> oauthParams = getProviderSupport().parseParameters(request);
if (parametersAreAdequate(oauthParams)) {
String consumerKey = oauthParams.get(OAuthConsumerParameter.oauth_consumer_key.toString());
if (consumerKey == null) {
throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingConsumerKey", "Missing consumer key."));
}
//load the consumer details.
ConsumerDetails consumerDetails = getConsumerDetailsService().loadConsumerByConsumerKey(consumerKey);
//validate the parameters for the consumer.
validateOAuthParams(consumerDetails, oauthParams);
//extract the credentials.
String token = oauthParams.get(OAuthConsumerParameter.oauth_token.toString());
String signatureMethod = oauthParams.get(OAuthConsumerParameter.oauth_signature_method.toString());
String signature = oauthParams.get(OAuthConsumerParameter.oauth_signature.toString());
String signatureBaseString = getProviderSupport().getSignatureBaseString(request);
ConsumerCredentials credentials = new ConsumerCredentials(consumerKey, signature, signatureMethod, signatureBaseString, token);
//create an authentication request.
ConsumerAuthentication authentication = new ConsumerAuthentication(consumerDetails, credentials, oauthParams);
authentication.setDetails(createDetails(request, consumerDetails));
Authentication previousAuthentication = SecurityContextHolder.getContext().getAuthentication();
try {
//set the authentication request (unauthenticated) into the context.
SecurityContextHolder.getContext().setAuthentication(authentication);
//validate the signature.
if (token == null) {
validateSignature(authentication);
} else {
if (!"OPTIONS".equalsIgnoreCase(request.getMethod().toUpperCase())) {
validateSignature(authentication);
}
}
//mark the authentication request as validated.
authentication.setSignatureValidated(true);
//mark that processing has been handled.
request.setAttribute(OAUTH_PROCESSING_HANDLED, Boolean.TRUE);
//go.
onValidSignature(request, response, chain);
}
finally {
//clear out the consumer authentication to make sure it doesn't get cached.
resetPreviousAuthentication(previousAuthentication);
}
}
else if (!isIgnoreInadequateCredentials()) {
throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingCredentials", "Inadequate OAuth consumer credentials."));
}
else {
chain.doFilter(request, response);
}
}
catch (AuthenticationException ae) {
fail(request, response, ae);
}
catch (ServletException e) {
if (e.getRootCause() instanceof AuthenticationException) {
fail(request, response, (AuthenticationException) e.getRootCause());
}
else {
throw e;
}
}
}
else {
chain.doFilter(servletRequest, servletResponse);
}
}
else {
chain.doFilter(servletRequest, servletResponse);
}
}
@Override
protected void validateSignature(ConsumerAuthentication authentication)
throws AuthenticationException {
SignatureSecret secret = authentication.getConsumerDetails()
.getSignatureSecret();
String token = authentication.getConsumerCredentials().getToken();
OAuthProviderToken authToken = null;
if (token != null && !"".equals(token)) {
authToken = getTokenServices().getToken(token);
}
String signatureMethod = authentication.getConsumerCredentials()
.getSignatureMethod();
OAuthSignatureMethod method;
try {
method = getSignatureMethodFactory().getSignatureMethod(
signatureMethod, secret,
authToken != null ? authToken.getSecret() : null);
} catch (UnsupportedSignatureMethodException e) {
throw new OAuthException(e.getMessage(), e);
}
String signatureBaseString = authentication.getConsumerCredentials()
.getSignatureBaseString();
String signature = authentication.getConsumerCredentials()
.getSignature();
method.verify(signatureBaseString, signature);
}
}
@@ -0,0 +1,63 @@
package org.mifosng.oauth;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth.provider.token.OAuthAccessProviderToken;
import org.springframework.security.oauth.provider.token.OAuthProviderToken;
import org.springframework.security.oauth.provider.token.OAuthProviderTokenImpl;
public class CustomTokenServices implements CustomOAuthProviderTokenServices {
private final ConcurrentHashMap<String, OAuthProviderTokenImpl> requestTokenStore = new ConcurrentHashMap<String, OAuthProviderTokenImpl>();
private final RemovableInMemoryProviderTokenServices delegate;
public CustomTokenServices(RemovableInMemoryProviderTokenServices delegate) {
this.delegate = delegate;
}
@Override
public OAuthProviderToken getToken(String token)
throws AuthenticationException {
return this.delegate.getToken(token);
}
@Override
public OAuthProviderToken createUnauthorizedRequestToken(
String consumerKey, String callbackUrl)
throws AuthenticationException {
return this.delegate.createUnauthorizedRequestToken(consumerKey, callbackUrl);
}
@Override
public void authorizeRequestToken(String requestToken, String verifier,
Authentication authentication) throws AuthenticationException {
this.delegate.authorizeRequestToken(requestToken, verifier, authentication);
}
@Override
public OAuthAccessProviderToken createAccessToken(String requestToken)
throws AuthenticationException {
OAuthProviderTokenImpl accessToken = (OAuthProviderTokenImpl)this.delegate.createAccessToken(requestToken);
this.requestTokenStore.put(requestToken, accessToken);
return accessToken;
}
@Override
public OAuthProviderToken getTokenByNonEncodedKey(String oauthToken) {
return this.requestTokenStore.get(oauthToken);
}
@Override
public void removeTokenByNonEncodedKey(String oauthToken) {
OAuthProviderTokenImpl accessToken = this.requestTokenStore.remove(oauthToken);
if (accessToken != null) {
String acccesTokenValue = accessToken.getValue();
this.delegate.removeAccessToken(acccesTokenValue);
} else {
this.delegate.removeAccessToken(oauthToken);
}
}
}
@@ -0,0 +1,44 @@
package org.mifosng.oauth;
import java.util.HashMap;
import java.util.Map;
import org.mifosng.platform.oauthconsumer.domain.OauthConsumerDetail;
import org.mifosng.platform.oauthconsumer.domain.OauthConsumerDetailRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth.common.OAuthException;
import org.springframework.security.oauth.provider.ConsumerDetails;
import org.springframework.security.oauth.provider.ConsumerDetailsService;
import org.springframework.security.oauth.provider.InvalidOAuthParametersException;
public class MifosNgConsumerDetailsService implements ConsumerDetailsService {
private Map<String, ConsumerDetails> consumerDetailsStore = new HashMap<String, ConsumerDetails>();
@Autowired
private OauthConsumerDetailRepository oauthConsumerDetailRepository;
/**
* consumer service is used for by spring security oauth for all requests so makes sense to store consumer details in memory
* rather than query the database based on each request.
*/
@Override
public ConsumerDetails loadConsumerByConsumerKey(final String consumerKey)
throws OAuthException {
ConsumerDetails detailsFromInMemoryStore = consumerDetailsStore.get(consumerKey);
OauthConsumerDetail detailsFromDatabase = null;
if (detailsFromInMemoryStore == null) {
detailsFromDatabase = this.oauthConsumerDetailRepository.findByConsumerKey(consumerKey);
}
ConsumerDetails consumerDetails = detailsFromInMemoryStore != null ? detailsFromInMemoryStore : detailsFromDatabase;
if (consumerDetails == null) {
throw new InvalidOAuthParametersException("Consumer not found: " + consumerKey);
} else {
consumerDetailsStore.put(consumerKey, detailsFromDatabase);
}
return consumerDetails;
}
}
@@ -0,0 +1,82 @@
package org.mifosng.oauth;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth.common.OAuthConsumerParameter;
import org.springframework.security.oauth.provider.OAuthProviderSupport;
import org.springframework.security.oauth.provider.filter.CoreOAuthProviderSupport;
import org.springframework.security.oauth.provider.token.OAuthProviderTokenImpl;
import org.springframework.security.oauth.provider.token.OAuthProviderTokenServices;
import org.springframework.web.filter.GenericFilterBean;
public class OAuthParametersAwareSecurityContextPerRequestFilter extends
GenericFilterBean {
private OAuthProviderSupport providerSupport = new CoreOAuthProviderSupport();
private final OAuthProviderTokenServices tokenServices;
public OAuthParametersAwareSecurityContextPerRequestFilter(OAuthProviderTokenServices tokenServices) {
this.tokenServices = tokenServices;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
Map<String, String> oauthParams = this.providerSupport.parseParameters(request);
String anonymousUserHash = "" + "anonymousUser".hashCode();
Collection<SimpleGrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority("ROLE_ANONYMOUS"));
Authentication authentication = new AnonymousAuthenticationToken(anonymousUserHash, "anonymousUser", authorities);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authentication);
if (oauthTokenExists(oauthParams)) {
String oauthToken = retrieveOAuthToken(oauthParams);
OAuthProviderTokenImpl token = (OAuthProviderTokenImpl) this.tokenServices.getToken(oauthToken);
if (token.getUserAuthentication() != null) {
context.setAuthentication(token.getUserAuthentication());
}
} else {
String requestToken = request.getParameter("requestToken");
if (StringUtils.isNotBlank(requestToken)) {
OAuthProviderTokenImpl token = (OAuthProviderTokenImpl) this.tokenServices.getToken(requestToken);
if (token.getUserAuthentication() != null) {
context.setAuthentication(token.getUserAuthentication());
}
}
}
chain.doFilter(request, response);
}
private String retrieveOAuthToken(Map<String, String> oauthParams) {
return oauthParams.get(OAuthConsumerParameter.oauth_token.toString());
}
private boolean oauthTokenExists(Map<String, String> oauthParams) {
return oauthParams.containsKey(OAuthConsumerParameter.oauth_token.toString());
}
}
@@ -0,0 +1,11 @@
package org.mifosng.oauth;
import org.springframework.security.oauth.provider.token.InMemoryProviderTokenServices;
public class RemovableInMemoryProviderTokenServices extends
InMemoryProviderTokenServices {
public void removeAccessToken(String acccesTokenValue) {
super.removeToken(acccesTokenValue);
}
}
@@ -0,0 +1,52 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.AdjustLoanTransactionCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
public class AdjustLoanTransactionCommandValidator {
private final AdjustLoanTransactionCommand command;
public AdjustLoanTransactionCommandValidator(AdjustLoanTransactionCommand command) {
this.command = command;
}
public void validate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
if (command.getLoanId() == null || command.getLoanId().longValue() <= 0) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.id.is.invalid", "loanId");
dataValidationErrors.add(error);
}
if (command.getRepaymentId() == null || command.getRepaymentId().longValue() <= 0) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.transaction.id.is.invalid", "repaymentId");
dataValidationErrors.add(error);
}
if (command.getPaymentDate() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.repayment.date.cannot.be.blank", "paymentDate");
dataValidationErrors.add(error);
}
if (command.getPaymentAmount() == null || command.getPaymentAmount().doubleValue() < 0) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.adjustment.must.be.zero.or.greater", "paymentAmount");
dataValidationErrors.add(error);
}
if (StringUtils.isNotBlank(command.getComment()) && command.getComment().length() > 1000) {
ErrorResponse error = new ErrorResponse("validation.msg.note.exceeds.max.length", "comment");
dataValidationErrors.add(error);
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
}
@@ -0,0 +1,209 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.CalculateLoanScheduleCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.InterestMethod;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
public class CalculateLoanScheduleCommandValidator {
private final CalculateLoanScheduleCommand command;
public CalculateLoanScheduleCommandValidator(CalculateLoanScheduleCommand command) {
this.command = command;
}
public void validate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
if (StringUtils.isBlank(command.getCurrencyCode())) {
ErrorResponse error = new ErrorResponse("validation.msg.product.currency.cannot.be.empty", "currencyCode");
dataValidationErrors.add(error);
}
if (command.getDigitsAfterDecimal() != null) {
if (command.getDigitsAfterDecimal() < 0 || command.getDigitsAfterDecimal() > 6) {
ErrorResponse error = new ErrorResponse("validation.msg.product.currency.digitsAfterDecimal.must.be.between.zero.and.six.inclusive", "selectedDigitsAfterDecimal", command.getDigitsAfterDecimal());
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.currency.digitsAfterDecimal.cannot.be.empty", "selectedDigitsAfterDecimal");
dataValidationErrors.add(error);
}
if (command.getPrincipal() != null) {
if (command.getPrincipal().doubleValue() <= Double.valueOf("0.0")) {
ErrorResponse error = new ErrorResponse("validation.msg.product.principal.amount.must.be.greater.than.zero", "principalMoney");
dataValidationErrors.add(error);
}
// TODO - check number of digits before decimal and after decimal
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.principal.amount.cannot.be.empty", "principalMoney");
dataValidationErrors.add(error);
}
if (command.getRepaymentFrequency() != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequency());
ErrorResponse error = null;
switch (frequencyType) {
case DAYS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 365);
break;
case WEEKS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 52);
break;
case MONTHS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 12);
break;
case YEARS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 2);
break;
default:
error = new ErrorResponse("validation.msg.product.repayment.frequency.invalid", "selectedRepaymentFrequencyOption");
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.repayment.frequency.cannot.be.empty", "selectedRepaymentFrequencyOption");
dataValidationErrors.add(error);
}
if (command.getNumberOfRepayments() != null) {
if (command.getNumberOfRepayments() < 1 || command.getNumberOfRepayments() > 100) {
ErrorResponse error = new ErrorResponse("validation.msg.product.number.of.repayments.is.out.of.range", "numberOfRepayments");
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.number.of.repayments.cannot.be.empty", "numberOfRepayments");
dataValidationErrors.add(error);
}
if (command.getAmortizationMethod() != null) {
AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod());
ErrorResponse error = null;
switch (amortizationMethod) {
case EQUAL_PRINCIPAL:
break;
case EQUAL_INSTALLMENTS:
break;
default:
error = new ErrorResponse("validation.msg.product.amortization.method.invalid", "selectedAmortizationMethodOption");
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.amortization.method.cannot.be.empty", "selectedAmortizationMethodOption");
dataValidationErrors.add(error);
}
if (command.getExpectedDisbursementDate() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.expectedDisbursementDate.method.cannot.be.blank", "expectedDisbursementDate");
dataValidationErrors.add(error);
} else {
if (command.getRepaymentsStartingFromDate() != null
&& command.getExpectedDisbursementDate().isAfter(command.getRepaymentsStartingFromDate())) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.expectedDisbursementDate.cannot.be.after.first.repayment.date", "expectedDisbursementDate");
dataValidationErrors.add(error);
}
}
if (command.getRepaymentsStartingFromDate() != null && command.getInterestCalculatedFromDate() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.interestCalculatedFromDate.must.be.entered.when.using.repayments.startfrom.field", "interestCalculatedFromDate");
dataValidationErrors.add(error);
} else if (command.getRepaymentsStartingFromDate() == null && command.getInterestCalculatedFromDate() != null) {
// validate interestCalculatedFromDate is after or on repaymentsStartingFromDate
if (command.getExpectedDisbursementDate().isAfter(command.getInterestCalculatedFromDate())) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.interestCalculatedFromDate.cannot.be.before.disbursement.date", "interestCalculatedFromDate");
dataValidationErrors.add(error);
}
}
if (command.getInterestRatePerPeriod()!= null) {
if (command.getInterestRatePerPeriod().doubleValue() < Double.valueOf("0.0")) {
ErrorResponse error = new ErrorResponse("validation.msg.product.interest.rate.per.period.amount.cannot.be.negative", "nominalInterestRate");
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.interest.rate.per.period.amount.cannot.be.empty", "nominalInterestRate");
dataValidationErrors.add(error);
}
// interest rate period type
if (command.getInterestRateFrequencyMethod() != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod());
ErrorResponse error = null;
switch (frequencyType) {
case DAYS:
break;
case WEEKS:
break;
case MONTHS:
break;
case YEARS:
break;
default:
error = new ErrorResponse("validation.msg.product.interest.frequency.invalid", "selectedInterestFrequencyOption");
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.interest.frequency.cannot.be.empty", "selectedInterestFrequencyOption");
dataValidationErrors.add(error);
}
if (command.getInterestMethod() != null) {
InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod());
ErrorResponse error = null;
switch (interestMethod) {
case DECLINING_BALANCE:
break;
case FLAT:
break;
default:
error = new ErrorResponse("validation.msg.product.interest.method.invalid", "selectedInterestMethodOption");
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.interest.method.cannot.be.empty", "selectedInterestMethodOption");
dataValidationErrors.add(error);
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Validation errors exist.");
}
}
private ErrorResponse validateNumberExistsAndInRange(String errorCodePreFix, String fieldName, Integer value, int min, int max) {
ErrorResponse error = null;
if (value == null) {
error = new ErrorResponse(errorCodePreFix + ".cannot.be.empty", fieldName);
} else {
if (value < min || value > max) {
error = new ErrorResponse(errorCodePreFix + ".outside.allowed.range", fieldName);
}
}
return error;
}
}
@@ -0,0 +1,14 @@
package org.mifosng.platform;
import org.joda.time.LocalDate;
import org.mifosng.data.LoanPayoffReadModel;
import org.mifosng.data.LoanSchedule;
import org.mifosng.data.command.CalculateLoanScheduleCommand;
public interface CalculationPlatformService {
LoanSchedule calculateLoanSchedule(CalculateLoanScheduleCommand command);
LoanPayoffReadModel calculatePayoffOn(Long valueOf, LocalDate payoffDate);
}
@@ -0,0 +1,142 @@
package org.mifosng.platform;
import java.math.BigDecimal;
import org.joda.time.LocalDate;
import org.mifosng.data.CurrencyData;
import org.mifosng.data.LoanPayoffReadModel;
import org.mifosng.data.LoanSchedule;
import org.mifosng.data.MoneyData;
import org.mifosng.data.command.CalculateLoanScheduleCommand;
import org.mifosng.platform.currency.domain.ApplicationCurrency;
import org.mifosng.platform.currency.domain.ApplicationCurrencyRepository;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.InterestMethod;
import org.mifosng.platform.loan.domain.Loan;
import org.mifosng.platform.loan.domain.LoanPayoffSummary;
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
import org.mifosng.platform.loan.domain.LoanRepository;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
import org.mifosng.platform.loanschedule.domain.DefaultLoanScheduleGeneratorFactory;
import org.mifosng.platform.loanschedule.domain.LoanScheduleGenerator;
import org.mifosng.platform.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CalculationPlatformServiceImpl implements
CalculationPlatformService {
private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final LoanRepository loanRepository;
private final ApplicationCurrencyRepository applicationCurrencyRepository;
@Autowired
public CalculationPlatformServiceImpl(final LoanRepository loanRepository, final ApplicationCurrencyRepository applicationCurrencyRepository) {
this.loanRepository = loanRepository;
this.applicationCurrencyRepository = applicationCurrencyRepository;
this.loanScheduleFactory = new DefaultLoanScheduleGeneratorFactory();
}
@Override
public LoanSchedule calculateLoanSchedule(final CalculateLoanScheduleCommand command) {
CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator(command);
validator.validate();
ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(command.getCurrencyCode());
CurrencyData currencyData = new CurrencyData(applicationCurrency.getCode(), applicationCurrency.getName(), command.getDigitsAfterDecimal(),
applicationCurrency.getDisplaySymbol(), applicationCurrency.getNameCode());
MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal());
final BigDecimal principalAmount = BigDecimal.valueOf(command.getPrincipal()
.doubleValue());
final BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(command
.getInterestRatePerPeriod().doubleValue());
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod());
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod());
final Integer repayEvery = command.getRepaymentEvery();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType
.fromInt(command.getRepaymentFrequency());
final Integer defaultNumberOfInstallments = command.getNumberOfRepayments();
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod());
final boolean flexibleRepaymentSchedule = command.isFlexibleRepaymentSchedule();
final boolean interestRebateAllowed = command.isInterestRebateAllowed();
// apr calculator
BigDecimal defaultAnnualNominalInterestRate = BigDecimal.ZERO;
switch (interestPeriodFrequencyType) {
case DAYS:
break;
case WEEKS:
defaultAnnualNominalInterestRate = command
.getInterestRatePerPeriod()
.multiply(BigDecimal.valueOf(52));
break;
case MONTHS:
defaultAnnualNominalInterestRate = command
.getInterestRatePerPeriod()
.multiply(BigDecimal.valueOf(12));
break;
case YEARS:
defaultAnnualNominalInterestRate = command
.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(1));
break;
case INVALID:
break;
}
LoanProductRelatedDetail loanScheduleInfo = new LoanProductRelatedDetail(currency, principalAmount,
defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod,
repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, BigDecimal.ZERO, flexibleRepaymentSchedule, interestRebateAllowed);
LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory
.create(interestMethod, amortizationMethod);
return loanScheduleGenerator.generate(loanScheduleInfo, new LocalDate(
command.getExpectedDisbursementDate()), command
.getRepaymentsStartingFromDate(), command.getInterestCalculatedFromDate(), currencyData);
}
@Override
public LoanPayoffReadModel calculatePayoffOn(final Long loanId,
final LocalDate payoffDate) {
// use jdbc approach to calculating payoff if possible.
final Loan loan = this.loanRepository.findOne(loanId);
final LoanPayoffSummary payoffSummary = loan.getPayoffSummaryOn(payoffDate);
final String currencyCode = payoffSummary.getTotalPaidToDate().getCurrencyCode();
ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(currencyCode);
final int currencyDigitsAfterDecimal = payoffSummary.getTotalPaidToDate().getCurrencyDigitsAfterDecimal();
CurrencyData currency = new CurrencyData(currencyCode, applicationCurrency.getName(), currencyDigitsAfterDecimal, applicationCurrency.getDisplaySymbol(), applicationCurrency.getNameCode());
MoneyData totalPaidToDate = MoneyData.of(currency, payoffSummary.getTotalPaidToDate()
.getAmount());
MoneyData totalInterestOutstandingBasedOnExpectedMaturityDate = MoneyData.of(currency, payoffSummary.getTotalOutstandingBasedOnExpectedMaturityDate()
.getAmount());
MoneyData totalInterestOutstandingBasedOnPayoffDate = MoneyData.of(currency, payoffSummary.getTotalOutstandingBasedOnPayoffDate()
.getAmount());
MoneyData interestRebateOwed = MoneyData.of(currency, payoffSummary.getRebateOwed().getAmount());
return new LoanPayoffReadModel(payoffSummary.getReference().toString(),
payoffSummary.getAcutalDisbursementDate(),
payoffSummary.getExpectedMaturityDate(),
payoffSummary.getProjectedMaturityDate(),
payoffSummary.getExpectedLoanTermInDays(),
payoffSummary.getProjectedLoanTermInDays(), totalPaidToDate,
totalInterestOutstandingBasedOnExpectedMaturityDate,
totalInterestOutstandingBasedOnPayoffDate, interestRebateOwed);
}
}
@@ -0,0 +1,9 @@
package org.mifosng.platform;
public class CannotDeleteLoanException extends RuntimeException {
public CannotDeleteLoanException(final String message) {
super(message);
}
}
@@ -0,0 +1,41 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.ChangePasswordCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
public class ChangePasswordCommandValidator {
private final ChangePasswordCommand command;
public ChangePasswordCommandValidator(ChangePasswordCommand command) {
this.command = command;
}
public void validate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
if (StringUtils.isBlank(command.getPassword())) {
ErrorResponse error = new ErrorResponse("validation.msg.changepassword.password.cannot.be.blank", "password");
dataValidationErrors.add(error);
}
if (StringUtils.isBlank(command.getPasswordrepeat())) {
ErrorResponse error = new ErrorResponse("validation.msg.changepassword.passwordrepeat.cannot.be.blank", "passwordrepeat");
dataValidationErrors.add(error);
} else {
if (!command.getPasswordrepeat().equals(command.getPassword())) {
ErrorResponse error = new ErrorResponse("validation.msg.changepassword.passwordrepeat.not.the.same", "passwordrepeat");
dataValidationErrors.add(error);
}
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
}
@@ -0,0 +1,15 @@
package org.mifosng.platform;
import java.util.Comparator;
import org.mifosng.data.LoanAccountData;
public class ClosedLoanComparator implements Comparator<LoanAccountData> {
@Override
public int compare(final LoanAccountData o1,
final LoanAccountData o2) {
return o1.getClosedOnDate().compareTo(o2.getClosedOnDate());
}
}
@@ -0,0 +1,60 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.EnrollClientCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
public class EnrollClientCommandValidator {
private final EnrollClientCommand command;
public EnrollClientCommandValidator(EnrollClientCommand command) {
this.command = command;
}
public void validate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
if (command.getOfficeId() == null || command.getOfficeId().equals(Long.valueOf(-1))) {
ErrorResponse error = new ErrorResponse("validation.msg.client.office.cannot.be.blank", "officeId");
dataValidationErrors.add(error);
}
if (StringUtils.isNotBlank(command.getFullname()) && (StringUtils.isNotBlank(command.getFirstname()) || StringUtils.isNotBlank(command.getLastname()))) {
ErrorResponse error = new ErrorResponse("validation.msg.client.fullname.cannot.entered.when.firstname.or.lastname.entered", "fullname");
dataValidationErrors.add(error);
}
if (StringUtils.isBlank(command.getFirstname()) && StringUtils.isBlank(command.getLastname()) ) {
if (StringUtils.isBlank(command.getFullname())) {
ErrorResponse error = new ErrorResponse("validation.msg.client.fullname.must.be.provide.or.firstname.lastname", "fullname");
dataValidationErrors.add(error);
}
} else {
if (StringUtils.isBlank(command.getFirstname())) {
ErrorResponse error = new ErrorResponse("validation.msg.client.firstname.cannot.be.blank", "firstname");
dataValidationErrors.add(error);
}
if (StringUtils.isBlank(command.getLastname())) {
ErrorResponse error = new ErrorResponse("validation.msg.client.lastname.cannot.be.blank", "lastname");
dataValidationErrors.add(error);
}
}
if (command.getJoiningDate() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.client.joining.date.cannot.be.blank", "joiningDate");
dataValidationErrors.add(error);
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation erros exist.");
}
}
}
@@ -0,0 +1,21 @@
package org.mifosng.platform;
import org.mifosng.data.command.ImportClientCommand;
import org.mifosng.data.command.ImportLoanCommand;
import org.mifosng.data.command.ImportLoanRepaymentsCommand;
public interface ImportPlatformService {
void importClients(ImportClientCommand command);
ImportClientCommand populateClientImportFromCsv();
void importLoans(ImportLoanCommand command);
ImportLoanCommand populateLoanImportFromCsv();
void importLoanRepayments(ImportLoanRepaymentsCommand command);
ImportLoanRepaymentsCommand populateLoanRepaymentsImportFromCsv();
}
@@ -0,0 +1,658 @@
package org.mifosng.platform;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.mifosng.data.LoanSchedule;
import org.mifosng.data.MoneyData;
import org.mifosng.data.ScheduledLoanInstallment;
import org.mifosng.data.command.CalculateLoanScheduleCommand;
import org.mifosng.data.command.EnrollClientCommand;
import org.mifosng.data.command.ImportClientCommand;
import org.mifosng.data.command.ImportLoanCommand;
import org.mifosng.data.command.ImportLoanRepaymentsCommand;
import org.mifosng.data.command.LoanStateTransitionCommand;
import org.mifosng.data.command.LoanTransactionCommand;
import org.mifosng.data.command.SubmitApproveDisburseLoanCommand;
import org.mifosng.data.command.SubmitLoanApplicationCommand;
import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.client.domain.ClientRepository;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.exceptions.ClientNotAuthenticatedException;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.DefaultLoanLifecycleStateMachine;
import org.mifosng.platform.loan.domain.InterestMethod;
import org.mifosng.platform.loan.domain.Loan;
import org.mifosng.platform.loan.domain.LoanBuilder;
import org.mifosng.platform.loan.domain.LoanLifecycleStateMachine;
import org.mifosng.platform.loan.domain.LoanProduct;
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
import org.mifosng.platform.loan.domain.LoanProductRepository;
import org.mifosng.platform.loan.domain.LoanRepaymentScheduleInstallment;
import org.mifosng.platform.loan.domain.LoanRepository;
import org.mifosng.platform.loan.domain.LoanStatus;
import org.mifosng.platform.loan.domain.LoanStatusRepository;
import org.mifosng.platform.loan.domain.LoanTransaction;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
import org.mifosng.platform.organisation.domain.Office;
import org.mifosng.platform.organisation.domain.OfficeRepository;
import org.mifosng.platform.organisation.domain.Organisation;
import org.mifosng.platform.user.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.csvreader.CsvReader;
@Service
public class ImportPlatformServiceImpl implements ImportPlatformService {
private final OfficeRepository officeRepository;
private final ClientRepository clientRepository;
private final LoanProductRepository loanProductRepository;
private final CalculationPlatformService calculationPlatformService;
private final LoanRepository loanRepository;
private final LoanStatusRepository loanStatusRepository;
@Autowired
public ImportPlatformServiceImpl(
final OfficeRepository officeRepository, final ClientRepository clientRepository, final LoanProductRepository loanProductRepository,
final CalculationPlatformService calculationPlatformService, final LoanRepository loanRepository, final LoanStatusRepository loanStatusRepository) {
this.officeRepository = officeRepository;
this.clientRepository = clientRepository;
this.loanProductRepository = loanProductRepository;
this.calculationPlatformService = calculationPlatformService;
this.loanRepository = loanRepository;
this.loanStatusRepository = loanStatusRepository;
}
private AppUser extractAuthenticatedUser() {
AppUser currentUser = null;
SecurityContext context = SecurityContextHolder.getContext();
if (context != null) {
Authentication auth = context.getAuthentication();
if (auth != null) {
currentUser = (AppUser) auth.getPrincipal();
}
}
if (currentUser == null) {
throw new ClientNotAuthenticatedException();
}
return currentUser;
}
@Transactional
@Override
public void importClients(ImportClientCommand command) {
AppUser currentUser = extractAuthenticatedUser();
List<Office> allOffices = this.officeRepository.findAll(officesBelongingTo(currentUser.getOrganisation()));
List<Client> newClientCollection = new ArrayList<Client>();
for (EnrollClientCommand client : command.getClients()) {
Office clientOffice = findById(allOffices, client.getOfficeId());
Client newClient = Client.newClient(currentUser.getOrganisation(), clientOffice, client.getFirstname(), client.getLastname(), client.getJoiningDate(), client.getExternalId());
newClientCollection.add(newClient);
}
this.clientRepository.save(newClientCollection);
}
@Transactional
@Override
public void importLoans(ImportLoanCommand importCommand) {
AppUser currentUser = extractAuthenticatedUser();
List<Loan> newLoanCollection = new ArrayList<Loan>();
for (SubmitApproveDisburseLoanCommand combinedCommand : importCommand.getLoans()) {
SubmitLoanApplicationCommand command = combinedCommand.getSubmitLoanApplicationCommand();
// most of this code can be extracted out into an Assembler
LoanProduct loanProduct = this.loanProductRepository.findOne(command
.getProductId());
Client client = this.clientRepository.findOne(command.getApplicantId());
MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal());
final BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(command
.getInterestRatePerPeriod().doubleValue());
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod());
// apr calculator
BigDecimal defaultAnnualNominalInterestRate = BigDecimal.ZERO;
switch (interestPeriodFrequencyType) {
case DAYS:
break;
case WEEKS:
defaultAnnualNominalInterestRate = command.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(52));
break;
case MONTHS:
defaultAnnualNominalInterestRate = command.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(12));
break;
case YEARS:
defaultAnnualNominalInterestRate = command.getInterestRatePerPeriod();
break;
case INVALID:
break;
}
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod());
final Integer repayEvery = command.getRepaymentEvery();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType
.fromInt(command.getRepaymentFrequency());
final Integer defaultNumberOfInstallments = command.getNumberOfRepayments();
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod());
final boolean flexibleRepaymentSchedule = command.isFlexibleRepaymentSchedule();
final boolean interestRebateAllowed = command.isInterestRebateAllowed();
LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency, command.getPrincipal(),
defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod,
repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, command.getInArrearsToleranceAmount(), flexibleRepaymentSchedule, interestRebateAllowed);
LoanSchedule loanSchedule = command.getLoanSchedule();
List<ScheduledLoanInstallment> loanRepaymentSchedule = loanSchedule
.getScheduledLoanInstallments();
Loan loan = new LoanBuilder().with(currentUser.getOrganisation())
.with(loanProduct).with(client)
.withExternalSystemId(command.getExternalId())
.with(loanRepaymentScheduleDetail).build();
for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) {
MoneyData readPrincipalDue = scheduledLoanInstallment
.getPrincipalDue();
MoneyData readInterestDue = scheduledLoanInstallment
.getInterestDue();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(
loan, scheduledLoanInstallment.getInstallmentNumber(),
scheduledLoanInstallment.getPeriodStart(),
scheduledLoanInstallment.getPeriodEnd(), readPrincipalDue.getAmount(),
readInterestDue.getAmount());
loan.addRepaymentScheduleInstallment(installment);
}
LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine();
loan.submitApplication(command.getSubmittedOnDate(), command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(), command.getInterestCalculatedFromDate(), loanLifecycleStateMachine);
LoanStateTransitionCommand approveLoanCommand = combinedCommand.getApproveLoanCommand();
if (approveLoanCommand != null) {
loan.approve(approveLoanCommand.getEventDate(), loanLifecycleStateMachine);
}
LoanStateTransitionCommand disburseLoanCommand = combinedCommand.getDisburseLoanCommand();
if (disburseLoanCommand != null) {
loan.disburse(disburseLoanCommand.getEventDate(), loanLifecycleStateMachine);
}
newLoanCollection.add(loan);
}
this.loanRepository.save(newLoanCollection);
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
List<LoanStatus> allowedLoanStatuses = this.loanStatusRepository.findAll();
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
}
@Transactional
@Override
public void importLoanRepayments(ImportLoanRepaymentsCommand command) {
AppUser currentUser = extractAuthenticatedUser();
LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine();
List<Loan> newLoanCollection = new ArrayList<Loan>();
List<Loan> allLoans = this.loanRepository.findAll(loansBelongingTo(currentUser.getOrganisation()));
for (LoanTransactionCommand repaymentDetail : command.getRepayments()) {
Loan loan = findLoanByIdentifier(allLoans, repaymentDetail.getLoanId().toString());
Money repaymentAmount = Money.of(loan.getCurrency(), repaymentDetail.getPaymentAmount());
LoanTransaction loanRepayment = LoanTransaction.repayment(repaymentAmount, repaymentDetail.getPaymentDate());
loan.makeRepayment(loanRepayment, loanLifecycleStateMachine);
}
this.loanRepository.save(newLoanCollection);
}
public static Specification<Office> officesBelongingTo(final Organisation organisation) {
return new Specification<Office>() {
@Override
public Predicate toPredicate(final Root<Office> root,
final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.equal(root.get("organisation"), organisation);
}
};
}
public static Specification<LoanProduct> productsBelongingTo(final Organisation organisation) {
return new Specification<LoanProduct>() {
@Override
public Predicate toPredicate(final Root<LoanProduct> root,
final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.equal(root.get("organisation"), organisation);
}
};
}
public static Specification<Loan> loansBelongingTo(final Organisation organisation) {
return new Specification<Loan>() {
@Override
public Predicate toPredicate(final Root<Loan> root,
final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.equal(root.get("organisation"), organisation);
}
};
}
public static Specification<Client> clientsBelongingTo(final Organisation organisation) {
return new Specification<Client>() {
@Override
public Predicate toPredicate(final Root<Client> root,
final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.equal(root.get("organisation"), organisation);
}
};
}
@Override
public ImportLoanRepaymentsCommand populateLoanRepaymentsImportFromCsv() {
ImportLoanRepaymentsCommand command = new ImportLoanRepaymentsCommand();
AppUser currentUser = extractAuthenticatedUser();
List<Loan> allLoans = this.loanRepository.findAll(loansBelongingTo(currentUser.getOrganisation()));
ClassPathResource repaymentsCsvFile = new ClassPathResource("creocore-loan-repayments.csv");
File file = null;
FileInputStream fileInputStream = null;
CsvReader loans = null;
try {
DateTimeFormatter isoParser = ISODateTimeFormat.date();
loans = new CsvReader(repaymentsCsvFile.getInputStream(),
Charset.defaultCharset());
loans.readHeaders();
List<LoanTransactionCommand> repaymentDetails = new ArrayList<LoanTransactionCommand>();
while (loans.readRecord()) {
String loanExternalId = loans.get("LoanExternalId");
Loan matchingLoan = findLoanByIdentifier(allLoans, loanExternalId.trim());
String paymentDate = loans.get("PaymentDate");
LocalDate paidOnDate = null;
if (StringUtils.isNotBlank(paymentDate)) {
paidOnDate = new LocalDate(isoParser.parseDateTime(paymentDate));
}
String paymentAmount = loans.get("PaymentAmount");
BigDecimal repayment = BigDecimal.ZERO;
if (StringUtils.isNotBlank(paymentAmount)) {
repayment = BigDecimal.valueOf(Double.valueOf(paymentAmount));
}
String noNoteComment = "No note due to migration from mifos.";
LoanTransactionCommand repaymentCommand = new LoanTransactionCommand(matchingLoan.getId(), paidOnDate, noNoteComment, repayment);
repaymentDetails.add(repaymentCommand);
}
command.setRepayments(repaymentDetails);
return command;
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (file != null) {
file = null;
}
if (fileInputStream != null) {
fileInputStream = null;
}
if (loans != null) {
loans = null;
}
}
}
@Override
public ImportLoanCommand populateLoanImportFromCsv() {
ImportLoanCommand command = new ImportLoanCommand();
AppUser currentUser = extractAuthenticatedUser();
List<LoanProduct> allProducts = this.loanProductRepository.findAll(productsBelongingTo(currentUser.getOrganisation()));
List<Client> allClients = this.clientRepository.findAll(clientsBelongingTo(currentUser.getOrganisation()));
ClassPathResource loanCsvFile = new ClassPathResource("creocore-loans.csv");
File file = null;
FileInputStream fileInputStream = null;
CsvReader loans = null;
try {
DateTimeFormatter isoParser = ISODateTimeFormat.date();
loans = new CsvReader(loanCsvFile.getInputStream(),
Charset.defaultCharset());
loans.readHeaders();
List<SubmitApproveDisburseLoanCommand> parsedLoanDetails = new ArrayList<SubmitApproveDisburseLoanCommand>();
while (loans.readRecord()) {
String clientIdentifier = loans.get("ClientExternalId");
Client matchingClient = findClientByIdentifier(allClients, clientIdentifier.trim());
String loanExternalId = loans.get("LoanExternalId");
String productIdentifier = loans.get("ProductExternalId");
LoanProduct matchingProduct = findProductByIdentifier(allProducts, productIdentifier);
String loanPrincipal = loans.get("LoanPrincipal");
Number principal = BigDecimal.ZERO;
if (StringUtils.isNotBlank(loanPrincipal)) {
principal = Double.valueOf(loanPrincipal);
}
String interestRateAsString = loans.get("InterestRate");
Number interestRatePerYear = BigDecimal.ZERO;
if (StringUtils.isNotBlank(interestRateAsString)) {
interestRatePerYear = Double.valueOf(interestRateAsString);
}
String submitted = loans.get("SubmittedOn");
LocalDate submittedOnDate = null;
if (StringUtils.isNotBlank(submitted)) {
submittedOnDate = new LocalDate(isoParser.parseDateTime(submitted));
}
String noNoteComment = "No note due to migration from mifos.";
String installmentsAsString = loans.get("Installments");
Integer numberOfRepayments = Integer.valueOf(0);
if (StringUtils.isNotBlank(installmentsAsString)) {
numberOfRepayments = Integer.valueOf(installmentsAsString);
}
String approved = loans.get("ApprovedOn");
LocalDate approvedDate = null;
if (StringUtils.isNotBlank(approved)) {
approvedDate = new LocalDate(isoParser.parseDateTime(approved));
}
String disbursed = loans.get("DateDisbursalTransaction");
LocalDate disbursedDate = null;
LocalDate expectedDisbursementDate = null;
if (StringUtils.isNotBlank(disbursed)) {
disbursedDate = new LocalDate(isoParser.parseDateTime(disbursed));
expectedDisbursementDate = disbursedDate;
} else {
expectedDisbursementDate = approvedDate;
}
String repaymentsStartOn = loans.get("FirstInstallmentDate");
LocalDate repaymentsStartingFromDate = null;
LocalDate interestCalculatedFromDate = null;
if (StringUtils.isNotBlank(repaymentsStartOn)) {
repaymentsStartingFromDate = new LocalDate(isoParser.parseDateTime(repaymentsStartOn));
interestCalculatedFromDate = expectedDisbursementDate;
}
String currencyCode = matchingProduct.getCurrency().getCode();
int digitsAfterDecimal = matchingProduct.getCurrency().getDigitsAfterDecimal();
boolean flexibleRepaymentSchedule = matchingProduct.isFlexibleRepaymentSchedule();
Integer repaymentEvery = matchingProduct.getRepayEvery();
Integer repaymentFrequency = matchingProduct.getRepaymentPeriodFrequencyType().getValue();
Integer amortizationMethod = matchingProduct.getAmortizationMethod().getValue();
final Money toleranceAmount = matchingProduct.getInArrearsTolerance();
Number interestRatePerPeriod = interestRatePerYear;
Integer interestRatePeriodFrequency = PeriodFrequencyType.YEARS.getValue();
Integer interestMethod = matchingProduct.getInterestMethod().getValue();
CalculateLoanScheduleCommand calculateLoanScheduleCommand = new CalculateLoanScheduleCommand(currencyCode, digitsAfterDecimal, principal,
interestRatePerPeriod,
interestRatePeriodFrequency,
interestMethod,
repaymentEvery,
repaymentFrequency, numberOfRepayments, amortizationMethod, flexibleRepaymentSchedule,
matchingProduct.isInterestRebateAllowed(), expectedDisbursementDate, repaymentsStartingFromDate, interestCalculatedFromDate);
LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(calculateLoanScheduleCommand);
SubmitLoanApplicationCommand parsedNewLoanDetails = new SubmitLoanApplicationCommand(matchingClient.getId(), matchingProduct.getId(), submittedOnDate, noNoteComment,
expectedDisbursementDate, repaymentsStartingFromDate, interestCalculatedFromDate, loanSchedule,
currencyCode, digitsAfterDecimal, principal,
interestRatePerPeriod, interestRatePeriodFrequency, interestMethod,
repaymentEvery, repaymentFrequency, numberOfRepayments, amortizationMethod, toleranceAmount.getAmount(),
flexibleRepaymentSchedule, matchingProduct.isInterestRebateAllowed());
parsedNewLoanDetails.setExternalId(loanExternalId);
LoanStateTransitionCommand approvedLoanCommand = null;
if (approvedDate != null) {
approvedLoanCommand = new LoanStateTransitionCommand(null, approvedDate, noNoteComment);
}
LoanStateTransitionCommand disburseLoanCommand = null;
if (disbursedDate != null) {
disburseLoanCommand = new LoanStateTransitionCommand(null, disbursedDate, noNoteComment);
}
SubmitApproveDisburseLoanCommand submitApproveDisburseLoanCommand = new SubmitApproveDisburseLoanCommand();
submitApproveDisburseLoanCommand.setSubmitLoanApplicationCommand(parsedNewLoanDetails);
submitApproveDisburseLoanCommand.setApproveLoanCommand(approvedLoanCommand);
submitApproveDisburseLoanCommand.setDisburseLoanCommand(disburseLoanCommand);
parsedLoanDetails.add(submitApproveDisburseLoanCommand);
}
command.setLoans(parsedLoanDetails);
return command;
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
// throw new BulkuploadFileNotFoundException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
// throw new BulkuploadInvalidDataException(e);
} catch (IOException e) {
throw new RuntimeException(e);
// throw new BulkuploadParseException(e);
} finally {
if (file != null) {
file = null;
}
if (fileInputStream != null) {
fileInputStream = null;
}
if (loans != null) {
loans = null;
}
}
}
@Override
public ImportClientCommand populateClientImportFromCsv() {
ImportClientCommand command = new ImportClientCommand();
AppUser currentUser = extractAuthenticatedUser();
List<Office> allOffices = this.officeRepository.findAll(officesBelongingTo(currentUser.getOrganisation()));
ClassPathResource clientCsv = new ClassPathResource("creocore-clients.csv");
File file = null;
FileInputStream fileInputStream = null;
CsvReader clients = null;
try {
DateTimeFormatter isoParser = ISODateTimeFormat.date();
clients = new CsvReader(clientCsv.getInputStream(),
Charset.defaultCharset());
clients.readHeaders();
// int clientCsvHeaderCount = clients.getHeaderCount();
// if (clientCsvHeaderCount != 5) {
// success = false;
// }
List<EnrollClientCommand> parsedClients = new ArrayList<EnrollClientCommand>();
while (clients.readRecord()) {
String externalId = clients.get("ExternalId");
String firstname = clients.get("FirstName");
String lastname = clients.get("LastName");
String joined = clients.get("Joined");
String officeIdentifier = clients.get("Office");
Office matchingOffice = findOfficeByIdentifier(allOffices, officeIdentifier.trim());
LocalDate joiningDate = new LocalDate(isoParser.parseDateTime(joined));
EnrollClientCommand parsedClient = new EnrollClientCommand(firstname, lastname, "", matchingOffice.getId(), joiningDate);
parsedClient.setExternalId(externalId);
parsedClients.add(parsedClient);
}
command.setClients(parsedClients);
return command;
} catch (FileNotFoundException e) {
// throw new BulkuploadFileNotFoundException(e);
} catch (IllegalArgumentException e) {
// throw new BulkuploadInvalidDataException(e);
} catch (IOException e) {
// throw new BulkuploadParseException(e);
} finally {
if (file != null) {
file = null;
}
if (fileInputStream != null) {
fileInputStream = null;
}
if (clients != null) {
clients = null;
}
}
return command;
}
private Office findById(List<Office> allOffices, Long id) {
Office match = null;
for (Office office : allOffices) {
if (id.equals(office.getId())) {
match = office;
break;
}
}
return match;
}
private Loan findLoanByIdentifier(List<Loan> allEntities, String identifier) {
Loan match = null;
for (Loan entity : allEntities) {
if (entity.identifiedBy(identifier)) {
match = entity;
break;
}
}
return match;
}
private Office findOfficeByIdentifier(List<Office> allOffices, String officeIdentifier) {
Office match = null;
for (Office office : allOffices) {
if (office.identifiedBy(officeIdentifier)) {
match = office;
break;
}
}
return match;
}
private Client findClientByIdentifier(List<Client> allClients, String identifier) {
Client match = null;
for (Client client : allClients) {
if (client.identifiedBy(identifier)) {
match = client;
break;
}
}
return match;
}
private LoanProduct findProductByIdentifier(List<LoanProduct> allProducts, String identifier) {
LoanProduct match = null;
for (LoanProduct client : allProducts) {
if (client.identifiedBy(identifier)) {
match = client;
break;
}
}
return match;
}
}
@@ -0,0 +1,17 @@
package org.mifosng.platform;
import java.sql.SQLException;
public class InvalidSqlException extends RuntimeException {
private final String sql;
public InvalidSqlException(SQLException e, String sql) {
super(e);
this.sql = sql;
}
public String getSql() {
return sql;
}
}
@@ -0,0 +1,219 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.CreateLoanProductCommand;
import org.mifosng.data.command.LoanProductCommandData;
import org.mifosng.data.command.UpdateLoanProductCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.InterestMethod;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
public class LoanProductValidator {
public void validateForUpdate(UpdateLoanProductCommand command) {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
Long productId = command.getId();
if (productId == null) {
ErrorResponse error = new ErrorResponse("validation.msg.product.id.cannot.be.empty", "id");
dataValidationErrors.add(error);
}
validateRemainingAttributes(command, dataValidationErrors);
}
public void validateForCreate(CreateLoanProductCommand command) {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
validateRemainingAttributes(command, dataValidationErrors);
}
private void validateRemainingAttributes(LoanProductCommandData command, List<ErrorResponse> dataValidationErrors) {
if (StringUtils.isBlank(command.getName())) {
ErrorResponse error = new ErrorResponse("validation.msg.product.name.cannot.be.blank", "name");
dataValidationErrors.add(error);
}
if (command.getDescription() != null) {
if (command.getDescription().length() > 500) {
ErrorResponse error = new ErrorResponse("validation.msg.product.description.exceeds.max.length", "description");
dataValidationErrors.add(error);
}
}
if (StringUtils.isBlank(command.getCurrencyCode())) {
ErrorResponse error = new ErrorResponse("validation.msg.product.currency.cannot.be.empty", "currencyCode");
dataValidationErrors.add(error);
}
if (command.getDigitsAfterDecimal() != null) {
if (command.getDigitsAfterDecimal() < 0 || command.getDigitsAfterDecimal() > 6) {
ErrorResponse error = new ErrorResponse("validation.msg.product.currency.digitsAfterDecimal.must.be.between.zero.and.six.inclusive", "selectedDigitsAfterDecimal", command.getDigitsAfterDecimal());
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.currency.digitsAfterDecimal.cannot.be.empty", "selectedDigitsAfterDecimal");
dataValidationErrors.add(error);
}
if (command.getPrincipal() != null) {
if (command.getPrincipal().doubleValue() <= Double.valueOf("0.0")) {
ErrorResponse error = new ErrorResponse("validation.msg.product.principal.amount.must.be.greater.than.zero", "amount");
dataValidationErrors.add(error);
}
// TODO - check number of digits before decimal and after decimal
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.principal.amount.cannot.be.empty", "amount");
dataValidationErrors.add(error);
}
if (command.getRepaymentFrequency() != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequency());
ErrorResponse error = null;
switch (frequencyType) {
case DAYS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 365);
break;
case WEEKS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 52);
break;
case MONTHS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 12);
break;
case YEARS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 2);
break;
default:
error = new ErrorResponse("validation.msg.product.repayment.frequency.invalid", "selectedRepaymentFrequencyOption");
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.repayment.frequency.cannot.be.empty", "selectedRepaymentFrequencyOption");
dataValidationErrors.add(error);
}
if (command.getNumberOfRepayments() != null) {
if (command.getNumberOfRepayments() < 1 || command.getNumberOfRepayments() > 100) {
ErrorResponse error = new ErrorResponse("validation.msg.product.number.of.repayments.is.out.of.range", "numberOfRepayments");
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.number.of.repayments.cannot.be.empty", "numberOfRepayments");
dataValidationErrors.add(error);
}
if (command.getAmortizationMethod() != null) {
AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod());
ErrorResponse error = null;
switch (amortizationMethod) {
case EQUAL_PRINCIPAL:
break;
case EQUAL_INSTALLMENTS:
break;
default:
error = new ErrorResponse("validation.msg.product.amortization.method.invalid", "selectedAmortizationMethodOption");
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.amortization.method.cannot.be.empty", "selectedAmortizationMethodOption");
dataValidationErrors.add(error);
}
// refactor in validation of monetary value.
if (command.getInArrearsToleranceAmount() != null) {
if (command.getInArrearsToleranceAmount().doubleValue() < Double.valueOf("0.0")) {
ErrorResponse error = new ErrorResponse("validation.msg.product.arrears.tolerance.amount.cannot.be.negative", "inArrearsTolerance");
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.arrears.tolerance.amount.cannot.be.empty", "inArrearsTolerance");
dataValidationErrors.add(error);
}
if (command.getInterestRatePerPeriod()!= null) {
if (command.getInterestRatePerPeriod().doubleValue() < Double.valueOf("0.0")) {
ErrorResponse error = new ErrorResponse("validation.msg.product.interest.rate.per.period.amount.cannot.be.negative", "nominalInterestRate");
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.interest.rate.per.period.amount.cannot.be.empty", "nominalInterestRate");
dataValidationErrors.add(error);
}
// interest rate period type
if (command.getInterestRateFrequencyMethod() != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod());
ErrorResponse error = null;
switch (frequencyType) {
case DAYS:
break;
case WEEKS:
break;
case MONTHS:
break;
case YEARS:
break;
default:
error = new ErrorResponse("validation.msg.product.interest.frequency.invalid", "selectedInterestFrequencyOption");
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.interest.frequency.cannot.be.empty", "selectedInterestFrequencyOption");
dataValidationErrors.add(error);
}
if (command.getInterestMethod() != null) {
InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod());
ErrorResponse error = null;
switch (interestMethod) {
case DECLINING_BALANCE:
break;
case FLAT:
break;
default:
error = new ErrorResponse("validation.msg.product.interest.method.invalid", "selectedInterestMethodOption");
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ErrorResponse error = new ErrorResponse("validation.msg.product.interest.method.cannot.be.empty", "selectedInterestMethodOption");
dataValidationErrors.add(error);
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Validation errors exist.");
}
}
private ErrorResponse validateNumberExistsAndInRange(String errorCodePreFix, String fieldName, Integer value, int min, int max) {
ErrorResponse error = null;
if (value == null) {
error = new ErrorResponse(errorCodePreFix + ".cannot.be.empty", fieldName);
} else {
if (value < min || value > max) {
error = new ErrorResponse(errorCodePreFix + ".outside.allowed.range", fieldName);
}
}
return error;
}
}
@@ -0,0 +1,42 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.LoanStateTransitionCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
public class LoanStateTransitionCommandValidator {
private final LoanStateTransitionCommand command;
public LoanStateTransitionCommandValidator(LoanStateTransitionCommand command) {
this.command = command;
}
public void validate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
if (command.getLoanId() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.id.is.invalid", "loanId");
dataValidationErrors.add(error);
}
if (command.getEventDate() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.state.transition.date.cannot.be.blank", "eventDate");
dataValidationErrors.add(error);
}
if (StringUtils.isNotBlank(command.getComment()) && command.getComment().length() > 1000) {
ErrorResponse error = new ErrorResponse("validation.msg.note.exceeds.max.length", "comment");
dataValidationErrors.add(error);
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
}
@@ -0,0 +1,47 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.LoanTransactionCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
public class LoanTransactionValidator {
private final LoanTransactionCommand command;
public LoanTransactionValidator(LoanTransactionCommand command) {
this.command = command;
}
public void validate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
if (command.getLoanId() == null || command.getLoanId().doubleValue() <= 0) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.id.is.invalid", "loanId");
dataValidationErrors.add(error);
}
if (command.getPaymentDate() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.repayment.date.cannot.be.blank", "transactionDate");
dataValidationErrors.add(error);
}
if (command.getPaymentAmount() == null || command.getPaymentAmount().doubleValue() <= 0) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.repayment.must.be.greater.than.zero", "transactionAmount");
dataValidationErrors.add(error);
}
if (StringUtils.isNotBlank(command.getComment()) && command.getComment().length() > 1000) {
ErrorResponse error = new ErrorResponse("validation.msg.note.exceeds.max.length", "transactionComment");
dataValidationErrors.add(error);
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
}
@@ -0,0 +1,56 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosng.data.ErrorResponse;
import org.mifosng.platform.exceptions.NewDataValidationException;
public class OfficeValidator {
private final String name;
private final Long parentId;
private final LocalDate openingDate;
private final String externalId;
public OfficeValidator(String name, Long parentId, LocalDate openingDate, String externalId) {
this.name = name;
this.parentId = parentId;
this.openingDate = openingDate;
this.externalId = externalId;
}
public void validate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
if (StringUtils.isBlank(this.name)) {
ErrorResponse error = new ErrorResponse("validation.msg.office.name.cannot.be.blank", "name");
dataValidationErrors.add(error);
}
if (this.parentId == null || parentId.equals(Long.valueOf(-1))) {
ErrorResponse error = new ErrorResponse("validation.msg.office.parent.cannot.be.blank", "parentId");
dataValidationErrors.add(error);
}
if (this.openingDate == null) {
ErrorResponse error = new ErrorResponse("validation.msg.office.opening.date.cannot.be.blank", "openingDate");
dataValidationErrors.add(error);
}
if (StringUtils.isNotBlank(this.externalId)) {
if (this.externalId.trim().length() > 100) {
ErrorResponse error = new ErrorResponse("validation.msg.office.externalId.exceeds.max.length", "externalId");
dataValidationErrors.add(error);
}
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
}
@@ -0,0 +1,100 @@
package org.mifosng.platform;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.mifosng.data.AppUserData;
import org.mifosng.data.ClientData;
import org.mifosng.data.ClientDataWithAccountsData;
import org.mifosng.data.CurrencyData;
import org.mifosng.data.EnumOptionReadModel;
import org.mifosng.data.ExtraDatasets;
import org.mifosng.data.LoanAccountData;
import org.mifosng.data.LoanProductData;
import org.mifosng.data.LoanRepaymentData;
import org.mifosng.data.NewLoanWorkflowStepOneData;
import org.mifosng.data.NoteData;
import org.mifosng.data.OfficeData;
import org.mifosng.data.OrganisationReadModel;
import org.mifosng.data.PermissionData;
import org.mifosng.data.RoleData;
import org.mifosng.data.reports.GenericResultset;
import org.springframework.security.access.prepost.PreAuthorize;
public interface ReadPlatformService {
Collection<OrganisationReadModel> retrieveAll();
Collection<ClientData> retrieveAllIndividualClients();
Collection<OfficeData> retrieveAllOffices();
ClientData retrieveIndividualClient(Long clientId);
ClientDataWithAccountsData retrieveClientAccountDetails(Long clientId);
NewLoanWorkflowStepOneData retrieveClientAndProductDetails(Long clientId, Long productId);
LoanAccountData retrieveLoanAccountDetails(Long loanId);
LoanRepaymentData retrieveNewLoanRepaymentDetails(Long loanId);
LoanRepaymentData retrieveNewLoanWaiverDetails(Long loanId);
LoanRepaymentData retrieveLoanRepaymentDetails(Long loanId, Long repaymentId);
Collection<LoanProductData> retrieveAllLoanProducts();
List<CurrencyData> retrieveAllowedCurrencies();
List<CurrencyData> retrieveAllPlatformCurrencies();
List<EnumOptionReadModel> retrieveLoanAmortizationMethodOptions();
List<EnumOptionReadModel> retrieveLoanInterestMethodOptions();
List<EnumOptionReadModel> retrieveRepaymentFrequencyOptions();
List<EnumOptionReadModel> retrieveInterestFrequencyOptions();
LoanProductData retrieveLoanProduct(Long productId);
LoanProductData retrieveNewLoanProductDetails();
OfficeData retrieveOffice(Long officeId);
Collection<AppUserData> retrieveAllUsers();
AppUserData retrieveNewUserDetails();
AppUserData retrieveUser(Long userId);
AppUserData retrieveCurrentUser();
Collection<RoleData> retrieveAllRoles();
RoleData retrieveRole(Long roleId);
Collection<EnumOptionReadModel> retrieveAllPermissionGroups();
Collection<PermissionData> retrieveAllPermissions();
@PreAuthorize(value = "hasAnyRole('REPORTING_SUPER_USER_ROLE')")
GenericResultset retrieveGenericResultset(String rptDB, String name,
String type, Map<String, String> extractedQueryParams);
ClientData retrieveNewClientDetails();
Collection<NoteData> retrieveAllClientNotes(Long clientId);
NoteData retrieveClientNote(Long clientId, Long noteId);
ExtraDatasets retrieveExtraDatasetNames(String datasetType);
GenericResultset retrieveExtraData(String datasetType, String datasetName,
String datasetPKValue);
void tempSaveExtraData(String datasetType, String datasetName,
String datasetPKValue, Map<String, String> queryParams);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,66 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.RoleCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
import org.springframework.util.ObjectUtils;
public class RoleValidator {
private final RoleCommand command;
public RoleValidator(RoleCommand command) {
this.command = command;
}
public void validateForCreate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
validate(dataValidationErrors);
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
public void validateForUpdate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
if (command.getId() == null || command.getId() < 1) {
ErrorResponse error = new ErrorResponse("validation.msg.role.id.not.provided", "id");
dataValidationErrors.add(error);
}
validate(dataValidationErrors);
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
private void validate(List<ErrorResponse> dataValidationErrors) {
if (StringUtils.isBlank(command.getName())) {
ErrorResponse error = new ErrorResponse("validation.msg.role.name.cannot.be.blank", "name");
dataValidationErrors.add(error);
}
if (StringUtils.isBlank(command.getDescription())) {
ErrorResponse error = new ErrorResponse("validation.msg.role.description.cannot.be.blank", "description");
dataValidationErrors.add(error);
} else {
if (command.getDescription().trim().length() > 500) {
ErrorResponse error = new ErrorResponse("validation.msg.role.description.exceeds.max.length", "description");
dataValidationErrors.add(error);
}
}
if (ObjectUtils.isEmpty(command.getPermissionIds())) {
ErrorResponse error = new ErrorResponse("validation.msg.role.permissions.cannot.be.empty", "selectedItems");
dataValidationErrors.add(error);
}
}
}
@@ -0,0 +1,105 @@
package org.mifosng.platform;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.loan.domain.Loan;
import org.mifosng.platform.loan.domain.LoanProduct;
import org.mifosng.platform.loan.domain.LoanTransaction;
import org.mifosng.platform.organisation.domain.Office;
import org.mifosng.platform.organisation.domain.Organisation;
import org.mifosng.platform.user.domain.AppUser;
import org.mifosng.platform.user.domain.Role;
import org.springframework.data.jpa.domain.Specification;
public final class Specifications {
public static Specification<AppUser> usersThatMatch(final Organisation organisation, final Long id) {
return new Specification<AppUser>() {
@Override
public Predicate toPredicate(final Root<AppUser> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id));
}
};
}
public static Specification<AppUser> usersThatMatch(final Organisation organisation, final String username) {
return new Specification<AppUser>() {
@Override
public Predicate toPredicate(final Root<AppUser> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("username"), username));
}
};
}
public static Specification<Role> rolesThatMatch(final Organisation organisation, final Long id) {
return new Specification<Role>() {
@Override
public Predicate toPredicate(final Root<Role> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id));
}
};
}
public static Specification<LoanTransaction> loanTransactionsThatMatch(final Organisation organisation, final Long id) {
return new Specification<LoanTransaction>() {
@Override
public Predicate toPredicate(final Root<LoanTransaction> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id));
}
};
}
public static Specification<Loan> loansThatMatch(final Organisation organisation, final Long id) {
return new Specification<Loan>() {
@Override
public Predicate toPredicate(final Root<Loan> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id));
}
};
}
public static Specification<Loan> loansThatMatch(final Organisation organisation, final Client client) {
return new Specification<Loan>() {
@Override
public Predicate toPredicate(final Root<Loan> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("client"), client));
}
};
}
public static Specification<LoanProduct> productThatMatches(final Organisation organisation, final Long id) {
return new Specification<LoanProduct>() {
@Override
public Predicate toPredicate(final Root<LoanProduct> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id));
}
};
}
public static Specification<Office> officesThatMatch(final Organisation organisation, final Long id) {
return new Specification<Office>() {
@Override
public Predicate toPredicate(final Root<Office> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id));
}
};
}
public static Specification<Client> clientsThatMatch(final Organisation organisation, final Long id) {
return new Specification<Client>() {
@Override
public Predicate toPredicate(final Root<Client> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id));
}
};
}
}
@@ -0,0 +1,64 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.CalculateLoanScheduleCommand;
import org.mifosng.data.command.SubmitLoanApplicationCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
public class SubmitLoanApplicationCommandValidator {
private final SubmitLoanApplicationCommand command;
public SubmitLoanApplicationCommandValidator(SubmitLoanApplicationCommand command) {
this.command = command;
}
public void validate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
if (command.getLoanSchedule() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.submit.loan.loan.schedule.cannot.be.blank", "loanSchedule");
dataValidationErrors.add(error);
}
if (command.getSubmittedOnDate() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.submitted.on.date.cannot.be.blank", "submittedOnDate");
dataValidationErrors.add(error);
} else {
if (command.getSubmittedOnDate().isAfter(command.getExpectedDisbursementDate())) {
ErrorResponse error = new ErrorResponse("validation.msg.loan.submitted.on.date.cannot.be.after.expectedDisbursementDate", "submittedOnDate");
dataValidationErrors.add(error);
}
}
try {
// reuse calculate loan schedule validator for now
CalculateLoanScheduleCommand calculateLoanScheduleCommand = new CalculateLoanScheduleCommand(
command.getCurrencyCode(), command.getDigitsAfterDecimal(),
command.getPrincipal(), command.getInterestRatePerPeriod(),
command.getInterestRateFrequencyMethod(),
command.getInterestMethod(), command.getRepaymentEvery(),
command.getRepaymentFrequency(),
command.getNumberOfRepayments(),
command.getAmortizationMethod(),
command.isFlexibleRepaymentSchedule(),
command.isInterestRebateAllowed(),
command.getExpectedDisbursementDate(),
command.getRepaymentsStartingFromDate(),
command.getInterestCalculatedFromDate());
CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator(
calculateLoanScheduleCommand);
validator.validate();
} catch (NewDataValidationException e) {
dataValidationErrors.addAll(e.getValidationErrors());
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
}
@@ -0,0 +1,71 @@
package org.mifosng.platform;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ErrorResponse;
import org.mifosng.data.command.UserCommand;
import org.mifosng.platform.exceptions.NewDataValidationException;
public class UserValidator {
private final UserCommand command;
public UserValidator(UserCommand command) {
this.command = command;
}
public void validate() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
validateAccountSettings(dataValidationErrors);
if (command.getOfficeId() == null || command.getOfficeId().equals(Long.valueOf(-1))) {
ErrorResponse error = new ErrorResponse("validation.msg.user.office.cannot.be.blank", "officeId");
dataValidationErrors.add(error);
}
if (command.getRoleIds() == null) {
ErrorResponse error = new ErrorResponse("validation.msg.user.roles.cannot.be.blank", "selectedItems");
dataValidationErrors.add(error);
}
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
public void validateAccountSettingDetails() {
List<ErrorResponse> dataValidationErrors = new ArrayList<ErrorResponse>();
validateAccountSettings(dataValidationErrors);
if (!dataValidationErrors.isEmpty()) {
throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist.");
}
}
private void validateAccountSettings(List<ErrorResponse> dataValidationErrors) {
if (StringUtils.isBlank(command.getUsername())) {
ErrorResponse error = new ErrorResponse("validation.msg.user.username.cannot.be.blank", "username");
dataValidationErrors.add(error);
}
if (StringUtils.isBlank(command.getFirstname())) {
ErrorResponse error = new ErrorResponse("validation.msg.user.firstname.cannot.be.blank", "firstname");
dataValidationErrors.add(error);
}
if (StringUtils.isBlank(command.getLastname())) {
ErrorResponse error = new ErrorResponse("validation.msg.user.lastname.cannot.be.blank", "lastname");
dataValidationErrors.add(error);
}
if (StringUtils.isBlank(command.getEmail())) {
ErrorResponse error = new ErrorResponse("validation.msg.user.email.cannot.be.blank", "email");
dataValidationErrors.add(error);
}
}
}
@@ -0,0 +1,94 @@
package org.mifosng.platform;
import org.mifosng.data.EntityIdentifier;
import org.mifosng.data.command.AdjustLoanTransactionCommand;
import org.mifosng.data.command.ChangePasswordCommand;
import org.mifosng.data.command.CreateLoanProductCommand;
import org.mifosng.data.command.EnrollClientCommand;
import org.mifosng.data.command.LoanStateTransitionCommand;
import org.mifosng.data.command.LoanTransactionCommand;
import org.mifosng.data.command.NoteCommand;
import org.mifosng.data.command.OfficeCommand;
import org.mifosng.data.command.RoleCommand;
import org.mifosng.data.command.SignupCommand;
import org.mifosng.data.command.SubmitLoanApplicationCommand;
import org.mifosng.data.command.UndoLoanApprovalCommand;
import org.mifosng.data.command.UndoLoanDisbursalCommand;
import org.mifosng.data.command.UpdateLoanProductCommand;
import org.mifosng.data.command.UpdateOrganisationCurrencyCommand;
import org.mifosng.data.command.UpdateUsernamePasswordCommand;
import org.mifosng.data.command.UserCommand;
import org.mifosng.platform.exceptions.InvalidSignupException;
import org.springframework.security.access.prepost.PreAuthorize;
public interface WritePlatformService {
Long createOffice(OfficeCommand command);
Long updateOffice(OfficeCommand command);
Long createUser(UserCommand command);
Long updateUser(UserCommand command);
Long updateCurrentUser(UserCommand command);
Long updateCurrentUserPassword(ChangePasswordCommand command);
void deleteUser(Long userId);
Long createRole(RoleCommand command);
Long updateRole(RoleCommand command);
/**
* @throws InvalidSignupException when sign up fails
*/
Long signup(SignupCommand command);
void updateUsernamePasswordOnFirstTimeLogin(UpdateUsernamePasswordCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_ENROLL_NEW_CLIENT_ROLE')")
Long enrollClient(EnrollClientCommand command);
void updateOrganisationCurrencies(UpdateOrganisationCurrencyCommand command);
EntityIdentifier createLoanProduct(CreateLoanProductCommand command);
EntityIdentifier updateLoanProduct(UpdateLoanProductCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_DELETE_LOAN_THAT_IS_SUBMITTED_AND_NOT_APPROVED')")
EntityIdentifier deleteLoan(Long loanId);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_SUBMIT_NEW_LOAN_APPLICATION_ROLE', 'CAN_SUBMIT_HISTORIC_LOAN_APPLICATION_ROLE')")
EntityIdentifier submitLoanApplication(SubmitLoanApplicationCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_APPROVE_LOAN_ROLE', 'CAN_APPROVE_LOAN_IN_THE_PAST_ROLE')")
EntityIdentifier approveLoanApplication(LoanStateTransitionCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_UNDO_LOAN_APPROVAL_ROLE')")
EntityIdentifier undoLoanApproval(UndoLoanApprovalCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_REJECT_LOAN_ROLE', 'CAN_REJECT_LOAN_IN_THE_PAST_ROLE')")
EntityIdentifier rejectLoan(LoanStateTransitionCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_WITHDRAW_LOAN_ROLE', 'CAN_WITHDRAW_LOAN_IN_THE_PAST_ROLE')")
EntityIdentifier withdrawLoan(LoanStateTransitionCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_DISBURSE_LOAN_ROLE', 'CAN_DISBURSE_LOAN_IN_THE_PAST_ROLE')")
EntityIdentifier disburseLoan(LoanStateTransitionCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_UNDO_LOAN_DISBURSAL_ROLE')")
public EntityIdentifier undloLoanDisbursal(UndoLoanDisbursalCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_MAKE_LOAN_REPAYMENT_ROLE', 'CAN_MAKE_LOAN_REPAYMENT_IN_THE_PAST_ROLE')")
public EntityIdentifier makeLoanRepayment(LoanTransactionCommand command);
EntityIdentifier adjustLoanTransaction(AdjustLoanTransactionCommand command);
EntityIdentifier waiveLoanAmount(LoanTransactionCommand command);
EntityIdentifier addClientNote(NoteCommand command);
EntityIdentifier updateNote(NoteCommand command);
}
@@ -0,0 +1,88 @@
package org.mifosng.platform.client.domain;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.UniqueConstraint;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
import org.mifosng.platform.organisation.domain.Office;
import org.mifosng.platform.organisation.domain.Organisation;
import org.mifosng.platform.user.domain.AppUser;
@Entity
@Table(name = "portfolio_client", uniqueConstraints = @UniqueConstraint(columnNames = {"org_id", "external_id" }))
public class Client extends AbstractAuditableCustom<AppUser, Long> {
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "org_id", nullable = false)
private final Organisation organisation;
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "office_id", nullable = false)
private final Office office;
@SuppressWarnings("unused")
@Column(name = "firstname", length=50)
private final String firstName;
@SuppressWarnings("unused")
@Column(name = "lastname", length=50)
private final String lastName;
@SuppressWarnings("unused")
@Column(name = "joining_date")
@Temporal(TemporalType.DATE)
private final Date joiningDate;
@Column(name = "external_id", length=100)
private final String externalId;
public static Client newClient(Organisation organisation,
Office clientOffice, String firstname, String lastname,
LocalDate joiningDate, String externalId) {
return new Client(organisation, clientOffice, firstname, lastname,
joiningDate, externalId);
}
public Client() {
this.organisation = null;
this.office = null;
this.joiningDate = null;
this.firstName = null;
this.lastName = null;
this.externalId = null;
}
public Client(final Organisation organisation, final Office office, final String firstName,
final String lastName, final LocalDate openingDate, final String externalId) {
this.organisation = organisation;
this.office = office;
if (StringUtils.isNotBlank(externalId)) {
this.externalId = externalId.trim();
} else {
this.externalId = null;
}
this.joiningDate = openingDate.toDateMidnight().toDate();
if (StringUtils.isNotBlank(firstName)) {
this.firstName = firstName.trim();
} else {
this.firstName = null;
}
this.lastName = lastName.trim();
}
public boolean identifiedBy(final String identifier) {
return identifier.equalsIgnoreCase(this.externalId);
}
}
@@ -0,0 +1,8 @@
package org.mifosng.platform.client.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface ClientRepository extends JpaRepository<Client, Long>, JpaSpecificationExecutor<Client> {
// no added behaviour
}
@@ -0,0 +1,116 @@
package org.mifosng.platform.client.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
import org.mifosng.platform.loan.domain.Loan;
import org.mifosng.platform.loan.domain.LoanTransaction;
import org.mifosng.platform.organisation.domain.Organisation;
import org.mifosng.platform.user.domain.AppUser;
@Entity
@Table(name = "portfolio_note")
public class Note extends AbstractAuditableCustom<AppUser, Long> {
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "org_id", nullable = false)
private final Organisation organisation;
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private final Client client;
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "loan_id", nullable = true)
private Loan loan;
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "loan_transaction_id", nullable = true)
private LoanTransaction loanTransaction;
@SuppressWarnings("unused")
@Column(name = "note", length=1000)
private String note;
@SuppressWarnings("unused")
@Column(name = "note_type_enum")
private Integer noteTypeId;
public enum NoteType {
CLIENT(100), LOAN(200), LOAN_TRANSACTION(300);
private int value;
NoteType(int value) { this.value = value; }
public int getValue() { return value; }
public static NoteType parse(int id) {
NoteType right = null; // Default
for (NoteType item : NoteType.values()) {
if (item.getValue()==id) {
right = item;
break;
}
}
return right;
}
}
public static Note clientNote(Organisation organisation, Client client, String note) {
return new Note(organisation, client, note);
}
public static Note loanNote(Organisation organisation, Loan loan, String note) {
return new Note(organisation, loan, note);
}
public static Note loanTransactionNote(Organisation organisation, Loan loan, LoanTransaction loanTransaction, String note) {
return new Note(organisation, loan, loanTransaction, note);
}
private Note(Organisation organisation, Client client, String note) {
this.organisation = organisation;
this.client = client;
this.note = note;
this.noteTypeId = NoteType.CLIENT.getValue();
}
private Note(Organisation organisation, Loan loan, String note) {
this.organisation = organisation;
this.loan = loan;
this.client = loan.getClient();
this.note = note;
this.noteTypeId = NoteType.LOAN.getValue();
}
private Note(Organisation organisation, Loan loan, LoanTransaction loanTransaction, String note) {
this.organisation = organisation;
this.loan = loan;
this.loanTransaction = loanTransaction;
this.client = loan.getClient();
this.note = note;
this.noteTypeId = NoteType.LOAN_TRANSACTION.getValue();
}
protected Note() {
this.organisation = null;
this.client = null;
this.loan = null;
this.loanTransaction = null;
this.note = null;
this.noteTypeId = null;
}
public void update(final String note) {
this.note = note;
}
}
@@ -0,0 +1,11 @@
package org.mifosng.platform.client.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface NoteRepository extends JpaRepository<Note, Long>, JpaSpecificationExecutor<Note> {
List<Note> findByLoanId(Long id);
}
@@ -0,0 +1,63 @@
package org.mifosng.platform.currency.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@Table(name = "ref_currency")
public class ApplicationCurrency extends AbstractPersistable<Long> {
@Column(name = "code", nullable = false, length=3)
private final String code;
@Column(name = "decimal_places", nullable = false)
private final Integer decimalPlaces;
@Column(name = "name", nullable = false, length=50)
private final String name;
@Column(name = "internationalized_name_code", nullable = false, length=50)
private final String nameCode;
@Column(name = "display_symbol", nullable = true, length=10)
private final String displaySymbol;
protected ApplicationCurrency() {
this.code = null;
this.name = null;
this.decimalPlaces = null;
this.nameCode = null;
this.displaySymbol = null;
}
public ApplicationCurrency(final String code, final String name, final int decimalPlaces, final String nameCode, final String displaySymbol) {
this.code = code;
this.name = name;
this.decimalPlaces = decimalPlaces;
this.nameCode = nameCode;
this.displaySymbol = displaySymbol;
}
public String getCode() {
return this.code;
}
public String getName() {
return this.name;
}
public Integer getDecimalPlaces() {
return this.decimalPlaces;
}
public String getNameCode() {
return nameCode;
}
public String getDisplaySymbol() {
return displaySymbol;
}
}
@@ -0,0 +1,9 @@
package org.mifosng.platform.currency.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface ApplicationCurrencyRepository extends JpaRepository<ApplicationCurrency, Long>, JpaSpecificationExecutor<ApplicationCurrency> {
ApplicationCurrency findOneByCode(String currencyCode);
}
@@ -0,0 +1,38 @@
package org.mifosng.platform.currency.domain;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
@Embeddable
public class MonetaryCurrency implements Serializable {
@Column(name = "currency_code", length = 3, nullable=false)
private final String code;
@Column(name = "currency_digits", nullable=false)
private final int digitsAfterDecimal;
protected MonetaryCurrency() {
this.code = null;
this.digitsAfterDecimal = 0;
}
public MonetaryCurrency(final String code, final int digitsAfterDecimal) {
this.code = code;
this.digitsAfterDecimal = digitsAfterDecimal;
}
public MonetaryCurrency copy() {
return new MonetaryCurrency(this.code, this.digitsAfterDecimal);
}
public String getCode() {
return code;
}
public int getDigitsAfterDecimal() {
return digitsAfterDecimal;
}
}
@@ -0,0 +1,240 @@
package org.mifosng.platform.currency.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Iterator;
import javax.persistence.Column;
import javax.persistence.Embeddable;
@Embeddable
public class Money implements Comparable<Money>, Serializable {
@Column(name = "currency_code", length=3)
private final String currencyCode;
@Column(name = "currency_digits")
private final int currencyDigitsAfterDecimal;
@Column(name = "amount", scale=6, precision=19)
private final BigDecimal amount;
public static Money total(final Money... monies) {
if (monies.length == 0) { throw new IllegalArgumentException("Money array must not be empty"); }
Money total = monies[0];
for (int i = 1; i < monies.length; i++) {
total = total.plus(monies[i]);
}
return total;
}
public static Money total(final Iterable<? extends Money> monies) {
Iterator<? extends Money> it = monies.iterator();
if (it.hasNext() == false) { throw new IllegalArgumentException("Money iterator must not be empty"); }
Money total = it.next();
while (it.hasNext()) {
total = total.plus(it.next());
}
return total;
}
public static Money of(MonetaryCurrency currency, BigDecimal newAmount) {
return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), newAmount);
}
public static Money zero(final MonetaryCurrency currency) {
return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), BigDecimal.ZERO);
}
protected Money() {
this.currencyCode = null;
this.currencyDigitsAfterDecimal = 0;
this.amount = null;
}
private Money(final String currencyCode, final int digitsAfterDecimal, final BigDecimal amount) {
this.currencyCode = currencyCode;
this.currencyDigitsAfterDecimal = digitsAfterDecimal;
BigDecimal amountStripped = amount.stripTrailingZeros();
this.amount = amountStripped.setScale(this.currencyDigitsAfterDecimal, RoundingMode.HALF_EVEN);
}
public Money copy() {
return new Money(this.currencyCode, this.currencyDigitsAfterDecimal, this.amount.stripTrailingZeros());
}
public Money plus(final Iterable<? extends Money> moniesToAdd) {
BigDecimal total = this.amount;
for (Money moneyProvider : moniesToAdd) {
Money money = this.checkCurrencyEqual(moneyProvider);
total = total.add(money.amount);
}
return Money.of(monetaryCurrency(), total);
}
public Money plus(final Money moneyToAdd) {
Money toAdd = this.checkCurrencyEqual(moneyToAdd);
return this.plus(toAdd.getAmount());
}
public Money plus(final BigDecimal amountToAdd) {
if (amountToAdd.compareTo(BigDecimal.ZERO) == 0) { return this; }
BigDecimal newAmount = this.amount.add(amountToAdd);
return Money.of(monetaryCurrency(), newAmount);
}
public Money plus(final double amountToAdd) {
if (amountToAdd == 0) { return this; }
BigDecimal newAmount = this.amount.add(BigDecimal.valueOf(amountToAdd));
return Money.of(monetaryCurrency(), newAmount);
}
public Money minus(final Money moneyToSubtract) {
Money toSubtract = this.checkCurrencyEqual(moneyToSubtract);
return this.minus(toSubtract.getAmount());
}
public Money minus(final BigDecimal amountToSubtract) {
if (amountToSubtract.compareTo(BigDecimal.ZERO) == 0) { return this; }
BigDecimal newAmount = this.amount.subtract(amountToSubtract);
return Money.of(monetaryCurrency(), newAmount);
}
private Money checkCurrencyEqual(final Money money) {
if (this.isSameCurrency(money) == false) { throw new UnsupportedOperationException("currencies are different."); }
return money;
}
public boolean isSameCurrency(final Money money) {
return this.currencyCode.equals(money.getCurrencyCode());
}
public Money dividedBy(final BigDecimal valueToDivideBy, final RoundingMode roundingMode) {
if (valueToDivideBy.compareTo(BigDecimal.ONE) == 0) { return this; }
BigDecimal newAmount = this.amount.divide(valueToDivideBy, roundingMode);
return Money.of(monetaryCurrency(), newAmount);
}
public Money dividedBy(final double valueToDivideBy, final RoundingMode roundingMode) {
if (valueToDivideBy == 1) { return this; }
BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), roundingMode);
return Money.of(monetaryCurrency(), newAmount);
}
public Money dividedBy(final long valueToDivideBy, final RoundingMode roundingMode) {
if (valueToDivideBy == 1) { return this; }
BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), roundingMode);
return Money.of(monetaryCurrency(), newAmount);
}
public Money multipliedBy(final BigDecimal valueToMultiplyBy) {
if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) { return this; }
BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy);
return Money.of(monetaryCurrency(), newAmount);
}
public Money multipliedBy(final double valueToMultiplyBy) {
if (valueToMultiplyBy == 1) { return this; }
BigDecimal newAmount = this.amount.multiply(BigDecimal.valueOf(valueToMultiplyBy));
return Money.of(monetaryCurrency(), newAmount);
}
public Money multipliedBy(final long valueToMultiplyBy) {
if (valueToMultiplyBy == 1) { return this; }
BigDecimal newAmount = this.amount.multiply(BigDecimal.valueOf(valueToMultiplyBy));
return Money.of(monetaryCurrency(), newAmount);
}
public Money multiplyRetainScale(final BigDecimal valueToMultiplyBy, final RoundingMode roundingMode) {
if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) { return this; }
BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy);
newAmount = newAmount.setScale(this.currencyDigitsAfterDecimal, roundingMode);
return Money.of(monetaryCurrency(), newAmount);
}
public Money multiplyRetainScale(final double valueToMultiplyBy, final RoundingMode roundingMode) {
return this.multiplyRetainScale(BigDecimal.valueOf(valueToMultiplyBy), roundingMode);
}
@Override
public int compareTo(final Money other) {
Money otherMoney = other;
if (this.currencyCode.equals(otherMoney.currencyCode) == false) {
throw new UnsupportedOperationException("currencies arent different");
}
return this.amount.compareTo(otherMoney.amount);
}
public boolean isZero() {
return this.isEqualTo(Money.zero(this.getCurrency()));
}
public boolean isEqualTo(final Money other) {
return this.compareTo(other) == 0;
}
public boolean isNotEqualTo(final Money other) {
return !isEqualTo(other);
}
public boolean isGreaterThanOrEqualTo(final Money other) {
return this.isGreaterThan(other) || this.isEqualTo(other);
}
public boolean isGreaterThan(final Money other) {
return this.compareTo(other) > 0;
}
public boolean isGreaterThanZero() {
return this.isGreaterThan(Money.zero(this.getCurrency()));
}
public boolean isLessThan(final Money other) {
return this.compareTo(other) < 0;
}
public boolean isLessThanZero() {
return this.isLessThan(Money.zero(this.getCurrency()));
}
public String getCurrencyCode() {
return this.currencyCode;
}
public int getCurrencyDigitsAfterDecimal() {
return this.currencyDigitsAfterDecimal;
}
public BigDecimal getAmount() {
return this.amount;
}
@Override
public String toString() {
return new StringBuilder()
.append(this.currencyCode)
.append(' ')
.append(this.amount.toPlainString())
.toString();
}
public Money negated() {
if (this.isZero()) {
return this;
}
return Money.of(monetaryCurrency(), this.amount.negate());
}
public Money abs() {
return this.isLessThanZero() ? this.negated() : this;
}
public MonetaryCurrency getCurrency() {
return monetaryCurrency();
}
private MonetaryCurrency monetaryCurrency() {
return new MonetaryCurrency(this.currencyCode, this.currencyDigitsAfterDecimal);
}
}
@@ -0,0 +1,29 @@
package org.mifosng.platform.exceptions;
import java.util.ArrayList;
import java.util.List;
import org.mifosng.data.ErrorResponse;
public class ApplicationDomainRuleException extends RuntimeException {
private final List<ErrorResponse> errors;
public ApplicationDomainRuleException(final String message) {
super(message);
this.errors = new ArrayList<ErrorResponse>();
}
public ApplicationDomainRuleException(List<ErrorResponse> errors, final String message) {
super(message);
this.errors = errors;
}
public ApplicationDomainRuleException(List<ErrorResponse> errors) {
this.errors = errors;
}
public List<ErrorResponse> getErrors() {
return errors;
}
}
@@ -0,0 +1,5 @@
package org.mifosng.platform.exceptions;
public class ClientNotAuthenticatedException extends RuntimeException {
// no added behaviour
}
@@ -0,0 +1,9 @@
package org.mifosng.platform.exceptions;
public class InvalidSignupException extends RuntimeException {
public InvalidSignupException(final Throwable e) {
super(e);
}
}
@@ -0,0 +1,25 @@
package org.mifosng.platform.exceptions;
import java.util.ArrayList;
import java.util.List;
import org.mifosng.data.ErrorResponse;
public class NewDataValidationException extends RuntimeException {
private final List<ErrorResponse> validationErrors;
public NewDataValidationException(final String message) {
super(message);
this.validationErrors = new ArrayList<ErrorResponse>();
}
public NewDataValidationException(List<ErrorResponse> errors, final String message) {
super(message);
this.validationErrors = errors;
}
public List<ErrorResponse> getValidationErrors() {
return validationErrors;
}
}
@@ -0,0 +1,25 @@
package org.mifosng.platform.exceptions;
import java.util.ArrayList;
import java.util.List;
import org.mifosng.data.ErrorResponse;
public class NoAuthorizationException extends RuntimeException {
private final List<ErrorResponse> authorizationErrors;
public NoAuthorizationException(final String message) {
super(message);
this.authorizationErrors = new ArrayList<ErrorResponse>();
}
public NoAuthorizationException(List<ErrorResponse> authorizationErrors, final String message) {
super(message);
this.authorizationErrors = authorizationErrors;
}
public List<ErrorResponse> getAuthorizationErrors() {
return authorizationErrors;
}
}
@@ -0,0 +1,142 @@
package org.mifosng.platform.infrastructure;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.JoinColumn;
import javax.persistence.MappedSuperclass;
import javax.persistence.OneToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.joda.time.DateTime;
import org.springframework.data.domain.Auditable;
import org.springframework.data.jpa.domain.AbstractAuditable;
import org.springframework.data.jpa.domain.AbstractPersistable;
/**
* A custom copy of {@link AbstractAuditable} to override the column names used on database.
*
* Abstract base class for auditable entities. Stores the audition values in
* persistent fields.
*
* @param <U> the auditing type. Typically some kind of user.
* @param <PK> the type of the auditing type's identifier
*/
@MappedSuperclass
public abstract class AbstractAuditableCustom<U, PK extends Serializable> extends AbstractPersistable<PK> implements Auditable<U, PK> {
private static final long serialVersionUID = 141481953116476081L;
@OneToOne
@JoinColumn(name="createdby_id")
private U createdBy;
@Column(name="created_date")
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@OneToOne
@JoinColumn(name="lastmodifiedby_id")
private U lastModifiedBy;
@Column(name="lastmodified_date")
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
/*
* (non-Javadoc)
*
* @see org.springframework.data.domain.Auditable#getCreatedBy()
*/
@Override
public U getCreatedBy() {
return this.createdBy;
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.data.domain.Auditable#setCreatedBy(java.lang.Object)
*/
@Override
public void setCreatedBy(final U createdBy) {
this.createdBy = createdBy;
}
/*
* (non-Javadoc)
*
* @see org.springframework.data.domain.Auditable#getCreatedDate()
*/
@Override
public DateTime getCreatedDate() {
return null == this.createdDate ? null : new DateTime(this.createdDate);
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.data.domain.Auditable#setCreatedDate(org.joda.time
* .DateTime)
*/
@Override
public void setCreatedDate(final DateTime createdDate) {
this.createdDate = null == createdDate ? null : createdDate.toDate();
}
/*
* (non-Javadoc)
*
* @see org.springframework.data.domain.Auditable#getLastModifiedBy()
*/
@Override
public U getLastModifiedBy() {
return this.lastModifiedBy;
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.data.domain.Auditable#setLastModifiedBy(java.lang
* .Object)
*/
@Override
public void setLastModifiedBy(final U lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
/*
* (non-Javadoc)
*
* @see org.springframework.data.domain.Auditable#getLastModifiedDate()
*/
@Override
public DateTime getLastModifiedDate() {
return null == this.lastModifiedDate ? null : new DateTime(this.lastModifiedDate);
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.data.domain.Auditable#setLastModifiedDate(org.joda
* .time.DateTime)
*/
@Override
public void setLastModifiedDate(final DateTime lastModifiedDate) {
this.lastModifiedDate = null == lastModifiedDate ? null : lastModifiedDate.toDate();
}
}
@@ -0,0 +1,37 @@
package org.mifosng.platform.infrastructure;
import org.mifosng.platform.user.domain.AppUser;
import org.mifosng.platform.user.domain.AppUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
public class AuditorAwareImpl implements AuditorAware<AppUser> {
@Autowired
private AppUserRepository userRepository;
@Override
public AppUser getCurrentAuditor() {
AppUser currentUser = null;
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext != null) {
Authentication authentication = securityContext.getAuthentication();
if (authentication != null) {
currentUser = (AppUser) authentication.getPrincipal();
} else {
currentUser = this.retrieveSuperUser();
}
} else {
currentUser = this.retrieveSuperUser();
}
return currentUser;
}
private AppUser retrieveSuperUser() {
return this.userRepository.findOne(Long.valueOf("1"));
}
}
@@ -0,0 +1,73 @@
package org.mifosng.platform.infrastructure;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public class BasicPasswordEncodablePlatformUser implements PlatformUser {
private final Long id;
private final String username;
private final String password;
public BasicPasswordEncodablePlatformUser(final Long id, final String username,
final String password) {
this.id = id;
this.username = username;
this.password = password;
}
public Long getId() {
return this.id;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
@Override
public boolean isFirstTimeLoginRemaining() {
return false;
}
@Override
public void updateUsernamePasswordOnFirstTimeLogin(final String newUsername, final String newPasswordEncoded) {
// dummy method
}
@Override
public void updatePasswordOnFirstTimeLogin(final String newPasswordEncoded) {
// dummy method
}
}
@@ -0,0 +1,124 @@
package org.mifosng.platform.infrastructure;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
protected final Log logger = LogFactory.getLog(getClass());
private String defaultFailureUrl;
private boolean forwardToDestination = false;
private boolean allowSessionCreation = true;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public CustomAuthenticationFailureHandler() {
}
/**
* Performs the redirect or forward to the {@code defaultFailureUrl} if set, otherwise returns a 401 error code.
* <p>
* If redirecting or forwarding, {@code saveException} will be called to cache the exception for use in
* the target view.
*/
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed: " + exception.getMessage());
} else {
saveException(request, exception);
if (forwardToDestination) {
logger.debug("Forwarding to " + defaultFailureUrl);
request.getRequestDispatcher(defaultFailureUrl).forward(request, response);
} else {
logger.debug("Redirecting to " + defaultFailureUrl);
String oauthToken = request.getParameter("oauth_token");
request.setAttribute("oauth_token", oauthToken);
String url = defaultFailureUrl + "?oauth_token=" + oauthToken;
redirectStrategy.sendRedirect(request, response, url);
}
}
}
/**
* Caches the {@code AuthenticationException} for use in view rendering.
* <p>
* If {@code forwardToDestination} is set to true, request scope will be used, otherwise it will attempt to store
* the exception in the session. If there is no session and {@code allowSessionCreation} is {@code true} a session
* will be created. Otherwise the exception will not be stored.
*/
protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
if (forwardToDestination) {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
} else {
HttpSession session = request.getSession(false);
if (session != null || allowSessionCreation) {
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
}
}
/**
* The URL which will be used as the failure destination.
*
* @param defaultFailureUrl the failure URL, for example "/loginFailed.jsp".
*/
public void setDefaultFailureUrl(String defaultFailureUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl),
"'" + defaultFailureUrl + "' is not a valid redirect URL");
this.defaultFailureUrl = defaultFailureUrl;
}
protected boolean isUseForward() {
return forwardToDestination;
}
/**
* If set to <tt>true</tt>, performs a forward to the failure destination URL instead of a redirect. Defaults to
* <tt>false</tt>.
*/
public void setUseForward(boolean forwardToDestination) {
this.forwardToDestination = forwardToDestination;
}
/**
* Allows overriding of the behaviour when redirecting to a target URL.
*/
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
protected RedirectStrategy getRedirectStrategy() {
return redirectStrategy;
}
protected boolean isAllowSessionCreation() {
return allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
}
@@ -0,0 +1,26 @@
package org.mifosng.platform.infrastructure;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.security.authentication.dao.SaltSource;
import org.springframework.security.authentication.encoding.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service(value = "applicationPasswordEncoder")
@Scope("singleton")
public class DefaultPlatformPasswordEncoder implements PlatformPasswordEncoder {
private final PasswordEncoder passwordEncoder;
private final SaltSource saltSource;
@Autowired
public DefaultPlatformPasswordEncoder(final PasswordEncoder passwordEncoder, final SaltSource saltSource) {
this.passwordEncoder = passwordEncoder;
this.saltSource = saltSource;
}
@Override
public String encode(final PlatformUser appUser) {
return this.passwordEncoder.encodePassword(appUser.getPassword(), this.saltSource.getSalt(appUser));
}
}
@@ -0,0 +1,32 @@
package org.mifosng.platform.infrastructure;
public class EmailDetail {
private final String organisationName;
private final String username;
private final String contactName;
private final String address;
public EmailDetail(String organisationName, String contactName, String address, String username) {
this.organisationName = organisationName;
this.contactName = contactName;
this.address = address;
this.username = username;
}
public String getOrganisationName() {
return this.organisationName;
}
public String getUsername() {
return this.username;
}
public String getContactName() {
return this.contactName;
}
public String getAddress() {
return this.address;
}
}
@@ -0,0 +1,106 @@
package org.mifosng.platform.infrastructure;
import java.security.Principal;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.command.UpdateUsernamePasswordCommand;
import org.mifosng.platform.WritePlatformService;
import org.mifosng.platform.user.domain.PasswordMustBeDifferentException;
import org.mifosng.platform.user.domain.UsernameMustBeDifferentException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
@Controller
@SessionAttributes("firstTimeLoginFormBean")
public class FirstTimeLoginController {
@Autowired
private WritePlatformService signupPlatformService;
@ModelAttribute("firstTimeLoginFormBean")
@RequestMapping(value = "/firsttimelogin", method = RequestMethod.GET)
public FirstTimeLoginFormBean loadSignupForm(final Principal principal,
final HttpSession httpSession) {
String successView = "/index";
String savedRequestView = (String) httpSession
.getAttribute("firsttimeloginview");
if (StringUtils.isNotBlank(savedRequestView)) {
successView = savedRequestView;
}
FirstTimeLoginFormBean bean = new FirstTimeLoginFormBean();
bean.setOldUsername(principal.getName());
bean.setSuccessView(successView);
return bean;
}
@ModelAttribute("firstTimeLoginFormBean")
@RequestMapping(value = "/firsttimelogin", method = RequestMethod.POST)
public ModelAndView handleSignup(
@ModelAttribute("firstTimeLoginFormBean") final FirstTimeLoginFormBean firstTimeLoginFormBean,
final BindingResult result, final SessionStatus status) {
if (result.hasErrors()) {
return new ModelAndView("firsttimelogin", "firstTimeLoginFormBean",
firstTimeLoginFormBean);
}
try {
UpdateUsernamePasswordCommand command = new UpdateUsernamePasswordCommand(
firstTimeLoginFormBean.getOldUsername(),
firstTimeLoginFormBean.getUsername(),
firstTimeLoginFormBean.getPassword());
this.signupPlatformService
.updateUsernamePasswordOnFirstTimeLogin(command);
status.setComplete();
RedirectView redirectView = new RedirectView(
firstTimeLoginFormBean.getSuccessView());
if (!firstTimeLoginFormBean.getSuccessView().startsWith("http")) {
redirectView.setContextRelative(true);
}
return new ModelAndView(redirectView);
} catch (UsernameAlreadyExistsException e) {
ObjectError error = new ObjectError("firstTimeLoginFormBean",
new String[] { "username.already.exists" },
new Object[] { firstTimeLoginFormBean.getUsername() },
"An user with username {0} already exists.");
result.addError(error);
return new ModelAndView("firsttimelogin", "firstTimeLoginFormBean",
firstTimeLoginFormBean);
} catch (UsernameMustBeDifferentException e) {
ObjectError error = new ObjectError("firstTimeLoginFormBean",
new String[] { "new.username.must.be.different" },
new Object[] {},
"The new username cannot be the same as existing username.");
result.addError(error);
return new ModelAndView("firsttimelogin", "firstTimeLoginFormBean",
firstTimeLoginFormBean);
} catch (PasswordMustBeDifferentException e) {
ObjectError error = new ObjectError("firstTimeLoginFormBean",
new String[] { "new.password.must.be.different" },
new Object[] {},
"The new password cannot be the same as existing password.");
result.addError(error);
return new ModelAndView("firsttimelogin", "firstTimeLoginFormBean",
firstTimeLoginFormBean);
}
}
}
@@ -0,0 +1,96 @@
package org.mifosng.platform.infrastructure;
import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth.common.OAuthConsumerParameter;
import org.springframework.security.oauth.provider.OAuthProviderSupport;
import org.springframework.security.oauth.provider.filter.CoreOAuthProviderSupport;
import org.springframework.security.oauth.provider.token.OAuthProviderTokenImpl;
import org.springframework.security.oauth.provider.token.OAuthProviderTokenServices;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
public class FirstTimeLoginDetectionAuthenticationSuccessHandler extends
SimpleUrlAuthenticationSuccessHandler {
private OAuthProviderSupport providerSupport = new CoreOAuthProviderSupport();
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private String firstTimeLoginUrl;
private final OAuthProviderTokenServices tokenServices;
public FirstTimeLoginDetectionAuthenticationSuccessHandler(final OAuthProviderTokenServices oauthProviderTokenServices) {
this.tokenServices = oauthProviderTokenServices;
}
@Override
public void onAuthenticationSuccess(final HttpServletRequest request,
final HttpServletResponse response,
final Authentication authentication) throws ServletException,
IOException {
// hardcoded success url
String authSuccessUrl = "/oauth/confirm_access"; // request.getParameter("successUrl");
Map<String, String> oauthParams = this.providerSupport.parseParameters(request);
String oauthToken = retrieveOAuthToken(oauthParams);
if (StringUtils.isBlank(oauthToken)) {
// throw exception
}
OAuthProviderTokenImpl token = (OAuthProviderTokenImpl) this.tokenServices.getToken(oauthToken);
PlatformUser loggedInUser = (PlatformUser) authentication.getPrincipal();
token.setUserAuthentication(new UsernamePasswordAuthenticationToken(loggedInUser, loggedInUser, loggedInUser.getAuthorities()));
String targetUrl = this.determineTargetUrl(request, response);
if (loggedInUser.isFirstTimeLoginRemaining()) {
// request.getSession().setAttribute("firsttimeloginview", targetUrl);
request.setAttribute("firsttimeloginview", targetUrl);
targetUrl = this.firstTimeLoginUrl;
}
clearAuthenticationAttributes(request);
getRedirectStrategy().sendRedirect(request, response, authSuccessUrl + "?oauth_token=" + oauthToken);
}
private String retrieveOAuthToken(Map<String, String> oauthParams) {
return oauthParams.get(OAuthConsumerParameter.oauth_token.toString());
}
@Override
protected void handle(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
String targetUrl = this.determineTargetUrl(request, response);
PlatformUser loggedInUser = (PlatformUser) authentication.getPrincipal();
if (loggedInUser.isFirstTimeLoginRemaining()) {
targetUrl = this.firstTimeLoginUrl;
}
if (response.isCommitted()) {
this.logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
this.redirectStrategy.sendRedirect(request, response, targetUrl);
}
public String getFirstTimeLoginUrl() {
return this.firstTimeLoginUrl;
}
public void setFirstTimeLoginUrl(final String firstTimeLoginUrl) {
this.firstTimeLoginUrl = firstTimeLoginUrl;
}
}
@@ -0,0 +1,77 @@
package org.mifosng.platform.infrastructure;
import org.apache.commons.lang.StringUtils;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.binding.validation.ValidationContext;
public class FirstTimeLoginFormBean {
private String oldUsername;
private String username;
private String password;
private String successView;
public void validateUpdateUsernamePasswordDetails(final ValidationContext validationContext) {
MessageContext messageContext = validationContext.getMessageContext();
if (StringUtils.isBlank(this.username)) {
MessageBuilder builder = new MessageBuilder().error().source("username")
.codes(new String[] { "username.blank" })
.defaultText("Username cannot be empty.")
.args(new Object[] {});
messageContext.addMessage(builder.build());
}
if (this.oldUsername.trim().equalsIgnoreCase(this.username.trim())) {
MessageBuilder builder = new MessageBuilder().error().source("username")
.codes(new String[] { "username.same.as.old.username" })
.defaultText("The new username cannot be the same as the old username.")
.args(new Object[] {});
messageContext.addMessage(builder.build());
}
if (StringUtils.isBlank(this.password)) {
MessageBuilder builder = new MessageBuilder().error().source("password")
.codes(new String[] { "password.blank" })
.defaultText("Password cannot be empty.")
.args(new Object[] {});
messageContext.addMessage(builder.build());
}
}
public String getOldUsername() {
return this.oldUsername;
}
public void setOldUsername(final String oldUsername) {
this.oldUsername = oldUsername;
}
public String getUsername() {
return this.username;
}
public void setUsername(final String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(final String password) {
this.password = password;
}
public String getSuccessView() {
return this.successView;
}
public void setSuccessView(final String successView) {
this.successView = successView;
}
}
@@ -0,0 +1,45 @@
package org.mifosng.platform.infrastructure;
import org.apache.commons.mail.DefaultAuthenticator;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.SimpleEmail;
import org.springframework.stereotype.Service;
@Service
public class GmailBackedPlatformEmailService implements PlatformEmailService {
@Override
public void sendToUserAccount(final EmailDetail emailDetail, final String unencodedPassword) {
Email email = new SimpleEmail();
String authuserName = "support@cloudmicrofinance.com";
String authuser = "support@cloudmicrofinance.com";
String authpwd = "support80";
// Very Important, Don't use email.setAuthentication()
email.setAuthenticator(new DefaultAuthenticator(authuser, authpwd));
email.setDebug(false); // true if you want to debug
email.setHostName("smtp.gmail.com");
try {
email.getMailSession().getProperties().put("mail.smtp.starttls.enable", "true");
email.setFrom(authuser, authuserName);
StringBuilder subjectBuilder = new StringBuilder().append("MifosX Prototype Demo: ").append(emailDetail.getContactName()).append(" user account creation.");
email.setSubject(subjectBuilder.toString());
String sendToEmail = emailDetail.getAddress();
StringBuilder messageBuilder = new StringBuilder().append("You are receiving this email as your email account: ").append(sendToEmail).append(" has being used to create a user account for an organisation named [").append(emailDetail.getOrganisationName()).append("] on MifosX Prototype Demo.").append("You can login using the following credentials: username: ").append(emailDetail.getUsername()).append(" password: ").append(unencodedPassword);
email.setMsg(messageBuilder.toString());
email.addTo(sendToEmail, emailDetail.getContactName());
email.send();
} catch (EmailException e) {
throw new PlatformEmailSendException(e);
}
}
}
@@ -0,0 +1,167 @@
package org.mifosng.platform.infrastructure;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
//@Controller
public class PentahoReportingController //implements ServletContextAware
{
// private int reportNum = 0; // used for building HTML reports
// private ServletContext myServletContext;
//private final static Logger logger = LoggerFactory.getLogger(PentahoReportingController.class);
public PentahoReportingController() {
// JPW commented out for now ClassicEngineBoot.getInstance().start();
}
@RequestMapping(value = "/pentahoreport", method = RequestMethod.GET)
public void handlePentahoReport(final HttpServletRequest request,
HttpServletResponse response) throws IOException {
/* JPW commented out for now
try {
// load report definition
ResourceManager manager = new ResourceManager();
manager.registerDefaults();
// various ways of pointing to the report directory - hardcoded here
// for testing so if you put the test.prpt in a directory
// c:\dev\PentahoTestReports it should find it
// String reportPath = "file:" +
// this.getServletContext().getRealPath(request.getParameter("pentahoReportName"));
// String reportPath = "file:" + request.getServletPath();
// String reportPath =
// "http://ec2-46-137-17-252.eu-west-1.compute.amazonaws.com:8080/PentahoReports/"
// + request.getParameter("pentahoReportName") + ".prpt";
// String reportPath = "file:///C:/dev/mifosbiGithub/bi/reports/standardReports/prpts/" + request.getParameter("pentahoReportName") + ".prpt";
String reportPath = "file:///C:/dev/PentahoTestReports/"
+ request.getParameter("pentahoReportName") + ".prpt";
logger.info("pentahoReportName: "
+ request.getParameter("pentahoReportName"));
logger.info("Report path: " + reportPath);
Resource res = manager.createDirectly(new URL(reportPath),
MasterReport.class);
MasterReport report = (MasterReport) res.getResource();
ReportParameterValues rptParamValues = report.getParameterValues();
String name;
String value;
@SuppressWarnings("unchecked")
Enumeration<String> parms = request.getParameterNames();
while (parms.hasMoreElements()) {
// Get the name of the request parameter
name = (String) parms.nextElement();
logger.info("parm name: " + name);
if (!((name.equals("pentahoReportName")) || (name
.equals("output-type")))) {
// Get the value of the request parameter
value = request.getParameter(name);
rptParamValues.put(name, value);
logger.info("parm name: " + name + " parm value: "
+ value);
}
}
String outputType = request.getParameter("output-type");
if ("PDF".equals(outputType)) {
response.setContentType("application/pdf");
PdfReportUtil.createPDF(report, response.getOutputStream());
} else if ("XLS".equals(outputType)) {
response.setContentType("application/vnd.ms-excel");
try {
ExcelReportUtil.createXLS(report,
response.getOutputStream());
} catch (ReportProcessingException e) {
e.printStackTrace();
}
} else if ("HTML".equals(outputType)) {
String reportLoc = "report_" + reportNum++;
logger.info("request context path: " + request.getContextPath());
logger.info("request.getServletPath(): "
+ request.getServletPath());
logger.info("hopoe: "
+ myServletContext.getRealPath(request.getContextPath()));
String path = myServletContext
.getRealPath("../ROOT/PentahoHTMLReports/" + reportLoc);
logger.info("The path is: " + path);
File folder = new File(path);
folder.mkdir();
try {
HtmlReportUtil.createDirectoryHTML(report, path
+ File.separator + "index.html");
} catch (ReportProcessingException e) {
e.printStackTrace();
}
String redirectLink = "/PentahoHTMLReports/" + reportLoc
+ "/index.html";
logger.info(redirectLink);
response.sendRedirect(redirectLink);
} else {
response.setContentType("application/rtf");
try {
RTFReportUtil.createRTF(report, response.getOutputStream());
} catch (ReportProcessingException e) {
e.printStackTrace();
}
}
} catch (ResourceException e) {
e.printStackTrace();
}*/
}
/* jpw comment out
@Override
public void setServletContext(ServletContext servletContext) {
myServletContext = servletContext;
String pentahoHTMLFolderName = myServletContext
.getRealPath("../ROOT/PentahoHTMLReports");
if (pentahoHTMLFolderName == null) {
logger.info("pentaho HTML directory doesn't exist for non-Tomcat use");
} else {
File pentahoHTMLFolder = new File(pentahoHTMLFolderName);
if (!(deleteDir(pentahoHTMLFolder)))
logger.info("Failed to delete folder: "
+ pentahoHTMLFolder.getName());
}
}*/
// Deletes all files and subdirectories under dir.
// Returns true if all deletions were successful.
// If a deletion fails, the method stops attempting to delete and returns
// false.
// private static boolean deleteDir(File dir) {
// if (dir.isDirectory()) {
// String[] children = dir.list();
// for (int i = 0; i < children.length; i++) {
// boolean success = deleteDir(new File(dir, children[i]));
// if (!success) {
// return false;
// }
// }
// }
//
// // The directory is now empty so delete it
// return dir.delete();
// }
}
@@ -0,0 +1,8 @@
package org.mifosng.platform.infrastructure;
public class PlatformEmailSendException extends RuntimeException {
public PlatformEmailSendException(final Throwable e) {
super(e);
}
}
@@ -0,0 +1,8 @@
package org.mifosng.platform.infrastructure;
public interface PlatformEmailService {
void sendToUserAccount(EmailDetail emailDetail, String unencodedPassword);
}
@@ -0,0 +1,8 @@
package org.mifosng.platform.infrastructure;
public interface PlatformPasswordEncoder {
String encode(PlatformUser appUser);
}
@@ -0,0 +1,15 @@
package org.mifosng.platform.infrastructure;
import org.springframework.security.core.userdetails.UserDetails;
/**
* Interface to protect platform from implementation detail of spring security.
*/
public interface PlatformUser extends UserDetails {
boolean isFirstTimeLoginRemaining();
void updateUsernamePasswordOnFirstTimeLogin(String newUsername, String newPasswordEncoded);
void updatePasswordOnFirstTimeLogin(String newPasswordEncoded);
}
@@ -0,0 +1,19 @@
package org.mifosng.platform.infrastructure;
public class RandomPasswordGenerator {
private final int numberOfCharactersInPassword;
public RandomPasswordGenerator(final int numberOfCharactersInPassword) {
this.numberOfCharactersInPassword = numberOfCharactersInPassword;
}
public String generate() {
StringBuilder passwordBuilder = new StringBuilder(this.numberOfCharactersInPassword);
for (int i = 0; i < this.numberOfCharactersInPassword; i++) {
passwordBuilder.append((char) ((int) (Math.random() * 26) + 97));
}
return passwordBuilder.toString();
}
}
@@ -0,0 +1,130 @@
package org.mifosng.platform.infrastructure;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.command.SignupCommand;
import org.mifosng.platform.WritePlatformService;
import org.mifosng.platform.exceptions.InvalidSignupException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.servlet.ModelAndView;
/**
* @deprecated - remove signup functionality for now.
*/
@Deprecated
@Controller
@SessionAttributes("signupFormBean")
public class SignupController {
private final WritePlatformService signupPlatformService;
@Autowired
public SignupController(WritePlatformService signupPlatformService) {
this.signupPlatformService = signupPlatformService;
}
@RequestMapping(value = "/signup", method = RequestMethod.GET)
public ModelAndView loadSignupForm() {
SignupFormBean bean = new SignupFormBean();
return new ModelAndView("/tenant/signup", "signupFormBean", bean);
}
@RequestMapping(value = "/signup", method = RequestMethod.POST)
public ModelAndView handleSignup(
@ModelAttribute("signupFormBean") final SignupFormBean signupFormBean,
final BindingResult result, final SessionStatus status) {
final String signupFormView = "/tenant/signup";
if (result.hasErrors()) {
return new ModelAndView(signupFormView, "signupFormBean",
signupFormBean);
}
if (StringUtils.isBlank(signupFormBean.getOrganisationName())) {
ObjectError error = new ObjectError("signupFormBean",
new String[] { "organisationName.blank" }, new Object[] {},
"Organisation name cannot be empty.");
result.addError(error);
}
if (signupFormBean.getOpeningDate() == null) {
ObjectError error = new ObjectError("signupFormBean",
new String[] { "openingDate.blank" }, new Object[] {},
"Organisation Founded date cannot be empty.");
result.addError(error);
}
if (StringUtils.isBlank(signupFormBean.getContactEmail())) {
ObjectError error = new ObjectError("signupFormBean",
new String[] { "contactEmail.blank" }, new Object[] {},
"Contact email cannot be empty.");
result.addError(error);
}
if (StringUtils.isBlank(signupFormBean.getContactName())) {
ObjectError error = new ObjectError("signupFormBean",
new String[] { "contactName.blank" }, new Object[] {},
"Contact name cannot be empty.");
result.addError(error);
}
if (result.hasErrors()) {
return new ModelAndView(signupFormView, "signupFormBean",
signupFormBean);
}
SignupCommand command = new SignupCommand(
signupFormBean.getOrganisationName(),
signupFormBean.getContactEmail(),
signupFormBean.getContactName(),
signupFormBean.getOpeningDate());
ModelAndView mav = null;
try {
signupPlatformService.signup(command);
status.setComplete();
mav = new ModelAndView("/tenant/signupsuccess", "signupFormBean",
signupFormBean);
} catch (InvalidSignupException e) {
// FIXME - InvalidSignupException is really
// TenantAlreadyExistsException as a tenant already exists with
// given organisationName
Object[] errorArgs = new Object[] { signupFormBean.getOrganisationName() };
ObjectError error = new ObjectError("signupFormBean",
new String[] { "organisation.name.already.exists" }, errorArgs,
"An organisation with name {0} already exists.");
result.addError(error);
} catch (PlatformEmailSendException e) {
Object[] errorArgs = new Object[] { signupFormBean.getContactEmail() };
ObjectError error = new ObjectError(
"signupFormBean",
new String[] { "email.failed.to.send"},
errorArgs,
"The provided email address [{0}] is not valid or our email service is presently not working, if you believe your email to be correct, please try again in a few minutes.");
result.addError(error);
}
if (result.hasErrors()) {
mav = new ModelAndView(signupFormView, "signupFormBean",
signupFormBean);
}
return mav;
}
}
@@ -0,0 +1,49 @@
package org.mifosng.platform.infrastructure;
import org.joda.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
public class SignupFormBean {
private String contactEmail;
private String contactName;
private String organisationName;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate openingDate = new LocalDate();
protected SignupFormBean() {
// no arg constructor
}
public String getContactEmail() {
return this.contactEmail;
}
public void setContactEmail(final String contactEmail) {
this.contactEmail = contactEmail;
}
public String getContactName() {
return this.contactName;
}
public void setContactName(final String contactName) {
this.contactName = contactName;
}
public String getOrganisationName() {
return this.organisationName;
}
public void setOrganisationName(final String organisationName) {
this.organisationName = organisationName;
}
public LocalDate getOpeningDate() {
return this.openingDate;
}
public void setOpeningDate(final LocalDate openingDate) {
this.openingDate = openingDate;
}
}
@@ -0,0 +1,7 @@
package org.mifosng.platform.infrastructure;
public class UsernameAlreadyExistsException extends RuntimeException {
public UsernameAlreadyExistsException(final Throwable e) {
super(e);
}
}
@@ -0,0 +1,36 @@
package org.mifosng.platform.loan.domain;
public enum AmortizationMethod {
EQUAL_PRINCIPAL(0), EQUAL_INSTALLMENTS(1), INVALID(2);
private final Integer value;
private AmortizationMethod(final Integer value) {
this.value = value;
}
public Integer getValue() {
return this.value;
}
public static AmortizationMethod fromInt(final Integer selectedMethod) {
if (selectedMethod == null) {
return null;
}
AmortizationMethod repaymentMethod = null;
switch (selectedMethod) {
case 0:
repaymentMethod = AmortizationMethod.EQUAL_PRINCIPAL;
break;
case 1:
repaymentMethod = AmortizationMethod.EQUAL_INSTALLMENTS;
break;
default:
repaymentMethod = AmortizationMethod.INVALID;
break;
}
return repaymentMethod;
}
}
@@ -0,0 +1,130 @@
package org.mifosng.platform.loan.domain;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DailyEquivalentDecliningBalanceInterestRebateCalculator implements
InterestRebateCalculator {
private final static Logger logger = LoggerFactory
.getLogger(DailyEquivalentDecliningBalanceInterestRebateCalculator.class);
@Override
public Money calculate(
final LocalDate actualDisbursementDate,
final LocalDate paidInFullDate,
final Money loanPrincipal,
final BigDecimal interestRatePerAnnum,
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments,
final List<LoanTransaction> loanRepayments) {
MonetaryCurrency currency = loanPrincipal.getCurrency();
Money totalOriginalInterest = getTotalInterestOnLoan(currency, repaymentScheduleInstallments);
BigDecimal dailyEquivalentPeriodicInterestRate = interestRatePerAnnum
.divide(BigDecimal.valueOf(36500), 9, RoundingMode.HALF_EVEN);
Money totalRecalculatedPrincipalPaid = Money.zero(loanPrincipal.getCurrency());
Money totalRecalculatedInterestPaid = Money.zero(loanPrincipal.getCurrency());
LocalDate lastBalanceDate = actualDisbursementDate;
Money outstandingBalance = loanPrincipal;
for (LoanTransaction loanRepayment : loanRepayments) {
int daysAtBalance = Days.daysBetween(lastBalanceDate,
loanRepayment.getTransactionDate()).getDays();
BigDecimal periodInterestRateForPaymentPeriod = dailyEquivalentPeriodicInterestRate.multiply(BigDecimal.valueOf(daysAtBalance));
BigDecimal interestDueOnPaymentDateAmount = outstandingBalance.getAmount().multiply(periodInterestRateForPaymentPeriod);
Money interestDueOnPaymentDate = Money.of(loanPrincipal.getCurrency(),interestDueOnPaymentDateAmount);
Money principalDueOnPaymentDate = loanRepayment.getAmount(currency).minus(interestDueOnPaymentDate);
totalRecalculatedPrincipalPaid = totalRecalculatedPrincipalPaid.plus(principalDueOnPaymentDate);
totalRecalculatedInterestPaid = totalRecalculatedInterestPaid.plus(interestDueOnPaymentDate);
logger.warn("Installment: " + lastBalanceDate + " - "
+ loanRepayment.getTransactionDate() + " principal: "
+ principalDueOnPaymentDate + "interest: "
+ interestDueOnPaymentDate + " total: "
+ loanRepayment.getAmount());
lastBalanceDate = loanRepayment.getTransactionDate();
outstandingBalance = outstandingBalance
.minus(principalDueOnPaymentDate);
}
// finally
// add up total paid to date - work out interest due on that
int daysAtBalance = Days.daysBetween(lastBalanceDate, paidInFullDate)
.getDays();
BigDecimal periodInterestRateForPaymentPeriod = dailyEquivalentPeriodicInterestRate
.multiply(BigDecimal.valueOf(daysAtBalance));
BigDecimal interestDueOnPaymentDateAmount = outstandingBalance
.getAmount().multiply(periodInterestRateForPaymentPeriod);
Money interestDueOnPaymentDate = Money.of(
loanPrincipal.getCurrency(),
interestDueOnPaymentDateAmount);
Money principalDueOnPaymentDate = outstandingBalance;
Money netOutstandingOnPaidInFullDate = principalDueOnPaymentDate
.plus(interestDueOnPaymentDate);
totalRecalculatedPrincipalPaid = totalRecalculatedPrincipalPaid.plus(principalDueOnPaymentDate);
totalRecalculatedInterestPaid = totalRecalculatedInterestPaid.plus(interestDueOnPaymentDate);
logger.warn("Installment: " + lastBalanceDate + " - " + paidInFullDate
+ " principal: " + principalDueOnPaymentDate + "interest: "
+ interestDueOnPaymentDate + " total: "
+ netOutstandingOnPaidInFullDate);
lastBalanceDate = paidInFullDate;
outstandingBalance = outstandingBalance
.minus(principalDueOnPaymentDate);
Money rebate = Money.zero(loanPrincipal.getCurrency());
// Interest can be greater for following reasons:
// 1. calculated interest may be slightly ahead based on daily
// equivalent
// method of calculating interest compared to original method.
// 2. payments were under expected amount or after expected date
// resulting in larger outstanding balance being used to calculate
// interest
// at each repayment point in time.
if (totalRecalculatedInterestPaid.isLessThan(totalOriginalInterest)
&& totalRecalculatedInterestPaid.isGreaterThanZero()) {
rebate = totalOriginalInterest.minus(totalRecalculatedInterestPaid);
}
return rebate;
}
private Money getTotalInterestOnLoan(
final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments) {
Money cumulativeInterest = Money.zero(currency);
for (LoanRepaymentScheduleInstallment scheduledRepayment : repaymentScheduleInstallments) {
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment
.getInterest(currency));
}
return cumulativeInterest;
}
}
@@ -0,0 +1,70 @@
package org.mifosng.platform.loan.domain;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
public class DailyEquivalentFlatInterestRebateCalculator implements
InterestRebateCalculator {
@Override
public Money calculate(
final LocalDate actualDisbursementDate,
final LocalDate paidInFullDate,
final Money loanPrincipal,
final BigDecimal interestRatePerAnnum,
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments,
final List<LoanTransaction> loanRepayments) {
MonetaryCurrency currency = loanPrincipal.getCurrency();
LocalDate maturityDate = repaymentScheduleInstallments.get(
repaymentScheduleInstallments.size() - 1).getDueDate();
int daysEquivalentOfLoanTerm = Days.daysBetween(actualDisbursementDate,
maturityDate).getDays();
Money totalOriginalInterest = getTotalInterestOnLoan(
currency,
repaymentScheduleInstallments);
int actualLoanTermInDays = Days.daysBetween(actualDisbursementDate,
paidInFullDate).getDays();
BigDecimal divisor = loanPrincipal.getAmount().multiply(
BigDecimal.valueOf(daysEquivalentOfLoanTerm));
BigDecimal periodicInterestRate = totalOriginalInterest.getAmount().divide(
divisor, 9, RoundingMode.HALF_EVEN);
Money totalRecalculatedInterest = loanPrincipal.multipliedBy(
periodicInterestRate).multipliedBy(actualLoanTermInDays);
Money rebate = Money.zero(loanPrincipal.getCurrency());
if (totalRecalculatedInterest.isLessThan(totalOriginalInterest)
&& totalRecalculatedInterest.isGreaterThanZero()) {
rebate = totalOriginalInterest.minus(totalRecalculatedInterest);
}
return rebate;
}
private Money getTotalInterestOnLoan(
final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments) {
Money cumulativeInterest = Money.zero(currency);
for (LoanRepaymentScheduleInstallment scheduledRepayment : repaymentScheduleInstallments) {
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment
.getInterest(currency));
}
return cumulativeInterest;
}
}
@@ -0,0 +1,34 @@
package org.mifosng.platform.loan.domain;
public class DailyEquivalentInterestRebateCalculatorFactory implements
InterestRebateCalculatorFactory {
@Override
public InterestRebateCalculator createCalcualtor(
final InterestMethod loanRepaymentScheduleMethod, final AmortizationMethod amortizationMethod) {
InterestRebateCalculator interestRebateCalculator = null;
switch (loanRepaymentScheduleMethod) {
case DECLINING_BALANCE:
switch (amortizationMethod) {
case EQUAL_INSTALLMENTS:
interestRebateCalculator = new DailyEquivalentDecliningBalanceInterestRebateCalculator();
break;
default:
interestRebateCalculator = new DailyEquivalentDecliningBalanceInterestRebateCalculator();
break;
}
break;
case FLAT:
interestRebateCalculator = new DailyEquivalentFlatInterestRebateCalculator();
break;
default:
interestRebateCalculator = new DailyEquivalentFlatInterestRebateCalculator();
break;
}
return interestRebateCalculator;
}
}
@@ -0,0 +1,113 @@
package org.mifosng.platform.loan.domain;
import java.util.List;
public class DefaultLoanLifecycleStateMachine implements
LoanLifecycleStateMachine {
private final List<LoanStatus> allowedLoanStatuses;
public DefaultLoanLifecycleStateMachine(List<LoanStatus> allowedLoanStatuses) {
this.allowedLoanStatuses = allowedLoanStatuses;
}
@Override
public LoanStatus transition(LoanEvent loanEvent, LoanStatus from) {
LoanStatus newState = null;
switch (loanEvent) {
case LOAN_CREATED:
if (from == null) {
newState = stateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL,
allowedLoanStatuses);
}
break;
case LOAN_REJECTED:
if (from.hasStateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL)) {
newState = stateOf(LoanStatus.REJECTED, allowedLoanStatuses);
}
break;
case LOAN_APPROVED:
if (from.hasStateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL)) {
newState = stateOf(LoanStatus.APPROVED, allowedLoanStatuses);
}
break;
case LOAN_WITHDRAWN:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.SUBMITED_AND_PENDING_APPROVAL, LoanStatus.APPROVED)) {
newState = stateOf(LoanStatus.WITHDRAWN_BY_CLIENT, allowedLoanStatuses);
}
break;
case LOAN_DISBURSED:
if (from.hasStateOf(LoanStatus.APPROVED)) {
newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses);
}
break;
case LOAN_APPROVAL_UNDO:
if (from.hasStateOf(LoanStatus.APPROVED)) {
newState = stateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL, allowedLoanStatuses);
}
break;
case LOAN_DISBURSAL_UNDO:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.APPROVED, allowedLoanStatuses);
}
break;
case LOAN_REPAYMENT:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses);
} else {
newState = from;
}
break;
case REPAID_IN_FULL:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses);
}
break;
case LOAN_WRITE_OFF:
if (this.anyOfAllowedWhenComingFrom(from,LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses);
}
break;
case LOAN_RESCHEDULE:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses);
}
break;
case INTERST_REBATE_OWED:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED)) {
newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses);
}
break;
}
return newState;
}
private LoanStatus stateOf(Integer state, List<LoanStatus> allowedLoanStatuses) {
LoanStatus match = null;
for (LoanStatus loanStatus : allowedLoanStatuses) {
if (loanStatus.hasStateOf(state)) {
match = loanStatus;
break;
}
}
return match;
}
private boolean anyOfAllowedWhenComingFrom(final LoanStatus state,
final Integer... allowedStates) {
boolean allowed = false;
for (Integer allowedState : allowedStates) {
if (state.hasStateOf(allowedState)) {
allowed = true;
break;
}
}
return allowed;
}
}
@@ -0,0 +1,489 @@
package org.mifosng.platform.loan.domain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.data.CurrencyData;
import org.mifosng.data.DerivedLoanData;
import org.mifosng.data.LoanAccountSummaryData;
import org.mifosng.data.LoanRepaymentData;
import org.mifosng.data.LoanRepaymentDataComparator;
import org.mifosng.data.LoanRepaymentPeriodData;
import org.mifosng.data.LoanRepaymentScheduleData;
import org.mifosng.data.MoneyData;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
// FIXME - The derived details of the loan schedule are now updated on the fly when someone does a transaction against a loan. So this is not fully needed.
// - we still use this to figure out how much of a given transactions amount was principal versus interest but might not need this and we should probably
// - derive and update that on the database on the fly
public class DerivedLoanDataProcessor {
public DerivedLoanData process(List<LoanRepaymentScheduleInstallment> repaymentSchedulePeriods, List<LoanTransaction> loanTransactions,
CurrencyData currencyDetail, Money arrearsTolerance) {
// 1. derive repayment schedule
LoanRepaymentScheduleData repaymentScheduleDetails = generateRepaymentScheduleData(repaymentSchedulePeriods, currencyDetail);
List<LoanRepaymentPeriodData> loanRepaymentPeriods = repaymentScheduleDetails.getPeriods();
// break up payments based on principal and interest
List<LoanRepaymentData> loanRepayments = new ArrayList<LoanRepaymentData>();
int repaymentScheduleIndex = 0;
for (LoanTransaction transaction : loanTransactions) {
if (transaction.isRepayment() || transaction.isWaiver()) {
MonetaryCurrency currency = new MonetaryCurrency(currencyDetail.getCode(), currencyDetail.getDecimalPlaces());
if (transaction.getAmount(currency).isGreaterThanZero()) {
LoanRepaymentPeriodData scheduledRepaymentPeriod = loanRepaymentPeriods
.get(repaymentScheduleIndex);
if (transaction.isRepayment()) {
LoanRepaymentData repaymentData = processLoanRepayment(
transaction, scheduledRepaymentPeriod,
repaymentScheduleIndex, loanRepaymentPeriods,
arrearsTolerance, currencyDetail);
loanRepayments.add(repaymentData);
}
if (transaction.isWaiver()) {
LoanRepaymentData repaymentData = processWaiver(
transaction, scheduledRepaymentPeriod,
repaymentScheduleIndex, loanRepaymentPeriods,
arrearsTolerance, currencyDetail);
loanRepayments.add(repaymentData);
}
if (scheduledRepaymentPeriod.isFullyPaid()) {
repaymentScheduleIndex++;
}
}
}
}
loanRepaymentPeriods = calculateArrears(loanRepaymentPeriods, arrearsTolerance, currencyDetail);
LoanRepaymentScheduleData processRepaymentScheduleData = new LoanRepaymentScheduleData(loanRepaymentPeriods);
LoanAccountSummaryData summaryData = calculateTotalsFromRepaymentScheduleData(loanRepaymentPeriods, currencyDetail);
Collections.sort(loanRepayments, new LoanRepaymentDataComparator());
return new DerivedLoanData(processRepaymentScheduleData, summaryData, loanRepayments);
}
private LoanRepaymentData processWaiver(LoanTransaction transaction,
LoanRepaymentPeriodData scheduledRepaymentPeriod,
int repaymentScheduleIndex,
List<LoanRepaymentPeriodData> loanRepaymentPeriods,
Money arrearsTolerance, CurrencyData currencyData) {
MonetaryCurrency currency = new MonetaryCurrency(currencyData.getCode(), currencyData.getDecimalPlaces());
Money totalToWaive = transaction.getAmount(currency);
Money interestToWaive = moneyFrom(scheduledRepaymentPeriod.getInterestOutstanding());
Money principalToWaive = moneyFrom(scheduledRepaymentPeriod.getPrincipalOutstanding());
Money interestWaived = Money.zero(currency);
if (totalToWaive.isGreaterThanOrEqualTo(interestToWaive)) {
totalToWaive = totalToWaive.minus(interestToWaive);
interestWaived = interestToWaive;
} else {
interestWaived = totalToWaive;
totalToWaive = Money.zero(currency);
}
Money principalWaived = Money.zero(currency);
if (totalToWaive.isGreaterThanOrEqualTo(principalToWaive)) {
totalToWaive = totalToWaive.minus(principalToWaive);
principalWaived = principalToWaive;
} else {
principalWaived = totalToWaive;
totalToWaive = Money.zero(currency);
}
MoneyData zero = moneyDataFrom(currencyData, Money.zero(currency));
Money totalWaivedInPeriodToDate = Money.zero(currency);
if (scheduledRepaymentPeriod.getTotalWaived() != null) {
totalWaivedInPeriodToDate = moneyFrom(scheduledRepaymentPeriod.getTotalWaived());
}
Money totalWaivedInTransaction = interestWaived.plus(principalWaived);
totalWaivedInPeriodToDate = totalWaivedInPeriodToDate.plus(totalWaivedInTransaction);
scheduledRepaymentPeriod.setTotalWaived(moneyDataFrom(currencyData, totalWaivedInPeriodToDate));
MoneyData totalWaived = moneyDataFrom(currencyData, totalWaivedInTransaction);
Money principalOutstanding = moneyFrom(scheduledRepaymentPeriod.getPrincipalOutstanding());
Money remainingPrincipalOutstanding = principalOutstanding.minus(principalWaived);
scheduledRepaymentPeriod.setPrincipalOutstanding(moneyDataFrom(currencyData, remainingPrincipalOutstanding));
Money interestOutstanding = moneyFrom(scheduledRepaymentPeriod.getInterestOutstanding());
Money remainingInterestOutstanding = interestOutstanding.minus(interestWaived);
scheduledRepaymentPeriod.setInterestOutstanding(moneyDataFrom(currencyData, remainingInterestOutstanding));
Money totalOutstanding = moneyFrom(scheduledRepaymentPeriod.getTotalOutstanding());
Money remainingOutstanding = totalOutstanding.minus(moneyFrom(totalWaived));
scheduledRepaymentPeriod.setTotalOutstanding(moneyDataFrom(currencyData, remainingOutstanding));
LoanRepaymentData loanRepaymentData = new LoanRepaymentData(transaction.getId(), transaction.getTransactionDate(), moneyDataFrom(currencyData, principalWaived),
moneyDataFrom(currencyData, interestWaived), totalWaived, zero);
loanRepaymentData.setTotalWaived(totalWaived);
return loanRepaymentData;
}
private List<LoanRepaymentPeriodData> calculateArrears(
List<LoanRepaymentPeriodData> loanRepaymentPeriods,
Money arrearsTolerance, CurrencyData currencyData) {
LocalDate today = new LocalDate();
int repaymentIndex = 0;
for (LoanRepaymentPeriodData currentRepaymentSchedule : loanRepaymentPeriods) {
if (currentRepaymentSchedule.isInArrearsWithToleranceOf(
moneyDataFrom(currencyData, arrearsTolerance), today)) {
currentRepaymentSchedule
.setTotalArrears(currentRepaymentSchedule
.getTotalOutstanding());
LocalDate arrearsFrom = determineInArrearsFrom(currentRepaymentSchedule, loanRepaymentPeriods, repaymentIndex);
LocalDate arrearsTo = determineInArrearsTo(currentRepaymentSchedule, loanRepaymentPeriods, repaymentIndex);
currentRepaymentSchedule.setArrearsFrom(arrearsFrom);
currentRepaymentSchedule.setArrearsTo(arrearsTo);
}
repaymentIndex++;
}
return loanRepaymentPeriods;
}
private LocalDate determineInArrearsTo(
LoanRepaymentPeriodData currentRepaymentSchedule,
List<LoanRepaymentPeriodData> loanRepaymentPeriods,
int repaymentIndex) {
LocalDate today = new LocalDate();
LocalDate arrearsDate = null;
if (loanRepaymentPeriods.size() - 1 > repaymentIndex) {
LoanRepaymentPeriodData nextRepaymentSchedule = loanRepaymentPeriods
.get(repaymentIndex);
arrearsDate = nextRepaymentSchedule.getDate().minusDays(1);
}
if (loanRepaymentPeriods.size() - 1 == repaymentIndex) {
arrearsDate = today;
}
return arrearsDate;
}
private LocalDate determineInArrearsFrom(
LoanRepaymentPeriodData currentRepaymentSchedule,
List<LoanRepaymentPeriodData> loanRepaymentPeriods,
int repaymentIndex) {
int previousScheduleIndex = repaymentIndex - 1;
LocalDate arrearsDate = null;
if (currentRepaymentSchedule.getLastAffectingPaymentOn() != null) {
if (currentRepaymentSchedule.getLastAffectingPaymentOn().isAfter(
currentRepaymentSchedule.getDate())) {
if (loanRepaymentPeriods.size() - 1 > previousScheduleIndex && previousScheduleIndex > -1) {
LoanRepaymentPeriodData lastRepaymentSchedule = loanRepaymentPeriods
.get(previousScheduleIndex);
arrearsDate = lastRepaymentSchedule.getDate().plusDays(1);
} else {
arrearsDate = currentRepaymentSchedule.getDate().minusMonths(1);
}
} else {
arrearsDate = currentRepaymentSchedule.getLastAffectingPaymentOn();
}
} else {
if (loanRepaymentPeriods.size() - 1 > previousScheduleIndex && previousScheduleIndex > -1) {
LoanRepaymentPeriodData lastRepaymentSchedule = loanRepaymentPeriods
.get(previousScheduleIndex);
arrearsDate = lastRepaymentSchedule.getDate().plusDays(1);
} else {
arrearsDate = currentRepaymentSchedule.getDate().minusMonths(1);
}
}
return arrearsDate;
}
private LoanAccountSummaryData calculateTotalsFromRepaymentScheduleData(
List<LoanRepaymentPeriodData> loanRepaymentPeriods, CurrencyData currencyData) {
MonetaryCurrency monetaryCurrency = new MonetaryCurrency(currencyData.getCode(), currencyData.getDecimalPlaces());
Money principalExpected = Money.zero(monetaryCurrency);
Money principalPaid = Money.zero(monetaryCurrency);
Money principalOutstanding = Money.zero(monetaryCurrency);
Money interestExpected = Money.zero(monetaryCurrency);
Money interestPaid = Money.zero(monetaryCurrency);
Money interestOutstanding = Money.zero(monetaryCurrency);
Money totalExpected = Money.zero(monetaryCurrency);
Money totalPaid = Money.zero(monetaryCurrency);
Money totalOutstanding = Money.zero(monetaryCurrency);
Money totalInArrears = Money.zero(monetaryCurrency);
Money totalWaived = Money.zero(monetaryCurrency);
for (LoanRepaymentPeriodData period : loanRepaymentPeriods) {
principalExpected = principalExpected.plus(moneyFrom(period.getPrincipal()));
principalPaid = principalPaid.plus(moneyFrom(period.getPrincipalPaid()));
principalOutstanding = principalOutstanding.plus(moneyFrom(period.getPrincipalOutstanding()));
interestExpected = interestExpected.plus(moneyFrom(period.getInterest()));
interestPaid = interestPaid.plus(moneyFrom(period.getInterestPaid()));
interestOutstanding = interestOutstanding.plus(moneyFrom(period.getInterestOutstanding()));
totalExpected = totalExpected.plus(moneyFrom(period.getTotal()));
totalPaid = totalPaid.plus(moneyFrom(period.getTotalPaid()));
totalOutstanding = totalOutstanding.plus(moneyFrom(period.getTotalOutstanding()));
if (period.getTotalWaived() != null) {
totalWaived = totalWaived.plus(moneyFrom(period.getTotalWaived()));
}
if (period.getTotalArrears() != null) {
totalInArrears = totalInArrears.plus(moneyFrom(period.getTotalArrears()));
}
}
return new LoanAccountSummaryData(moneyDataFrom(currencyData, principalExpected), moneyDataFrom(currencyData, principalPaid),
moneyDataFrom(currencyData, principalOutstanding), moneyDataFrom(currencyData, interestExpected),
moneyDataFrom(currencyData, interestPaid), moneyDataFrom(currencyData, interestOutstanding),
moneyDataFrom(currencyData, totalExpected), moneyDataFrom(currencyData, totalPaid), moneyDataFrom(currencyData, totalOutstanding),
moneyDataFrom(currencyData, totalInArrears), moneyDataFrom(currencyData, totalWaived));
}
/**
* TODO - pays off interest first then principal so should make this configurable when generating 'derived view of loan account'
*/
private LoanRepaymentData processLoanRepayment(LoanTransaction repayment,
LoanRepaymentPeriodData scheduledRepayment,
int repaymentScheduleIndex,
List<LoanRepaymentPeriodData> repaymentSchedulePeriods,
Money arrearsTolerance, CurrencyData currencyDetail) {
MoneyData zero = MoneyData.zero(currencyDetail);
MoneyData cumulativeInterestPaid = MoneyData.zero(currencyDetail);
MoneyData cumulativePrincipalPaid = MoneyData.zero(currencyDetail);
LoanRepaymentPeriodData currentRepaymentSchedule = scheduledRepayment;
int currentRepaymentScheduleIndex = repaymentScheduleIndex;
MoneyData overpaid = null;
MonetaryCurrency currency = new MonetaryCurrency(currencyDetail.getCode(), currencyDetail.getDecimalPlaces());
Money remaining = repayment.getAmount(currency);
while (remaining.isGreaterThanZero()) {
Money interestOutstanding = moneyFrom(currentRepaymentSchedule.getInterestOutstanding());
Money principalOutstanding = moneyFrom(currentRepaymentSchedule.getPrincipalOutstanding());
// pay off interest component
if (remaining.isGreaterThanOrEqualTo(interestOutstanding)) {
Money previousInterestPaid = moneyFrom(cumulativeInterestPaid);
cumulativeInterestPaid = moneyDataFrom(currencyDetail, previousInterestPaid.plus(interestOutstanding));
MoneyData interestPaid = moneyDataFrom(currencyDetail, interestOutstanding);
Money paidToDate = moneyFrom(currentRepaymentSchedule.getInterestPaid());
Money amountPaid = moneyFrom(interestPaid);
paidToDate = paidToDate.plus(amountPaid);
MoneyData totalInterest = moneyDataFrom(currencyDetail, paidToDate);
currentRepaymentSchedule.setInterestPaid(totalInterest);
currentRepaymentSchedule.setInterestOutstanding(zero);
Money totalPaidToDate = moneyFrom(currentRepaymentSchedule.getTotalPaid());
totalPaidToDate = totalPaidToDate.plus(interestOutstanding);
currentRepaymentSchedule.setTotalPaid(moneyDataFrom(currencyDetail, totalPaidToDate));
Money outstanding = moneyFrom(currentRepaymentSchedule.getTotal()).minus(moneyFrom(currentRepaymentSchedule.getTotalPaid()));
currentRepaymentSchedule.setTotalOutstanding(moneyDataFrom(currencyDetail, outstanding));
if (interestOutstanding.isGreaterThanZero()) {
currentRepaymentSchedule.setLastAffectingPaymentOn(repayment.getTransactionDate());
}
remaining = remaining.minus(interestOutstanding);
} else {
// partial payment of principal
Money previousInterestPaid = moneyFrom(cumulativeInterestPaid);
cumulativeInterestPaid = moneyDataFrom(currencyDetail, previousInterestPaid.plus(remaining));
MoneyData interestPaid = moneyDataFrom(currencyDetail, remaining);
Money paidToDate = moneyFrom(currentRepaymentSchedule.getInterestPaid());
Money amountPaid = moneyFrom(interestPaid);
paidToDate = paidToDate.plus(amountPaid);
MoneyData totalInterest = moneyDataFrom(currencyDetail, paidToDate);
currentRepaymentSchedule.setInterestPaid(totalInterest);
interestOutstanding = interestOutstanding.minus(amountPaid);
currentRepaymentSchedule.setInterestOutstanding(moneyDataFrom(currencyDetail, interestOutstanding));
Money totalPaidToDate = moneyFrom(currentRepaymentSchedule.getTotalPaid());
totalPaidToDate = totalPaidToDate.plus(amountPaid);
currentRepaymentSchedule.setTotalPaid(moneyDataFrom(currencyDetail, totalPaidToDate));
Money outstanding = moneyFrom(currentRepaymentSchedule.getTotal()).minus(moneyFrom(currentRepaymentSchedule.getTotalPaid()));
currentRepaymentSchedule.setTotalOutstanding(moneyDataFrom(currencyDetail, outstanding));
if (interestPaid.isGreaterThanZero()) {
currentRepaymentSchedule.setLastAffectingPaymentOn(repayment.getTransactionDate());
}
remaining = remaining.minus(amountPaid);
}
// pay off principal component
if (remaining.isGreaterThanOrEqualTo(principalOutstanding)) {
Money previousPrincipalPaid = moneyFrom(cumulativePrincipalPaid);
cumulativePrincipalPaid = moneyDataFrom(currencyDetail, previousPrincipalPaid.plus(principalOutstanding));
MoneyData principalPaid = moneyDataFrom(currencyDetail, principalOutstanding);
Money paidToDate = moneyFrom(currentRepaymentSchedule.getPrincipalPaid());
Money amountPaid = moneyFrom(principalPaid);
paidToDate = paidToDate.plus(amountPaid);
MoneyData totalPrincipal = moneyDataFrom(currencyDetail, paidToDate);
currentRepaymentSchedule.setPrincipalPaid(totalPrincipal);
currentRepaymentSchedule.setPrincipalOutstanding(zero);
Money totalPaidToDate = moneyFrom(currentRepaymentSchedule.getTotalPaid());
totalPaidToDate = totalPaidToDate.plus(principalOutstanding);
currentRepaymentSchedule.setTotalPaid(moneyDataFrom(currencyDetail, totalPaidToDate));
Money outstanding = moneyFrom(currentRepaymentSchedule.getTotal()).minus(moneyFrom(currentRepaymentSchedule.getTotalPaid()));
currentRepaymentSchedule.setTotalOutstanding(moneyDataFrom(currencyDetail, outstanding));
remaining = remaining.minus(principalOutstanding);
} else {
// partial payment of principal
Money previousPrincipalPaid = moneyFrom(cumulativePrincipalPaid);
cumulativePrincipalPaid = moneyDataFrom(currencyDetail, previousPrincipalPaid.plus(remaining));
MoneyData principalPaid = moneyDataFrom(currencyDetail, remaining);
Money paidToDate = moneyFrom(currentRepaymentSchedule.getPrincipalPaid());
Money amountPaid = moneyFrom(principalPaid);
paidToDate = paidToDate.plus(amountPaid);
MoneyData totalPrincipal = moneyDataFrom(currencyDetail, paidToDate);
currentRepaymentSchedule.setPrincipalPaid(totalPrincipal);
principalOutstanding = principalOutstanding.minus(amountPaid);
currentRepaymentSchedule.setPrincipalOutstanding(moneyDataFrom(currencyDetail, principalOutstanding));
Money totalPaidToDate = moneyFrom(currentRepaymentSchedule.getTotalPaid());
totalPaidToDate = totalPaidToDate.plus(amountPaid);
currentRepaymentSchedule.setTotalPaid(moneyDataFrom(currencyDetail, totalPaidToDate));
Money outstanding = moneyFrom(currentRepaymentSchedule.getTotal()).minus(moneyFrom(currentRepaymentSchedule.getTotalPaid()));
currentRepaymentSchedule.setTotalOutstanding(moneyDataFrom(currencyDetail, outstanding));
if (principalPaid.isGreaterThanZero()) {
currentRepaymentSchedule.setLastAffectingPaymentOn(repayment.getTransactionDate());
}
remaining = remaining.minus(amountPaid);
}
// mark schedule as paid in full
if (currentRepaymentSchedule.isFullyPaid()) {
currentRepaymentSchedule.setPaidInFullOn(repayment.getTransactionDate());
}
// check if this installment has
if (currentRepaymentSchedule.isInArrearsWithToleranceOf(moneyDataFrom(currencyDetail, arrearsTolerance), repayment.getTransactionDate())) {
currentRepaymentSchedule.setTotalArrears(currentRepaymentSchedule.getTotalOutstanding());
} else {
currentRepaymentSchedule.setTotalArrears(MoneyData.zero(currencyDetail));
}
if (remaining.isGreaterThanZero()) {
currentRepaymentScheduleIndex++;
if (repaymentSchedulePeriods.size() > currentRepaymentScheduleIndex) {
currentRepaymentSchedule = repaymentSchedulePeriods.get(currentRepaymentScheduleIndex);
} else {
// loan has been overpaid by an amount so
overpaid = moneyDataFrom(currencyDetail, remaining);
// note - maybe should set the overpayment 'amount' against repayment period.
remaining = Money.zero(remaining.getCurrency());
}
}
}
Money totalInterestComponent = moneyFrom(cumulativeInterestPaid);
Money totalPrincipalComponent= moneyFrom(cumulativePrincipalPaid);
MoneyData totalPaid = moneyDataFrom(currencyDetail, totalPrincipalComponent.plus(totalInterestComponent));
LoanRepaymentData loanRepaymentData = new LoanRepaymentData(repayment.getId(), repayment.getTransactionDate(), cumulativePrincipalPaid, cumulativeInterestPaid, totalPaid, overpaid);
return loanRepaymentData;
}
private LoanRepaymentScheduleData generateRepaymentScheduleData(
List<LoanRepaymentScheduleInstallment> repaymentSchedulePeriods,
CurrencyData currencyData) {
MoneyData zero = MoneyData.zero(currencyData);
MonetaryCurrency currency = new MonetaryCurrency(currencyData.getCode(), currencyData.getDecimalPlaces());
List<LoanRepaymentPeriodData> loanScheduleDetails = new ArrayList<LoanRepaymentPeriodData>();
for (LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedulePeriods) {
MoneyData principal = MoneyData.of(currencyData, scheduledRepayment.getPrincipal(currency).getAmount());
MoneyData interest = MoneyData.of(currencyData, scheduledRepayment.getInterest(currency).getAmount());
MoneyData total = MoneyData.of(currencyData, scheduledRepayment.getTotal(currency).getAmount());
LoanRepaymentPeriodData periodData = new LoanRepaymentPeriodData(
scheduledRepayment.getInstallmentNumber(),
scheduledRepayment.getDueDate(), zero, principal, interest,
total, total);
loanScheduleDetails.add(periodData);
}
return new LoanRepaymentScheduleData(loanScheduleDetails);
}
private Money moneyFrom(MoneyData money) {
MonetaryCurrency monetaryCurrency = new MonetaryCurrency(money.getCurrencyCode(), money.getCurrencyDigitsAfterDecimal());
return Money.of(monetaryCurrency, money.getAmount());
}
private MoneyData moneyDataFrom(CurrencyData currencyData, Money money) {
return MoneyData.of(currencyData, money.getAmount());
}
}
@@ -0,0 +1,32 @@
package org.mifosng.platform.loan.domain;
public enum InterestMethod {
DECLINING_BALANCE(0), FLAT(1), INVALID(2);
private final Integer value;
private InterestMethod(final Integer value) {
this.value = value;
}
public Integer getValue() {
return this.value;
}
public static InterestMethod fromInt(final Integer selectedMethod) {
InterestMethod repaymentMethod = null;
switch (selectedMethod) {
case 0:
repaymentMethod = InterestMethod.DECLINING_BALANCE;
break;
case 1:
repaymentMethod = InterestMethod.FLAT;
break;
default:
repaymentMethod = InterestMethod.INVALID;
break;
}
return repaymentMethod;
}
}
@@ -0,0 +1,19 @@
package org.mifosng.platform.loan.domain;
import java.math.BigDecimal;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.Money;
public interface InterestRebateCalculator {
Money calculate(
LocalDate actualDisbursementDate,
LocalDate paidInFullDate,
Money loanPrincipal,
BigDecimal interestRatePerAnnum,
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments,
List<LoanTransaction> loanRepayments);
}
@@ -0,0 +1,9 @@
package org.mifosng.platform.loan.domain;
public interface InterestRebateCalculatorFactory {
InterestRebateCalculator createCalcualtor(
InterestMethod loanRepaymentScheduleMethod,
AmortizationMethod amortizationMethod);
}
@@ -0,0 +1,15 @@
package org.mifosng.platform.loan.domain;
public class InvalidLoanTimelineDate extends RuntimeException {
private final String errorCode;
public InvalidLoanTimelineDate(final String message, final String errorCode) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return this.errorCode;
}
}
@@ -0,0 +1,947 @@
package org.mifosng.platform.loan.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.mifosng.data.CurrencyData;
import org.mifosng.data.DerivedLoanData;
import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
import org.mifosng.platform.organisation.domain.Organisation;
import org.mifosng.platform.user.domain.AppUser;
@Entity
@Table(name = "portfolio_loan", uniqueConstraints = @UniqueConstraint(columnNames = {"org_id", "external_id" }))
public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@ManyToOne
@JoinColumn(name = "org_id", nullable = false)
private final Organisation organisation;
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private final Client client;
@ManyToOne
@JoinColumn(name = "product_id")
private final LoanProduct loanProduct;
@Column(name = "external_id")
private String externalId;
@Embedded
private final LoanProductRelatedDetail loanRepaymentScheduleDetail;
@ManyToOne
@JoinColumn(name = "loan_status_id", nullable = false)
private LoanStatus loanStatus;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "submittedon_date")
private Date submittedOnDate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "rejectedon_date")
private Date rejectedOnDate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "withdrawnon_date")
private Date withdrawnOnDate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "approvedon_date")
private Date approvedOnDate;
@Temporal(TemporalType.DATE)
@Column(name = "expected_disbursedon_date")
private Date expectedDisbursedOnDate;
@Temporal(TemporalType.DATE)
@Column(name = "expected_firstrepaymenton_date")
private Date expectedFirstRepaymentOnDate;
@Temporal(TemporalType.DATE)
@Column(name = "interest_calculated_from_date")
private Date interestCalculatedFromDate;
@Temporal(TemporalType.DATE)
@Column(name = "disbursedon_date")
private Date disbursedOnDate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "closedon_date")
private Date closedOnDate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "writtenoffon_date")
private Date writtenOffOnDate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "rescheduledon_date")
private Date rescheduledOnDate;
@Temporal(TemporalType.DATE)
@Column(name = "expected_maturedon_date")
private Date expectedMaturityDate;
@Temporal(TemporalType.DATE)
@Column(name = "maturedon_date")
private Date maturedOnDate;
// see
// http://stackoverflow.com/questions/4334970/hibernate-cannot-simultaneously-fetch-multiple-bags
@LazyCollection(LazyCollectionOption.FALSE)
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true)
private final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = new ArrayList<LoanRepaymentScheduleInstallment>();
// see
// http://stackoverflow.com/questions/4334970/hibernate-cannot-simultaneously-fetch-multiple-bags
@OrderBy(value = "dateOf, id")
@LazyCollection(LazyCollectionOption.FALSE)
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true)
private final List<LoanTransaction> loanTransactions = new ArrayList<LoanTransaction>();
@Column(name = "interest_rebate_amount", scale = 6, precision = 19)
private BigDecimal interestRebateOwed;
@Transient
private final InterestRebateCalculatorFactory interestRebateCalculatorFactory = new DailyEquivalentInterestRebateCalculatorFactory();
public Loan() {
this.organisation = null;
this.client = null;
this.loanProduct = null;
this.loanRepaymentScheduleDetail = null;
}
public Loan(final Organisation organisation, final Client client,
final LoanProduct loanProduct,
final LoanProductRelatedDetail loanRepaymentScheduleDetail,
final LoanStatus loanStatus) {
this.organisation = organisation;
this.client = client;
this.loanProduct = loanProduct;
this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail;
this.loanStatus = loanStatus;
this.interestRebateOwed = BigDecimal.ZERO;
}
public Organisation getOrganisation() {
return this.organisation;
}
public Client getClient() {
return this.client;
}
public LoanProduct getLoanProduct() {
return this.loanProduct;
}
public LoanProductRelatedDetail getLoanRepaymentScheduleDetail() {
return this.loanRepaymentScheduleDetail;
}
public void submitApplication(final LocalDate submittedOn, final LocalDate expectedDisbursementDate,
LocalDate repaymentsStartingFromDate, LocalDate interestCalculatedFromDate, LoanLifecycleStateMachine lifecycleStateMachine) {
this.loanStatus = lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, this.loanStatus);
this.submittedOnDate = submittedOn.toDateTimeAtCurrentTime().toDate();
this.expectedMaturityDate = this.repaymentScheduleInstallments
.get(this.repaymentScheduleInstallments.size() - 1)
.getDueDate().toDateMidnight().toDate();
if (expectedDisbursementDate != null) {
// can be null during bulk upload of loans
this.expectedDisbursedOnDate = expectedDisbursementDate.toDateMidnight().toDate();
}
if (repaymentsStartingFromDate != null) {
this.expectedFirstRepaymentOnDate = repaymentsStartingFromDate.toDateMidnight().toDate();
}
if (interestCalculatedFromDate != null) {
this.interestCalculatedFromDate = interestCalculatedFromDate.toDateMidnight().toDate();
}
if (new LocalDate(this.submittedOnDate).isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The loan application submit date cannot be in the future.",
"invalid.submit.date.as.date.is.in.future");
}
if (new LocalDate(this.submittedOnDate).isAfter(new LocalDate(
this.expectedDisbursedOnDate))) {
throw new InvalidLoanTimelineDate(
"The loan application submit date cannot be after its expected disbursement date.",
"invalid.submit.date.as.date.is.after.expected.disbursement.date");
}
}
public void reject(final LocalDate rejectedOn, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, this.loanStatus);
this.rejectedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate();
this.closedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate();
if (new LocalDate(this.rejectedOnDate).isBefore(new LocalDate(
this.submittedOnDate))) {
throw new InvalidLoanTimelineDate(
"The loan rejection date cannot be before its submittal date.",
"invalid.rejection.date.as.date.is.before.submittal.date");
}
if (new LocalDate(this.rejectedOnDate).isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The loan application rejection date cannot be in the future.",
"invalid.rejection.date.as.date.is.in.future");
}
}
public void withdraw(final LocalDate withdrawnOn, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, this.loanStatus);
this.withdrawnOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate();
this.closedOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate();
if (new LocalDate(this.withdrawnOnDate).isBefore(new LocalDate(
this.submittedOnDate))) {
throw new InvalidLoanTimelineDate(
"The date of when loan is withdrawn cannot be before its submittal date.",
"invalid.withdrawal.date.as.date.is.before.submittal.date");
}
if (new LocalDate(this.withdrawnOnDate).isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The loan application withdraw date cannot be in the future.",
"invalid.withdrawal.date.as.date.is.in.future");
}
}
public void approve(final LocalDate approvedOn, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, this.loanStatus);
this.approvedOnDate = approvedOn.toDateTimeAtCurrentTime().toDate();
if (new LocalDate(this.approvedOnDate).isBefore(new LocalDate(
this.submittedOnDate))) {
throw new InvalidLoanTimelineDate(
"The date of when loan is approved cannot be before its submittal date.",
"invalid.approval.date.as.date.is.before.submittal.date");
}
if (new LocalDate(this.approvedOnDate).isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The date of when loan is approved cannot be in the future.",
"invalid.approval.date.as.date.is.in.future");
}
}
public void undoApproval(LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVAL_UNDO, this.loanStatus);
this.approvedOnDate = null;
}
public void disburseWithModifiedRepaymentSchedule(
final LocalDate disbursedOn,
final String comment,
final List<LoanRepaymentScheduleInstallment> modifiedLoanRepaymentSchedule, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.repaymentScheduleInstallments.clear();
for (LoanRepaymentScheduleInstallment modifiedInstallment : modifiedLoanRepaymentSchedule) {
modifiedInstallment.updateOrgnaisation(this.organisation);
modifiedInstallment.updateLoan(this);
this.repaymentScheduleInstallments.add(modifiedInstallment);
}
disburse(disbursedOn, loanLifecycleStateMachine);
}
public void disburse(final LocalDate disbursedOn, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, this.loanStatus);
this.disbursedOnDate = disbursedOn.toDateTimeAtCurrentTime().toDate();
this.expectedMaturityDate = this.repaymentScheduleInstallments
.get(this.repaymentScheduleInstallments.size() - 1)
.getDueDate().toDateMidnight().toDate();
LoanTransaction loanTransaction = LoanTransaction.disbursement(
this.loanRepaymentScheduleDetail.getPrincipal(), disbursedOn);
loanTransaction.updateLoan(this);
loanTransaction.updateOrganisation(organisation);
this.loanTransactions.add(loanTransaction);
if (disbursedOn.isBefore(new LocalDate(this.approvedOnDate))) {
throw new InvalidLoanTimelineDate(
"The date of when loan is disbursed cannot be before its approval date.",
"invalid.disbursal.as.disbursement.date.is.before.approved.date");
}
if (disbursedOn.isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The date of when loan is disbursed cannot be in the future.",
"invalid.disbursal.as.disbursement.date.is.in.future");
}
LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments
.get(0).getDueDate();
if (disbursedOn.isAfter(firstRepaymentDueDate)) {
throw new InvalidLoanTimelineDate(
"The date of when loan is disbursed cannot be after the first expected repayment.",
"invalid.disbursal.as.disbursement.date.is.after.first.repayment.due.date");
}
}
public void undoDisbursal(LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSAL_UNDO, this.loanStatus);
this.loanTransactions.clear();
this.disbursedOnDate = null;
}
public void waive(LoanTransaction loanTransaction, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus);
loanTransaction.updateLoan(this);
loanTransaction.updateOrganisation(organisation);
this.loanTransactions.add(loanTransaction);
if (loanTransaction.getTransactionDate().isBefore(
this.getDisbursementDate())) {
throw new InvalidLoanTimelineDate(
"The repayment date cannot be before the loan disbursement date.",
"invalid.repayment.date.as.date.is.before.disbursement.date");
}
if (loanTransaction.getTransactionDate().isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The repayment date cannot be in the future.",
"invalid.repayment.date.as.date.is.in.future");
}
if (this.isRepaidInFull()) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus);
this.closedOnDate = loanTransaction.getTransactionDate().toDate();
this.maturedOnDate = loanTransaction.getTransactionDate().toDate();
if (isInterestRebateAllowed()) {
Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction
.getTransactionDate());
if (rebateDue.isGreaterThanZero()) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.INTERST_REBATE_OWED, this.loanStatus);
this.interestRebateOwed = rebateDue.getAmount();
}
}
}
}
public void makeRepayment(final LoanTransaction loanTransaction, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus);
loanTransaction.updateLoan(this);
loanTransaction.updateOrganisation(organisation);
this.loanTransactions.add(loanTransaction);
deriveLoanRepaymentScheduleCompletedData();
if (loanTransaction.isNotRepayment()) {
throw new IllegalArgumentException(
"Only repayment transactions can be passed to makeRepayment.");
}
if (loanTransaction.getTransactionDate().isBefore(
this.getDisbursementDate())) {
throw new InvalidLoanTimelineDate(
"The repayment date cannot be before the loan disbursement date.",
"invalid.repayment.date.as.date.is.before.disbursement.date");
}
if (loanTransaction.getTransactionDate().isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The repayment date cannot be in the future.",
"invalid.repayment.date.as.date.is.in.future");
}
if (this.isRepaidInFull()) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus);
this.closedOnDate = loanTransaction.getTransactionDate().toDate();
this.maturedOnDate = loanTransaction.getTransactionDate().toDate();
if (isInterestRebateAllowed()) {
Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction
.getTransactionDate());
if (rebateDue.isGreaterThanZero()) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.INTERST_REBATE_OWED, this.loanStatus);
this.interestRebateOwed = rebateDue.getAmount();
}
}
}
}
private void deriveLoanRepaymentScheduleCompletedData() {
Money totalRepaidOrWaivedAgainstLoan = calculateTotalPaidOrWaived();
Money remainingToPayoffAgainstLoanSchedule = totalRepaidOrWaivedAgainstLoan;
Money totalOverpaid = Money.zero(totalRepaidOrWaivedAgainstLoan.getCurrency());
int repaymentInstallmentIndex = 0;
while (remainingToPayoffAgainstLoanSchedule.isGreaterThanZero()) {
if (repaymentInstallmentIndex == this.repaymentScheduleInstallments.size()) {
totalOverpaid = remainingToPayoffAgainstLoanSchedule;
// to exit while loop
remainingToPayoffAgainstLoanSchedule = remainingToPayoffAgainstLoanSchedule.minus(totalOverpaid);
} else {
LoanRepaymentScheduleInstallment scheduledRepaymentInstallment = this.repaymentScheduleInstallments.get(repaymentInstallmentIndex);
remainingToPayoffAgainstLoanSchedule = scheduledRepaymentInstallment.updateDerivedComponents(remainingToPayoffAgainstLoanSchedule);
repaymentInstallmentIndex++;
}
}
}
private Money calculateTotalPaidOrWaived() {
Money totalRepaidOrWaived = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency());
for (LoanTransaction transaction : this.loanTransactions) {
if (transaction.isRepayment() || transaction.isWaiver()) {
totalRepaidOrWaived = totalRepaidOrWaived.plus(transaction.getAmount());
}
}
return totalRepaidOrWaived;
}
public LocalDate possibleNextRepaymentDate() {
LocalDate earliestUnpaidInstallmentDate = new LocalDate();
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (installment.unpaid()) {
earliestUnpaidInstallmentDate = installment.getDueDate();
break;
}
}
LocalDate lastTransactionDate = null;
for (LoanTransaction transaction : this.loanTransactions) {
if (transaction.isRepayment() && transaction.isNonZero()) {
lastTransactionDate = transaction.getTransactionDate();
}
}
LocalDate possibleNextRepaymentDate = earliestUnpaidInstallmentDate;
if (lastTransactionDate != null && lastTransactionDate.isAfter(earliestUnpaidInstallmentDate)) {
possibleNextRepaymentDate = lastTransactionDate;
}
return possibleNextRepaymentDate;
}
public Money possibleNextRepaymentAmount() {
MonetaryCurrency currency = this.loanRepaymentScheduleDetail.getPrincipal().getCurrency();
Money possibleNextRepaymentAmount = Money.zero(currency);
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (installment.unpaid()) {
possibleNextRepaymentAmount = installment.getTotalDue(currency);
break;
}
}
return possibleNextRepaymentAmount;
}
public void adjustExistingTransaction(LoanTransaction transactionForAdjustment,
LoanTransaction newTransactionDetail, LoanLifecycleStateMachine loanLifecycleStateMachine) {
if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotWaiver()) {
throw new IllegalArgumentException("Only repayment and waiver transactions can be adjusted.");
}
transactionForAdjustment.contra();
if (newTransactionDetail.isRepayment()) {
makeRepayment(newTransactionDetail, loanLifecycleStateMachine);
}
if (newTransactionDetail.isWaiver()) {
waive(newTransactionDetail, loanLifecycleStateMachine);
}
}
private boolean isRepaidInFull() {
Money cumulativePrincipal = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
Money cumulativeInterest = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
Money cumulativeTotal = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
Money cumulativeWaived = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativePrincipal = cumulativePrincipal.plus(scheduledRepayment.getPrincipal(loanCurrency()));
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment
.getInterest(loanCurrency()));
cumulativeTotal = cumulativeTotal.plus(scheduledRepayment
.getTotal(loanCurrency()));
}
for (LoanTransaction transaction : this.loanTransactions) {
if (transaction.isRepayment()) {
cumulativePaid = cumulativePaid.plus(transaction.getAmount());
}
if (transaction.isWaiver()) {
cumulativeWaived = cumulativeWaived.plus(transaction
.getAmount());
}
}
return cumulativePaid.plus(cumulativeWaived).isGreaterThanOrEqualTo(
cumulativeTotal);
}
private MonetaryCurrency loanCurrency() {
return this.loanRepaymentScheduleDetail.getCurrency();
}
public void writeOff(final DateTime writtenOffOn, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WRITE_OFF, this.loanStatus);
this.closedOnDate = writtenOffOn.toDate();
this.writtenOffOnDate = writtenOffOn.toDate();
if (new LocalDate(this.writtenOffOnDate).isBefore(this
.getDisbursementDate())) {
throw new InvalidLoanTimelineDate(
"The date the loan is written off cannot be before the loan disbursement date.",
"invalid.writeoff.date.as.date.is.before.disbursement.date");
}
if (new LocalDate(this.writtenOffOnDate).isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The date the loan is written off cannot be in the future.",
"invalid.writeoff.date.as.date.is.in.future");
}
}
public void reschedule(final DateTime rescheduledOn, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, this.loanStatus);
this.closedOnDate = rescheduledOn.toDate();
this.rescheduledOnDate = rescheduledOn.toDate();
if (new LocalDate(this.rescheduledOnDate).isBefore(this
.getDisbursementDate())) {
throw new InvalidLoanTimelineDate(
"The date the loan is rescheduled cannot be before the loan disbursement date.",
"invalid.reschedule.date.as.date.is.before.disbursement.date");
}
if (new LocalDate(this.rescheduledOnDate).isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The date the loan is rescheduled cannot be in the future.",
"invalid.reschedule.date.as.date.is.in.future");
}
}
public boolean isNotSubmittedAndPendingApproval() {
return !isSubmittedAndPendingApproval();
}
public boolean isSubmittedAndPendingApproval() {
return this.loanStatus.isSubmittedAndPendingApproval();
}
public boolean isApproved() {
return this.loanStatus.isApproved();
}
public boolean isNotApproved() {
return !isApproved();
}
public boolean isWaitingForDisbursal() {
return this.isApproved() && this.isNotDisbursed();
}
public boolean isNotDisbursed() {
return !this.isDisbursed();
}
public boolean isDisbursed() {
return hasDisbursementTransaction();
}
public boolean isUndoDisbursalAllowed() {
return isDisbursed() && this.hasNoRepaymentTransaction();
}
public boolean isClosed() {
return this.loanStatus.isClosed() || this.isCancelled();
}
public boolean isCancelled() {
return this.isRejected() || this.isWithdrawn();
}
public boolean isWithdrawn() {
return this.loanStatus.isWithdrawnByClient();
}
public boolean isRejected() {
return this.loanStatus.isRejected();
}
public boolean isNotClosed() {
return !this.isClosed();
}
public boolean isOpen() {
return this.loanStatus.isActive();
}
public boolean isOpenWithNoRepaymentMade() {
return this.isOpen() && hasNoRepaymentTransaction();
}
private boolean hasNoRepaymentTransaction() {
return !hasRepaymentTransaction();
}
private boolean hasRepaymentTransaction() {
boolean hasRepaymentTransaction = false;
for (LoanTransaction loanTransaction : this.loanTransactions) {
if (loanTransaction.isRepayment()) {
hasRepaymentTransaction = true;
break;
}
}
return hasRepaymentTransaction;
}
private boolean hasDisbursementTransaction() {
boolean hasRepaymentTransaction = false;
for (LoanTransaction loanTransaction : this.loanTransactions) {
if (loanTransaction.isDisbursement()) {
hasRepaymentTransaction = true;
break;
}
}
return hasRepaymentTransaction;
}
public boolean isOpenWithRepaymentMade() {
return this.isOpen() && this.hasRepaymentTransaction();
}
public List<LoanRepaymentScheduleInstallment> getRepaymentScheduleInstallments() {
return this.repaymentScheduleInstallments;
}
public DerivedLoanData deriveLoanData(CurrencyData currencyData) {
List<LoanTransaction> repaymentTransactions = new ArrayList<LoanTransaction>();
for (LoanTransaction loanTransaction : this.loanTransactions) {
if (loanTransaction.isRepayment() || loanTransaction.isWaiver()) {
repaymentTransactions.add(loanTransaction);
}
}
Money arrearsTolerance = this.loanRepaymentScheduleDetail
.getInArrearsTolerance();
return new DerivedLoanDataProcessor().process(
new ArrayList<LoanRepaymentScheduleInstallment>(
this.repaymentScheduleInstallments),
repaymentTransactions, currencyData, arrearsTolerance);
}
public String getExternalId() {
return this.externalId;
}
public void setExternalId(final String externalSystemIdentifer) {
if (StringUtils.isNotBlank(externalSystemIdentifer)) {
this.externalId = externalSystemIdentifer.trim();
} else {
this.externalId = externalSystemIdentifer;
}
}
public LocalDate getSubmittedOnDate() {
return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(
this.submittedOnDate), null);
}
public LocalDate getRejectedOnDate() {
return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(
this.rejectedOnDate), null);
}
public LocalDate getApprovedOnDate() {
LocalDate date = null;
if (this.approvedOnDate != null) {
date = new LocalDate(this.approvedOnDate);
}
return date;
}
public LocalDate getDisbursedOnDate() {
LocalDate date = null;
if (this.disbursedOnDate != null) {
date = new LocalDate(this.disbursedOnDate);
}
return date;
}
public LocalDate getClosedOnDate() {
LocalDate date = null;
if (this.closedOnDate != null) {
date = new LocalDate(this.closedOnDate);
}
return date;
}
public LocalDate getWrittenOffOnDate() {
LocalDate date = null;
if (this.writtenOffOnDate != null) {
date = new LocalDate(this.writtenOffOnDate);
}
return date;
}
public Date getExpectedDisbursedOnDate() {
return this.expectedDisbursedOnDate;
}
public LocalDate getExpectedFirstRepaymentOnDate() {
LocalDate firstRepaymentDate = null;
if (this.expectedFirstRepaymentOnDate != null) {
firstRepaymentDate = new LocalDate(this.expectedFirstRepaymentOnDate);
}
return firstRepaymentDate;
}
public LocalDate getDisbursementDate() {
LocalDate disbursementDate = new LocalDate(this.expectedDisbursedOnDate);
if (this.disbursedOnDate != null) {
disbursementDate = new LocalDate(this.disbursedOnDate);
}
return disbursementDate;
}
public LocalDate getExpectedMaturityDate() {
LocalDate possibleMaturityDate = null;
if (this.expectedMaturityDate != null) {
possibleMaturityDate = new LocalDate(this.expectedMaturityDate);
}
return possibleMaturityDate;
}
public LocalDate getActualMaturityDate() {
LocalDate possibleMaturityDate = null;
if (this.maturedOnDate != null) {
possibleMaturityDate = new LocalDate(this.maturedOnDate);
}
return possibleMaturityDate;
}
public LocalDate getMaturityDate() {
LocalDate possibleMaturityDate = null;
if (this.expectedMaturityDate != null) {
possibleMaturityDate = new LocalDate(this.expectedMaturityDate);
}
if (this.maturedOnDate != null) {
possibleMaturityDate = new LocalDate(this.maturedOnDate);
}
return possibleMaturityDate;
}
public void addRepaymentScheduleInstallment(final LoanRepaymentScheduleInstallment installment) {
installment.updateOrgnaisation(this.organisation);
installment.updateLoan(this);
this.repaymentScheduleInstallments.add(installment);
}
public boolean isActualDisbursedOnDateEarlierOrLaterThanExpected() {
return isActualDisbursedOnDateEarlierOrLaterThanExpected(new LocalDate(
this.disbursedOnDate));
}
public boolean isActualDisbursedOnDateEarlierOrLaterThanExpected(
final LocalDate actualDisbursedOnDate) {
return !new LocalDate(this.expectedDisbursedOnDate)
.isEqual(actualDisbursedOnDate);
}
public boolean isFlexibleRepaymentSchedule() {
return this.loanProduct.isFlexibleRepaymentSchedule();
}
public boolean isInterestRebateAllowed() {
return this.loanProduct.isInterestRebateAllowed();
}
public boolean isRepaymentScheduleRegenerationRequiredForDisbursement(
final LocalDate actualDisbursementDate) {
boolean regenerationRequired = false;
if (isFlexibleRepaymentSchedule()) {
regenerationRequired = false;
} else {
if (isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate)) {
regenerationRequired = true;
}
}
return regenerationRequired;
}
public LoanPayoffSummary getPayoffSummaryOn(
final LocalDate projectedPayoffDate) {
LocalDate acutalDisbursementDate = new LocalDate(this.disbursedOnDate);
Money totalPaidToDate = this.getTotalPaid();
Money totalOutstandingBasedOnExpectedMaturityDate = this
.getTotalOutstanding();
Money totalOutstandingBasedOnPayoffDate = totalOutstandingBasedOnExpectedMaturityDate;
Money rebateGivenOnProjectedPayoffDate = Money
.zero(this.loanRepaymentScheduleDetail.getPrincipal()
.getCurrency());
if (isInterestRebateAllowed()) {
rebateGivenOnProjectedPayoffDate = calculateRebateWhenPaidInFullOn(projectedPayoffDate);
totalOutstandingBasedOnPayoffDate = totalOutstandingBasedOnExpectedMaturityDate
.minus(rebateGivenOnProjectedPayoffDate);
}
return new LoanPayoffSummary(this.getId(), acutalDisbursementDate,
this.getMaturityDate(), projectedPayoffDate, totalPaidToDate,
totalOutstandingBasedOnExpectedMaturityDate,
totalOutstandingBasedOnPayoffDate,
rebateGivenOnProjectedPayoffDate);
}
public Money getTotalOutstanding() {
return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus(
getTotalPaid());
}
private Money getTotalPaid() {
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
for (LoanTransaction repayment : this.loanTransactions) {
if (repayment.isRepayment()) {
cumulativePaid = cumulativePaid.plus(repayment.getAmount());
}
}
return cumulativePaid;
}
public Money calculateRebateWhenPaidInFullOn(final LocalDate paidInFullDate) {
Money loanPrincipal = this.loanRepaymentScheduleDetail.getPrincipal();
Money rebate = Money.zero(loanPrincipal.getCurrency());
if (this.isDisbursed()
&& !paidInFullDate.isBefore(this.getDisbursementDate())) {
InterestRebateCalculator interestRebateCalculator = this.interestRebateCalculatorFactory
.createCalcualtor(this.loanRepaymentScheduleDetail
.getInterestMethod(),
this.loanRepaymentScheduleDetail
.getAmortizationMethod());
rebate = interestRebateCalculator.calculate(this
.getDisbursementDate(), paidInFullDate, loanPrincipal,
this.loanRepaymentScheduleDetail
.getAnnualNominalInterestRate(),
this.repaymentScheduleInstallments, this.loanTransactions);
}
return rebate;
}
private Money getTotalInterestOnLoan() {
Money cumulativeInterest = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterest(loanCurrency()));
}
return cumulativeInterest;
}
private Money getTotalPrincipalOnLoan() {
Money cumulativePrincipal = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativePrincipal = cumulativePrincipal.plus(scheduledRepayment
.getPrincipal(loanCurrency()));
}
return cumulativePrincipal;
}
public Money getInterestRebateOwed() {
return Money.of(this.loanRepaymentScheduleDetail.getCurrency(), this.interestRebateOwed);
}
public Money getInArrearsTolerance() {
return this.loanRepaymentScheduleDetail.getInArrearsTolerance();
}
public boolean identifiedBy(String identifier) {
return identifier.equalsIgnoreCase(this.externalId)
|| identifier.equalsIgnoreCase(this.getId().toString());
}
public String getLoanStatusDisplayName() {
return this.loanStatus.getDisplayName();
}
public MonetaryCurrency getCurrency() {
return this.loanRepaymentScheduleDetail.getCurrency();
}
public LocalDate getInterestCalculatedFromDate() {
LocalDate interestCalculatedFrom = null;
if (this.interestCalculatedFromDate != null) {
interestCalculatedFrom = new LocalDate(this.interestCalculatedFromDate);
}
return interestCalculatedFrom;
}
public LocalDate getLoanStatusSinceDate() {
LocalDate statusSinceDate = new LocalDate(this.submittedOnDate);
if (isApproved()) {
statusSinceDate = new LocalDate(this.approvedOnDate);
}
if (isDisbursed()) {
statusSinceDate = new LocalDate(this.disbursedOnDate);
}
if (isClosed()) {
statusSinceDate = new LocalDate(this.closedOnDate);
}
return statusSinceDate;
}
}
@@ -0,0 +1,45 @@
package org.mifosng.platform.loan.domain;
import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.organisation.domain.Organisation;
public class LoanBuilder {
private Organisation organisation;
private Client client;
private LoanProduct loanProduct;
private LoanProductRelatedDetail loanRepaymentScheduleDetail;
private LoanStatus loanStatus;
private String externalSystemId;
public Loan build() {
Loan loan = new Loan(this.organisation, this.client, this.loanProduct, this.loanRepaymentScheduleDetail, this.loanStatus);
loan.setExternalId(this.externalSystemId);
return loan;
}
public LoanBuilder with(final Organisation withOrg) {
this.organisation = withOrg;
return this;
}
public LoanBuilder with(final LoanProduct withLoanProduct) {
this.loanProduct = withLoanProduct;
return this;
}
public LoanBuilder with(final Client withClient) {
this.client = withClient;
return this;
}
public LoanBuilder with(final LoanProductRelatedDetail withLoanRepaymentScheduleDetail) {
this.loanRepaymentScheduleDetail = withLoanRepaymentScheduleDetail;
return this;
}
public LoanBuilder withExternalSystemId(final String withExternalSystemId) {
this.externalSystemId = withExternalSystemId;
return this;
}
}
@@ -0,0 +1,8 @@
package org.mifosng.platform.loan.domain;
public enum LoanEvent {
LOAN_CREATED, LOAN_REJECTED, LOAN_WITHDRAWN, LOAN_APPROVED, LOAN_APPROVAL_UNDO, LOAN_DISBURSED, LOAN_DISBURSAL_UNDO, LOAN_REPAYMENT,
//
REPAID_IN_FULL, LOAN_WRITE_OFF, LOAN_RESCHEDULE, INTERST_REBATE_OWED;
}
@@ -0,0 +1,7 @@
package org.mifosng.platform.loan.domain;
public interface LoanLifecycleStateMachine {
LoanStatus transition(LoanEvent loanEvent, LoanStatus from);
}
@@ -0,0 +1,76 @@
package org.mifosng.platform.loan.domain;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.Money;
public class LoanPayoffSummary {
private final Long reference;
private final LocalDate acutalDisbursementDate;
private final LocalDate expectedMaturityDate;
private final LocalDate projectedMaturityDate;
private final Money totalPaidToDate;
private final Money totalOutstandingBasedOnExpectedMaturityDate;
private final Money totalOutstandingBasedOnPayoffDate;
private final Money rebateOwed;
public LoanPayoffSummary(final Long reference,
final LocalDate acutalDisbursementDate,
final LocalDate expectedMaturityDate,
final LocalDate projectedMaturityDate, final Money totalPaidToDate,
final Money totalOutstandingBasedOnExpectedMaturityDate,
final Money totalOutstandingBasedOnPayoffDate,
final Money rebateOwed) {
this.reference = reference;
this.acutalDisbursementDate = acutalDisbursementDate;
this.expectedMaturityDate = expectedMaturityDate;
this.projectedMaturityDate = projectedMaturityDate;
this.totalPaidToDate = totalPaidToDate;
this.totalOutstandingBasedOnExpectedMaturityDate = totalOutstandingBasedOnExpectedMaturityDate;
this.totalOutstandingBasedOnPayoffDate = totalOutstandingBasedOnPayoffDate;
this.rebateOwed = rebateOwed;
}
public Long getReference() {
return this.reference;
}
public LocalDate getAcutalDisbursementDate() {
return this.acutalDisbursementDate;
}
public LocalDate getExpectedMaturityDate() {
return this.expectedMaturityDate;
}
public LocalDate getProjectedMaturityDate() {
return this.projectedMaturityDate;
}
public Money getTotalPaidToDate() {
return this.totalPaidToDate;
}
public Money getRebateOwed() {
return this.rebateOwed;
}
public Integer getExpectedLoanTermInDays() {
return Days.daysBetween(this.acutalDisbursementDate,
this.expectedMaturityDate).getDays();
}
public Integer getProjectedLoanTermInDays() {
return Days.daysBetween(this.acutalDisbursementDate,
this.projectedMaturityDate).getDays();
}
public Money getTotalOutstandingBasedOnExpectedMaturityDate() {
return this.totalOutstandingBasedOnExpectedMaturityDate;
}
public Money getTotalOutstandingBasedOnPayoffDate() {
return this.totalOutstandingBasedOnPayoffDate;
}
}
@@ -0,0 +1,140 @@
package org.mifosng.platform.loan.domain;
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.command.UpdateLoanProductCommand;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
import org.mifosng.platform.organisation.domain.Organisation;
import org.mifosng.platform.user.domain.AppUser;
/**
* Loan products allow for categorisation of an organisations loans into something meaningful to them.
*
* They provide a means of simplifying creation/maintenance of loans.
* They can also allow for product comparison to take place when reporting.
*
* They allow for constraints to be added at product level.
*/
@Entity
@Table(name = "portfolio_product_loan")
public class LoanProduct extends AbstractAuditableCustom<AppUser, Long> {
@ManyToOne
@JoinColumn(name = "org_id", nullable = false)
private final Organisation organisation;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "description")
private String description;
@Embedded
private final LoanProductRelatedDetail loanProductRelatedDetail;
public LoanProduct() {
this.organisation = null;
this.name = null;
this.description = null;
this.loanProductRelatedDetail = null;
}
public LoanProduct(final Organisation organisation, final String name, final String description, final MonetaryCurrency currency, final BigDecimal defaultPrincipal,
final BigDecimal defaultNominalInterestRatePerPeriod, final PeriodFrequencyType interestPeriodFrequencyType, final BigDecimal defaultAnnualNominalInterestRate, final InterestMethod interestMethod,
final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfInstallments, final AmortizationMethod amortizationMethod,
final BigDecimal inArrearsTolerance,
final boolean flexibleRepaymentSchedule,
final boolean interestRebateAllowed) {
this.organisation = organisation;
this.name = name.trim();
if (StringUtils.isNotBlank(description)) {
this.description = description.trim();
} else {
this.description = null;
}
this.loanProductRelatedDetail = new LoanProductRelatedDetail(currency,
defaultPrincipal, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod,
repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, inArrearsTolerance, flexibleRepaymentSchedule, interestRebateAllowed);
}
public Organisation getOrganisation() {
return this.organisation;
}
public MonetaryCurrency getCurrency() {
return this.loanProductRelatedDetail.getCurrency();
}
public Money getInArrearsTolerance() {
return this.loanProductRelatedDetail.getInArrearsTolerance();
}
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
public Integer getRepayEvery() {
return this.loanProductRelatedDetail.getRepayEvery();
}
public boolean isFlexibleRepaymentSchedule() {
return this.loanProductRelatedDetail.isFlexibleRepaymentSchedule();
}
public boolean isInterestRebateAllowed() {
return this.loanProductRelatedDetail.isInterestRebateAllowed();
}
public boolean identifiedBy(final String identifier) {
return identifier.equalsIgnoreCase(this.name);
}
public BigDecimal getDefaultNominalInterestRatePerPeriod() {
return this.loanProductRelatedDetail.getNominalInterestRatePerPeriod();
}
public PeriodFrequencyType getInterestPeriodFrequencyType() {
return this.loanProductRelatedDetail.getInterestPeriodFrequencyType();
}
public BigDecimal getDefaultAnnualNominalInterestRate() {
return this.loanProductRelatedDetail.getAnnualNominalInterestRate();
}
public InterestMethod getInterestMethod() {
return this.loanProductRelatedDetail.getInterestMethod();
}
public PeriodFrequencyType getRepaymentPeriodFrequencyType() {
return this.loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
}
public Integer getDefaultNumberOfRepayments() {
return this.loanProductRelatedDetail.getNumberOfRepayments();
}
public AmortizationMethod getAmortizationMethod() {
return this.loanProductRelatedDetail.getAmortizationMethod();
}
public void update(UpdateLoanProductCommand command) {
this.name = command.getName();
this.description = command.getDescription();
this.loanProductRelatedDetail.update(command);
}
}
@@ -0,0 +1,15 @@
package org.mifosng.platform.loan.domain;
import java.io.Serializable;
/**
* Represents the bare minimum repayment details needed for activities related to generating repayment schedules.
*/
public interface LoanProductMinimumRepaymentScheduleRelatedDetail extends Serializable {
Integer getRepayEvery();
PeriodFrequencyType getRepaymentPeriodFrequencyType();
Integer getNumberOfRepayments();
}
@@ -0,0 +1,179 @@
package org.mifosng.platform.loan.domain;
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import org.mifosng.data.command.UpdateLoanProductCommand;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
/**
* LoanRepaymentScheduleDetail encapsulates all the details of a
* {@link LoanProduct} that are also used and persisted by a {@link Loan}.
*/
@Embeddable
public class LoanProductRelatedDetail implements
LoanProductMinimumRepaymentScheduleRelatedDetail {
@Embedded
private MonetaryCurrency currency;
@Column(name = "principal_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal principal;
@Column(name = "nominal_interest_rate_per_period", scale = 6, precision = 19, nullable = false)
private BigDecimal nominalInterestRatePerPeriod;
@Enumerated(EnumType.ORDINAL)
@Column(name = "interest_period_frequency_enum", nullable = false)
private PeriodFrequencyType interestPeriodFrequencyType;
@Column(name = "annual_nominal_interest_rate", scale = 6, precision = 19, nullable = false)
private BigDecimal annualNominalInterestRate;
@Enumerated(EnumType.ORDINAL)
@Column(name = "interest_method_enum", nullable = false)
private InterestMethod interestMethod;
@Column(name = "repay_every", nullable = false)
private Integer repayEvery;
@Enumerated(EnumType.ORDINAL)
@Column(name = "repayment_period_frequency_enum", nullable = false)
private PeriodFrequencyType repaymentPeriodFrequencyType;
@Column(name = "number_of_repayments", nullable = false)
private Integer numberOfRepayments;
@Enumerated(EnumType.ORDINAL)
@Column(name = "amortization_method_enum", nullable = false)
private AmortizationMethod amortizationMethod;
@Column(name = "flexible_repayment_schedule", nullable = false)
private boolean flexibleRepaymentSchedule;
@Column(name = "interest_rebate", nullable = false)
private boolean interestRebateAllowed;
@Column(name = "arrearstolerance_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal inArrearsTolerance;
protected LoanProductRelatedDetail() {
this.principal = null;
this.nominalInterestRatePerPeriod = null;
this.interestPeriodFrequencyType = null;
this.annualNominalInterestRate = null;
this.interestMethod = null;
this.repayEvery = null;
this.repaymentPeriodFrequencyType = null;
this.numberOfRepayments = null;
this.amortizationMethod = null;
this.inArrearsTolerance = null;
this.flexibleRepaymentSchedule = false;
this.interestRebateAllowed = false;
}
public LoanProductRelatedDetail(final MonetaryCurrency currency,
final BigDecimal defaultPrincipal,
final BigDecimal defaultNominalInterestRatePerPeriod,
final PeriodFrequencyType interestPeriodFrequencyType,
final BigDecimal defaultAnnualNominalInterestRate,
final InterestMethod interestMethod, final Integer repayEvery,
final PeriodFrequencyType repaymentFrequencyType,
final Integer defaultNumberOfRepayments,
final AmortizationMethod amortizationMethod,
final BigDecimal inArrearsTolerance,
final boolean flexibleRepaymentSchedule,
final boolean interestRebateAllowed) {
this.currency = currency;
this.principal = defaultPrincipal;
this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod;
this.interestPeriodFrequencyType = interestPeriodFrequencyType;
this.annualNominalInterestRate = defaultAnnualNominalInterestRate;
this.interestMethod = interestMethod;
this.repayEvery = repayEvery;
this.repaymentPeriodFrequencyType = repaymentFrequencyType;
this.numberOfRepayments = defaultNumberOfRepayments;
this.amortizationMethod = amortizationMethod;
this.inArrearsTolerance = inArrearsTolerance;
this.flexibleRepaymentSchedule = flexibleRepaymentSchedule;
this.interestRebateAllowed = interestRebateAllowed;
}
public MonetaryCurrency getCurrency() {
return this.currency.copy();
}
public Money getPrincipal() {
return Money.of(this.currency, this.principal);
}
public Money getInArrearsTolerance() {
return Money.of(this.currency, this.inArrearsTolerance);
}
public BigDecimal getNominalInterestRatePerPeriod() {
return BigDecimal.valueOf(Double.valueOf(this.nominalInterestRatePerPeriod.stripTrailingZeros().toString())) ;
}
public PeriodFrequencyType getInterestPeriodFrequencyType() {
return interestPeriodFrequencyType;
}
public BigDecimal getAnnualNominalInterestRate() {
return BigDecimal.valueOf(Double.valueOf(this.annualNominalInterestRate.stripTrailingZeros().toString())) ;
}
public InterestMethod getInterestMethod() {
return interestMethod;
}
@Override
public Integer getRepayEvery() {
return repayEvery;
}
@Override
public PeriodFrequencyType getRepaymentPeriodFrequencyType() {
return repaymentPeriodFrequencyType;
}
@Override
public Integer getNumberOfRepayments() {
return numberOfRepayments;
}
public AmortizationMethod getAmortizationMethod() {
return amortizationMethod;
}
public boolean isFlexibleRepaymentSchedule() {
return flexibleRepaymentSchedule;
}
public boolean isInterestRebateAllowed() {
return interestRebateAllowed;
}
public void update(UpdateLoanProductCommand command) {
this.currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal());
this.principal = command.getPrincipal();
this.repayEvery = command.getRepaymentEvery();
this.repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequency());
this.numberOfRepayments = command.getNumberOfRepayments();
this.amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod());
this.inArrearsTolerance = command.getInArrearsToleranceAmount();
this.nominalInterestRatePerPeriod = command.getInterestRatePerPeriod();
this.interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod());
this.interestMethod = InterestMethod.fromInt(command.getInterestMethod());
}
}
@@ -0,0 +1,9 @@
package org.mifosng.platform.loan.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LoanProductRepository extends
JpaRepository<LoanProduct, Long>, JpaSpecificationExecutor<LoanProduct> {
// no behaviour added
}
@@ -0,0 +1,166 @@
package org.mifosng.platform.loan.domain;
import java.math.BigDecimal;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
import org.mifosng.platform.organisation.domain.Organisation;
import org.mifosng.platform.user.domain.AppUser;
@Entity
@Table(name = "portfolio_loan_repayment_schedule")
public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<AppUser, Long> {
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "org_id", nullable = false)
private Organisation organisation;
@ManyToOne(optional = false)
@JoinColumn(name = "loan_id")
private Loan loan;
@Column(name = "installment")
private final Integer installmentNumber;
@Column(name = "principal_amount", scale=6, precision=19)
private BigDecimal principal;
@Column(name = "interest_amount", scale=6, precision=19)
private BigDecimal interest;
@Column(name = "principal_completed_derived", scale=6, precision=19)
private BigDecimal principalCompleted;
@Column(name = "interest_completed_derived", scale = 6, precision = 19)
private BigDecimal interestCompleted;
@Column(name="completed_derived")
private boolean completed;
@Temporal(TemporalType.DATE)
@Column(name = "duedate")
private final Date dueDate;
protected LoanRepaymentScheduleInstallment() {
this.organisation = null;
this.loan = null;
this.installmentNumber = null;
this.dueDate = null;
this.principal = null;
this.interest = null;
this.principalCompleted = null;
this.interestCompleted = null;
this.completed = false;
}
public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate from,
final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest) {
this.loan = loan;
this.installmentNumber = installmentNumber;
this.dueDate = dueDate.toDateMidnight().toDate();
this.principal = principal;
this.principalCompleted = BigDecimal.ZERO;
this.interest = interest;
this.interestCompleted = BigDecimal.ZERO;
this.completed = false;
}
public Loan getLoan() {
return this.loan;
}
public Integer getInstallmentNumber() {
return this.installmentNumber;
}
public LocalDate getDueDate() {
return new LocalDate(this.dueDate);
}
public Money updateDerivedComponents(Money totalAvailable) {
Money remaining = totalAvailable.copy();
boolean principalCompletedInFull = false;
boolean interestCompletedInFull = false;
Money interest = getInterest(totalAvailable.getCurrency());
Money principal = getPrincipal(totalAvailable.getCurrency());
// TODO - configuration around order components of loan are paid off in.
// pay off interest
if (remaining.isGreaterThanOrEqualTo(interest)) {
this.interestCompleted = interest.getAmount();
interestCompletedInFull = true;
} else {
this.interestCompleted = remaining.getAmount();
}
remaining = remaining.minus(this.interestCompleted);
// pay off principal
if (remaining.isGreaterThanOrEqualTo(principal)) {
this.principalCompleted = principal.getAmount();
principalCompletedInFull = true;
} else {
this.principalCompleted = remaining.getAmount();
}
remaining = remaining.minus(this.principalCompleted);
this.completed = (principalCompletedInFull && interestCompletedInFull);
return remaining;
}
public Money getPrincipal(MonetaryCurrency currency) {
return Money.of(currency, this.principal);
}
public Money getPrincipalCompleted(MonetaryCurrency currency) {
return Money.of(currency, this.principalCompleted);
}
public Money getInterest(MonetaryCurrency currency) {
return Money.of(currency, this.interest);
}
public Money getInterestCompleted(MonetaryCurrency currency) {
return Money.of(currency, this.interestCompleted);
}
public Money getTotal(MonetaryCurrency currency) {
return getPrincipal(currency).plus(getInterest(currency));
}
public void updateOrgnaisation(Organisation organisation) {
this.organisation = organisation;
}
public void updateLoan(final Loan loan) {
this.loan = loan;
}
public boolean unpaid() {
return !this.completed;
}
public Money getTotalDue(MonetaryCurrency currency) {
return getTotal(currency).minus(getTotalCompleted(currency));
}
private Money getTotalCompleted(MonetaryCurrency currency) {
return getPrincipalCompleted(currency).plus(getInterestCompleted(currency));
}
}
@@ -0,0 +1,9 @@
package org.mifosng.platform.loan.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LoanRepository extends JpaRepository<Loan, Long>,
JpaSpecificationExecutor<Loan> {
// no added behaviour
}
@@ -0,0 +1,72 @@
package org.mifosng.platform.loan.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.data.domain.Persistable;
@Entity
@Table(name = "ref_loan_status")
public class LoanStatus implements Persistable<Integer> {
public static final Integer SUBMITED_AND_PENDING_APPROVAL = 100;
public static final Integer APPROVED = 200;
public static final Integer ACTIVE = 300;
public static final Integer WITHDRAWN_BY_CLIENT = 400;
public static final Integer REJECTED = 500;
public static final Integer CLOSED = 600;
@Id
@Column(name = "id")
private Integer id;
@Column(name = "display_name")
private String displayName;
public LoanStatus() {
}
public String getDisplayName() {
return this.displayName;
}
@Override
public Integer getId() {
return null;
}
@Override
public boolean isNew() {
return false;
}
public boolean hasStateOf(Integer state) {
return this.id.equals(state);
}
public boolean isSubmittedAndPendingApproval() {
return hasStateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL);
}
public boolean isApproved() {
return hasStateOf(LoanStatus.APPROVED);
}
public boolean isClosed() {
return hasStateOf(LoanStatus.CLOSED);
}
public boolean isWithdrawnByClient() {
return hasStateOf(LoanStatus.WITHDRAWN_BY_CLIENT);
}
public boolean isRejected() {
return hasStateOf(LoanStatus.REJECTED);
}
public boolean isActive() {
return hasStateOf(LoanStatus.ACTIVE);
}
}
@@ -0,0 +1,8 @@
package org.mifosng.platform.loan.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LoanStatusRepository extends JpaRepository<LoanStatus, Long>, JpaSpecificationExecutor<LoanStatus> {
// no added behaviour
}
@@ -0,0 +1,161 @@
package org.mifosng.platform.loan.domain;
import java.math.BigDecimal;
import java.util.Date;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
import org.mifosng.platform.organisation.domain.Organisation;
import org.mifosng.platform.user.domain.AppUser;
import org.springframework.format.annotation.DateTimeFormat;
/**
* All monetary transactions against a loan are modelled through this entity.
* Disbursements, Repayments, Waivers, Write-off etc
*/
@Entity
@Table(name = "portfolio_loan_transaction")
public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
@ManyToOne
@JoinColumn(name = "org_id", nullable = false)
private Organisation organisation;
@ManyToOne(optional = false)
@JoinColumn(name = "loan_id", nullable=false)
private Loan loan;
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private final BigDecimal amount;
@Temporal(TemporalType.DATE)
@Column(name = "transaction_date", nullable=false)
private final Date dateOf;
@Enumerated(EnumType.ORDINAL)
@Column(name = "transaction_type_enum", nullable = false)
private LoanTransactionType typeOf;
@OneToOne(optional=true, cascade={CascadeType.PERSIST})
@JoinColumn(name="contra_id")
private LoanTransaction contra;
protected LoanTransaction() {
this.organisation = null;
this.loan = null;
this.amount = null;
this.dateOf = null;
this.typeOf = null;
}
public static LoanTransaction disbursement(Money amount, LocalDate disbursementDate) {
return new LoanTransaction(LoanTransactionType.DISBURSEMENT, amount.getAmount(), disbursementDate);
}
public static LoanTransaction repayment(Money amount, LocalDate paymentDate) {
return new LoanTransaction(LoanTransactionType.REPAYMENT, amount.getAmount(), paymentDate);
}
public static LoanTransaction waiver(Money waived, LocalDate waiveDate) {
return new LoanTransaction(LoanTransactionType.WAIVED, waived.getAmount(), waiveDate);
}
private static LoanTransaction contra(LoanTransaction originalTransaction) {
LoanTransaction contra = new LoanTransaction(LoanTransactionType.REVERSAL, originalTransaction.getAmount().negate(), new LocalDate(originalTransaction.getDateOf()));
contra.updateContra(originalTransaction);
return contra;
}
public void updateContra(LoanTransaction transaction) {
this.contra = transaction;
}
private LoanTransaction(LoanTransactionType type, final BigDecimal amount, final LocalDate date) {
this.typeOf = type;
this.amount = amount;
this.dateOf = date.toDateMidnight().toDate();
}
public BigDecimal getAmount() {
return this.amount;
}
public Money getAmount(MonetaryCurrency currency) {
return Money.of(currency, this.amount);
}
@DateTimeFormat(style="-M")
public LocalDate getTransactionDate() {
return new LocalDate(this.dateOf);
}
public Date getDateOf() {
return dateOf;
}
public LoanTransactionType getTypeOf() {
return typeOf;
}
public boolean isRepayment() {
return LoanTransactionType.REPAYMENT.equals(typeOf) && isNotContra();
}
public boolean isNotRepayment() {
return !isRepayment();
}
public boolean isNotContra() {
return this.contra == null;
}
public boolean isDisbursement() {
return LoanTransactionType.DISBURSEMENT.equals(typeOf);
}
public boolean isWaiver() {
return LoanTransactionType.WAIVED.equals(typeOf) && isNotContra();
}
public boolean isNotWaiver() {
return !isWaiver();
}
public boolean isIdentifiedBy(Long identifier) {
return this.getId().equals(identifier);
}
public void contra() {
this.contra = LoanTransaction.contra(this);
contra.updateLoan(this.loan);
contra.updateOrganisation(this.organisation);
}
public void updateOrganisation(Organisation organisation) {
this.organisation = organisation;
}
public void updateLoan(final Loan loan) {
this.loan = loan;
}
public boolean isNonZero() {
return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0;
}
}
@@ -0,0 +1,9 @@
package org.mifosng.platform.loan.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LoanTransactionRepository extends JpaRepository<LoanTransaction, Long>,
JpaSpecificationExecutor<LoanTransaction> {
// no added behaviour
}
@@ -0,0 +1,42 @@
package org.mifosng.platform.loan.domain;
public enum LoanTransactionType {
UNKNOWN(0), DISBURSEMENT(1), REPAYMENT(2), REVERSAL(3), WAIVED(4);
private final Integer value;
private LoanTransactionType(final Integer value) {
this.value = value;
}
public Integer getValue() {
return this.value;
}
public static LoanTransactionType fromInt(final Integer transactionType) {
if (transactionType == null) {
return LoanTransactionType.UNKNOWN;
}
LoanTransactionType loanTransactionType = null;
switch (transactionType) {
case 1:
loanTransactionType = LoanTransactionType.DISBURSEMENT;
break;
case 2:
loanTransactionType = LoanTransactionType.REPAYMENT;
break;
case 3:
loanTransactionType = LoanTransactionType.REVERSAL;
break;
case 4:
loanTransactionType = LoanTransactionType.WAIVED;
break;
default:
loanTransactionType = LoanTransactionType.UNKNOWN;
break;
}
return loanTransactionType;
}
}
@@ -0,0 +1,38 @@
package org.mifosng.platform.loan.domain;
public enum PeriodFrequencyType {
DAYS(0), WEEKS(1), MONTHS(2), YEARS(3), INVALID(4);
private final Integer value;
private PeriodFrequencyType(final Integer value) {
this.value = value;
}
public Integer getValue() {
return this.value;
}
public static PeriodFrequencyType fromInt(final Integer frequency) {
PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.INVALID;
switch (frequency) {
case 0:
repaymentFrequencyType = PeriodFrequencyType.DAYS;
break;
case 1:
repaymentFrequencyType = PeriodFrequencyType.WEEKS;
break;
case 2:
repaymentFrequencyType = PeriodFrequencyType.MONTHS;
break;
case 3:
repaymentFrequencyType = PeriodFrequencyType.YEARS;
break;
default:
repaymentFrequencyType = PeriodFrequencyType.INVALID;
break;
}
return repaymentFrequencyType;
}
}
@@ -0,0 +1,268 @@
package org.mifosng.platform.loanschedule.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.data.CurrencyData;
import org.mifosng.data.LoanSchedule;
import org.mifosng.data.MoneyData;
import org.mifosng.data.ScheduledLoanInstallment;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
public class DecliningBalanceEqualInstallmentsLoanScheduleGenerator implements
LoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
@Override
public LoanSchedule generate(
final LoanProductRelatedDetail loanScheduleInfo,
final LocalDate disbursementDate,
final LocalDate firstRepaymentDate,
final LocalDate interestCalculatedFrom, CurrencyData currencyData) {
List<ScheduledLoanInstallment> scheduledLoanInstallments = new ArrayList<ScheduledLoanInstallment>();
List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(
loanScheduleInfo, disbursementDate, firstRepaymentDate);
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate(loanScheduleInfo, scheduledDates);
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
BigDecimal paymentPeriodsInYear = BigDecimal
.valueOf(this.paymentPeriodsInOneYearCalculator.calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType()));
BigDecimal periodicInterestRate = loanScheduleInfo
.getAnnualNominalInterestRate()
.divide(paymentPeriodsInYear, mc)
.divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc)
.multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery()));
double interestRateFraction = periodicInterestRate.doubleValue();
double futureValue = 0;
double numberOfPeriods = loanScheduleInfo.getNumberOfRepayments().doubleValue();
double principal = loanScheduleInfo.getPrincipal().getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue();
double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principal, futureValue, false);
double totalRepayment = paymentPerInstallment * numberOfPeriods;
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
Money totalDuePerInstallment = Money.of(monetaryCurrency, BigDecimal.valueOf(paymentPerInstallment));
Money totalDue = Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment));
Money totalInterestDue = totalDue.minus(loanScheduleInfo.getPrincipal());
Money outstandingBalance = loanScheduleInfo.getPrincipal();
Money totalPrincipal = Money.zero(monetaryCurrency);
Money totalInterest = Money.zero(monetaryCurrency);
double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator.calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(),
loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, idealDisbursementDateBasedOnFirstRepaymentDate);
LocalDate startDate = disbursementDate;
int installmentNumber = 1;
for (LocalDate scheduledDueDate : scheduledDates) {
Money interestPerInstallment = outstandingBalance.multiplyRetainScale(periodicInterestRate, RoundingMode.HALF_EVEN);
Money principalPerInstallment = totalDuePerInstallment.minus(interestPerInstallment);
if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) {
Money graceOnInterestForRepaymentPeriod = interestPerInstallment;
interestPerInstallment = interestPerInstallment.minus(graceOnInterestForRepaymentPeriod);
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - Integer.valueOf(1).doubleValue();
} else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) {
Money graceOnInterestForRepaymentPeriod = interestPerInstallment.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction);
interestPerInstallment = interestPerInstallment.minus(graceOnInterestForRepaymentPeriod);
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0");
}
totalPrincipal = totalPrincipal.plus(principalPerInstallment);
totalInterest = totalInterest.plus(interestPerInstallment);
if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) {
Money principalDifference = totalPrincipal.minus(loanScheduleInfo
.getPrincipal());
if (principalDifference.isLessThanZero()) {
principalPerInstallment = principalPerInstallment
.plus(principalDifference.abs());
} else if (principalDifference.isGreaterThanZero()) {
principalPerInstallment = principalPerInstallment
.minus(principalDifference.abs());
}
Money interestDifference = totalInterest.minus(totalInterestDue);
if (interestDifference.isLessThanZero()) {
interestPerInstallment = interestPerInstallment
.plus(interestDifference.abs());
} else if (interestDifference.isGreaterThanZero()) {
interestPerInstallment = interestPerInstallment
.minus(interestDifference.abs());
}
}
Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment);
outstandingBalance = outstandingBalance.minus(principalPerInstallment);
MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalPerInstallment.getAmount());
MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestPerInstallment.getAmount());
MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount());
MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount());
ScheduledLoanInstallment installment = new ScheduledLoanInstallment(
Integer.valueOf(installmentNumber), startDate,
scheduledDueDate, principalPerInstallmentValue,
interestPerInstallmentValue,
totalInstallmentDueValue,
outstandingBalanceValue);
scheduledLoanInstallments.add(installment);
startDate = scheduledDueDate;
installmentNumber++;
}
return new LoanSchedule(scheduledLoanInstallments);
}
/**
* Future value of an amount given the number of payments, rate, amount of
* individual payment, present value and boolean value indicating whether
* payments are due at the beginning of period (false => payments are due at
* end of period)
*
* @param r
* rate
* @param n
* num of periods
* @param y
* pmt per period
* @param p
* future value
* @param t
* type (true=pmt at end of period, false=pmt at begining of
* period)
*/
public static double fv(double r, double n, double y, double p, boolean t) {
double retval = 0;
if (r == 0) {
retval = -1 * (p + (n * y));
} else {
double r1 = r + 1;
retval = ((1 - Math.pow(r1, n)) * (t ? r1 : 1) * y) / r - p
* Math.pow(r1, n);
}
return retval;
}
/**
* Present value of an amount given the number of future payments, rate,
* amount of individual payment, future value and boolean value indicating
* whether payments are due at the beginning of period (false => payments
* are due at end of period)
*
* @param r
* @param n
* @param y
* @param f
* @param t
*/
public static double pv(double r, double n, double y, double f, boolean t) {
double retval = 0;
if (r == 0) {
retval = -1 * ((n * y) + f);
} else {
double r1 = r + 1;
retval = (((1 - Math.pow(r1, n)) / r) * (t ? r1 : 1) * y - f)
/ Math.pow(r1, n);
}
return retval;
}
/**
* calculates the Net Present Value of a principal amount given the discount
* rate and a sequence of cash flows (supplied as an array). If the amounts
* are income the value should be positive, else if they are payments and
* not income, the value should be negative.
*
* @param r
* @param cfs
* cashflow amounts
*/
public static double npv(double r, double[] cfs) {
double npv = 0;
double r1 = r + 1;
double trate = r1;
for (int i = 0, iSize = cfs.length; i < iSize; i++) {
npv += cfs[i] / trate;
trate *= r1;
}
return npv;
}
/**
* PMT calculates a fixed monthly payment to be paid by borrower every 'period' to ensure loan is paid off in full (with interest).
*
* This monthly payment c depends upon
* the monthly interest rate r (expressed as a fraction, not a percentage,
* i.e., divide the quoted yearly percentage rate by 100 and by 12 to obtain
* the monthly interest rate), the number of monthly payments N called the
* loan's term, and the amount borrowed P known as the loan's principal; c
* is given by the formula:
*
* c = (r / (1 - (1 + r)^-N))P
*
* @param interestRateFraction
* @param numberOfPayments
* @param principal
* @param futureValue
* @param type
*/
public static double pmt(double interestRateFraction, double numberOfPayments, double principal, double futureValue, boolean type) {
double payment = 0;
if (interestRateFraction == 0) {
payment = -1 * (futureValue + principal) / numberOfPayments;
} else {
double r1 = interestRateFraction + 1;
payment = (futureValue + principal * Math.pow(r1, numberOfPayments)) * interestRateFraction
/ ((type ? r1 : 1) * (1 - Math.pow(r1, numberOfPayments)));
}
return payment;
}
/**
*
* @param r
* @param y
* @param p
* @param f
* @param t
*/
public static double nper(double r, double y, double p, double f, boolean t) {
double retval = 0;
if (r == 0) {
retval = -1 * (f + p) / y;
} else {
double r1 = r + 1;
double ryr = (t ? r1 : 1) * y / r;
double a1 = ((ryr - f) < 0) ? Math.log(f - ryr) : Math.log(ryr - f);
double a2 = ((ryr - f) < 0) ? Math.log(-p - ryr) : Math
.log(p + ryr);
double a3 = Math.log(r1);
retval = (a1 - a2) / a3;
}
return retval;
}
}
@@ -0,0 +1,107 @@
package org.mifosng.platform.loanschedule.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.data.CurrencyData;
import org.mifosng.data.LoanSchedule;
import org.mifosng.data.MoneyData;
import org.mifosng.data.ScheduledLoanInstallment;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.exceptions.NoAuthorizationException;
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
public class DecliningBalanceLoanScheduleGenerator implements
LoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
@Override
public LoanSchedule generate(
final LoanProductRelatedDetail loanScheduleInfo,
final LocalDate disbursementDate, final LocalDate firstRepaymentDate,
LocalDate interestCalculatedFrom, CurrencyData currencyData) {
if (firstRepaymentDate != null
&& disbursementDate.isAfter(firstRepaymentDate)) {
throw new NoAuthorizationException(
"cannot have disbursement date after first repayment date.");
}
List<ScheduledLoanInstallment> scheduledLoanInstallments = new ArrayList<ScheduledLoanInstallment>();
List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(
loanScheduleInfo, disbursementDate, firstRepaymentDate);
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
Money principalPerInstallment = loanScheduleInfo.getPrincipal()
.dividedBy(loanScheduleInfo.getNumberOfRepayments(),
RoundingMode.HALF_EVEN);
BigDecimal paymentPeriodsInYear = BigDecimal
.valueOf(this.paymentPeriodsInOneYearCalculator
.calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType()));
BigDecimal periodicInterestRate = loanScheduleInfo
.getAnnualNominalInterestRate()
.divide(paymentPeriodsInYear, mc)
.divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc)
.multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery()));
Money outstandingBalance = loanScheduleInfo.getPrincipal();
Money totalPrincipal = Money.zero(outstandingBalance.getCurrency());
LocalDate startDate = disbursementDate;
int installmentNumber = 1;
for (LocalDate scheduledDueDate : scheduledDates) {
totalPrincipal = totalPrincipal.plus(principalPerInstallment);
Money interestPerInstallment = outstandingBalance
.multiplyRetainScale(periodicInterestRate,
RoundingMode.HALF_EVEN);
if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) {
Money difference = totalPrincipal.minus(loanScheduleInfo
.getPrincipal());
if (difference.isLessThanZero()) {
principalPerInstallment = principalPerInstallment
.plus(difference.abs());
} else if (difference.isGreaterThanZero()) {
principalPerInstallment = principalPerInstallment
.minus(difference.abs());
}
}
Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment);
outstandingBalance = outstandingBalance
.minus(principalPerInstallment);
MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalPerInstallment.getAmount());
MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestPerInstallment.getAmount());
MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount());
MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount());
ScheduledLoanInstallment installment = new ScheduledLoanInstallment(
Integer.valueOf(installmentNumber), startDate,
scheduledDueDate, principalPerInstallmentValue,
interestPerInstallmentValue,
totalInstallmentDueValue,
outstandingBalanceValue);
scheduledLoanInstallments.add(installment);
startDate = scheduledDueDate;
installmentNumber++;
}
return new LoanSchedule(scheduledLoanInstallments);
}
}
@@ -0,0 +1,46 @@
package org.mifosng.platform.loanschedule.domain;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.InterestMethod;
public class DefaultLoanScheduleGeneratorFactory implements
LoanScheduleGeneratorFactory {
@Override
public LoanScheduleGenerator create(final InterestMethod interestMethod,
AmortizationMethod amortizationMethod) {
LoanScheduleGenerator loanScheduleGenerator = null;
switch (amortizationMethod) {
case EQUAL_PRINCIPAL:
switch (interestMethod) {
case FLAT:
loanScheduleGenerator = new FlatLoanScheduleGenerator();
break;
case DECLINING_BALANCE:
loanScheduleGenerator = new DecliningBalanceLoanScheduleGenerator();
break;
case INVALID:
break;
}
break;
case EQUAL_INSTALLMENTS:
switch (interestMethod) {
case FLAT:
loanScheduleGenerator = new FlatLoanScheduleGenerator();
break;
case DECLINING_BALANCE:
loanScheduleGenerator = new DecliningBalanceEqualInstallmentsLoanScheduleGenerator();
break;
case INVALID:
break;
}
break;
case INVALID:
break;
}
return loanScheduleGenerator;
}
}
@@ -0,0 +1,97 @@
package org.mifosng.platform.loanschedule.domain;
import java.util.List;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
public class DefaultPaymentPeriodsInOneYearCalculator implements
PaymentPeriodsInOneYearCalculator {
@Override
public Integer calculate(final PeriodFrequencyType repaymentFrequencyType) {
Integer paymentPeriodsInOneYear = Integer.valueOf(0);
switch (repaymentFrequencyType) {
case DAYS:
paymentPeriodsInOneYear = Integer.valueOf(365);
break;
case WEEKS:
paymentPeriodsInOneYear = Integer.valueOf(52);
break;
case MONTHS:
paymentPeriodsInOneYear = Integer.valueOf(12);
break;
case YEARS:
paymentPeriodsInOneYear = Integer.valueOf(1);
break;
case INVALID:
paymentPeriodsInOneYear = Integer.valueOf(0);
break;
}
return paymentPeriodsInOneYear;
}
/**
* calculates the number of grace repayment periods and can be a fraction of a repayment period.
*/
@Override
public double calculateRepaymentPeriodAsAFractionOfDays(
PeriodFrequencyType repaymentPeriodFrequencyType,
Integer every, LocalDate interestCalculatedFrom,
List<LocalDate> scheduledDates, LocalDate disbursementDate) {
Double periodFraction = Double.valueOf("0");
if (interestCalculatedFrom != null && !interestCalculatedFrom.isBefore(disbursementDate)) {
Integer repaymentPeriodIndex = 0;
LocalDate lastRepaymentPeriod = disbursementDate;
for (LocalDate repaymentPeriodDueDate : scheduledDates) {
if (interestCalculatedFrom.isBefore(repaymentPeriodDueDate)) {
// fraction is
int numberOfDaysInterestCalculationGraceInPeriod = Days
.daysBetween(
lastRepaymentPeriod.toDateMidnight()
.toDateTime(),
interestCalculatedFrom.toDateMidnight()
.toDateTime()).getDays();
periodFraction = calculateRepaymentPeriodFraction(
repaymentPeriodFrequencyType, every,
numberOfDaysInterestCalculationGraceInPeriod);
periodFraction = periodFraction
+ repaymentPeriodIndex.doubleValue();
break;
}
repaymentPeriodIndex++;
lastRepaymentPeriod = repaymentPeriodDueDate;
}
}
return periodFraction;
}
private double calculateRepaymentPeriodFraction(PeriodFrequencyType repaymentPeriodFrequencyType, Integer every, Integer numberOfDaysInterestCalculationGrace) {
Double fraction = Double.valueOf("0");
switch (repaymentPeriodFrequencyType) {
case DAYS:
fraction = numberOfDaysInterestCalculationGrace.doubleValue() * every.doubleValue();
break;
case WEEKS:
fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("7.0") * every.doubleValue());
break;
case MONTHS:
fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("30.0") * every.doubleValue());
break;
case YEARS:
fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("365.0") * every.doubleValue());
break;
case INVALID:
fraction = Double.valueOf("0");
break;
}
return fraction;
}
}
@@ -0,0 +1,86 @@
package org.mifosng.platform.loanschedule.domain;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.platform.loan.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
@Override
public List<LocalDate> generate(final LoanProductMinimumRepaymentScheduleRelatedDetail loanScheduleDateInfo, final LocalDate disbursementDate, final LocalDate firstRepaymentPeriodDate) {
List<LocalDate> dueRepaymentPeriodDates = new ArrayList<LocalDate>(loanScheduleDateInfo.getNumberOfRepayments());
LocalDate startDate = disbursementDate;
for (int repaymentPeriod = 1; repaymentPeriod <= loanScheduleDateInfo.getNumberOfRepayments(); repaymentPeriod++) {
LocalDate dueRepaymentPeriodDate = startDate;
if (repaymentPeriod == 1 && firstRepaymentPeriodDate != null) {
dueRepaymentPeriodDate = firstRepaymentPeriodDate;
} else {
switch (loanScheduleDateInfo.getRepaymentPeriodFrequencyType()) {
case DAYS:
dueRepaymentPeriodDate = startDate.plusDays(loanScheduleDateInfo
.getRepayEvery());
break;
case WEEKS:
dueRepaymentPeriodDate = startDate.plusWeeks(loanScheduleDateInfo
.getRepayEvery());
break;
case MONTHS:
dueRepaymentPeriodDate = startDate.plusMonths(loanScheduleDateInfo
.getRepayEvery());
break;
case YEARS:
dueRepaymentPeriodDate = startDate.plusYears(loanScheduleDateInfo
.getRepayEvery());
break;
case INVALID:
break;
}
}
dueRepaymentPeriodDates.add(dueRepaymentPeriodDate);
startDate = dueRepaymentPeriodDate;
}
return dueRepaymentPeriodDates;
}
@Override
public LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(
LoanProductRelatedDetail loanScheduleDateInfo,
List<LocalDate> scheduledDates) {
LocalDate firstRepaymentDate = scheduledDates.get(0);
LocalDate idealDisbursementDate = null;
switch (loanScheduleDateInfo.getRepaymentPeriodFrequencyType()) {
case DAYS:
idealDisbursementDate = firstRepaymentDate.minusDays(loanScheduleDateInfo.getRepayEvery());
break;
case WEEKS:
idealDisbursementDate = firstRepaymentDate.minusWeeks(loanScheduleDateInfo
.getRepayEvery());
break;
case MONTHS:
idealDisbursementDate = firstRepaymentDate.minusMonths(loanScheduleDateInfo
.getRepayEvery());
break;
case YEARS:
idealDisbursementDate = firstRepaymentDate.minusYears(loanScheduleDateInfo
.getRepayEvery());
break;
case INVALID:
break;
}
return idealDisbursementDate;
}
}
@@ -0,0 +1,96 @@
package org.mifosng.platform.loanschedule.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.data.CurrencyData;
import org.mifosng.data.LoanSchedule;
import org.mifosng.data.MoneyData;
import org.mifosng.data.ScheduledLoanInstallment;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
public class FlatLoanScheduleGenerator implements LoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
@Override
public LoanSchedule generate(
final LoanProductRelatedDetail loanScheduleInfo,
final LocalDate disbursementDate, final LocalDate firstRepaymentDate,
final LocalDate interestCalculatedFrom, final CurrencyData currencyData) {
List<ScheduledLoanInstallment> scheduledLoanInstallments = new ArrayList<ScheduledLoanInstallment>();
List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(
loanScheduleInfo, disbursementDate, firstRepaymentDate);
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
Money principalPerInstallment = loanScheduleInfo.getPrincipal()
.dividedBy(loanScheduleInfo.getNumberOfRepayments(),
RoundingMode.HALF_EVEN);
BigDecimal paymentPeriodsInYear = BigDecimal
.valueOf(this.paymentPeriodsInOneYearCalculator
.calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType()));
BigDecimal periodicInterestRate = loanScheduleInfo
.getAnnualNominalInterestRate()
.divide(paymentPeriodsInYear, mc)
.divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc)
.multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery()));
Money interestPerInstallment = loanScheduleInfo.getPrincipal()
.multiplyRetainScale(periodicInterestRate,
RoundingMode.HALF_EVEN);
Money outstandingBalance = loanScheduleInfo.getPrincipal();
Money totalPrincipal = Money.zero(outstandingBalance.getCurrency());
LocalDate startDate = disbursementDate;
int installmentNumber = 1;
for (LocalDate scheduledDueDate : scheduledDates) {
totalPrincipal = totalPrincipal.plus(principalPerInstallment);
if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) {
Money difference = totalPrincipal.minus(loanScheduleInfo
.getPrincipal());
if (difference.isLessThanZero()) {
principalPerInstallment = principalPerInstallment
.plus(difference.abs());
} else if (difference.isGreaterThanZero()) {
principalPerInstallment = principalPerInstallment
.minus(difference.abs());
}
}
Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment);
outstandingBalance = outstandingBalance.minus(principalPerInstallment);
MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalPerInstallment.getAmount());
MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestPerInstallment.getAmount());
MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount());
MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount());
ScheduledLoanInstallment installment = new ScheduledLoanInstallment(
Integer.valueOf(installmentNumber), startDate,
scheduledDueDate, principalPerInstallmentValue,
interestPerInstallmentValue, totalInstallmentDueValue,
outstandingBalanceValue);
scheduledLoanInstallments.add(installment);
startDate = scheduledDueDate;
installmentNumber++;
}
return new LoanSchedule(scheduledLoanInstallments);
}
}
@@ -0,0 +1,14 @@
package org.mifosng.platform.loanschedule.domain;
import org.joda.time.LocalDate;
import org.mifosng.data.CurrencyData;
import org.mifosng.data.LoanSchedule;
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
public interface LoanScheduleGenerator {
LoanSchedule generate(LoanProductRelatedDetail loanScheduleInfo,
LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom,
CurrencyData currencyData);
}
@@ -0,0 +1,10 @@
package org.mifosng.platform.loanschedule.domain;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.InterestMethod;
public interface LoanScheduleGeneratorFactory {
LoanScheduleGenerator create(InterestMethod interestMethod, AmortizationMethod amortizationMethod);
}
@@ -0,0 +1,17 @@
package org.mifosng.platform.loanschedule.domain;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
public interface PaymentPeriodsInOneYearCalculator {
Integer calculate(PeriodFrequencyType repaymentFrequencyType);
double calculateRepaymentPeriodAsAFractionOfDays(
PeriodFrequencyType repaymentPeriodFrequencyType,
Integer every, LocalDate interestCalculatedFrom,
List<LocalDate> scheduledDates, LocalDate disbursementDate);
}
@@ -0,0 +1,19 @@
package org.mifosng.platform.loanschedule.domain;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.platform.loan.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
public interface ScheduledDateGenerator {
List<LocalDate> generate(
LoanProductMinimumRepaymentScheduleRelatedDetail loanScheduleDateInfo,
LocalDate disbursementDate, LocalDate firstRepaymentDate);
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(
LoanProductRelatedDetail loanScheduleInfo,
List<LocalDate> scheduledDates);
}
@@ -0,0 +1,127 @@
package org.mifosng.platform.oauthconsumer.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.springframework.data.jpa.domain.AbstractPersistable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth.common.signature.SharedConsumerSecret;
import org.springframework.security.oauth.common.signature.SignatureSecret;
import org.springframework.security.oauth.provider.ResourceSpecificConsumerDetails;
/**
* TODO - revisit when tackling better administration support for oauth consumer control with OAuth 2 implementation.
*/
@Entity
@Table(name = "admin_oauth_consumer_application")
public class OauthConsumerDetail extends AbstractPersistable<Long> implements
ResourceSpecificConsumerDetails {
@Column(name = "app_key", nullable = false, unique = true, length=50)
private String consumerKey;
@Column(name = "app_shared_secret", nullable = false, unique = true, length=50)
private String consumerSharedSecret;
@Column(name = "app_name", nullable = false, unique = true, length=100)
private String consumerName;
@SuppressWarnings("unused")
@Column(name = "app_description", nullable = true, length=500)
private String consumerDescription;
@SuppressWarnings("unused")
@Column(name = "app_developedby", nullable = true, length=100)
private String consumerDevelopedBy;
@SuppressWarnings("unused")
@Column(name = "app_location_url", nullable = true, length=200)
private String consumerLocationUrl;
@Column(name = "resource_name", nullable = false)
private String resourceName;
@Column(name = "resource_description", nullable = false)
private String resourceDescription;
@Transient
private List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
@Transient
private boolean requiredToObtainAuthenticatedToken = true;
// for jpa/hibernate
protected OauthConsumerDetail() {
//
}
public OauthConsumerDetail(String consumerKey, String consumerName, String consumerDescription, String consumerDevelopedby, String consumerLocationUrl,
String resourceName, String resourceDesription) {
this.consumerKey = consumerKey;
this.consumerName = consumerName;
this.consumerDescription = consumerDescription;
this.consumerDevelopedBy = consumerDevelopedby;
this.consumerLocationUrl = consumerLocationUrl;
this.resourceName = resourceName;
resourceDescription = resourceDesription;
}
@Override
public String getConsumerKey() {
return this.consumerKey;
}
@Override
public String getConsumerName() {
return this.consumerName;
}
@Override
public SignatureSecret getSignatureSecret() {
return new SharedConsumerSecret(this.consumerSharedSecret);
}
@Override
public List<GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getResourceName() {
return this.resourceName;
}
@Override
public String getResourceDescription() {
return this.resourceDescription;
}
/**
* Whether this consumer is required to obtain an authenticated oauth token.
*
* @return Whether this consumer is required to obtain an authenticated
* oauth token.
*/
public boolean isRequiredToObtainAuthenticatedToken() {
return requiredToObtainAuthenticatedToken;
}
/**
* Whether this consumer is required to obtain an authenticated oauth token.
*
* @param requiredToObtainAuthenticatedToken
* Whether this consumer is required to obtain an authenticated
* oauth token.
*/
public void setRequiredToObtainAuthenticatedToken(
boolean requiredToObtainAuthenticatedToken) {
this.requiredToObtainAuthenticatedToken = requiredToObtainAuthenticatedToken;
}
}
@@ -0,0 +1,11 @@
package org.mifosng.platform.oauthconsumer.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface OauthConsumerDetailRepository extends
JpaRepository<OauthConsumerDetail, Long>,
JpaSpecificationExecutor<OauthConsumerDetail> {
OauthConsumerDetail findByConsumerKey(String consumerKey);
}

Some files were not shown because too many files have changed in this diff Show More