add provider project source.
This commit is contained in:
+15
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
+154
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+11
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+209
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
+142
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+41
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -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);
|
||||||
|
}
|
||||||
+1142
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -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);
|
||||||
|
}
|
||||||
+63
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -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);
|
||||||
|
}
|
||||||
+38
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package org.mifosng.platform.exceptions;
|
||||||
|
|
||||||
|
public class ClientNotAuthenticatedException extends RuntimeException {
|
||||||
|
// no added behaviour
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package org.mifosng.platform.exceptions;
|
||||||
|
|
||||||
|
public class InvalidSignupException extends RuntimeException {
|
||||||
|
|
||||||
|
public InvalidSignupException(final Throwable e) {
|
||||||
|
super(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+25
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+142
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+124
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+167
@@ -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();
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package org.mifosng.platform.infrastructure;
|
||||||
|
|
||||||
|
public class PlatformEmailSendException extends RuntimeException {
|
||||||
|
|
||||||
|
public PlatformEmailSendException(final Throwable e) {
|
||||||
|
super(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package org.mifosng.platform.infrastructure;
|
||||||
|
|
||||||
|
|
||||||
|
public interface PlatformEmailService {
|
||||||
|
|
||||||
|
void sendToUserAccount(EmailDetail emailDetail, String unencodedPassword);
|
||||||
|
|
||||||
|
}
|
||||||
+8
@@ -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);
|
||||||
|
}
|
||||||
+19
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package org.mifosng.platform.infrastructure;
|
||||||
|
|
||||||
|
public class UsernameAlreadyExistsException extends RuntimeException {
|
||||||
|
public UsernameAlreadyExistsException(final Throwable e) {
|
||||||
|
super(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+70
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+113
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+489
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package org.mifosng.platform.loan.domain;
|
||||||
|
|
||||||
|
public interface InterestRebateCalculatorFactory {
|
||||||
|
|
||||||
|
InterestRebateCalculator createCalcualtor(
|
||||||
|
InterestMethod loanRepaymentScheduleMethod,
|
||||||
|
AmortizationMethod amortizationMethod);
|
||||||
|
|
||||||
|
}
|
||||||
+15
@@ -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;
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package org.mifosng.platform.loan.domain;
|
||||||
|
|
||||||
|
public interface LoanLifecycleStateMachine {
|
||||||
|
|
||||||
|
LoanStatus transition(LoanEvent loanEvent, LoanStatus from);
|
||||||
|
|
||||||
|
}
|
||||||
+76
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -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();
|
||||||
|
}
|
||||||
+179
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -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
|
||||||
|
}
|
||||||
+166
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -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
|
||||||
|
}
|
||||||
+42
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+268
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+97
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+86
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
+10
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
+17
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
+19
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
+127
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+11
@@ -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
Reference in New Issue
Block a user