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