MIFOSX-97: support audit for loan application creation, loan application modification, approval, undo approval, disbursal and undo of disbursal.

This commit is contained in:
Keith Woodlock
2012-12-18 20:41:12 +00:00
parent 43b1cdd3ef
commit 013276d9e4
53 changed files with 3469 additions and 2356 deletions
@@ -163,4 +163,32 @@ public class CommandWrapper {
public boolean isIdentifiersSubResource() {
return "IDENTIFIERS".equalsIgnoreCase(this.subResource);
}
public boolean isLoanResource() {
return this.resource.equalsIgnoreCase("LOANS");
}
public boolean isApproveLoanApplication() {
return this.actionName.equalsIgnoreCase("APPROVE") && this.entityName.equalsIgnoreCase("LOAN");
}
public boolean isUndoApprovalOfLoanApplication() {
return this.actionName.equalsIgnoreCase("APPROVALUNDO") && this.entityName.equalsIgnoreCase("LOAN");
}
public boolean isApplicantWithdrawalFromLoanApplication() {
return this.actionName.equalsIgnoreCase("WITHDRAW") && this.entityName.equalsIgnoreCase("LOAN");
}
public boolean isRejectionOfLoanApplication() {
return this.actionName.equalsIgnoreCase("REJECT") && this.entityName.equalsIgnoreCase("LOAN");
}
public boolean isDisbursementOfLoan() {
return this.actionName.equalsIgnoreCase("DISBURSE") && this.entityName.equalsIgnoreCase("LOAN");
}
public boolean isUndoDisbursementOfLoan() {
return this.actionName.equalsIgnoreCase("DISBURSALUNDO") && this.entityName.equalsIgnoreCase("LOAN");
}
}
@@ -210,6 +210,29 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
} else {
throw new UnsupportedCommandException(wrapper.commandName());
}
} else if (wrapper.isLoanResource()) {
if (wrapper.isApproveLoanApplication()) {
handler = applicationContext.getBean("approveLoanApplicationCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUndoApprovalOfLoanApplication()) {
handler = applicationContext.getBean("undoApprovalLoanApplicationCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isApplicantWithdrawalFromLoanApplication()) {
handler = applicationContext.getBean("applicantWithdrawsFromLoanApplicationCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isRejectionOfLoanApplication()) {
handler = applicationContext.getBean("rejectLoanApplicationCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDisbursementOfLoan()) {
handler = applicationContext.getBean("disburseLoanCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUndoDisbursementOfLoan()) {
handler = applicationContext.getBean("undoDisbursalLoanCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isCreate()) {
handler = applicationContext.getBean("createLoanApplicationCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUpdate()) {
handler = applicationContext.getBean("updateLoanApplicationCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDelete()) {
handler = applicationContext.getBean("deleteLoanApplicationCommandHandler", NewCommandSourceHandler.class);
} else {
throw new UnsupportedCommandException(wrapper.commandName());
}
} else {
throw new UnsupportedCommandException(wrapper.commandName());
}
@@ -18,7 +18,6 @@ import org.mifosplatform.portfolio.group.data.GroupData;
import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData;
import org.mifosplatform.portfolio.loanaccount.gaurantor.data.GuarantorData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.savingsaccount.data.SavingAccountData;
import org.mifosplatform.portfolio.savingsaccount.data.SavingScheduleData;
import org.mifosplatform.portfolio.savingsaccountproduct.data.SavingProductData;
@@ -59,8 +58,8 @@ public class GoogleGsonPortfolioApiJsonSerializerService implements PortfolioApi
"interestCompoundedEveryPeriodType", "renewalAllowed", "preClosureAllowed", "preClosureInterestRate", "withdrawnonDate",
"rejectedonDate", "closedonDate", "transactions", "permissions", "isInterestWithdrawable", "interestPaid",
"interestCompoundingAllowed", "availableInterestForWithdrawal", "availableWithdrawalAmount", "todaysDate",
"isLockinPeriodAllowed", "lockinPeriod", "lockinPeriodType", "printFDdetailsLocation","availableInterest", "interestPostedAmount",
"lastInterestPostedDate", "nextInterestPostedDate","fatherName","address","imageKey"));
"isLockinPeriodAllowed", "lockinPeriod", "lockinPeriodType", "printFDdetailsLocation", "availableInterest",
"interestPostedAmount", "lastInterestPostedDate", "nextInterestPostedDate", "fatherName", "address", "imageKey"));
private static final Set<String> DOCUMENT_DATA_PARAMETERS = new HashSet<String>(Arrays.asList("id", "parentEntityType",
"parentEntityId", "name", "fileName", "type", "size", "description"));
@@ -79,9 +78,6 @@ public class GoogleGsonPortfolioApiJsonSerializerService implements PortfolioApi
private static final Set<String> GROUP_DATA_PARAMETERS = new HashSet<String>(Arrays.asList("id", "officeId", "name", "externalId",
"clientMembers", "allowedClients", "allowedOffices"));
private static final Set<String> LOAN_SCHEDULE_DATA_PARAMETERS = new HashSet<String>(Arrays.asList("periods",
"cumulativePrincipalDisbursed"));
private static final Set<String> LOAN_TRANSACTION_NEW_DATA_PARAMETERS = new HashSet<String>(Arrays.asList("id", "type", "date",
"currency", "amount"));
@@ -103,7 +99,7 @@ public class GoogleGsonPortfolioApiJsonSerializerService implements PortfolioApi
private static final Set<String> GUARANTOR_DATA_PARAMETERS = new HashSet<String>(Arrays.asList("externalGuarantor", "existingClientId",
"firstname", "lastname", "addressLine1", "addressLine2", "city", "state", "zip", "country", "mobileNumber", "housePhoneNumber",
"comment", "dob"));
private static final Set<String> SAVING_SCHEDULE_DATA_PARAMETERS = new HashSet<String>(Arrays.asList("periods", "cumulativeDepositDue"));
private final GoogleGsonSerializerHelper helper;
@@ -210,14 +206,6 @@ public class GoogleGsonPortfolioApiJsonSerializerService implements PortfolioApi
return helper.serializedJsonFrom(gsonDeserializer, group);
}
@Override
public String serializeLoanScheduleDataToJson(final boolean prettyPrint, final Set<String> responseParameters,
final LoanScheduleData loanSchedule) {
final Gson gsonDeserializer = helper.createGsonBuilderWithParameterExclusionSerializationStrategy(LOAN_SCHEDULE_DATA_PARAMETERS,
prettyPrint, responseParameters);
return helper.serializedJsonFrom(gsonDeserializer, loanSchedule);
}
@Override
public String serializeLoanTransactionDataToJson(final boolean prettyPrint, final Set<String> responseParameters,
final LoanTransactionData transaction) {
@@ -55,6 +55,10 @@ public final class JsonCommand {
return this.jsonCommand;
}
public JsonElement parsedJson() {
return this.parsedCommand;
}
public Long commandId() {
return this.commandId;
}
@@ -0,0 +1,261 @@
package org.mifosplatform.infrastructure.core.api;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Map;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.infrastructure.security.domain.BasicPasswordEncodablePlatformUser;
import org.mifosplatform.infrastructure.security.domain.PlatformUser;
import org.mifosplatform.infrastructure.security.service.PlatformPasswordEncoder;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
/**
* Immutable representation of a query.
*
* Wraps the provided JSON with convenience functions for extracting parameter
* values.
*/
public final class JsonQuery {
private final String jsonQuery;
private final JsonElement parsedQuery;
private final FromJsonHelper fromApiJsonHelper;
public static JsonQuery from(final String jsonCommand, final JsonElement parsedQuery, final FromJsonHelper fromApiJsonHelper) {
return new JsonQuery(jsonCommand, parsedQuery, fromApiJsonHelper);
}
public JsonQuery(final String jsonCommand, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper) {
this.jsonQuery = jsonCommand;
this.parsedQuery = parsedCommand;
this.fromApiJsonHelper = fromApiJsonHelper;
}
public String json() {
return this.jsonQuery;
}
public JsonElement parsedJson() {
return this.parsedQuery;
}
private boolean differenceExists(final LocalDate baseValue, final LocalDate workingCopyValue) {
boolean differenceExists = false;
if (baseValue != null) {
differenceExists = !baseValue.equals(workingCopyValue);
} else {
differenceExists = workingCopyValue != null;
}
return differenceExists;
}
private boolean differenceExists(final String baseValue, final String workingCopyValue) {
boolean differenceExists = false;
if (StringUtils.isNotBlank(baseValue)) {
differenceExists = !baseValue.equals(workingCopyValue);
} else {
differenceExists = StringUtils.isNotBlank(workingCopyValue);
}
return differenceExists;
}
private boolean differenceExists(final String[] baseValue, final String[] workingCopyValue) {
Arrays.sort(baseValue);
Arrays.sort(workingCopyValue);
return !Arrays.equals(baseValue, workingCopyValue);
}
private boolean differenceExists(final Number baseValue, final Number workingCopyValue) {
boolean differenceExists = false;
if (baseValue != null) {
differenceExists = !baseValue.equals(workingCopyValue);
} else {
differenceExists = workingCopyValue != null;
}
return differenceExists;
}
private boolean differenceExists(final BigDecimal baseValue, final BigDecimal workingCopyValue) {
boolean differenceExists = false;
if (baseValue != null) {
differenceExists = baseValue.compareTo(workingCopyValue) != 0;
} else {
differenceExists = workingCopyValue != null;
}
return differenceExists;
}
private boolean differenceExists(final Boolean baseValue, final Boolean workingCopyValue) {
boolean differenceExists = false;
if (baseValue != null) {
differenceExists = !baseValue.equals(workingCopyValue);
} else {
differenceExists = workingCopyValue != null;
}
return differenceExists;
}
private boolean parameterExists(final String parameterName) {
return this.fromApiJsonHelper.parameterExists(parameterName, parsedQuery);
}
public boolean hasParameter(final String parameterName) {
return parameterExists(parameterName);
}
public String dateFormat() {
return stringValueOfParameterNamed("dateFormat");
}
public String locale() {
return stringValueOfParameterNamed("locale");
}
public Map<String, Boolean> mapValueOfParameterNamed(final String parameterName) {
final Type typeOfMap = new TypeToken<Map<String, Boolean>>() {}.getType();
if (parsedQuery.getAsJsonObject().has(parameterName)) {
parsedQuery.getAsJsonObject().get(parameterName);
}
return this.fromApiJsonHelper.extractMap(typeOfMap, jsonQuery);
}
public boolean isChangeInLongParameterNamed(final String parameterName, final Long existingValue) {
boolean isChanged = false;
if (parameterExists(parameterName)) {
final Long workingValue = longValueOfParameterNamed(parameterName);
isChanged = differenceExists(existingValue, workingValue);
}
return isChanged;
}
public Long longValueOfParameterNamed(final String parameterName) {
return this.fromApiJsonHelper.extractLongNamed(parameterName, parsedQuery);
}
public boolean isChangeInLocalDateParameterNamed(final String parameterName, final LocalDate existingValue) {
boolean isChanged = false;
if (parameterExists(parameterName)) {
final LocalDate workingValue = localDateValueOfParameterNamed(parameterName);
isChanged = differenceExists(existingValue, workingValue);
}
return isChanged;
}
public LocalDate localDateValueOfParameterNamed(final String parameterName) {
return this.fromApiJsonHelper.extractLocalDateNamed(parameterName, parsedQuery);
}
public boolean isChangeInStringParameterNamed(final String parameterName, final String existingValue) {
boolean isChanged = false;
if (parameterExists(parameterName)) {
final String workingValue = stringValueOfParameterNamed(parameterName);
isChanged = differenceExists(existingValue, workingValue);
}
return isChanged;
}
public String stringValueOfParameterNamed(final String parameterName) {
final String value = this.fromApiJsonHelper.extractStringNamed(parameterName, parsedQuery);
return StringUtils.defaultIfEmpty(value, "");
}
public boolean isChangeInBigDecimalParameterNamed(final String parameterName, final BigDecimal existingValue) {
boolean isChanged = false;
if (parameterExists(parameterName)) {
final BigDecimal workingValue = bigDecimalValueOfParameterNamed(parameterName);
isChanged = differenceExists(existingValue, workingValue);
}
return isChanged;
}
public BigDecimal bigDecimalValueOfParameterNamed(final String parameterName) {
return this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(parameterName, parsedQuery);
}
public boolean isChangeInIntegerParameterNamed(final String parameterName, final Integer existingValue) {
boolean isChanged = false;
if (parameterExists(parameterName)) {
final Integer workingValue = integerValueOfParameterNamed(parameterName);
isChanged = differenceExists(existingValue, workingValue);
}
return isChanged;
}
public Integer integerValueOfParameterNamed(final String parameterName) {
return this.fromApiJsonHelper.extractIntegerWithLocaleNamed(parameterName, parsedQuery);
}
public boolean isChangeInBooleanParameterNamed(final String parameterName, final Boolean existingValue) {
boolean isChanged = false;
if (parameterExists(parameterName)) {
final Boolean workingValue = booleanObjectValueOfParameterNamed(parameterName);
isChanged = differenceExists(existingValue, workingValue);
}
return isChanged;
}
/**
* Returns {@link Boolean} that could possibly be null.
*/
public Boolean booleanObjectValueOfParameterNamed(final String parameterName) {
return this.fromApiJsonHelper.extractBooleanNamed(parameterName, parsedQuery);
}
/**
* always returns true or false
*/
public boolean booleanPrimitiveValueOfParameterNamed(final String parameterName) {
final Boolean value = this.fromApiJsonHelper.extractBooleanNamed(parameterName, parsedQuery);
return (Boolean) ObjectUtils.defaultIfNull(value, Boolean.FALSE);
}
public boolean isChangeInArrayParameterNamed(final String parameterName, final String[] existingValue) {
boolean isChanged = false;
if (parameterExists(parameterName)) {
final String[] workingValue = arrayValueOfParameterNamed(parameterName);
isChanged = differenceExists(existingValue, workingValue);
}
return isChanged;
}
public String[] arrayValueOfParameterNamed(final String parameterName) {
return this.fromApiJsonHelper.extractArrayNamed(parameterName, parsedQuery);
}
public boolean isChangeInPasswordParameterNamed(final String parameterName, final String existingValue,
final PlatformPasswordEncoder platformPasswordEncoder, final Long saltValue) {
boolean isChanged = false;
if (parameterExists(parameterName)) {
final String workingValue = passwordValueOfParameterNamed(parameterName, platformPasswordEncoder, saltValue);
isChanged = differenceExists(existingValue, workingValue);
}
return isChanged;
}
public String passwordValueOfParameterNamed(final String parameterName, final PlatformPasswordEncoder platformPasswordEncoder,
final Long saltValue) {
final String passwordPlainText = stringValueOfParameterNamed(parameterName);
final PlatformUser dummyPlatformUser = new BasicPasswordEncodablePlatformUser(saltValue, "", passwordPlainText);
return platformPasswordEncoder.encode(dummyPlatformUser);
}
}
@@ -5,9 +5,7 @@ import org.mifosplatform.portfolio.client.command.NoteCommand;
import org.mifosplatform.portfolio.client.data.ClientData;
import org.mifosplatform.portfolio.group.command.GroupCommand;
import org.mifosplatform.portfolio.loanaccount.command.AdjustLoanTransactionCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanApplicationCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanStateTransitionCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanTransactionCommand;
import org.mifosplatform.portfolio.loanaccount.gaurantor.command.GuarantorCommand;
import org.mifosplatform.portfolio.savingsaccount.command.SavingAccountCommand;
@@ -29,12 +27,8 @@ public interface PortfolioApiDataConversionService {
GroupCommand convertJsonToGroupCommand(Long resourceIdentifier, String json);
LoanApplicationCommand convertApiRequestJsonToLoanApplicationCommand(Long resourceIdentifier, String json);
LoanChargeCommand convertJsonToLoanChargeCommand(Long loanChargeId, Long loanId, String json);
LoanStateTransitionCommand convertJsonToLoanStateTransitionCommand(Long resourceIdentifier, String json);
LoanTransactionCommand convertJsonToLoanTransactionCommand(Long resourceIdentifier, String json);
AdjustLoanTransactionCommand convertJsonToAdjustLoanTransactionCommand(Long loanId, Long transactionId, String json);
@@ -21,7 +21,6 @@ import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.exception.UnsupportedParameterException;
import org.mifosplatform.infrastructure.core.serialization.JsonParserHelper;
import org.mifosplatform.organisation.staff.command.BulkTransferLoanOfficerCommand;
import org.mifosplatform.portfolio.client.command.ClientCommand;
import org.mifosplatform.portfolio.client.command.NoteCommand;
@@ -29,9 +28,7 @@ import org.mifosplatform.portfolio.client.data.ClientData;
import org.mifosplatform.portfolio.client.serialization.ClientCommandFromApiJsonDeserializer;
import org.mifosplatform.portfolio.group.command.GroupCommand;
import org.mifosplatform.portfolio.loanaccount.command.AdjustLoanTransactionCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanApplicationCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanStateTransitionCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanTransactionCommand;
import org.mifosplatform.portfolio.loanaccount.gaurantor.command.GuarantorCommand;
import org.mifosplatform.portfolio.savingsaccount.command.SavingAccountCommand;
@@ -178,106 +175,10 @@ public class PortfolioApiDataConversionServiceImpl implements PortfolioApiDataCo
return new GroupCommand(modifiedParameters, resourceIdentifier, externalId, name, officeId, clientMembers);
}
@Override
public LoanApplicationCommand convertApiRequestJsonToLoanApplicationCommand(final Long resourceIdentifier, final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
final Map<String, Object> requestMap = gsonConverter.fromJson(json, typeOfMap);
final Set<String> supportedParams = new HashSet<String>(Arrays.asList("clientId", "groupId", "productId", "externalId", "fundId",
"transactionProcessingStrategyId", "principal", "inArrearsTolerance", "interestRatePerPeriod", "repaymentEvery",
"numberOfRepayments", "loanTermFrequency", "loanTermFrequencyType", "charges", "repaymentFrequencyType",
"interestRateFrequencyType", "amortizationType", "interestType", "interestCalculationPeriodType",
"expectedDisbursementDate", "repaymentsStartingFromDate", "interestChargedFromDate", "submittedOnDate", "submittedOnNote",
"locale", "dateFormat", "loanOfficerId", "id"));
checkForUnsupportedParameters(requestMap, supportedParams);
final Set<String> parametersPassedInCommand = new HashSet<String>();
final JsonParser parser = new JsonParser();
final JsonElement element = parser.parse(json);
final JsonParserHelper helper = new JsonParserHelper();
final Long clientId = helper.extractLongNamed("clientId", element, parametersPassedInCommand);
final Long groupId = helper.extractLongNamed("groupId", element, parametersPassedInCommand);
final Long productId = helper.extractLongNamed("productId", element, parametersPassedInCommand);
final Long fundId = helper.extractLongNamed("fundId", element, parametersPassedInCommand);
final Long loanOfficerId = helper.extractLongNamed("loanOfficerId", element, parametersPassedInCommand);
final Long transactionProcessingStrategyId = helper.extractLongNamed("transactionProcessingStrategyId", element,
parametersPassedInCommand);
final String externalId = helper.extractStringNamed("externalId", element, parametersPassedInCommand);
final BigDecimal principal = helper.extractBigDecimalWithLocaleNamed("principal", element, parametersPassedInCommand);
final BigDecimal inArrearsToleranceValue = helper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element,
parametersPassedInCommand);
final BigDecimal interestRatePerPeriod = helper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element,
parametersPassedInCommand);
final Integer repaymentEvery = helper.extractIntegerWithLocaleNamed("repaymentEvery", element, parametersPassedInCommand);
final Integer numberOfRepayments = helper.extractIntegerWithLocaleNamed("numberOfRepayments", element, parametersPassedInCommand);
final Integer repaymentFrequencyType = helper.extractIntegerWithLocaleNamed("repaymentFrequencyType", element,
parametersPassedInCommand);
final Integer loanTermFrequency = helper.extractIntegerWithLocaleNamed("loanTermFrequency", element, parametersPassedInCommand);
final Integer loanTermFrequencyType = helper.extractIntegerWithLocaleNamed("loanTermFrequencyType", element,
parametersPassedInCommand);
final Integer interestRateFrequencyType = helper.extractIntegerWithLocaleNamed("interestRateFrequencyType", element,
parametersPassedInCommand);
final Integer amortizationType = helper.extractIntegerWithLocaleNamed("amortizationType", element, parametersPassedInCommand);
final Integer interestType = helper.extractIntegerWithLocaleNamed("interestType", element, parametersPassedInCommand);
final Integer interestCalculationPeriodType = helper.extractIntegerWithLocaleNamed("interestCalculationPeriodType", element,
parametersPassedInCommand);
final LocalDate expectedDisbursementDate = helper.extractLocalDateNamed("expectedDisbursementDate", element,
parametersPassedInCommand);
final LocalDate repaymentsStartingFromDate = helper.extractLocalDateNamed("repaymentsStartingFromDate", element,
parametersPassedInCommand);
final LocalDate interestChargedFromDate = helper.extractLocalDateNamed("interestChargedFromDate", element,
parametersPassedInCommand);
final LocalDate submittedOnDate = helper.extractLocalDateNamed("submittedOnDate", element, parametersPassedInCommand);
final String submittedOnNote = helper.extractStringNamed("submittedOnNote", element, parametersPassedInCommand);
LoanChargeCommand[] charges = null;
if (element.isJsonObject()) {
final JsonObject topLevelJsonElement = element.getAsJsonObject();
final String dateFormat = helper.extractDateFormatParameter(topLevelJsonElement);
final Locale locale = helper.extractLocaleParameter(topLevelJsonElement);
if (topLevelJsonElement.has("charges") && topLevelJsonElement.get("charges").isJsonArray()) {
parametersPassedInCommand.add("charges");
final JsonArray array = topLevelJsonElement.get("charges").getAsJsonArray();
charges = new LoanChargeCommand[array.size()];
for (int i = 0; i < array.size(); i++) {
final JsonObject loanChargeElement = array.get(i).getAsJsonObject();
final Set<String> parametersPassedInForChargesCommand = new HashSet<String>();
final Long id = helper.extractLongNamed("id", loanChargeElement, parametersPassedInForChargesCommand);
final Long chargeId = helper.extractLongNamed("chargeId", loanChargeElement, parametersPassedInForChargesCommand);
final BigDecimal amount = helper.extractBigDecimalNamed("amount", loanChargeElement, locale,
parametersPassedInForChargesCommand);
final Integer chargeTimeType = helper.extractIntegerNamed("chargeTimeType", loanChargeElement, locale,
parametersPassedInForChargesCommand);
final Integer chargeCalculationType = helper.extractIntegerNamed("chargeCalculationType", loanChargeElement, locale,
parametersPassedInForChargesCommand);
final LocalDate specifiedDueDate = helper.extractLocalDateNamed("specifiedDueDate", loanChargeElement, dateFormat,
locale, parametersPassedInForChargesCommand);
charges[i] = new LoanChargeCommand(parametersPassedInForChargesCommand, id, null, chargeId, amount, chargeTimeType,
chargeCalculationType, specifiedDueDate);
}
}
}
return new LoanApplicationCommand(parametersPassedInCommand, resourceIdentifier, clientId, groupId, productId, externalId, fundId,
transactionProcessingStrategyId, submittedOnDate, submittedOnNote, expectedDisbursementDate, repaymentsStartingFromDate,
interestChargedFromDate, principal, interestRatePerPeriod, interestRateFrequencyType, interestType,
interestCalculationPeriodType, repaymentEvery, repaymentFrequencyType, numberOfRepayments, amortizationType,
loanTermFrequency, loanTermFrequencyType, inArrearsToleranceValue, charges, loanOfficerId);
}
/**
* @deprecated - to be removed soon
*/
@Deprecated
@Override
public LoanChargeCommand convertJsonToLoanChargeCommand(final Long loanChargeId, final Long loanId, final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
@@ -302,25 +203,6 @@ public class PortfolioApiDataConversionServiceImpl implements PortfolioApiDataCo
submittedOnDate);
}
@Override
public LoanStateTransitionCommand convertJsonToLoanStateTransitionCommand(final Long resourceIdentifier, final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
Map<String, Object> requestMap = gsonConverter.fromJson(json, typeOfMap);
Set<String> supportedParams = new HashSet<String>(Arrays.asList("eventDate", "note", "locale", "dateFormat"));
checkForUnsupportedParameters(requestMap, supportedParams);
Set<String> modifiedParameters = new HashSet<String>();
LocalDate eventDate = extractLocalDateParameter("eventDate", requestMap, modifiedParameters);
String note = extractStringParameter("note", requestMap, modifiedParameters);
return new LoanStateTransitionCommand(resourceIdentifier, eventDate, note);
}
@Override
public LoanTransactionCommand convertJsonToLoanTransactionCommand(final Long resourceIdentifier, final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
@@ -15,7 +15,6 @@ import org.mifosplatform.portfolio.group.data.GroupData;
import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData;
import org.mifosplatform.portfolio.loanaccount.gaurantor.data.GuarantorData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.savingsaccount.data.SavingAccountData;
import org.mifosplatform.portfolio.savingsaccount.data.SavingScheduleData;
import org.mifosplatform.portfolio.savingsaccountproduct.data.SavingProductData;
@@ -52,8 +51,6 @@ public interface PortfolioApiJsonSerializerService {
String serializeGroupDataToJson(boolean prettyPrint, Set<String> responseParameters, GroupData group);
String serializeLoanScheduleDataToJson(boolean prettyPrint, Set<String> responseParameters, LoanScheduleData loanSchedule);
String serializeLoanTransactionDataToJson(boolean prettyPrint, Set<String> responseParameters, LoanTransactionData newTransactionData);
String serializeLoanReassignmentDataToJson(boolean prettyPrint, Set<String> responseParameters,
@@ -77,5 +74,4 @@ public interface PortfolioApiJsonSerializerService {
String serializeSavingScheduleDataToJson(boolean prettyPrint, Set<String> responseParameters, SavingScheduleData savingScheduleData);
}
@@ -17,6 +17,7 @@ import org.springframework.stereotype.Component;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@Component
@@ -98,6 +99,11 @@ public class FromJsonHelper {
public LocalDate extractLocalDateNamed(final String parameterName, JsonElement element) {
return helperDelegator.extractLocalDateNamed(parameterName, element, new HashSet<String>());
}
public LocalDate extractLocalDateNamed(final String parameterName, final JsonElement element, final String dateFormat,
final Locale locale) {
return helperDelegator.extractLocalDateNamed(parameterName, element.getAsJsonObject(), dateFormat, locale, new HashSet<String>());
}
public LocalDate extractLocalDateNamed(final String parameterName, final JsonElement element,
final Set<String> parametersPassedInRequest) {
@@ -118,11 +124,15 @@ public class FromJsonHelper {
return helperDelegator.extractBigDecimalWithLocaleNamed(parameterName, element, parametersPassedInRequest);
}
public BigDecimal extractBigDecimalNamed(final String parameterName, final JsonElement element, final Locale locale) {
return helperDelegator.extractBigDecimalNamed(parameterName, element.getAsJsonObject(), locale, new HashSet<String>());
}
public BigDecimal extractBigDecimalNamed(final String parameterName, final JsonElement element,
final Set<String> parametersPassedInRequest) {
return helperDelegator.extractBigDecimalNamed(parameterName, element.getAsJsonObject(), Locale.US, parametersPassedInRequest);
}
public Integer extractIntegerWithLocaleNamed(final String parameterName, final JsonElement element) {
return helperDelegator.extractIntegerWithLocaleNamed(parameterName, element.getAsJsonObject(), new HashSet<String>());
}
@@ -131,9 +141,21 @@ public class FromJsonHelper {
final Set<String> parametersPassedInRequest) {
return helperDelegator.extractIntegerWithLocaleNamed(parameterName, element.getAsJsonObject(), parametersPassedInRequest);
}
public Integer extractIntegerNamed(final String parameterName, final JsonElement element, final Locale locale) {
return helperDelegator.extractIntegerNamed(parameterName, element.getAsJsonObject(), locale, new HashSet<String>());
}
public Integer extractIntegerNamed(final String parameterName, final JsonElement element,
final Set<String> parametersPassedInRequest) {
return helperDelegator.extractIntegerNamed(parameterName, element.getAsJsonObject(), Locale.US, parametersPassedInRequest);
}
public Locale extractLocaleParameter(final JsonObject element) {
return helperDelegator.extractLocaleParameter(element);
}
public String extractDateFormatParameter(final JsonObject element) {
return helperDelegator.extractDateFormatParameter(element);
}
}
@@ -21,8 +21,10 @@ import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.commands.service.PortfolioCommandSourceWritePlatformService;
import org.mifosplatform.infrastructure.core.api.ApiParameterHelper;
import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper;
import org.mifosplatform.infrastructure.core.api.JsonQuery;
import org.mifosplatform.infrastructure.core.api.PortfolioApiDataConversionService;
import org.mifosplatform.infrastructure.core.api.PortfolioApiJsonSerializerService;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
@@ -30,6 +32,7 @@ import org.mifosplatform.infrastructure.core.data.EnumOptionData;
import org.mifosplatform.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
@@ -45,11 +48,8 @@ import org.mifosplatform.portfolio.charge.service.ChargeReadPlatformService;
import org.mifosplatform.portfolio.fund.data.FundData;
import org.mifosplatform.portfolio.fund.service.FundReadPlatformService;
import org.mifosplatform.portfolio.loanaccount.command.AdjustLoanTransactionCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanApplicationCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanStateTransitionCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanTransactionCommand;
import org.mifosplatform.portfolio.loanaccount.command.UndoStateTransitionCommand;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData;
import org.mifosplatform.portfolio.loanaccount.data.LoanBasicDetailsData;
@@ -58,9 +58,8 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanPermissionData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData;
import org.mifosplatform.portfolio.loanaccount.gaurantor.data.GuarantorData;
import org.mifosplatform.portfolio.loanaccount.gaurantor.service.GuarantorReadPlatformService;
import org.mifosplatform.portfolio.loanaccount.loanschedule.command.CalculateLoanScheduleCommand;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.service.CalculationPlatformService;
import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
import org.mifosplatform.portfolio.loanaccount.service.LoanReadPlatformService;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.mifosplatform.portfolio.loanproduct.data.LoanProductData;
@@ -72,6 +71,8 @@ import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import com.google.gson.JsonElement;
@Path("/loans")
@Component
@Scope("singleton")
@@ -88,7 +89,7 @@ public class LoansApiResource {
"interestRateFrequencyTypeOptions", "fundOptions", "repaymentStrategyOptions", "chargeOptions", "loanOfficerId",
"loanOfficerName", "loanOfficerOptions", "chargeTemplate"));
// private final String resourceNameForPermissions = "OFFICE";
private final String resourceNameForPermissions = "LOAN";
private final PlatformSecurityContext context;
private final LoanReadPlatformService loanReadPlatformService;
@@ -97,25 +98,31 @@ public class LoansApiResource {
private final LoanDropdownReadPlatformService dropdownReadPlatformService;
private final FundReadPlatformService fundReadPlatformService;
private final ChargeReadPlatformService chargeReadPlatformService;
private final CalculationPlatformService calculationPlatformService;
private final LoanScheduleCalculationPlatformService calculationPlatformService;
private final StaffReadPlatformService staffReadPlatformService;
private final OfficeReadPlatformService officeReadPlatformService;
private final GuarantorReadPlatformService guarantorReadPlatformService;
private final PortfolioApiJsonSerializerService apiJsonSerializerService;
private final PortfolioApiDataConversionService apiDataConversionService;
private final DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer;
private final DefaultToApiJsonSerializer<LoanScheduleData> loanScheduleToApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final FromJsonHelper fromJsonHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@Autowired
public LoansApiResource(final PlatformSecurityContext context, final LoanReadPlatformService loanReadPlatformService,
final LoanWritePlatformService loanWritePlatformService, final LoanProductReadPlatformService loanProductReadPlatformService,
final LoanDropdownReadPlatformService dropdownReadPlatformService, final FundReadPlatformService fundReadPlatformService,
final ChargeReadPlatformService chargeReadPlatformService, final CalculationPlatformService calculationPlatformService,
final ChargeReadPlatformService chargeReadPlatformService,
final LoanScheduleCalculationPlatformService calculationPlatformService,
final StaffReadPlatformService staffReadPlatformService, final OfficeReadPlatformService officeReadPlatformService,
final GuarantorReadPlatformService guarantorReadPlatformService,
final DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer,
final DefaultToApiJsonSerializer<LoanScheduleData> loanScheduleToApiJsonSerializer,
final ApiRequestParameterHelper apiRequestParameterHelper, final PortfolioApiJsonSerializerService apiJsonSerializerService,
final PortfolioApiDataConversionService apiDataConversionService) {
final PortfolioApiDataConversionService apiDataConversionService, final FromJsonHelper fromJsonHelper,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) {
this.context = context;
this.loanReadPlatformService = loanReadPlatformService;
this.loanWritePlatformService = loanWritePlatformService;
@@ -128,9 +135,12 @@ public class LoansApiResource {
this.officeReadPlatformService = officeReadPlatformService;
this.guarantorReadPlatformService = guarantorReadPlatformService;
this.toApiJsonSerializer = toApiJsonSerializer;
this.loanScheduleToApiJsonSerializer = loanScheduleToApiJsonSerializer;
this.apiRequestParameterHelper = apiRequestParameterHelper;
this.apiJsonSerializerService = apiJsonSerializerService;
this.apiDataConversionService = apiDataConversionService;
this.fromJsonHelper = fromJsonHelper;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
}
@GET
@@ -140,7 +150,7 @@ public class LoansApiResource {
public String retrieveDetailsForNewLoanApplicationStepOne(@QueryParam("clientId") final Long clientId,
@QueryParam("groupId") final Long groupId, @QueryParam("productId") final Long productId, @Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission("LOAN");
context.authenticatedUser().validateHasReadPermission(resourceNameForPermissions);
// tempate related
Collection<LoanProductData> productOptions = this.loanProductReadPlatformService.retrieveAllLoanProductsForLookup();
@@ -193,7 +203,7 @@ public class LoansApiResource {
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveLoanAccountDetails(@PathParam("loanId") final Long loanId, @Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission("LOAN");
context.authenticatedUser().validateHasReadPermission(resourceNameForPermissions);
final LoanBasicDetailsData loanBasicDetails = this.loanReadPlatformService.retrieveLoanAccountDetails(loanId);
@@ -319,94 +329,83 @@ public class LoansApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String calculateLoanScheduleOrSubmitLoanApplication(@QueryParam("command") final String commandParam,
@Context final UriInfo uriInfo, final String jsonRequestBody) {
final LoanApplicationCommand command = this.apiDataConversionService.convertApiRequestJsonToLoanApplicationCommand(null,
jsonRequestBody);
@Context final UriInfo uriInfo, final String apiRequestBodyAsJson) {
if (is(commandParam, "calculateLoanSchedule")) {
CalculateLoanScheduleCommand calculateLoanScheduleCommand = command.toCalculateLoanScheduleCommand();
return calculateLoanSchedule(uriInfo, calculateLoanScheduleCommand);
final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper);
final LoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.loanScheduleToApiJsonSerializer.serialize(settings, loanSchedule, new HashSet<String>());
}
final EntityIdentifier identifier = this.loanWritePlatformService.submitLoanApplication(command);
final EntityIdentifier result = this.commandsSourceWritePlatformService.logCommandSource("CREATE", "LOAN", "CREATE", "loans", null,
apiRequestBodyAsJson);
return this.toApiJsonSerializer.serialize(identifier);
}
private String calculateLoanSchedule(final UriInfo uriInfo, final CalculateLoanScheduleCommand command) {
final LoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanSchedule(command);
final Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serializeLoanScheduleDataToJson(prettyPrint, responseParameters, loanSchedule);
return this.toApiJsonSerializer.serialize(result);
}
@PUT
@Path("{loanId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String modifyLoanApplication(@PathParam("loanId") final Long loanId, final String jsonRequestBody) {
public String modifyLoanApplication(@PathParam("loanId") final Long loanId, final String apiRequestBodyAsJson) {
final LoanApplicationCommand command = this.apiDataConversionService.convertApiRequestJsonToLoanApplicationCommand(loanId,
jsonRequestBody);
final EntityIdentifier result = this.commandsSourceWritePlatformService.logCommandSource("UPDATE", "LOAN", "UPDATE", "loans",
loanId, apiRequestBodyAsJson);
final EntityIdentifier identifier = this.loanWritePlatformService.modifyLoanApplication(command);
return this.toApiJsonSerializer.serialize(identifier);
return this.toApiJsonSerializer.serialize(result);
}
@DELETE
@Path("{loanId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public Response deleteLoanApplication(@PathParam("loanId") final Long loanId) {
public String deleteLoanApplication(@PathParam("loanId") final Long loanId) {
EntityIdentifier identifier = this.loanWritePlatformService.deleteLoan(loanId);
final EntityIdentifier result = this.commandsSourceWritePlatformService.logCommandSource("DELETE", "LOAN", "DELETE", "loans",
loanId, "{}");
return Response.ok().entity(identifier).build();
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("{loanId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public Response stateTransitions(@PathParam("loanId") final Long loanId, @QueryParam("command") final String commandParam,
final String jsonRequestBody) {
public String stateTransitions(@PathParam("loanId") final Long loanId, @QueryParam("command") final String commandParam,
final String apiRequestBodyAsJson) {
LoanStateTransitionCommand command = this.apiDataConversionService.convertJsonToLoanStateTransitionCommand(loanId, jsonRequestBody);
Response response = null;
EntityIdentifier result = null;
if (is(commandParam, "reject")) {
EntityIdentifier identifier = this.loanWritePlatformService.rejectLoan(command);
response = Response.ok().entity(identifier).build();
result = this.commandsSourceWritePlatformService.logCommandSource("REJECT", "LOAN", "N/A", "loans", loanId,
apiRequestBodyAsJson);
} else if (is(commandParam, "withdrewbyclient")) {
EntityIdentifier identifier = this.loanWritePlatformService.withdrawLoan(command);
response = Response.ok().entity(identifier).build();
result = this.commandsSourceWritePlatformService.logCommandSource("WITHDRAW", "LOAN", "N/A", "loans", loanId,
apiRequestBodyAsJson);
} else if (is(commandParam, "approve")) {
EntityIdentifier identifier = this.loanWritePlatformService.approveLoanApplication(command);
response = Response.ok().entity(identifier).build();
result = this.commandsSourceWritePlatformService.logCommandSource("APPROVE", "LOAN", "N/A", "loans", loanId,
apiRequestBodyAsJson);
} else if (is(commandParam, "disburse")) {
EntityIdentifier identifier = this.loanWritePlatformService.disburseLoan(command);
response = Response.ok().entity(identifier).build();
result = this.commandsSourceWritePlatformService.logCommandSource("DISBURSE", "LOAN", "N/A", "loans", loanId,
apiRequestBodyAsJson);
}
UndoStateTransitionCommand undoCommand = new UndoStateTransitionCommand(loanId, command.getNote());
if (is(commandParam, "undoapproval")) {
EntityIdentifier identifier = this.loanWritePlatformService.undoLoanApproval(undoCommand);
response = Response.ok().entity(identifier).build();
result = this.commandsSourceWritePlatformService.logCommandSource("APPROVALUNDO", "LOAN", "N/A", "loans", loanId,
apiRequestBodyAsJson);
} else if (is(commandParam, "undodisbursal")) {
EntityIdentifier identifier = this.loanWritePlatformService.undoLoanDisbursal(undoCommand);
response = Response.ok().entity(identifier).build();
result = this.commandsSourceWritePlatformService.logCommandSource("DISBURSALUNDO", "LOAN", "N/A", "loans", loanId,
apiRequestBodyAsJson);
}
if (response == null) { throw new UnrecognizedQueryParamException("command", commandParam); }
if (result == null) { throw new UnrecognizedQueryParamException("command", commandParam); }
return response;
return this.toApiJsonSerializer.serialize(result);
}
private boolean is(final String commandParam, final String commandValue) {
@@ -1,11 +1,15 @@
package org.mifosplatform.portfolio.loanaccount.command;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosplatform.portfolio.loanaccount.loanschedule.command.CalculateLoanScheduleCommand;
import org.mifosplatform.portfolio.loanproduct.command.LoanProductCommand;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.query.CalculateLoanScheduleQuery;
/**
* Immutable command for submitting new loan application.
@@ -90,8 +94,8 @@ public class LoanApplicationCommand {
this.charges = charges;
}
public CalculateLoanScheduleCommand toCalculateLoanScheduleCommand() {
return new CalculateLoanScheduleCommand(this.productId, this.principal, this.interestRatePerPeriod, this.interestRateFrequencyType,
public CalculateLoanScheduleQuery toCalculateLoanScheduleCommand() {
return new CalculateLoanScheduleQuery(this.productId, this.principal, this.interestRatePerPeriod, this.interestRateFrequencyType,
this.interestType, this.interestCalculationPeriodType, this.repaymentEvery, this.repaymentFrequencyType,
this.numberOfRepayments, this.amortizationType, this.loanTermFrequency, this.loanTermFrequencyType,
this.expectedDisbursementDate, this.repaymentsStartingFromDate, this.interestChargedFromDate, this.charges);
@@ -197,16 +201,6 @@ public class LoanApplicationCommand {
return charges;
}
public LoanProductCommand toLoanProductCommand() {
// FIXME - KW - CHARGE - FIX UP FOR CHARGES - take look at loan product
// charges also and align usage with other loan areas.
return new LoanProductCommand(null, null, this.fundId, this.transactionProcessingStrategyId, null, null, this.principal,
this.inArrearsTolerance, this.numberOfRepayments, this.repaymentEvery, this.interestRatePerPeriod,
this.repaymentFrequencyType, this.interestRateFrequencyType, this.amortizationType, this.interestType,
this.interestCalculationPeriodType, null);
}
public boolean isClientChanged() {
return this.modifiedParameters.contains("clientId");
}
@@ -258,4 +252,52 @@ public class LoanApplicationCommand {
public Long getLoanOfficerId() {
return loanOfficerId;
}
public void validate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
if (this.groupId != null) {
baseDataValidator.reset().parameter("clientId").value(this.clientId).mustBeBlankWhenParameterProvided("groupId", this.groupId)
.longGreaterThanZero();
}
if (this.clientId != null) {
baseDataValidator.reset().parameter("groupId").value(this.groupId).mustBeBlankWhenParameterProvided("clientId", this.clientId)
.longGreaterThanZero();
}
if (this.submittedOnDate == null) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.submitted.on.date.cannot.be.blank",
"The parameter submittedOnDate cannot be empty.", "submittedOnDate");
dataValidationErrors.add(error);
} else {
if (this.submittedOnDate.isAfter(this.expectedDisbursementDate)) {
ApiParameterError error = ApiParameterError.parameterError(
"validation.msg.loan.submitted.on.date.cannot.be.after.expectedDisbursementDate",
"The date of parameter submittedOnDate cannot fall after the date given for expectedDisbursementDate.",
"submittedOnDate", this.submittedOnDate, this.expectedDisbursementDate);
dataValidationErrors.add(error);
}
}
if (this.transactionProcessingStrategyId == null) {
baseDataValidator.reset().parameter("transactionProcessingStrategyId").value(this.transactionProcessingStrategyId).notNull()
.inMinMaxRange(2, 2);
}
if (this.charges != null) {
for (LoanChargeCommand loanChargeCommand : this.charges) {
try {
LoanChargeCommandValidator validator = new LoanChargeCommandValidator(loanChargeCommand);
validator.validateForCreate();
} catch (PlatformApiDataValidationException e) {
dataValidationErrors.addAll(e.getErrors());
}
}
}
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
}
@@ -1,78 +0,0 @@
package org.mifosplatform.portfolio.loanaccount.command;
import java.util.ArrayList;
import java.util.List;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.command.CalculateLoanScheduleCommand;
import org.mifosplatform.portfolio.loanaccount.loanschedule.command.CalculateLoanScheduleCommandValidator;
public class LoanApplicationCommandValidator {
private final LoanApplicationCommand command;
public LoanApplicationCommandValidator(LoanApplicationCommand command) {
this.command = command;
}
public void validate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
// if (command.getLoanSchedule() == null) {
// ApiParameterError error = ApiParameterError.parameterError("validation.msg.submit.loan.loan.schedule.cannot.be.blank",
// "The parameter loanSchedule cannot be empty.", "loanSchedule");
// dataValidationErrors.add(error);
// }
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
if (command.getGroupId() != null){
baseDataValidator.reset().parameter("clientId").value(command.getClientId()).mustBeBlankWhenParameterProvided("groupId", command.getGroupId()).longGreaterThanZero();
}
if (command.getClientId() != null){
baseDataValidator.reset().parameter("groupId").value(command.getGroupId()).mustBeBlankWhenParameterProvided("clientId", command.getClientId()).longGreaterThanZero();
}
if (command.getSubmittedOnDate() == null) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.submitted.on.date.cannot.be.blank",
"The parameter submittedOnDate cannot be empty.", "submittedOnDate");
dataValidationErrors.add(error);
} else {
if (command.getSubmittedOnDate().isAfter(command.getExpectedDisbursementDate())) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.submitted.on.date.cannot.be.after.expectedDisbursementDate",
"The date of parameter submittedOnDate cannot fall after the date given for expectedDisbursementDate.", "submittedOnDate",
command.getSubmittedOnDate(), command.getExpectedDisbursementDate());
dataValidationErrors.add(error);
}
}
if (command.getTransactionProcessingStrategyId() == null) {
baseDataValidator.reset().parameter("transactionProcessingStrategyId").value(command.getTransactionProcessingStrategyId()).notNull().inMinMaxRange(2, 2);
}
try {
// reuse calculate loan schedule validator for now
CalculateLoanScheduleCommand calculateLoanScheduleCommand = this.command.toCalculateLoanScheduleCommand();
CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator(calculateLoanScheduleCommand);
validator.validate();
} catch (PlatformApiDataValidationException e) {
dataValidationErrors.addAll(e.getErrors());
}
if (this.command.getCharges() != null){
for (LoanChargeCommand loanChargeCommand : this.command.getCharges()){
try {
LoanChargeCommandValidator validator = new LoanChargeCommandValidator(loanChargeCommand);
validator.validateForCreate();
} catch (PlatformApiDataValidationException e) {
dataValidationErrors.addAll(e.getErrors());
}
}
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors);
}
}
}
@@ -21,25 +21,19 @@ public class LoanChargeCommand {
private final Integer chargeCalculationType;
/**
* Used to capture what parameters were passed in the json api request.
* It does not indicate that these values are modified from their original values when tyring to update.
* Used to capture what parameters were passed in the json api request. It
* does not indicate that these values are modified from their original
* values when tyring to update.
*/
private final Set<String> requestParameters;
public static LoanChargeCommand forWaiver(final Long id, final Long loanId) {
final Set<String> parametersPassedInCommand = new HashSet<String>();
return new LoanChargeCommand(parametersPassedInCommand, id, loanId, null, null, null, null, null);
}
public LoanChargeCommand(
final Set<String> parametersPassedInCommand,
final Long id,
final Long loanId,
final Long chargeId,
final BigDecimal amount,
final Integer chargeTimeType,
final Integer chargeCalculationType,
final LocalDate specifiedDueDate) {
public static LoanChargeCommand forWaiver(final Long id, final Long loanId) {
final Set<String> parametersPassedInCommand = new HashSet<String>();
return new LoanChargeCommand(parametersPassedInCommand, id, loanId, null, null, null, null, null);
}
public LoanChargeCommand(final Set<String> parametersPassedInCommand, final Long id, final Long loanId, final Long chargeId,
final BigDecimal amount, final Integer chargeTimeType, final Integer chargeCalculationType, final LocalDate specifiedDueDate) {
this.requestParameters = parametersPassedInCommand;
this.id = id;
this.chargeId = chargeId;
@@ -47,7 +41,7 @@ public class LoanChargeCommand {
this.amount = amount;
this.chargeTimeType = chargeTimeType;
this.chargeCalculationType = chargeCalculationType;
this.specifiedDueDate = specifiedDueDate;
this.specifiedDueDate = specifiedDueDate;
}
public Long getId() {
@@ -69,28 +63,28 @@ public class LoanChargeCommand {
public Integer getChargeTimeType() {
return chargeTimeType;
}
public LocalDate getSpecifiedDueDate() {
return specifiedDueDate;
}
public Integer getChargeCalculationType() {
public LocalDate getSpecifiedDueDate() {
return specifiedDueDate;
}
public Integer getChargeCalculationType() {
return chargeCalculationType;
}
public boolean isAmountChanged(){
public boolean isAmountChanged() {
return this.requestParameters.contains("amount");
}
public boolean isChargeTimeTypeChanged(){
public boolean isChargeTimeTypeChanged() {
return this.requestParameters.contains("chargeTimeType");
}
public boolean isSpecifiedDueDateChanged(){
public boolean isSpecifiedDueDateChanged() {
return this.requestParameters.contains("specifiedDueDate");
}
public boolean isChargeCalculationTypeChanged(){
public boolean isChargeCalculationTypeChanged() {
return this.requestParameters.contains("chargeCalculationType");
}
}
@@ -1,31 +1,77 @@
package org.mifosplatform.portfolio.loanaccount.command;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
/**
* Immutable command for any of the loan state transitions eg. reject, withdrawn by client, approve, disburse
* Immutable command for any of the loan state transitions eg. reject, withdrawn
* by client, approve, disburse
*/
public class LoanStateTransitionCommand {
private final Long loanId;
private final LocalDate eventDate;
private final String note;
private final LocalDate eventDate;
private final String note;
private final LocalDate approvedOnDate;
private final LocalDate rejectedOnDate;
private final LocalDate withdrawnOnDate;
private final LocalDate disbursedOnDate;
public LoanStateTransitionCommand(final Long loanId, final LocalDate eventDate, final String note) {
this.loanId = loanId;
this.eventDate = eventDate;
this.note = note;
}
public LoanStateTransitionCommand(final LocalDate eventDate, final String note, final LocalDate approvedOnDate,
final LocalDate rejectedOnDate, final LocalDate withdrawnOnDate, final LocalDate disbursedOnDate) {
this.eventDate = eventDate;
this.note = note;
this.approvedOnDate = approvedOnDate;
this.rejectedOnDate = rejectedOnDate;
this.withdrawnOnDate = withdrawnOnDate;
this.disbursedOnDate = disbursedOnDate;
}
public Long getLoanId() {
return loanId;
}
public LocalDate getApprovedOnDate() {
LocalDate dateValue = this.approvedOnDate;
if (this.approvedOnDate == null) {
dateValue = eventDate;
}
return dateValue;
}
public LocalDate getEventDate() {
return eventDate;
}
public LocalDate getRejectedOnDate() {
LocalDate dateValue = this.rejectedOnDate;
if (this.rejectedOnDate == null) {
dateValue = eventDate;
}
return dateValue;
}
public String getNote() {
return note;
}
public LocalDate getWithdrawnOnDate() {
LocalDate dateValue = this.withdrawnOnDate;
if (this.withdrawnOnDate == null) {
dateValue = eventDate;
}
return dateValue;
}
public LocalDate getDisbursedOnDate() {
LocalDate dateValue = this.disbursedOnDate;
if (this.disbursedOnDate == null) {
dateValue = eventDate;
}
return dateValue;
}
public void validate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transition");
baseDataValidator.reset().parameter("eventDate").value(this.eventDate).notNull();
baseDataValidator.reset().parameter("note").value(this.note).notExceedingLengthOf(1000);
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
}
@@ -1,32 +0,0 @@
package org.mifosplatform.portfolio.loanaccount.command;
import java.util.ArrayList;
import java.util.List;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
public class LoanStateTransitionCommandValidator {
private final LoanStateTransitionCommand command;
public LoanStateTransitionCommandValidator(LoanStateTransitionCommand command) {
this.command = command;
}
public void validate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transition");
baseDataValidator.reset().parameter("loanId").value(command.getLoanId()).notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("eventDate").value(command.getEventDate()).notNull();
baseDataValidator.reset().parameter("note").value(command.getNote()).notExceedingLengthOf(1000);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors);
}
}
}
@@ -2,9 +2,13 @@ package org.mifosplatform.portfolio.loanaccount.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.persistence.CascadeType;
@@ -26,7 +30,11 @@ import org.apache.commons.lang.StringUtils;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.domain.AbstractAuditableCustom;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.staff.domain.Staff;
@@ -34,17 +42,24 @@ import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeAddedExcep
import org.mifosplatform.portfolio.client.domain.Client;
import org.mifosplatform.portfolio.fund.domain.Fund;
import org.mifosplatform.portfolio.group.domain.Group;
import org.mifosplatform.portfolio.loanaccount.command.LoanApplicationCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.DefaultLoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations;
import org.mifosplatform.useradministration.domain.AppUser;
@Entity
@@ -68,7 +83,6 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@JoinColumn(name = "product_id")
private LoanProduct loanProduct;
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "fund_id", nullable = true)
private Fund fund;
@@ -87,11 +101,9 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@Embedded
private LoanProductRelatedDetail loanRepaymentScheduleDetail;
@SuppressWarnings("unused")
@Column(name = "term_frequency", nullable = false)
private Integer termFrequency;
@SuppressWarnings("unused")
@Column(name = "term_period_frequency_enum", nullable = false)
private Integer termPeriodFrequencyType;
@@ -178,6 +190,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@Transient
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessor = new LoanRepaymentScheduleTransactionProcessorFactory();
@Deprecated
public static Loan createNew(final Fund fund, final Staff loanOfficer,
final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProduct loanProduct, final Client client,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges) {
@@ -185,6 +198,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
loanCharges);
}
@Deprecated
public static Loan createNew(final Fund fund, final Staff loanOfficer,
final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProduct loanProduct, final Group group,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges) {
@@ -192,13 +206,24 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
loanCharges);
}
public static Loan newIndividualLoanApplication(final Client client, final LoanProduct loanProduct, final Fund fund,
final Staff officer, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule,
final Set<LoanCharge> loanCharges) {
final LoanStatus status = null;
LoanProductRelatedDetail loanRepaymentScheduleDetail = loanSchedule.loanProductRelatedDetail();
return new Loan(client, fund, officer, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, status, loanCharges);
}
public static Loan newGroupLoanApplication(final Group group, final LoanProduct loanProduct, final Fund fund, final Staff officer,
final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule,
final Set<LoanCharge> loanCharges) {
final LoanStatus status = null;
LoanProductRelatedDetail loanRepaymentScheduleDetail = loanSchedule.loanProductRelatedDetail();
return new Loan(group, fund, officer, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, status, loanCharges);
}
protected Loan() {
this.client = null;
this.group = null;
this.loanProduct = null;
this.loanRepaymentScheduleDetail = null;
this.charges = null;
this.loanOfficerHistory = null;
//
}
private Loan(final Client client, Fund fund, Staff loanOfficer, final LoanTransactionProcessingStrategy transactionProcessingStrategy,
@@ -472,79 +497,31 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return this.loanRepaymentScheduleDetail;
}
public void modifyLoanApplication(final LoanApplicationCommand command, final Client client, final LoanProduct loanProduct,
final Fund fund, final LoanTransactionProcessingStrategy strategy, final LoanScheduleData modifiedLoanSchedule,
final Set<LoanCharge> charges, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
public void updateClient(final Client client) {
this.client = client;
}
// FIXME - KW - whilst the methods are named isXXXChanged, this really
// only is asking has the parameter for that field been passed in
// request,
// an additional check to see if the value passed is different to the
// current value may bee need for all these like it is for
// expectedDisbursementDate
if (command.isClientChanged()) {
this.client = client;
}
if (command.isProductChanged()) {
this.loanProduct = loanProduct;
}
if (command.isFundChanged()) {
this.fund = fund;
}
if (command.isTransactionStrategyChanged()) {
this.transactionProcessingStrategy = strategy;
}
public void updateLoanProduct(final LoanProduct loanProduct) {
this.loanProduct = loanProduct;
}
if (command.isTermFrequencyChanged()) {
this.termFrequency = command.getLoanTermFrequency();
}
public void updateFund(final Fund fund) {
this.fund = fund;
}
if (command.isTermFrequencyTypeChanged()) {
this.termPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getLoanTermFrequencyType()).getValue();
}
public void updateTransactionProcessingStrategy(final LoanTransactionProcessingStrategy strategy) {
this.transactionProcessingStrategy = strategy;
}
if (command.isSubmittedOnDateChanged()) {
this.submittedOnDate = command.getSubmittedOnDate().toDate();
}
public void updateLoanCharges(final Set<LoanCharge> loanCharges) {
this.charges.clear();
this.charges.addAll(associateChargesWithThisLoan(loanCharges));
this.totalChargesDueAtDisbursement = deriveSumTotalOfChargesDueAtDisbursement();
}
if (command.isExpectedDisbursementDatePassed()) {
if (dateHasChanged(getExpectedDisbursedOnLocalDate(), command.getExpectedDisbursementDate())) {
this.expectedDisbursedOnDate = command.getExpectedDisbursementDate().toDate();
removeFirstDisbursementTransaction();
disburse(command.getExpectedDisbursementDate(), loanLifecycleStateMachine, false);
}
}
if (command.isRepaymentsStartingFromDateChanged()) {
if (command.getRepaymentsStartingFromDate() != null) {
this.expectedFirstRepaymentOnDate = command.getRepaymentsStartingFromDate().toDate();
} else {
this.expectedFirstRepaymentOnDate = null;
}
}
if (command.isInterestChargedFromDateChanged()) {
if (command.getInterestChargedFromDate() != null) {
this.interestChargedFromDate = command.getInterestChargedFromDate().toDate();
} else {
this.interestChargedFromDate = null;
}
}
if (command.isChargesChanged()) {
this.charges.clear();
this.charges.addAll(associateChargesWithThisLoan(charges));
this.totalChargesDueAtDisbursement = deriveSumTotalOfChargesDueAtDisbursement();
}
// FIXME - kw - break modify loan application at present.
// this.loanRepaymentScheduleDetail.update(command.toLoanProductCommand());
// FIXME - rewrite over loan schedule by default for now but worth
// putting in check to see if required
// i.e. only a client change wouldn't require it, only if one of
// parameters related to loan schedule calculation is changed.
public void updateLoanSchedule(final LoanScheduleData modifiedLoanSchedule) {
this.repaymentScheduleInstallments.clear();
for (LoanSchedulePeriodData scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) {
if (scheduledLoanInstallment.isRepaymentPeriod()) {
@@ -567,17 +544,147 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.repaymentScheduleInstallments, this.charges);
}
private boolean dateHasChanged(final LocalDate originalLocalDate, final LocalDate providedLocalDate) {
public void updateLoanScheduleDependentDerivedFields() {
this.expectedMaturityDate = determineExpectedMaturityDate().toDate();
}
boolean dateHasChanged = false;
public Map<String, Object> modifyLoanApplication(final JsonCommand command, final LoanChargeCommand[] loanChargeCommands,
final AprCalculator aprCalculator) {
if (originalLocalDate != null) {
dateHasChanged = !originalLocalDate.equals(providedLocalDate);
} else {
dateHasChanged = (providedLocalDate != null);
final Map<String, Object> actualChanges = this.loanRepaymentScheduleDetail.updateLoanApplicationAttributes(command, aprCalculator);
if (!actualChanges.isEmpty()) {
boolean recalculateLoanSchedule = !(actualChanges.size() == 1 && actualChanges.containsKey("inArrearsTolerance"));
actualChanges.put("recalculateLoanSchedule", recalculateLoanSchedule);
}
return dateHasChanged;
final String dateFormatAsInput = command.dateFormat();
final String localeAsInput = command.locale();
final String externalIdParamName = "externalId";
if (command.isChangeInStringParameterNamed(externalIdParamName, this.externalId)) {
final String newValue = command.stringValueOfParameterNamed(externalIdParamName);
actualChanges.put(externalIdParamName, newValue);
this.externalId = StringUtils.defaultIfEmpty(newValue, null);
}
final String clientIdParamName = "clientId";
if (command.isChangeInLongParameterNamed(clientIdParamName, this.client.getId())) {
final Long newValue = command.longValueOfParameterNamed(clientIdParamName);
actualChanges.put(clientIdParamName, newValue);
}
final String productIdParamName = "productId";
if (command.isChangeInLongParameterNamed(productIdParamName, this.loanProduct.getId())) {
final Long newValue = command.longValueOfParameterNamed(productIdParamName);
actualChanges.put(productIdParamName, newValue);
actualChanges.put("recalculateLoanSchedule", true);
}
Long existingFundId = null;
if (this.fund != null) {
existingFundId = this.fund.getId();
}
final String fundIdParamName = "fundId";
if (command.isChangeInLongParameterNamed(fundIdParamName, existingFundId)) {
final Long newValue = command.longValueOfParameterNamed(fundIdParamName);
actualChanges.put(fundIdParamName, newValue);
}
final String strategyIdParamName = "transactionProcessingStrategyId";
if (command.isChangeInLongParameterNamed(strategyIdParamName, this.transactionProcessingStrategy.getId())) {
final Long newValue = command.longValueOfParameterNamed(strategyIdParamName);
actualChanges.put(strategyIdParamName, newValue);
}
final String chargesParamName = "charges";
if (command.parameterExists(chargesParamName)) {
LoanChargeCommand[] existingLoanChargeData = getLoanCharges();
if (differenceExistsBetween(existingLoanChargeData, loanChargeCommands)) {
actualChanges.put(chargesParamName, loanChargeCommands);
actualChanges.put("recalculateLoanSchedule", true);
}
}
final String submittedOnDateParamName = "submittedOnDate";
if (command.isChangeInLocalDateParameterNamed(submittedOnDateParamName, getSubmittedOnDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(submittedOnDateParamName);
actualChanges.put(submittedOnDateParamName, valueAsInput);
actualChanges.put("dateFormat", dateFormatAsInput);
actualChanges.put("locale", localeAsInput);
final LocalDate newValue = command.localDateValueOfParameterNamed(submittedOnDateParamName);
this.submittedOnDate = newValue.toDate();
}
final String expectedDisbursementDateParamName = "expectedDisbursementDate";
if (command.isChangeInLocalDateParameterNamed(expectedDisbursementDateParamName, getExpectedDisbursedOnLocalDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(expectedDisbursementDateParamName);
actualChanges.put(expectedDisbursementDateParamName, valueAsInput);
actualChanges.put("dateFormat", dateFormatAsInput);
actualChanges.put("locale", localeAsInput);
actualChanges.put("recalculateLoanSchedule", true);
final LocalDate newValue = command.localDateValueOfParameterNamed(expectedDisbursementDateParamName);
this.expectedDisbursedOnDate = newValue.toDate();
removeFirstDisbursementTransaction();
disburse(getExpectedDisbursedOnLocalDate());
}
final String repaymentsStartingFromDateParamName = "repaymentsStartingFromDate";
if (command.isChangeInLocalDateParameterNamed(repaymentsStartingFromDateParamName, getExpectedFirstRepaymentOnDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(repaymentsStartingFromDateParamName);
actualChanges.put(repaymentsStartingFromDateParamName, valueAsInput);
actualChanges.put("dateFormat", dateFormatAsInput);
actualChanges.put("locale", localeAsInput);
actualChanges.put("recalculateLoanSchedule", true);
final LocalDate newValue = command.localDateValueOfParameterNamed(repaymentsStartingFromDateParamName);
if (newValue != null) {
this.expectedFirstRepaymentOnDate = newValue.toDate();
} else {
this.expectedFirstRepaymentOnDate = null;
}
}
final String interestChargedFromDateParamName = "interestChargedFromDate";
if (command.isChangeInLocalDateParameterNamed(interestChargedFromDateParamName, getInterestChargedFromDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(interestChargedFromDateParamName);
actualChanges.put(interestChargedFromDateParamName, valueAsInput);
actualChanges.put("dateFormat", dateFormatAsInput);
actualChanges.put("locale", localeAsInput);
actualChanges.put("recalculateLoanSchedule", true);
final LocalDate newValue = command.localDateValueOfParameterNamed(interestChargedFromDateParamName);
if (newValue != null) {
this.interestChargedFromDate = newValue.toDate();
} else {
this.interestChargedFromDate = null;
}
}
return actualChanges;
}
private boolean differenceExistsBetween(final LoanChargeCommand[] existingLoanChargeData, final LoanChargeCommand[] newLoanChargeData) {
Arrays.sort(existingLoanChargeData);
Arrays.sort(newLoanChargeData);
return !Arrays.equals(existingLoanChargeData, newLoanChargeData);
}
private LoanChargeCommand[] getLoanCharges() {
LoanChargeCommand[] existingLoanCharges = null;
if (this.charges != null && !this.charges.isEmpty()) {
List<LoanChargeCommand> loanChargesList = new ArrayList<LoanChargeCommand>();
for (LoanCharge loanCharge : this.charges) {
loanChargesList.add(loanCharge.toCommand());
}
existingLoanCharges = loanChargesList.toArray(new LoanChargeCommand[loanChargesList.size()]);
}
return existingLoanCharges;
}
private void removeFirstDisbursementTransaction() {
@@ -589,9 +696,11 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
}
public void submitApplication(final Integer loanTermFrequency, final PeriodFrequencyType loanTermFrequencyType,
final LocalDate submittedOn, final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate,
final LocalDate interestChargedFromDate, final LoanLifecycleStateMachine lifecycleStateMachine) {
public void loanApplicationSubmittal(final LoanSchedule loanSchedule, final LoanLifecycleStateMachine lifecycleStateMachine,
final LocalDate submittedOn, final String externalId) {
final LoanScheduleData loanScheduleData = loanSchedule.generate();
updateLoanSchedule(loanScheduleData);
LoanStatus from = null;
if (this.loanStatus != null) {
@@ -601,22 +710,22 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
LoanStatus statusEnum = lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, from);
this.loanStatus = statusEnum.getValue();
this.termFrequency = loanTermFrequency;
this.termPeriodFrequencyType = loanTermFrequencyType.getValue();
this.termFrequency = loanSchedule.getLoanTermFrequency();
this.termPeriodFrequencyType = loanSchedule.getLoanTermPeriodFrequencyType().getValue();
this.submittedOnDate = submittedOn.toDate();
this.expectedMaturityDate = determineExpectedMaturityDate().toDate();
if (expectedDisbursementDate != null) {
// can be null during bulk upload of loans
this.expectedDisbursedOnDate = expectedDisbursementDate.toDate();
if (loanSchedule.getDisbursementDate() != null) {
this.expectedDisbursedOnDate = loanSchedule.getDisbursementDate().toDate();
}
if (repaymentsStartingFromDate != null) {
this.expectedFirstRepaymentOnDate = repaymentsStartingFromDate.toDate();
if (loanSchedule.getRepaymentStartFromDate() != null) {
this.expectedFirstRepaymentOnDate = loanSchedule.getRepaymentStartFromDate().toDate();
}
if (interestChargedFromDate != null) {
this.interestChargedFromDate = interestChargedFromDate.toDate();
if (loanSchedule.getInterestChargedFromDate() != null) {
this.interestChargedFromDate = loanSchedule.getInterestChargedFromDate().toDate();
}
if (submittedOn.isAfter(new LocalDate())) {
@@ -637,6 +746,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
validateChargeHasValidSpecifiedDateIfApplicable(loanCharge, getDisbursementDate(), getLastRepaymentPeriodDueDate());
}
}
this.externalId = externalId;
}
private LocalDate determineExpectedMaturityDate() {
@@ -644,13 +755,30 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return this.repaymentScheduleInstallments.get(numberOfInstallments - 1).getDueDate();
}
public void reject(final LocalDate rejectedOn, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
public Map<String, Object> loanApplicationRejection(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>();
this.rejectedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate();
this.closedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate();
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, LoanStatus.fromInt(this.loanStatus));
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
}
LocalDate rejectedOn = command.localDateValueOfParameterNamed("rejectedOnDate");
if (rejectedOn == null) {
rejectedOn = command.localDateValueOfParameterNamed("eventDate");
}
final Locale locale = new Locale(command.locale());
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
this.rejectedOnDate = rejectedOn.toDate();
this.closedOnDate = rejectedOn.toDate();
actualChanges.put("locale", command.locale());
actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("rejectedOnDate", rejectedOn.toString(fmt));
actualChanges.put("closedOnDate", rejectedOn.toString(fmt));
if (rejectedOn.isBefore(getSubmittedOnDate())) {
final String errorMessage = "The date on which a loan is rejected cannot be before its submittal date: "
@@ -662,34 +790,73 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
final String errorMessage = "The date on which a loan is rejected cannot be in the future.";
throw new InvalidLoanStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, rejectedOn);
}
return actualChanges;
}
public void withdraw(final LocalDate withdrawnOn, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
public Map<String, Object> loanApplicationWithdrawnByApplicant(final JsonCommand command,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>();
this.withdrawnOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate();
this.closedOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate();
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, LoanStatus.fromInt(this.loanStatus));
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
}
LocalDate withdrawnOn = command.localDateValueOfParameterNamed("withdrawnOnDate");
if (withdrawnOn == null) {
withdrawnOn = command.localDateValueOfParameterNamed("eventDate");
}
final Locale locale = new Locale(command.locale());
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
this.withdrawnOnDate = withdrawnOn.toDate();
this.closedOnDate = withdrawnOn.toDate();
actualChanges.put("locale", command.locale());
actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("withdrawnOnDate", withdrawnOn.toString(fmt));
actualChanges.put("closedOnDate", withdrawnOn.toString(fmt));
if (withdrawnOn.isBefore(getSubmittedOnDate())) {
final String errorMessage = "The date on which a loan is withdrawn cannot be before its submittal date: "
+ getSubmittedOnDate().toString();
throw new InvalidLoanStateTransitionException("reject", "cannot.be.before.submittal.date", errorMessage, withdrawnOn,
throw new InvalidLoanStateTransitionException("reject", "cannot.be.before.submittal.date", errorMessage, command,
getSubmittedOnDate());
}
if (withdrawnOn.isAfter(new LocalDate())) {
final String errorMessage = "The date on which a loan is withdrawn cannot be in the future.";
throw new InvalidLoanStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, withdrawnOn);
throw new InvalidLoanStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, command);
}
return actualChanges;
}
public void approve(final LocalDate approvedOn, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
public Map<String, Object> loanApplicationApproval(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>();
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
}
this.approvedOnDate = approvedOn.toDateTimeAtCurrentTime().toDate();
LocalDate approvedOn = command.localDateValueOfParameterNamed("approvedOnDate");
if (approvedOn == null) {
approvedOn = command.localDateValueOfParameterNamed("eventDate");
}
final Locale locale = new Locale(command.locale());
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
this.approvedOnDate = approvedOn.toDate();
actualChanges.put("locale", command.locale());
actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("approvedOnDate", approvedOn.toString(fmt));
final LocalDate submittalDate = new LocalDate(this.submittedOnDate);
if (approvedOn.isBefore(submittalDate)) {
@@ -708,47 +875,112 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.loanofficer, approvedOn);
this.loanOfficerHistory.add(loanOfficerAssignmentHistory);
}
return actualChanges;
}
public void undoApproval(LoanLifecycleStateMachine loanLifecycleStateMachine) {
public Map<String, Object> undoApproval(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVAL_UNDO,
LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>();
final LoanStatus currentStatus = LoanStatus.fromInt(this.loanStatus);
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVAL_UNDO, currentStatus);
if (!statusEnum.hasStateOf(currentStatus)) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
}
this.approvedOnDate = null;
actualChanges.put("approvedOnDate", "");
this.loanOfficerHistory.clear();
return actualChanges;
}
public void disburseWithModifiedRepaymentSchedule(final LocalDate disbursedOn,
final List<LoanRepaymentScheduleInstallment> modifiedLoanRepaymentSchedule, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.repaymentScheduleInstallments.clear();
for (LoanRepaymentScheduleInstallment modifiedInstallment : modifiedLoanRepaymentSchedule) {
modifiedInstallment.updateLoan(this);
this.repaymentScheduleInstallments.add(modifiedInstallment);
}
disburse(disbursedOn, loanLifecycleStateMachine, true);
private void disburse(final LocalDate expectedDisbursedOnLocalDate) {
this.disbursedOnDate = expectedDisbursedOnLocalDate.toDate();
this.expectedMaturityDate = determineExpectedMaturityDate().toDate();
handleDisbursementTransaction(expectedDisbursedOnLocalDate);
}
public void disburse(final LocalDate disbursedOn, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final boolean statusTransition) {
public Map<String, Object> disburse(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final ApplicationCurrency currency) {
if (statusTransition) {
// ensure loan is in pre-disbursal state
updateLoanToPreDisbursalState();
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>();
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, LoanStatus.fromInt(this.loanStatus));
updateLoanToPreDisbursalState();
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, LoanStatus.fromInt(this.loanStatus));
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
}
this.disbursedOnDate = disbursedOn.toDateTimeAtCurrentTime().toDate();
this.expectedMaturityDate = this.repaymentScheduleInstallments.get(this.repaymentScheduleInstallments.size() - 1).getDueDate()
.toDateMidnight().toDate();
LocalDate disbursedOn = command.localDateValueOfParameterNamed("disbursedOnDate");
if (disbursedOn == null) {
disbursedOn = command.localDateValueOfParameterNamed("eventDate");
}
// add disbursement transaction to track outgoing money from mfi to
// client
LoanTransaction loanTransaction = LoanTransaction.disbursement(this.loanRepaymentScheduleDetail.getPrincipal(), disbursedOn);
final Locale locale = new Locale(command.locale());
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
this.disbursedOnDate = disbursedOn.toDate();
this.expectedMaturityDate = determineExpectedMaturityDate().toDate();
actualChanges.put("locale", command.locale());
actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("disbursedOnDate", disbursedOn.toString(fmt));
actualChanges.put("expectedMaturityDate", LocalDate.fromDateFields(this.expectedDisbursedOnDate).toString(fmt));
handleDisbursementTransaction(disbursedOn);
if (isRepaymentScheduleRegenerationRequiredForDisbursement(disbursedOn)) {
regenerateRepaymentSchedule(currency);
}
return actualChanges;
}
/*
* Ability to regenerate the repayment schedule based on the loans current
* details/state.
*/
private void regenerateRepaymentSchedule(final ApplicationCurrency applicationCurrency) {
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = new DefaultLoanScheduleGeneratorFactory().create(interestMethod);
final BigDecimal principal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount();
final BigDecimal inArrearsTolerance = this.loanRepaymentScheduleDetail.getInArrearsTolerance().getAmount();
final BigDecimal interestRatePerPeriod = this.loanRepaymentScheduleDetail.getNominalInterestRatePerPeriod();
final PeriodFrequencyType interestRatePeriodFrequencyType = this.loanRepaymentScheduleDetail.getInterestPeriodFrequencyType();
final BigDecimal defaultAnnualNominalInterestRate = this.loanRepaymentScheduleDetail.getAnnualNominalInterestRate();
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = this.loanRepaymentScheduleDetail
.getInterestCalculationPeriodMethod();
final Integer repaymentEvery = this.loanRepaymentScheduleDetail.getRepayEvery();
final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType();
final Integer numberOfRepayments = this.loanRepaymentScheduleDetail.getNumberOfRepayments();
final AmortizationMethod amortizationMethod = this.loanRepaymentScheduleDetail.getAmortizationMethod();
final Integer loanTermFrequency = this.termFrequency;
final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType);
final LoanSchedule loanSchedule = new LoanSchedule(loanScheduleGenerator, applicationCurrency, principal, interestRatePerPeriod,
interestRatePeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod,
repaymentEvery, repaymentPeriodFrequencyType, numberOfRepayments, amortizationMethod, loanTermFrequency,
loanTermPeriodFrequencyType, this.charges, this.getDisbursementDate(), this.getExpectedFirstRepaymentOnDate(),
this.getInterestChargedFromDate(), inArrearsTolerance);
final LoanScheduleData generatedData = loanSchedule.generate();
updateLoanSchedule(generatedData);
}
private void handleDisbursementTransaction(LocalDate disbursedOn) {
// track disbursement transaction
final LoanTransaction loanTransaction = LoanTransaction.disbursement(this.loanRepaymentScheduleDetail.getPrincipal(), disbursedOn);
loanTransaction.updateLoan(this);
this.loanTransactions.add(loanTransaction);
@@ -769,7 +1001,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
}
if (disbursedOn.isBefore(getApprovedOnDate())) {
if (getApprovedOnDate() != null && disbursedOn.isBefore(getApprovedOnDate())) {
final String errorMessage = "The date on which a loan is disbursed cannot be before its approval date: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.before.approval.date", errorMessage, disbursedOn,
@@ -790,13 +1022,23 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
}
public void undoDisbursal(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
public Map<String, Object> undoDisbursal(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSAL_UNDO,
LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>();
final LoanStatus currentStatus = LoanStatus.fromInt(this.loanStatus);
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSAL_UNDO, currentStatus);
if (!statusEnum.hasStateOf(currentStatus)) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
}
this.approvedOnDate = null;
actualChanges.put("disbursedOnDate", "");
updateLoanToPreDisbursalState();
return actualChanges;
}
private void updateLoanToPreDisbursalState() {
@@ -1489,14 +1731,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
public boolean isRepaymentScheduleRegenerationRequiredForDisbursement(final LocalDate actualDisbursementDate) {
boolean regenerationRequired = false;
if (isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate)) {
regenerationRequired = true;
}
return regenerationRequired;
return isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate);
}
private Money getTotalPaidInRepayments() {
@@ -1655,5 +1890,4 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
public Client getClient() {
return client;
}
}
@@ -86,6 +86,14 @@ public class LoanCharge extends AbstractPersistable<Long> {
return new LoanCharge(chargeDefinition, command, loanPrincipal);
}
/*
* loanPrincipal is required for charges that are percentage based
*/
public static LoanCharge createNewWithoutLoan(final Charge chargeDefinition, final BigDecimal loanPrincipal, final BigDecimal amount,
final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate specifiedDueDate) {
return new LoanCharge(chargeDefinition, loanPrincipal, amount, chargeTime, chargeCalculation, specifiedDueDate);
}
public static LoanCharge createNew(final Charge chargeDefinition) {
return new LoanCharge(null, chargeDefinition);
}
@@ -94,6 +102,45 @@ public class LoanCharge extends AbstractPersistable<Long> {
//
}
public LoanCharge(final Charge chargeDefinition, final BigDecimal loanPrincipal, final BigDecimal amount,
final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate specifiedDueDate) {
this.loan = null;
this.charge = chargeDefinition;
this.penaltyCharge = chargeDefinition.isPenalty();
this.chargeTime = chargeDefinition.getChargeTime();
if (chargeTime != null) {
this.chargeTime = chargeTime.getValue();
}
if (ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.SPECIFIED_DUE_DATE)) {
if (specifiedDueDate == null) {
final String defaultUserMessage = "Loan charge is missing specified due date";
throw new LoanChargeWithoutMandatoryFieldException("loancharge", "specifiedDueDate", defaultUserMessage,
chargeDefinition.getId(), chargeDefinition.getName());
}
this.dueForCollectionAsOfDate = specifiedDueDate.toDate();
} else {
this.dueForCollectionAsOfDate = null;
}
this.chargeCalculation = chargeDefinition.getChargeCalculation();
if (chargeCalculation != null) {
this.chargeCalculation = chargeCalculation.getValue();
}
BigDecimal chargeAmount = chargeDefinition.getAmount();
if (amount != null) {
chargeAmount = amount;
}
populateDerivedFields(loanPrincipal, chargeAmount);
this.paid = determineIfFullyPaid();
}
@Deprecated
public LoanCharge(final Charge chargeDefinition, final LoanChargeCommand command, final BigDecimal loanPrincipal) {
this.loan = null;
this.charge = chargeDefinition;
@@ -133,6 +180,7 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.paid = determineIfFullyPaid();
}
@Deprecated
private LoanCharge(final Loan loan, final Charge chargeDefinition, final LoanChargeCommand command) {
this.loan = loan;
this.charge = chargeDefinition;
@@ -270,6 +318,40 @@ public class LoanCharge extends AbstractPersistable<Long> {
public void update(final Loan loan) {
this.loan = loan;
}
public void update(final BigDecimal amount, final LocalDate specifiedDueDate, final BigDecimal loanPrincipal) {
if (specifiedDueDate != null) {
this.dueForCollectionAsOfDate = specifiedDueDate.toDate();
}
if (amount != null) {
switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
case INVALID:
break;
case FLAT:
this.amount = amount;
break;
case PERCENT_OF_AMOUNT:
this.percentage = amount;
this.amountPercentageAppliedTo = loanPrincipal;
this.amount = percentageOf(this.amountPercentageAppliedTo, this.percentage);
this.amountOutstanding = calculateOutstanding();
break;
case PERCENT_OF_AMOUNT_AND_INTEREST:
this.percentage = amount;
this.amount = null;
this.amountPercentageAppliedTo = null;
this.amountOutstanding = null;
break;
case PERCENT_OF_INTEREST:
this.percentage = amount;
this.amount = null;
this.amountPercentageAppliedTo = null;
this.amountOutstanding = null;
break;
}
}
}
public void update(final LoanChargeCommand command, final BigDecimal loanPrincipal) {
@@ -4,106 +4,106 @@ package org.mifosplatform.portfolio.loanaccount.domain;
* Enum representation of loan status states.
*/
public enum LoanStatus {
INVALID(0, "loanStatusType.invalid"), //
SUBMITED_AND_PENDING_APPROVAL(100, "loanStatusType.submitted.and.pending.approval"), //
APPROVED(200, "loanStatusType.approved"), //
ACTIVE(300, "loanStatusType.active"), //
WITHDRAWN_BY_CLIENT(400, "loanStatusType.withdrawn.by.client"), //
REJECTED(500, "loanStatusType.rejected"), //
CLOSED_OBLIGATIONS_MET(600, "loanStatusType.closed.obligations.met"), //
CLOSED_WRITTEN_OFF(601, "loanStatusType.closed.written.off"), //
CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT(602, "loanStatusType.closed.reschedule.outstanding.amount"), //
OVERPAID(700, "loanStatusType.overpaid");
INVALID(0, "loanStatusType.invalid"), //
SUBMITED_AND_PENDING_APPROVAL(100, "loanStatusType.submitted.and.pending.approval"), //
APPROVED(200, "loanStatusType.approved"), //
ACTIVE(300, "loanStatusType.active"), //
WITHDRAWN_BY_CLIENT(400, "loanStatusType.withdrawn.by.client"), //
REJECTED(500, "loanStatusType.rejected"), //
CLOSED_OBLIGATIONS_MET(600, "loanStatusType.closed.obligations.met"), //
CLOSED_WRITTEN_OFF(601, "loanStatusType.closed.written.off"), //
CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT(602, "loanStatusType.closed.reschedule.outstanding.amount"), //
OVERPAID(700, "loanStatusType.overpaid");
private final Integer value;
private final String code;
public static LoanStatus fromInt(final Integer statusValue) {
private final String code;
LoanStatus enumeration = LoanStatus.INVALID;
switch (statusValue) {
case 100:
enumeration = LoanStatus.SUBMITED_AND_PENDING_APPROVAL;
break;
case 200:
enumeration = LoanStatus.APPROVED;
break;
case 300:
enumeration = LoanStatus.ACTIVE;
break;
case 400:
enumeration = LoanStatus.WITHDRAWN_BY_CLIENT;
break;
case 500:
enumeration = LoanStatus.REJECTED;
break;
case 600:
enumeration = LoanStatus.CLOSED_OBLIGATIONS_MET;
break;
case 601:
enumeration = LoanStatus.CLOSED_WRITTEN_OFF;
break;
case 602:
enumeration = LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT;
break;
case 700:
enumeration = LoanStatus.OVERPAID;
break;
}
return enumeration;
public static LoanStatus fromInt(final Integer statusValue) {
LoanStatus enumeration = LoanStatus.INVALID;
switch (statusValue) {
case 100:
enumeration = LoanStatus.SUBMITED_AND_PENDING_APPROVAL;
break;
case 200:
enumeration = LoanStatus.APPROVED;
break;
case 300:
enumeration = LoanStatus.ACTIVE;
break;
case 400:
enumeration = LoanStatus.WITHDRAWN_BY_CLIENT;
break;
case 500:
enumeration = LoanStatus.REJECTED;
break;
case 600:
enumeration = LoanStatus.CLOSED_OBLIGATIONS_MET;
break;
case 601:
enumeration = LoanStatus.CLOSED_WRITTEN_OFF;
break;
case 602:
enumeration = LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT;
break;
case 700:
enumeration = LoanStatus.OVERPAID;
break;
}
return enumeration;
}
private LoanStatus(final Integer value, final String code) {
this.value = value;
this.code = code;
this.code = code;
}
public boolean hasStateOf(final LoanStatus state) {
return this.value.equals(state.getValue());
}
return this.value.equals(state.getValue());
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return code;
}
return code;
}
public boolean isSubmittedAndPendingApproval() {
return this.value.equals(LoanStatus.SUBMITED_AND_PENDING_APPROVAL.getValue());
}
public boolean isApproved() {
return this.value.equals(LoanStatus.APPROVED.getValue());
}
public boolean isActive() {
return this.value.equals(LoanStatus.ACTIVE.getValue());
}
public boolean isSubmittedAndPendingApproval() {
return this.value.equals(LoanStatus.SUBMITED_AND_PENDING_APPROVAL.getValue());
}
public boolean isClosed() {
return isClosedObligationsMet() || isClosedWrittenOff() || isClosedWithOutsandingAmountMarkedForReschedule();
}
public boolean isClosedObligationsMet() {
return this.value.equals(LoanStatus.CLOSED_OBLIGATIONS_MET.getValue());
}
public boolean isClosedWrittenOff() {
return this.value.equals(LoanStatus.CLOSED_WRITTEN_OFF.getValue());
}
public boolean isApproved() {
return this.value.equals(LoanStatus.APPROVED.getValue());
}
public boolean isClosedWithOutsandingAmountMarkedForReschedule() {
return this.value.equals(LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT.getValue());
}
public boolean isWithdrawnByClient() {
return this.value.equals(LoanStatus.WITHDRAWN_BY_CLIENT.getValue());
}
public boolean isActive() {
return this.value.equals(LoanStatus.ACTIVE.getValue());
}
public boolean isRejected() {
return this.value.equals(LoanStatus.REJECTED.getValue());
}
public boolean isClosed() {
return isClosedObligationsMet() || isClosedWrittenOff() || isClosedWithOutsandingAmountMarkedForReschedule();
}
public boolean isClosedObligationsMet() {
return this.value.equals(LoanStatus.CLOSED_OBLIGATIONS_MET.getValue());
}
public boolean isClosedWrittenOff() {
return this.value.equals(LoanStatus.CLOSED_WRITTEN_OFF.getValue());
}
public boolean isClosedWithOutsandingAmountMarkedForReschedule() {
return this.value.equals(LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT.getValue());
}
public boolean isWithdrawnByClient() {
return this.value.equals(LoanStatus.WITHDRAWN_BY_CLIENT.getValue());
}
public boolean isRejected() {
return this.value.equals(LoanStatus.REJECTED.getValue());
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ApplicantWithdrawsFromLoanApplicationCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public ApplicantWithdrawsFromLoanApplicationCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public EntityIdentifier processCommand(final JsonCommand command) {
return this.writePlatformService.applicantWithdrawsFromLoanApplication(command.resourceId(), command);
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ApproveLoanApplicationCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public ApproveLoanApplicationCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public EntityIdentifier processCommand(final JsonCommand command) {
return this.writePlatformService.approveLoanApplication(command.resourceId(), command);
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CreateLoanApplicationCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public CreateLoanApplicationCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public EntityIdentifier processCommand(final JsonCommand command) {
return this.writePlatformService.submitLoanApplication(command);
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeleteLoanApplicationCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public DeleteLoanApplicationCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public EntityIdentifier processCommand(final JsonCommand command) {
return this.writePlatformService.deleteLoan(command.resourceId());
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DisburseLoanCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public DisburseLoanCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public EntityIdentifier processCommand(final JsonCommand command) {
return this.writePlatformService.disburseLoan(command.resourceId(), command);
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class RejectLoanApplicationCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public RejectLoanApplicationCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public EntityIdentifier processCommand(final JsonCommand command) {
return this.writePlatformService.rejectLoanApplication(command.resourceId(), command);
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UndoApprovalLoanApplicationCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public UndoApprovalLoanApplicationCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public EntityIdentifier processCommand(final JsonCommand command) {
return this.writePlatformService.undoLoanApplicationApproval(command.resourceId(), command);
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UndoDisbursalLoanCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public UndoDisbursalLoanCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public EntityIdentifier processCommand(final JsonCommand command) {
return this.writePlatformService.undoLoanDisbursal(command.resourceId(), command);
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UpdateLoanApplicationCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public UpdateLoanApplicationCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public EntityIdentifier processCommand(final JsonCommand command) {
return this.writePlatformService.modifyLoanApplication(command.resourceId(), command);
}
}
@@ -1,125 +0,0 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.command;
import java.math.BigDecimal;
import org.joda.time.LocalDate;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
/**
* Immutable command used when auto-calculating loan schedules based on loan terms.
*/
public class CalculateLoanScheduleCommand {
private final Long productId;
private final BigDecimal principal;
private final Integer loanTermFrequency;
private final Integer loanTermFrequencyType;
private final Integer repaymentEvery;
private final Integer repaymentFrequencyType;
private final Integer numberOfRepayments;
private final BigDecimal interestRatePerPeriod;
private final Integer interestRateFrequencyType;
private final Integer amortizationType;
private final Integer interestType;
private final Integer interestCalculationPeriodType;
private final LocalDate expectedDisbursementDate;
private final LocalDate repaymentsStartingFromDate;
private final LocalDate interestChargedFromDate;
private final LoanChargeCommand[] charges;
public CalculateLoanScheduleCommand(
final Long productId,
final BigDecimal principal,
final BigDecimal interestRatePerPeriod, Integer interestRateFrequencyMethod, final Integer interestMethod, final Integer interestCalculationPeriodMethod,
final Integer repaymentEvery, final Integer repaymentFrequency, final Integer numberOfRepayments, Integer amortizationMethod,
final Integer loanTermFrequency, final Integer loanTermFrequencyType,
final LocalDate expectedDisbursementDate,
final LocalDate repaymentsStartingFromDate, final LocalDate interestCalculatedFromDate, final LoanChargeCommand[] charges) {
this.productId = productId;
this.principal = principal;
this.interestRatePerPeriod = interestRatePerPeriod;
this.interestRateFrequencyType = interestRateFrequencyMethod;
this.interestType = interestMethod;
this.interestCalculationPeriodType = interestCalculationPeriodMethod;
this.repaymentEvery = repaymentEvery;
this.repaymentFrequencyType = repaymentFrequency;
this.numberOfRepayments = numberOfRepayments;
this.amortizationType = amortizationMethod;
this.loanTermFrequency = loanTermFrequency;
this.loanTermFrequencyType = loanTermFrequencyType;
this.expectedDisbursementDate = expectedDisbursementDate;
this.repaymentsStartingFromDate = repaymentsStartingFromDate;
this.interestChargedFromDate= interestCalculatedFromDate;
this.charges = charges;
}
public Long getProductId() {
return productId;
}
public BigDecimal getPrincipal() {
return principal;
}
public Integer getLoanTermFrequency() {
return loanTermFrequency;
}
public Integer getLoanTermFrequencyType() {
return loanTermFrequencyType;
}
public Integer getRepaymentEvery() {
return repaymentEvery;
}
public Integer getRepaymentFrequencyType() {
return repaymentFrequencyType;
}
public Integer getNumberOfRepayments() {
return numberOfRepayments;
}
public BigDecimal getInterestRatePerPeriod() {
return interestRatePerPeriod;
}
public Integer getInterestRateFrequencyType() {
return interestRateFrequencyType;
}
public Integer getAmortizationType() {
return amortizationType;
}
public Integer getInterestType() {
return interestType;
}
public Integer getInterestCalculationPeriodType() {
return interestCalculationPeriodType;
}
public LocalDate getExpectedDisbursementDate() {
return expectedDisbursementDate;
}
public LocalDate getRepaymentsStartingFromDate() {
return repaymentsStartingFromDate;
}
public LocalDate getInterestChargedFromDate() {
return interestChargedFromDate;
}
public LoanChargeCommand[] getCharges() {
return charges;
}
}
@@ -1,101 +0,0 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.command;
import java.util.ArrayList;
import java.util.List;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
public class CalculateLoanScheduleCommandValidator {
private final CalculateLoanScheduleCommand command;
public CalculateLoanScheduleCommandValidator(CalculateLoanScheduleCommand command) {
this.command = command;
}
public void validate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
baseDataValidator.reset().parameter("principal").value(command.getPrincipal()).notNull().positiveAmount();
baseDataValidator.reset().parameter("loanTermFrequency").value(command.getLoanTermFrequency()).notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("loanTermFrequencyType").value(command.getLoanTermFrequencyType()).notNull().inMinMaxRange(0, 4);
baseDataValidator.reset().parameter("numberOfRepayments").value(command.getNumberOfRepayments()).notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("repaymentEvery").value(command.getRepaymentEvery()).notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("repaymentFrequencyType").value(command.getRepaymentFrequencyType()).notNull().inMinMaxRange(0, 3);
// FIXME - this constraint doesnt really need to be here. should be possible to express loan term as say 12 months whilst also saying
// - that the repayment structure is 6 repayments every bi-monthly.
if (command.getLoanTermFrequencyType() != null && !command.getLoanTermFrequencyType().equals(command.getRepaymentFrequencyType())) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.loanTermFrequencyType.not.the.same.as.repaymentFrequencyType",
"The parameters loanTermFrequencyType and repaymentFrequencyType must be the same.", "loanTermFrequencyType",
command.getLoanTermFrequencyType(), command.getRepaymentFrequencyType());
dataValidationErrors.add(error);
} else {
if (command.getLoanTermFrequency() != null && command.getRepaymentEvery() != null && command.getNumberOfRepayments() != null) {
int suggestsedLoanTerm = command.getRepaymentEvery() * command.getNumberOfRepayments();
if (command.getLoanTermFrequency().intValue() < suggestsedLoanTerm) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.loanTermFrequency.less.than.repayment.structure.suggests",
"The parameter loanTermFrequency is less than the suggest loan term as indicated by numberOfRepayments and repaymentEvery.", "loanTermFrequency",
command.getLoanTermFrequency(), command.getNumberOfRepayments(), command.getRepaymentEvery());
dataValidationErrors.add(error);
}
}
}
baseDataValidator.reset().parameter("interestRatePerPeriod").value(command.getInterestRatePerPeriod()).notNull();
baseDataValidator.reset().parameter("interestRateFrequencyType").value(command.getInterestRateFrequencyType()).notNull().inMinMaxRange(0, 3);
baseDataValidator.reset().parameter("amortizationType").value(command.getAmortizationType()).notNull().inMinMaxRange(0, 1);
baseDataValidator.reset().parameter("interestType").value(command.getInterestType()).notNull().inMinMaxRange(0, 1);
baseDataValidator.reset().parameter("interestCalculationPeriodType").value(command.getInterestCalculationPeriodType()).notNull().inMinMaxRange(0, 1);
baseDataValidator.reset().parameter("expectedDisbursementDate").value(command.getExpectedDisbursementDate()).notNull();
if (command.getExpectedDisbursementDate() != null) {
if (command.getRepaymentsStartingFromDate() != null
&& command.getExpectedDisbursementDate().isAfter(command.getRepaymentsStartingFromDate())) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.expectedDisbursementDate.cannot.be.after.first.repayment.date",
"The parameter expectedDisbursementDate has a date which falls after the given first repayment date.", "expectedDisbursementDate",
command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate());
dataValidationErrors.add(error);
}
}
if (command.getRepaymentsStartingFromDate() != null && command.getInterestChargedFromDate() == null) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.interestCalculatedFromDate.must.be.entered.when.using.repayments.startfrom.field",
"The parameter interestCalculatedFromDate cannot be empty when first repayment date is provided.", "interestChargedFromDate", command.getRepaymentsStartingFromDate());
dataValidationErrors.add(error);
} else if (command.getRepaymentsStartingFromDate() == null && command.getInterestChargedFromDate() != null) {
// validate interestCalculatedFromDate is after or on repaymentsStartingFromDate
if (command.getExpectedDisbursementDate().isAfter(command.getInterestChargedFromDate())) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.interestChargedFromDate.cannot.be.before.disbursement.date",
"The parameter interestCalculatedFromDate cannot be before the date given for expected disbursement.", "interestChargedFromDate", command.getInterestChargedFromDate(), command.getExpectedDisbursementDate());
dataValidationErrors.add(error);
}
}
// if (command.getCharges() != null) {
// for (LoanChargeCommand loanCharge : command.getCharges()) {
// try {
// LoanChargeCommandValidator validator = new LoanChargeCommandValidator(loanCharge);
// validator.validateForCreate();
// } catch (PlatformApiDataValidationException e) {
// dataValidationErrors.addAll(e.getErrors());
// }
// }
// }
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors);
}
}
}
@@ -5,301 +5,284 @@ import java.util.Collection;
import org.mifosplatform.organisation.monetary.data.CurrencyData;
/**
* Immutable data object to represent aspects of a loan schedule such as:
*
* <ul>
* <li>summary information - the totals for each part of schedule monitored</li>
* <li>repayment schedule - the principal due, outstanding balance and cost of loan items such as interest and charges</li>
* <li>summary information - the totals for each part of schedule monitored</li>
* <li>repayment schedule - the principal due, outstanding balance and cost of
* loan items such as interest and charges</li>
* </ul>
*/
public class LoanScheduleData {
@SuppressWarnings("unused")
private final Integer loanTermInDays;
/**
* The currency associated with all monetary values in loan schedule.
*/
@SuppressWarnings("unused")
private final CurrencyData currency;
/**
* The cumulative total of all money (principal) disbursed to the loan applicant.
*/
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalDisbursed;
/**
* The cumulative total of all <code>principalDue</code> for each period of loan schedule. (originalPrincipal)
*/
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalDue;
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalOutstanding;
/**
* The cumulative total of interest expected on any principal disbursed. (originalInterest)
*/
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestExpected;
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestWaived;
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestOutstanding;
/**
* The cumulative total of all fee charges applied on the loan to date.
*/
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesToDate;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesWaived;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesOutstanding;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesToDate;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesWaived;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesOutstanding;
/**
* The cumulative total of all costs on the loan.
* Costs tracked are:
* <ul>
* <li>Interest</li>
* <li>Charges (fees and penalties)</li>
* </ul>
*/
@SuppressWarnings("unused")
private final BigDecimal totalCostOfLoan;
/**
* The sum of <code>principalDisbursed</code> and <code>totalCostOfLoan</code>. (originalTotal)
*/
@SuppressWarnings("unused")
private final BigDecimal totalExpectedRepayment;
/**
* The cumulative sum of all repayments to date. (totalPaid)
*/
@SuppressWarnings("unused")
private final BigDecimal totalPaidToDate;
/**
* The cumulative sum of all waivers to date.
*/
@SuppressWarnings("unused")
private final BigDecimal totalWaivedToDate;
/**
* The cumulative sum of all waivers to date.
*/
@SuppressWarnings("unused")
private final BigDecimal totalWrittenOffToDate;
/**
* The cumulative sum of all principal, interest & charges outstanding.
*/
private final BigDecimal totalOutstanding;
/**
* The amount by which this loan is is arrears.
*
* Different to outstanding as arrears is based on whether a portion of the total outstanding is late and above any tolerance setting.
*/
@SuppressWarnings("unused")
private final BigDecimal totalInArrears;
/**
* <code>periods</code> is collection of data objects containing specific
* information to each period of the loan schedule including disbursement and
* repayment information.
*/
private final Collection<LoanSchedulePeriodData> periods;
@SuppressWarnings("unused")
private final Integer loanTermInDays;
public LoanScheduleData() {
this.cumulativePrincipalDisbursed = null;
this.cumulativePrincipalPaid = null;
this.cumulativePrincipalWrittenOff = null;
this.cumulativePrincipalDue = null;
this.cumulativePrincipalOutstanding = null;
this.cumulativeInterestExpected = null;
this.cumulativeInterestPaid = null;
this.cumulativeInterestWaived = null;
this.cumulativeInterestWrittenOff = null;
this.cumulativeInterestOutstanding = null;
this.cumulativeFeeChargesToDate = null;
this.cumulativeFeeChargesPaid = null;
this.cumulativeFeeChargesWaived = null;
this.cumulativeFeeChargesWrittenOff = null;
this.cumulativeFeeChargesOutstanding = null;
this.cumulativePenaltyChargesToDate = null;
this.cumulativePenaltyChargesPaid = null;
this.cumulativePenaltyChargesWaived = null;
this.cumulativePenaltyChargesWrittenOff = null;
this.cumulativePenaltyChargesOutstanding = null;
this.totalCostOfLoan = null;
this.totalExpectedRepayment = null;
this.totalPaidToDate = null;
this.totalWaivedToDate = null;
this.totalWrittenOffToDate = null;
this.totalOutstanding = null;
this.totalInArrears = null;
this.currency = null;
this.periods = null;
this.loanTermInDays = null;
}
/**
* The currency associated with all monetary values in loan schedule.
*/
@SuppressWarnings("unused")
private final CurrencyData currency;
public LoanScheduleData(
final CurrencyData currency,
final Collection<LoanSchedulePeriodData> periods,
final Integer loanTermInDays,
final BigDecimal cumulativePrincipalDisbursed,
final BigDecimal cumulativePrincipalDue,
final BigDecimal cumulativePrincipalOutstanding,
final BigDecimal cumulativeInterestExpected,
final BigDecimal cumulativeFeeChargesToDate,
final BigDecimal totalExpectedRepayment) {
this.currency = currency;
this.periods = periods;
this.loanTermInDays = loanTermInDays;
this.cumulativePrincipalDisbursed = cumulativePrincipalDisbursed;
this.cumulativePrincipalDue = cumulativePrincipalDue;
this.cumulativePrincipalPaid = null;
this.cumulativePrincipalWrittenOff = null;
this.cumulativePrincipalOutstanding = cumulativePrincipalOutstanding;
this.cumulativeInterestExpected = cumulativeInterestExpected;
this.cumulativeInterestPaid = null;
this.cumulativeInterestWaived = null;
this.cumulativeInterestWrittenOff = null;
this.cumulativeInterestOutstanding = null;
this.cumulativeFeeChargesToDate = cumulativeFeeChargesToDate;
this.cumulativeFeeChargesPaid = null;
this.cumulativeFeeChargesWaived = null;
this.cumulativeFeeChargesWrittenOff = null;
this.cumulativeFeeChargesOutstanding = null;
this.cumulativePenaltyChargesToDate = null;
this.cumulativePenaltyChargesPaid = null;
this.cumulativePenaltyChargesWaived = null;
this.cumulativePenaltyChargesWrittenOff = null;
this.cumulativePenaltyChargesOutstanding = null;
if (cumulativeFeeChargesToDate != null) {
this.totalCostOfLoan = cumulativeInterestExpected.add(cumulativeFeeChargesToDate);
} else if (cumulativeInterestExpected != null) {
this.totalCostOfLoan = cumulativeInterestExpected;
} else {
this.totalCostOfLoan = null;
}
this.totalExpectedRepayment = totalExpectedRepayment;
this.totalPaidToDate = BigDecimal.ZERO;
this.totalWaivedToDate = BigDecimal.ZERO;
this.totalWrittenOffToDate = BigDecimal.ZERO;
this.totalOutstanding = totalExpectedRepayment;
this.totalInArrears = BigDecimal.ZERO;
}
/**
* The cumulative total of all money (principal) disbursed to the loan
* applicant.
*/
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalDisbursed;
/*
* Used when fully populating
*/
public LoanScheduleData(final CurrencyData currency, final Collection<LoanSchedulePeriodData> periods,
final Integer loanTermInDays,
final BigDecimal cumulativePrincipalDisbursed,
final BigDecimal cumulativePrincipalDue,
final BigDecimal cumulativePrincipalPaid,
final BigDecimal cumulativePrincipalWrittenOff,
final BigDecimal cumulativePrincipalOutstanding,
final BigDecimal cumulativeInterestExpected,
final BigDecimal cumulativeInterestPaid,
final BigDecimal cumulativeInterestWaived,
final BigDecimal cumulativeInterestWrittenOff,
final BigDecimal cumulativeInterestOutstanding,
final BigDecimal cumulativeFeeChargesToDate,
final BigDecimal cumulativeFeeChargesPaid,
final BigDecimal cumulativeFeeChargesWaived,
final BigDecimal cumulativeFeeChargesWrittenOff,
final BigDecimal cumulativeFeeChargesOutstanding,
final BigDecimal cumulativePenaltyChargesToDate,
final BigDecimal cumulativePenaltyChargesPaid,
final BigDecimal cumulativePenaltyChargesWaived,
final BigDecimal cumulativePenaltyChargesWrittenOff,
final BigDecimal cumulativePenaltyChargesOutstanding,
final BigDecimal totalCostOfLoan,
final BigDecimal totalExpectedRepayment,
final BigDecimal totalPaidToDate,
final BigDecimal totalWaivedToDate,
final BigDecimal totalWrittenOffToDate,
final BigDecimal totalOutstanding,
final BigDecimal totalInArrears) {
this.currency = currency;
this.periods = periods;
this.loanTermInDays = loanTermInDays;
this.cumulativePrincipalDisbursed = cumulativePrincipalDisbursed;
this.cumulativePrincipalDue = cumulativePrincipalDue;
this.cumulativePrincipalPaid = cumulativePrincipalPaid;
this.cumulativePrincipalWrittenOff = cumulativePrincipalWrittenOff;
this.cumulativePrincipalOutstanding = cumulativePrincipalOutstanding;
this.cumulativeInterestExpected = cumulativeInterestExpected;
this.cumulativeInterestPaid = cumulativeInterestPaid;
this.cumulativeInterestWrittenOff = cumulativeInterestWrittenOff;
this.cumulativeInterestWaived = cumulativeInterestWaived;
this.cumulativeInterestOutstanding = cumulativeInterestOutstanding;
this.cumulativeFeeChargesToDate = cumulativeFeeChargesToDate;
this.cumulativeFeeChargesPaid = cumulativeFeeChargesPaid;
this.cumulativeFeeChargesWaived = cumulativeFeeChargesWaived;
this.cumulativeFeeChargesWrittenOff = cumulativeFeeChargesWrittenOff;
this.cumulativeFeeChargesOutstanding = cumulativeFeeChargesOutstanding;
this.cumulativePenaltyChargesToDate = cumulativePenaltyChargesToDate;
this.cumulativePenaltyChargesPaid = cumulativePenaltyChargesPaid;
this.cumulativePenaltyChargesWaived = cumulativePenaltyChargesWaived;
this.cumulativePenaltyChargesWrittenOff = cumulativePenaltyChargesWrittenOff;
this.cumulativePenaltyChargesOutstanding = cumulativePenaltyChargesOutstanding;
this.totalCostOfLoan = totalCostOfLoan;
this.totalExpectedRepayment = totalExpectedRepayment;
this.totalPaidToDate = totalPaidToDate;
this.totalWaivedToDate = totalWaivedToDate;
this.totalWrittenOffToDate = totalWrittenOffToDate;
this.totalOutstanding = totalOutstanding;
this.totalInArrears = totalInArrears;
}
/**
* The cumulative total of all <code>principalDue</code> for each period of
* loan schedule. (originalPrincipal)
*/
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalDue;
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativePrincipalOutstanding;
public Collection<LoanSchedulePeriodData> getPeriods() {
return periods;
}
/**
* The cumulative total of interest expected on any principal disbursed.
* (originalInterest)
*/
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestExpected;
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestWaived;
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativeInterestOutstanding;
public BigDecimal totalOutstanding() {
return this.totalOutstanding;
}
/**
* The cumulative total of all fee charges applied on the loan to date.
*/
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesToDate;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesWaived;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesOutstanding;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesToDate;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesWaived;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesOutstanding;
/**
* The cumulative total of all costs on the loan. Costs tracked are:
* <ul>
* <li>Interest</li>
* <li>Charges (fees and penalties)</li>
* </ul>
*/
@SuppressWarnings("unused")
private final BigDecimal totalCostOfLoan;
/**
* The sum of <code>principalDisbursed</code> and
* <code>totalCostOfLoan</code>. (originalTotal)
*/
@SuppressWarnings("unused")
private final BigDecimal totalExpectedRepayment;
/**
* The cumulative sum of all repayments to date. (totalPaid)
*/
@SuppressWarnings("unused")
private final BigDecimal totalPaidToDate;
/**
* The cumulative sum of all waivers to date.
*/
@SuppressWarnings("unused")
private final BigDecimal totalWaivedToDate;
/**
* The cumulative sum of all waivers to date.
*/
@SuppressWarnings("unused")
private final BigDecimal totalWrittenOffToDate;
/**
* The cumulative sum of all principal, interest & charges outstanding.
*/
private final BigDecimal totalOutstanding;
/**
* The amount by which this loan is is arrears.
*
* Different to outstanding as arrears is based on whether a portion of the
* total outstanding is late and above any tolerance setting.
*/
@SuppressWarnings("unused")
private final BigDecimal totalInArrears;
/**
* <code>periods</code> is collection of data objects containing specific
* information to each period of the loan schedule including disbursement
* and repayment information.
*/
private final Collection<LoanSchedulePeriodData> periods;
public LoanScheduleData() {
this.cumulativePrincipalDisbursed = null;
this.cumulativePrincipalPaid = null;
this.cumulativePrincipalWrittenOff = null;
this.cumulativePrincipalDue = null;
this.cumulativePrincipalOutstanding = null;
this.cumulativeInterestExpected = null;
this.cumulativeInterestPaid = null;
this.cumulativeInterestWaived = null;
this.cumulativeInterestWrittenOff = null;
this.cumulativeInterestOutstanding = null;
this.cumulativeFeeChargesToDate = null;
this.cumulativeFeeChargesPaid = null;
this.cumulativeFeeChargesWaived = null;
this.cumulativeFeeChargesWrittenOff = null;
this.cumulativeFeeChargesOutstanding = null;
this.cumulativePenaltyChargesToDate = null;
this.cumulativePenaltyChargesPaid = null;
this.cumulativePenaltyChargesWaived = null;
this.cumulativePenaltyChargesWrittenOff = null;
this.cumulativePenaltyChargesOutstanding = null;
this.totalCostOfLoan = null;
this.totalExpectedRepayment = null;
this.totalPaidToDate = null;
this.totalWaivedToDate = null;
this.totalWrittenOffToDate = null;
this.totalOutstanding = null;
this.totalInArrears = null;
this.currency = null;
this.periods = null;
this.loanTermInDays = null;
}
public LoanScheduleData(final CurrencyData currency, final Collection<LoanSchedulePeriodData> periods, final Integer loanTermInDays,
final BigDecimal cumulativePrincipalDisbursed, final BigDecimal cumulativePrincipalDue,
final BigDecimal cumulativePrincipalOutstanding, final BigDecimal cumulativeInterestExpected,
final BigDecimal cumulativeFeeChargesToDate, final BigDecimal totalExpectedRepayment) {
this.currency = currency;
this.periods = periods;
this.loanTermInDays = loanTermInDays;
this.cumulativePrincipalDisbursed = cumulativePrincipalDisbursed;
this.cumulativePrincipalDue = cumulativePrincipalDue;
this.cumulativePrincipalPaid = null;
this.cumulativePrincipalWrittenOff = null;
this.cumulativePrincipalOutstanding = cumulativePrincipalOutstanding;
this.cumulativeInterestExpected = cumulativeInterestExpected;
this.cumulativeInterestPaid = null;
this.cumulativeInterestWaived = null;
this.cumulativeInterestWrittenOff = null;
this.cumulativeInterestOutstanding = null;
this.cumulativeFeeChargesToDate = cumulativeFeeChargesToDate;
this.cumulativeFeeChargesPaid = null;
this.cumulativeFeeChargesWaived = null;
this.cumulativeFeeChargesWrittenOff = null;
this.cumulativeFeeChargesOutstanding = null;
this.cumulativePenaltyChargesToDate = null;
this.cumulativePenaltyChargesPaid = null;
this.cumulativePenaltyChargesWaived = null;
this.cumulativePenaltyChargesWrittenOff = null;
this.cumulativePenaltyChargesOutstanding = null;
if (cumulativeFeeChargesToDate != null) {
this.totalCostOfLoan = cumulativeInterestExpected.add(cumulativeFeeChargesToDate);
} else if (cumulativeInterestExpected != null) {
this.totalCostOfLoan = cumulativeInterestExpected;
} else {
this.totalCostOfLoan = null;
}
this.totalExpectedRepayment = totalExpectedRepayment;
this.totalPaidToDate = BigDecimal.ZERO;
this.totalWaivedToDate = BigDecimal.ZERO;
this.totalWrittenOffToDate = BigDecimal.ZERO;
this.totalOutstanding = totalExpectedRepayment;
this.totalInArrears = BigDecimal.ZERO;
}
/*
* Used when fully populating
*/
public LoanScheduleData(final CurrencyData currency, final Collection<LoanSchedulePeriodData> periods, final Integer loanTermInDays,
final BigDecimal cumulativePrincipalDisbursed, final BigDecimal cumulativePrincipalDue,
final BigDecimal cumulativePrincipalPaid, final BigDecimal cumulativePrincipalWrittenOff,
final BigDecimal cumulativePrincipalOutstanding, final BigDecimal cumulativeInterestExpected,
final BigDecimal cumulativeInterestPaid, final BigDecimal cumulativeInterestWaived,
final BigDecimal cumulativeInterestWrittenOff, final BigDecimal cumulativeInterestOutstanding,
final BigDecimal cumulativeFeeChargesToDate, final BigDecimal cumulativeFeeChargesPaid,
final BigDecimal cumulativeFeeChargesWaived, final BigDecimal cumulativeFeeChargesWrittenOff,
final BigDecimal cumulativeFeeChargesOutstanding, final BigDecimal cumulativePenaltyChargesToDate,
final BigDecimal cumulativePenaltyChargesPaid, final BigDecimal cumulativePenaltyChargesWaived,
final BigDecimal cumulativePenaltyChargesWrittenOff, final BigDecimal cumulativePenaltyChargesOutstanding,
final BigDecimal totalCostOfLoan, final BigDecimal totalExpectedRepayment, final BigDecimal totalPaidToDate,
final BigDecimal totalWaivedToDate, final BigDecimal totalWrittenOffToDate, final BigDecimal totalOutstanding,
final BigDecimal totalInArrears) {
this.currency = currency;
this.periods = periods;
this.loanTermInDays = loanTermInDays;
this.cumulativePrincipalDisbursed = cumulativePrincipalDisbursed;
this.cumulativePrincipalDue = cumulativePrincipalDue;
this.cumulativePrincipalPaid = cumulativePrincipalPaid;
this.cumulativePrincipalWrittenOff = cumulativePrincipalWrittenOff;
this.cumulativePrincipalOutstanding = cumulativePrincipalOutstanding;
this.cumulativeInterestExpected = cumulativeInterestExpected;
this.cumulativeInterestPaid = cumulativeInterestPaid;
this.cumulativeInterestWrittenOff = cumulativeInterestWrittenOff;
this.cumulativeInterestWaived = cumulativeInterestWaived;
this.cumulativeInterestOutstanding = cumulativeInterestOutstanding;
this.cumulativeFeeChargesToDate = cumulativeFeeChargesToDate;
this.cumulativeFeeChargesPaid = cumulativeFeeChargesPaid;
this.cumulativeFeeChargesWaived = cumulativeFeeChargesWaived;
this.cumulativeFeeChargesWrittenOff = cumulativeFeeChargesWrittenOff;
this.cumulativeFeeChargesOutstanding = cumulativeFeeChargesOutstanding;
this.cumulativePenaltyChargesToDate = cumulativePenaltyChargesToDate;
this.cumulativePenaltyChargesPaid = cumulativePenaltyChargesPaid;
this.cumulativePenaltyChargesWaived = cumulativePenaltyChargesWaived;
this.cumulativePenaltyChargesWrittenOff = cumulativePenaltyChargesWrittenOff;
this.cumulativePenaltyChargesOutstanding = cumulativePenaltyChargesOutstanding;
this.totalCostOfLoan = totalCostOfLoan;
this.totalExpectedRepayment = totalExpectedRepayment;
this.totalPaidToDate = totalPaidToDate;
this.totalWaivedToDate = totalWaivedToDate;
this.totalWrittenOffToDate = totalWrittenOffToDate;
this.totalOutstanding = totalOutstanding;
this.totalInArrears = totalInArrears;
}
public Collection<LoanSchedulePeriodData> getPeriods() {
return periods;
}
public BigDecimal totalOutstanding() {
return this.totalOutstanding;
}
}
@@ -8,25 +8,25 @@ import org.springframework.stereotype.Component;
@Component
public class AprCalculator {
public BigDecimal calculateFrom(PeriodFrequencyType interestPeriodFrequencyType, BigDecimal interestRatePerPeriod) {
BigDecimal defaultAnnualNominalInterestRate = BigDecimal.ZERO;
switch (interestPeriodFrequencyType) {
case DAYS:
break;
case WEEKS:
defaultAnnualNominalInterestRate = interestRatePerPeriod.multiply(BigDecimal.valueOf(52));
break;
case MONTHS:
defaultAnnualNominalInterestRate = interestRatePerPeriod.multiply(BigDecimal.valueOf(12));
break;
case YEARS:
defaultAnnualNominalInterestRate = interestRatePerPeriod.multiply(BigDecimal.valueOf(1));
break;
case INVALID:
break;
}
return defaultAnnualNominalInterestRate;
}
public BigDecimal calculateFrom(PeriodFrequencyType interestPeriodFrequencyType, BigDecimal interestRatePerPeriod) {
BigDecimal defaultAnnualNominalInterestRate = BigDecimal.ZERO;
switch (interestPeriodFrequencyType) {
case DAYS:
break;
case WEEKS:
defaultAnnualNominalInterestRate = interestRatePerPeriod.multiply(BigDecimal.valueOf(52));
break;
case MONTHS:
defaultAnnualNominalInterestRate = interestRatePerPeriod.multiply(BigDecimal.valueOf(12));
break;
case YEARS:
defaultAnnualNominalInterestRate = interestRatePerPeriod.multiply(BigDecimal.valueOf(1));
break;
case INVALID:
break;
}
return defaultAnnualNominalInterestRate;
}
}
@@ -14,55 +14,56 @@ import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
/**
* <p>
* Declining balance can amortized (see {@link AmortizationMethod}) in two ways at present:
* <ol><li>Equal principal payments</li><li>Equal installment payments</li></ol>
* Declining balance can be amortized (see {@link AmortizationMethod}) in two ways
* at present:
* <ol>
* <li>Equal principal payments</li>
* <li>Equal installment payments</li>
* </ol>
* </p>
*
* <p>
* When amortized using <i>equal principal payments</i>, the <b>principal component</b> of each installment is fixed and <b>interest due</b>
* is calculated from the <b>outstanding principal balance</b> resulting in a different <b>total payment due</b> for each installment.
* When amortized using <i>equal principal payments</i>, the <b>principal
* component</b> of each installment is fixed and <b>interest due</b> is
* calculated from the <b>outstanding principal balance</b> resulting in a
* different <b>total payment due</b> for each installment.
* </p>
*
* <p>
* When amortized using <i>equal installments</i>, the <b>total payment due</b> for each installment is fixed and
* is calculated using the excel like <code>pmt</code> function. The <b>interest due</b> is calculated from the <b>outstanding principal balance</b>
* which results in a <b>principal component</b> that is <b>total payment due</b> minus <b>interest due</b>.
* </p>
* When amortized using <i>equal installments</i>, the <b>total payment due</b>
* for each installment is fixed and is calculated using the excel like
* <code>pmt</code> function. The <b>interest due</b> is calculated from the
* <b>outstanding principal balance</b> which results in a <b>principal
* component</b> that is <b>total payment due</b> minus <b>interest due</b>.
* </p>
*/
public class DecliningBalanceMethodLoanScheduleGenerator implements LoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator();
private final AmortizationLoanScheduleGeneratorFactory amortizationLoanScheduleGeneratorFactory = new AmortizationLoanScheduleGeneratorFactory();
@Override
public LoanScheduleData generate(
final ApplicationCurrency applicationCurrency,
final LoanProductRelatedDetail loanScheduleInfo,
final Integer loanTermFrequency,
final PeriodFrequencyType loanTermFrequencyType,
final LocalDate disbursementDate,
final LocalDate firstRepaymentDate,
final LocalDate interestCalculatedFrom,
final Set<LoanCharge> loanCharges) {
// 1. generate valid set of 'due dates' based on some of the 'loan attributes'
final List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate);
final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate(loanScheduleInfo, scheduledDates);
// 2. determine the 'periodic' interest rate based on the 'repayment periods' so we can use
final BigDecimal periodInterestRateForRepaymentPeriod = this.periodicInterestRateCalculator.calculateFrom(loanScheduleInfo);
// Determine with 'amortisation' approach to use
final AmortizationLoanScheduleGenerator generator = this.amortizationLoanScheduleGeneratorFactory.createGenerator(loanScheduleInfo.getAmortizationMethod());
return generator.generate(applicationCurrency,
loanScheduleInfo,
disbursementDate,
interestCalculatedFrom,
periodInterestRateForRepaymentPeriod,
idealDisbursementDateBasedOnFirstRepaymentDate,
scheduledDates, loanCharges);
}
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator();
private final AmortizationLoanScheduleGeneratorFactory amortizationLoanScheduleGeneratorFactory = new AmortizationLoanScheduleGeneratorFactory();
@Override
public LoanScheduleData generate(final ApplicationCurrency applicationCurrency, final LoanProductRelatedDetail loanScheduleInfo,
final Integer loanTermFrequency, final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate,
final LocalDate firstRepaymentDate, final LocalDate interestCalculatedFrom, final Set<LoanCharge> loanCharges) {
// 1. generate valid set of 'due dates' based on some of the 'loan
// attributes'
final List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate);
final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate = this.scheduledDateGenerator
.idealDisbursementDateBasedOnFirstRepaymentDate(loanScheduleInfo, scheduledDates);
// 2. determine the 'periodic' interest rate based on the 'repayment
// periods' so we can use
final BigDecimal periodInterestRateForRepaymentPeriod = this.periodicInterestRateCalculator.calculateFrom(loanScheduleInfo);
// Determine with 'amortisation' approach to use
final AmortizationLoanScheduleGenerator generator = this.amortizationLoanScheduleGeneratorFactory.createGenerator(loanScheduleInfo
.getAmortizationMethod());
return generator.generate(applicationCurrency, loanScheduleInfo, disbursementDate, interestCalculatedFrom,
periodInterestRateForRepaymentPeriod, idealDisbursementDateBasedOnFirstRepaymentDate, scheduledDates, loanCharges);
}
}
@@ -1,26 +1,27 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.domain;
import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod;
import org.springframework.stereotype.Component;
public class DefaultLoanScheduleGeneratorFactory implements
LoanScheduleGeneratorFactory {
@Component
public class DefaultLoanScheduleGeneratorFactory implements LoanScheduleGeneratorFactory {
@Override
public LoanScheduleGenerator create(final InterestMethod interestMethod) {
@Override
public LoanScheduleGenerator create(final InterestMethod interestMethod) {
LoanScheduleGenerator loanScheduleGenerator = null;
LoanScheduleGenerator loanScheduleGenerator = null;
switch (interestMethod) {
case FLAT:
loanScheduleGenerator = new FlatMethodLoanScheduleGenerator();
break;
case DECLINING_BALANCE:
loanScheduleGenerator = new DecliningBalanceMethodLoanScheduleGenerator();
break;
case INVALID:
break;
}
return loanScheduleGenerator;
}
switch (interestMethod) {
case FLAT:
loanScheduleGenerator = new FlatMethodLoanScheduleGenerator();
break;
case DECLINING_BALANCE:
loanScheduleGenerator = new DecliningBalanceMethodLoanScheduleGenerator();
break;
case INVALID:
break;
}
return loanScheduleGenerator;
}
}
@@ -22,130 +22,117 @@ import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
public class FlatMethodLoanScheduleGenerator implements LoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
@Override
public LoanScheduleData generate(
final ApplicationCurrency currency,
final LoanProductRelatedDetail loanScheduleInfo,
final Integer loanTermFrequency,
final PeriodFrequencyType loanTermFrequencyType,
final LocalDate disbursementDate,
final LocalDate firstRepaymentDate,
final LocalDate interestCalculatedFrom,
final Set<LoanCharge> loanCharges) {
final Collection<LoanSchedulePeriodData> periods = new ArrayList<LoanSchedulePeriodData>();
final List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate);
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
BigDecimal loanTermPeriodsInYear = BigDecimal.valueOf(this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType));
BigDecimal interestRateForLoanTerm = loanScheduleInfo
.getAnnualNominalInterestRate()
.divide(loanTermPeriodsInYear, mc)
.divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc)
.multiply(BigDecimal.valueOf(loanTermFrequency));
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
Money totalInterestForLoanTerm = loanScheduleInfo.getPrincipal().multiplyRetainScale(interestRateForLoanTerm, RoundingMode.HALF_EVEN);
@Override
public LoanScheduleData generate(final ApplicationCurrency currency, final LoanProductRelatedDetail loanScheduleInfo,
final Integer loanTermFrequency, final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate,
final LocalDate firstRepaymentDate, final LocalDate interestCalculatedFrom, final Set<LoanCharge> loanCharges) {
Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(loanScheduleInfo.getNumberOfRepayments()), RoundingMode.HALF_EVEN);
Money principalPerInstallment = loanScheduleInfo.getPrincipal()
.dividedBy(loanScheduleInfo.getNumberOfRepayments(),
RoundingMode.HALF_EVEN);
final Collection<LoanSchedulePeriodData> periods = new ArrayList<LoanSchedulePeriodData>();
Money outstandingBalance = loanScheduleInfo.getPrincipal();
Money principalDisbursed = loanScheduleInfo.getPrincipal();
Money totalPrincipal = Money.zero(outstandingBalance.getCurrency());
Money totalInterest = Money.zero(outstandingBalance.getCurrency());
BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO;
for (LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueAtDisbursement()) {
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
}
}
BigDecimal cumulativeChargesToDate = chargesDueAtTimeOfDisbursement;
// create entries of disbursement period on loan schedule
final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(disbursementDate, principalDisbursed.getAmount(), chargesDueAtTimeOfDisbursement, false);
periods.add(disbursementPeriod);
int loanTermInDays = Integer.valueOf(0);
BigDecimal cumulativePrincipalDisbursed = principalDisbursed.getAmount();
BigDecimal cumulativePrincipalDue = BigDecimal.ZERO;
BigDecimal cumulativeInterestExpected = BigDecimal.ZERO;
BigDecimal totalExpectedRepayment = chargesDueAtTimeOfDisbursement;
LocalDate startDate = disbursementDate;
int periodNumber = 1;
for (LocalDate scheduledDueDate : scheduledDates) {
totalPrincipal = totalPrincipal.plus(principalPerInstallment);
totalInterest = totalInterest.plus(interestPerInstallment);
// number of days from startDate to this scheduledDate
int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()).getDays();
final List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate);
if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) {
final Money difference = totalPrincipal.minus(loanScheduleInfo.getPrincipal());
if (difference.isLessThanZero()) {
principalPerInstallment = principalPerInstallment.plus(difference.abs());
} else if (difference.isGreaterThanZero()) {
principalPerInstallment = principalPerInstallment.minus(difference.abs());
}
final Money interestDifference = totalInterest.minus(totalInterestForLoanTerm);
if (interestDifference.isLessThanZero()) {
interestPerInstallment = interestPerInstallment.plus(interestDifference.abs());
} else if (interestDifference.isGreaterThanZero()) {
interestPerInstallment = interestPerInstallment.minus(interestDifference.abs());
}
}
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
outstandingBalance = outstandingBalance.minus(principalPerInstallment);
BigDecimal loanTermPeriodsInYear = BigDecimal.valueOf(this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType));
BigDecimal interestRateForLoanTerm = loanScheduleInfo.getAnnualNominalInterestRate().divide(loanTermPeriodsInYear, mc)
.divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc).multiply(BigDecimal.valueOf(loanTermFrequency));
Money feeChargesForInstallment = Money.zero(monetaryCurrency);
Money penaltyChargesForInstallment = Money.zero(monetaryCurrency);
Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment).plus(feeChargesForInstallment).plus(penaltyChargesForInstallment);
cumulativeChargesToDate = cumulativeChargesToDate.add(feeChargesForInstallment.getAmount()).add(penaltyChargesForInstallment.getAmount());
LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, startDate,
scheduledDueDate,
principalPerInstallment.getAmount(),
outstandingBalance.getAmount(),
interestPerInstallment.getAmount(),
feeChargesForInstallment.getAmount(),
penaltyChargesForInstallment.getAmount(),
totalInstallmentDue.getAmount());
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
Money totalInterestForLoanTerm = loanScheduleInfo.getPrincipal().multiplyRetainScale(interestRateForLoanTerm,
RoundingMode.HALF_EVEN);
periods.add(installment);
// handle cumulative fields
loanTermInDays += daysInPeriod;
cumulativePrincipalDue = cumulativePrincipalDue.add(principalPerInstallment.getAmount());
cumulativeInterestExpected = cumulativeInterestExpected.add(interestPerInstallment.getAmount());
totalExpectedRepayment = totalExpectedRepayment.add(totalInstallmentDue.getAmount());
startDate = scheduledDueDate;
Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(loanScheduleInfo.getNumberOfRepayments()),
RoundingMode.HALF_EVEN);
periodNumber++;
}
final BigDecimal cumulativePrincipalOutstanding = cumulativePrincipalDisbursed.subtract(cumulativePrincipalDue);
CurrencyData currencyData = new CurrencyData(
currency.getCode(),
currency.getName(),
monetaryCurrency.getDigitsAfterDecimal(),
currency.getDisplaySymbol(),
currency.getNameCode());
return new LoanScheduleData(currencyData, periods, loanTermInDays, cumulativePrincipalDisbursed, cumulativePrincipalDue,
cumulativePrincipalOutstanding, cumulativeInterestExpected, cumulativeChargesToDate, totalExpectedRepayment);
}
Money principalPerInstallment = loanScheduleInfo.getPrincipal().dividedBy(loanScheduleInfo.getNumberOfRepayments(),
RoundingMode.HALF_EVEN);
Money outstandingBalance = loanScheduleInfo.getPrincipal();
Money principalDisbursed = loanScheduleInfo.getPrincipal();
Money totalPrincipal = Money.zero(outstandingBalance.getCurrency());
Money totalInterest = Money.zero(outstandingBalance.getCurrency());
BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO;
for (LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueAtDisbursement()) {
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
}
}
BigDecimal cumulativeChargesToDate = chargesDueAtTimeOfDisbursement;
// create entries of disbursement period on loan schedule
final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(disbursementDate,
principalDisbursed.getAmount(), chargesDueAtTimeOfDisbursement, false);
periods.add(disbursementPeriod);
int loanTermInDays = Integer.valueOf(0);
BigDecimal cumulativePrincipalDisbursed = principalDisbursed.getAmount();
BigDecimal cumulativePrincipalDue = BigDecimal.ZERO;
BigDecimal cumulativeInterestExpected = BigDecimal.ZERO;
BigDecimal totalExpectedRepayment = chargesDueAtTimeOfDisbursement;
LocalDate startDate = disbursementDate;
int periodNumber = 1;
for (LocalDate scheduledDueDate : scheduledDates) {
totalPrincipal = totalPrincipal.plus(principalPerInstallment);
totalInterest = totalInterest.plus(interestPerInstallment);
// number of days from startDate to this scheduledDate
int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime())
.getDays();
if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) {
final Money difference = totalPrincipal.minus(loanScheduleInfo.getPrincipal());
if (difference.isLessThanZero()) {
principalPerInstallment = principalPerInstallment.plus(difference.abs());
} else if (difference.isGreaterThanZero()) {
principalPerInstallment = principalPerInstallment.minus(difference.abs());
}
final Money interestDifference = totalInterest.minus(totalInterestForLoanTerm);
if (interestDifference.isLessThanZero()) {
interestPerInstallment = interestPerInstallment.plus(interestDifference.abs());
} else if (interestDifference.isGreaterThanZero()) {
interestPerInstallment = interestPerInstallment.minus(interestDifference.abs());
}
}
outstandingBalance = outstandingBalance.minus(principalPerInstallment);
Money feeChargesForInstallment = Money.zero(monetaryCurrency);
Money penaltyChargesForInstallment = Money.zero(monetaryCurrency);
Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment).plus(feeChargesForInstallment)
.plus(penaltyChargesForInstallment);
cumulativeChargesToDate = cumulativeChargesToDate.add(feeChargesForInstallment.getAmount()).add(
penaltyChargesForInstallment.getAmount());
LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, startDate, scheduledDueDate,
principalPerInstallment.getAmount(), outstandingBalance.getAmount(), interestPerInstallment.getAmount(),
feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount(), totalInstallmentDue.getAmount());
periods.add(installment);
// handle cumulative fields
loanTermInDays += daysInPeriod;
cumulativePrincipalDue = cumulativePrincipalDue.add(principalPerInstallment.getAmount());
cumulativeInterestExpected = cumulativeInterestExpected.add(interestPerInstallment.getAmount());
totalExpectedRepayment = totalExpectedRepayment.add(totalInstallmentDue.getAmount());
startDate = scheduledDueDate;
periodNumber++;
}
final BigDecimal cumulativePrincipalOutstanding = cumulativePrincipalDisbursed.subtract(cumulativePrincipalDue);
CurrencyData currencyData = new CurrencyData(currency.getCode(), currency.getName(), monetaryCurrency.getDigitsAfterDecimal(),
currency.getDisplaySymbol(), currency.getNameCode());
return new LoanScheduleData(currencyData, periods, loanTermInDays, cumulativePrincipalDisbursed, cumulativePrincipalDue,
cumulativePrincipalOutstanding, cumulativeInterestExpected, cumulativeChargesToDate, totalExpectedRepayment);
}
}
@@ -0,0 +1,105 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.domain;
import java.math.BigDecimal;
import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
/**
* Domain representation of a Loan Schedule (not used for persistence)
*/
public final class LoanSchedule {
private final LoanScheduleGenerator loanScheduleGenerator;
private final ApplicationCurrency applicationCurrency;
private final BigDecimal principal;
private final BigDecimal nominalInterestRatePerPeriod;
private final PeriodFrequencyType interestRatePeriodFrequencyType;
private final BigDecimal nominalAnnualInterestRate;
private final InterestMethod interestMethod;
private final InterestCalculationPeriodMethod interestCalculationPeriodMethod;
private final Integer repaymentEvery;
private final PeriodFrequencyType repaymentPeriodFrequencyType;
private final Integer numberOfRepayments;
private final AmortizationMethod amortizationMethod;
private final Integer loanTermFrequency;
private final PeriodFrequencyType loanTermPeriodFrequencyType;
private final LocalDate disbursementDate;
private final LocalDate repaymentStartFromDate;
private final LocalDate interestChargedFromDate;
private final BigDecimal inArrearsTolerance;
private final Set<LoanCharge> loanCharges;
public LoanSchedule(final LoanScheduleGenerator loanScheduleGenerator, final ApplicationCurrency applicationCurrency,
final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod,
final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalAnnualInterestRate,
final InterestMethod interestMethod, final InterestCalculationPeriodMethod interestCalculationPeriodMethod,
final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer numberOfRepayments,
final AmortizationMethod amortizationMethod, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Set<LoanCharge> loanCharges, final LocalDate disbursementDate,
final LocalDate repaymentStartFromDate, final LocalDate interestChargedFromDate,
final BigDecimal inArrearsTolerance) {
this.loanScheduleGenerator = loanScheduleGenerator;
this.applicationCurrency = applicationCurrency;
this.principal = principal;
this.nominalInterestRatePerPeriod = nominalInterestRatePerPeriod;
this.interestRatePeriodFrequencyType = interestRatePeriodFrequencyType;
this.nominalAnnualInterestRate = nominalAnnualInterestRate;
this.interestMethod = interestMethod;
this.interestCalculationPeriodMethod = interestCalculationPeriodMethod;
this.repaymentEvery = repaymentEvery;
this.repaymentPeriodFrequencyType = repaymentPeriodFrequencyType;
this.numberOfRepayments = numberOfRepayments;
this.amortizationMethod = amortizationMethod;
this.loanTermFrequency = loanTermFrequency;
this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType;
this.loanCharges = loanCharges;
this.disbursementDate = disbursementDate;
this.repaymentStartFromDate = repaymentStartFromDate;
this.interestChargedFromDate = interestChargedFromDate;
this.inArrearsTolerance = inArrearsTolerance;
}
public LoanScheduleData generate() {
return loanScheduleGenerator.generate(applicationCurrency, loanProductRelatedDetail(), loanTermFrequency,
loanTermPeriodFrequencyType, disbursementDate, repaymentStartFromDate, interestChargedFromDate, loanCharges);
}
public LoanProductRelatedDetail loanProductRelatedDetail() {
final MonetaryCurrency currency = new MonetaryCurrency(applicationCurrency.getCode(), applicationCurrency.getDecimalPlaces());
return LoanProductRelatedDetail.createFrom(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType,
nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentPeriodFrequencyType,
numberOfRepayments, amortizationMethod, this.inArrearsTolerance);
}
public Integer getLoanTermFrequency() {
return this.loanTermFrequency;
}
public PeriodFrequencyType getLoanTermPeriodFrequencyType() {
return this.loanTermPeriodFrequencyType;
}
public LocalDate getDisbursementDate() {
return this.disbursementDate;
}
public LocalDate getRepaymentStartFromDate() {
return this.repaymentStartFromDate;
}
public LocalDate getInterestChargedFromDate() {
return this.interestChargedFromDate;
}
}
@@ -11,10 +11,8 @@ import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
public interface LoanScheduleGenerator {
LoanScheduleData generate(ApplicationCurrency applicationCurrency,
LoanProductRelatedDetail loanScheduleInfo,
Integer loanTermFrequency,
PeriodFrequencyType loanTermFrequencyType,
LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom, Set<LoanCharge> loanCharges);
LoanScheduleData generate(ApplicationCurrency applicationCurrency, LoanProductRelatedDetail loanScheduleInfo,
Integer loanTermFrequency, PeriodFrequencyType loanTermFrequencyType, LocalDate disbursementDate, LocalDate firstRepaymentDate,
LocalDate interestCalculatedFrom, Set<LoanCharge> loanCharges);
}
@@ -0,0 +1,207 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.query;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
/**
* Immutable query used when auto-calculating loan schedules based on loan
* terms.
*/
public class CalculateLoanScheduleQuery {
private final Long productId;
private final BigDecimal principal;
private final Integer loanTermFrequency;
private final Integer loanTermFrequencyType;
private final Integer repaymentEvery;
private final Integer repaymentFrequencyType;
private final Integer numberOfRepayments;
private final BigDecimal interestRatePerPeriod;
private final Integer interestRateFrequencyType;
private final Integer amortizationType;
private final Integer interestType;
private final Integer interestCalculationPeriodType;
private final LocalDate expectedDisbursementDate;
private final LocalDate repaymentsStartingFromDate;
private final LocalDate interestChargedFromDate;
private final LoanChargeCommand[] charges;
public CalculateLoanScheduleQuery(final Long productId, final BigDecimal principal, final BigDecimal interestRatePerPeriod,
Integer interestRateFrequencyMethod, final Integer interestMethod, final Integer interestCalculationPeriodMethod,
final Integer repaymentEvery, final Integer repaymentFrequency, final Integer numberOfRepayments, Integer amortizationMethod,
final Integer loanTermFrequency, final Integer loanTermFrequencyType, final LocalDate expectedDisbursementDate,
final LocalDate repaymentsStartingFromDate, final LocalDate interestCalculatedFromDate, final LoanChargeCommand[] charges) {
this.productId = productId;
this.principal = principal;
this.interestRatePerPeriod = interestRatePerPeriod;
this.interestRateFrequencyType = interestRateFrequencyMethod;
this.interestType = interestMethod;
this.interestCalculationPeriodType = interestCalculationPeriodMethod;
this.repaymentEvery = repaymentEvery;
this.repaymentFrequencyType = repaymentFrequency;
this.numberOfRepayments = numberOfRepayments;
this.amortizationType = amortizationMethod;
this.loanTermFrequency = loanTermFrequency;
this.loanTermFrequencyType = loanTermFrequencyType;
this.expectedDisbursementDate = expectedDisbursementDate;
this.repaymentsStartingFromDate = repaymentsStartingFromDate;
this.interestChargedFromDate = interestCalculatedFromDate;
this.charges = charges;
}
public Long getProductId() {
return productId;
}
public BigDecimal getPrincipal() {
return principal;
}
public Integer getLoanTermFrequency() {
return loanTermFrequency;
}
public Integer getLoanTermFrequencyType() {
return loanTermFrequencyType;
}
public Integer getRepaymentEvery() {
return repaymentEvery;
}
public Integer getRepaymentFrequencyType() {
return repaymentFrequencyType;
}
public Integer getNumberOfRepayments() {
return numberOfRepayments;
}
public BigDecimal getInterestRatePerPeriod() {
return interestRatePerPeriod;
}
public Integer getInterestRateFrequencyType() {
return interestRateFrequencyType;
}
public Integer getAmortizationType() {
return amortizationType;
}
public Integer getInterestType() {
return interestType;
}
public Integer getInterestCalculationPeriodType() {
return interestCalculationPeriodType;
}
public LocalDate getExpectedDisbursementDate() {
return expectedDisbursementDate;
}
public LocalDate getRepaymentsStartingFromDate() {
return repaymentsStartingFromDate;
}
public LocalDate getInterestChargedFromDate() {
return interestChargedFromDate;
}
public LoanChargeCommand[] getCharges() {
return charges;
}
public void validate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
baseDataValidator.reset().parameter("principal").value(this.principal).notNull().positiveAmount();
baseDataValidator.reset().parameter("loanTermFrequency").value(this.loanTermFrequency).notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("loanTermFrequencyType").value(this.loanTermFrequencyType).notNull().inMinMaxRange(0, 4);
baseDataValidator.reset().parameter("numberOfRepayments").value(this.numberOfRepayments).notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("repaymentEvery").value(this.repaymentEvery).notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("repaymentFrequencyType").value(this.repaymentFrequencyType).notNull().inMinMaxRange(0, 3);
// FIXME - this constraint doesnt really need to be here. should be
// possible to express loan term as say 12 months whilst also saying
// - that the repayment structure is 6 repayments every bi-monthly.
if (this.loanTermFrequencyType != null && !this.loanTermFrequencyType.equals(this.repaymentFrequencyType)) {
ApiParameterError error = ApiParameterError.parameterError(
"validation.msg.loan.loanTermFrequencyType.not.the.same.as.repaymentFrequencyType",
"The parameters loanTermFrequencyType and repaymentFrequencyType must be the same.", "loanTermFrequencyType",
this.loanTermFrequencyType, this.repaymentFrequencyType);
dataValidationErrors.add(error);
} else {
if (this.loanTermFrequency != null && this.repaymentEvery != null && this.numberOfRepayments != null) {
int suggestsedLoanTerm = this.repaymentEvery * this.numberOfRepayments;
if (this.loanTermFrequency.intValue() < suggestsedLoanTerm) {
ApiParameterError error = ApiParameterError
.parameterError(
"validation.msg.loan.loanTermFrequency.less.than.repayment.structure.suggests",
"The parameter loanTermFrequency is less than the suggest loan term as indicated by numberOfRepayments and repaymentEvery.",
"loanTermFrequency", this.loanTermFrequency, this.numberOfRepayments, this.repaymentEvery);
dataValidationErrors.add(error);
}
}
}
baseDataValidator.reset().parameter("interestRatePerPeriod").value(this.interestRatePerPeriod).notNull();
baseDataValidator.reset().parameter("interestRateFrequencyType").value(this.interestRateFrequencyType).notNull()
.inMinMaxRange(0, 3);
baseDataValidator.reset().parameter("amortizationType").value(this.amortizationType).notNull().inMinMaxRange(0, 1);
baseDataValidator.reset().parameter("interestType").value(this.interestType).notNull().inMinMaxRange(0, 1);
baseDataValidator.reset().parameter("interestCalculationPeriodType").value(this.interestCalculationPeriodType).notNull()
.inMinMaxRange(0, 1);
baseDataValidator.reset().parameter("expectedDisbursementDate").value(this.expectedDisbursementDate).notNull();
if (this.expectedDisbursementDate != null) {
if (this.repaymentsStartingFromDate != null && this.expectedDisbursementDate.isAfter(this.repaymentsStartingFromDate)) {
ApiParameterError error = ApiParameterError.parameterError(
"validation.msg.loan.expectedDisbursementDate.cannot.be.after.first.repayment.date",
"The parameter expectedDisbursementDate has a date which falls after the given first repayment date.",
"expectedDisbursementDate", this.expectedDisbursementDate, this.repaymentsStartingFromDate);
dataValidationErrors.add(error);
}
}
if (this.repaymentsStartingFromDate != null && this.interestChargedFromDate == null) {
ApiParameterError error = ApiParameterError.parameterError(
"validation.msg.loan.interestCalculatedFromDate.must.be.entered.when.using.repayments.startfrom.field",
"The parameter interestCalculatedFromDate cannot be empty when first repayment date is provided.",
"interestChargedFromDate", this.repaymentsStartingFromDate);
dataValidationErrors.add(error);
} else if (this.repaymentsStartingFromDate == null && this.interestChargedFromDate != null) {
// validate interestCalculatedFromDate is after or on
// repaymentsStartingFromDate
if (this.expectedDisbursementDate.isAfter(this.interestChargedFromDate)) {
ApiParameterError error = ApiParameterError.parameterError(
"validation.msg.loan.interestChargedFromDate.cannot.be.before.disbursement.date",
"The parameter interestCalculatedFromDate cannot be before the date given for expected disbursement.",
"interestChargedFromDate", this.interestChargedFromDate, this.expectedDisbursementDate);
dataValidationErrors.add(error);
}
}
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
}
@@ -1,9 +0,0 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.service;
import org.mifosplatform.portfolio.loanaccount.loanschedule.command.CalculateLoanScheduleCommand;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
public interface CalculationPlatformService {
LoanScheduleData calculateLoanSchedule(CalculateLoanScheduleCommand command);
}
@@ -1,71 +0,0 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.service;
import java.util.Set;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepository;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.loanschedule.command.CalculateLoanScheduleCommand;
import org.mifosplatform.portfolio.loanaccount.loanschedule.command.CalculateLoanScheduleCommandValidator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.DefaultLoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanaccount.service.LoanChargeAssembler;
import org.mifosplatform.portfolio.loanaccount.service.LoanProductRelatedDetailAssembler;
import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CalculationPlatformServiceImpl implements CalculationPlatformService {
private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final ApplicationCurrencyRepository applicationCurrencyRepository;
private final PlatformSecurityContext context;
private final LoanProductRelatedDetailAssembler loanProductRelatedDetailAssembler;
private final LoanChargeAssembler loanChargeAssembler;
@Autowired
public CalculationPlatformServiceImpl(
final PlatformSecurityContext context,
final ApplicationCurrencyRepository applicationCurrencyRepository,
final LoanProductRelatedDetailAssembler loanProductRelatedDetailAssembler,
final LoanChargeAssembler loanChargeAssembler) {
this.context = context;
this.applicationCurrencyRepository = applicationCurrencyRepository;
this.loanScheduleFactory = new DefaultLoanScheduleGeneratorFactory();
this.loanProductRelatedDetailAssembler = loanProductRelatedDetailAssembler;
this.loanChargeAssembler = loanChargeAssembler;
}
@Override
public LoanScheduleData calculateLoanSchedule(final CalculateLoanScheduleCommand command) {
context.authenticatedUser();
final CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator(command);
validator.validate();
final LoanProductRelatedDetail loanScheduleRelatedDetails = this.loanProductRelatedDetailAssembler.assembleFrom(command);
final Integer loanTermFrequency = command.getLoanTermFrequency();
final PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.fromInt(command.getLoanTermFrequencyType());
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType());
final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(interestMethod);
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(loanScheduleRelatedDetails.getCurrency().getCode());
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.assembleFrom(command.getCharges(), loanScheduleRelatedDetails.getPrincipal().getAmount());
return loanScheduleGenerator.generate(applicationCurrency,
loanScheduleRelatedDetails, loanTermFrequency,
loanTermFrequencyType, command.getExpectedDisbursementDate(),
command.getRepaymentsStartingFromDate(),
command.getInterestChargedFromDate(), loanCharges);
}
}
@@ -0,0 +1,108 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.service;
import java.math.BigDecimal;
import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepository;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanaccount.service.LoanChargeAssembler;
import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.google.gson.JsonElement;
@Service
public class LoanScheduleAssembler {
private final FromJsonHelper fromApiJsonHelper;
private final LoanProductRepository loanProductRepository;
private final ApplicationCurrencyRepository applicationCurrencyRepository;
private final LoanChargeAssembler loanChargeAssembler;
private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final AprCalculator aprCalculator;
@Autowired
public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository,
final ApplicationCurrencyRepository applicationCurrencyRepository, final LoanScheduleGeneratorFactory loanScheduleFactory,
final AprCalculator aprCalculator, final LoanChargeAssembler loanChargeAssembler) {
this.fromApiJsonHelper = fromApiJsonHelper;
this.loanProductRepository = loanProductRepository;
this.applicationCurrencyRepository = applicationCurrencyRepository;
this.loanScheduleFactory = loanScheduleFactory;
this.aprCalculator = aprCalculator;
this.loanChargeAssembler = loanChargeAssembler;
}
public LoanSchedule fromJson(final String json) {
final JsonElement element = fromApiJsonHelper.parse(json);
return fromJson(element);
}
public LoanSchedule fromJson(final JsonElement element) {
return fromJson(element, null);
}
public LoanSchedule fromJson(final JsonElement element, final BigDecimal inArrearsTolerance) {
final Long loanProductId = fromApiJsonHelper.extractLongNamed("productId", element);
final LoanProduct loanProduct = this.loanProductRepository.findOne(loanProductId);
if (loanProduct == null) { throw new LoanProductNotFoundException(loanProductId); }
final MonetaryCurrency currency = loanProduct.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(currency.getCode());
final BigDecimal principal = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element);
final BigDecimal interestRatePerPeriod = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element);
final Integer interestRateFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestRateFrequencyType", element);
final Integer interestType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element);
final Integer interestCalculationPeriodType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestCalculationPeriodType",
element);
final InterestMethod interestMethod = InterestMethod.fromInt(interestType);
final PeriodFrequencyType interestRatePeriodFrequencyType = PeriodFrequencyType.fromInt(interestRateFrequencyType);
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod
.fromInt(interestCalculationPeriodType);
final BigDecimal defaultAnnualNominalInterestRate = this.aprCalculator.calculateFrom(interestRatePeriodFrequencyType,
interestRatePerPeriod);
final Integer repaymentEvery = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element);
final Integer repaymentFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyType", element);
final Integer numberOfRepayments = fromApiJsonHelper.extractIntegerWithLocaleNamed("numberOfRepayments", element);
final Integer amortizationType = fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element);
final PeriodFrequencyType repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(repaymentFrequencyType);
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(amortizationType);
final Integer loanTermFrequency = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequency", element);
final Integer loanTermFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequencyType", element);
final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(loanTermFrequencyType);
final LocalDate expectedDisbursementDate = fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element);
final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element);
final LocalDate interestChargedFromDate = fromApiJsonHelper.extractLocalDateNamed("interestChargedFromDate", element);
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element);
final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(interestMethod);
return new LoanSchedule(loanScheduleGenerator, applicationCurrency, principal, interestRatePerPeriod,
interestRatePeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod,
repaymentEvery, repaymentPeriodFrequencyType, numberOfRepayments, amortizationMethod, loanTermFrequency,
loanTermPeriodFrequencyType, loanCharges, expectedDisbursementDate, repaymentsStartingFromDate, interestChargedFromDate, inArrearsTolerance);
}
}
@@ -0,0 +1,9 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.service;
import org.mifosplatform.infrastructure.core.api.JsonQuery;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
public interface LoanScheduleCalculationPlatformService {
LoanScheduleData calculateLoanSchedule(JsonQuery query);
}
@@ -0,0 +1,38 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.service;
import org.mifosplatform.infrastructure.core.api.JsonQuery;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule;
import org.mifosplatform.portfolio.loanaccount.loanschedule.query.CalculateLoanScheduleQuery;
import org.mifosplatform.portfolio.loanaccount.serialization.CalculateLoanScheduleQueryFromApiJsonDeserializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleCalculationPlatformService {
private final PlatformSecurityContext context;
private final CalculateLoanScheduleQueryFromApiJsonDeserializer fromApiJsonDeserializer;
private final LoanScheduleAssembler loanScheduleAssembler;
@Autowired
public LoanScheduleCalculationPlatformServiceImpl(final PlatformSecurityContext context,
final CalculateLoanScheduleQueryFromApiJsonDeserializer fromApiJsonDeserializer,
final LoanScheduleAssembler loanScheduleAssembler) {
this.context = context;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.loanScheduleAssembler = loanScheduleAssembler;
}
@Override
public LoanScheduleData calculateLoanSchedule(final JsonQuery query) {
context.authenticatedUser();
CalculateLoanScheduleQuery calculateLoanScheduleQuery = this.fromApiJsonDeserializer.commandFromApiJson(query.json());
calculateLoanScheduleQuery.validate();
final LoanSchedule loanSchedule = this.loanScheduleAssembler.fromJson(query.parsedJson());
return loanSchedule.generate();
}
}
@@ -0,0 +1,114 @@
package org.mifosplatform.portfolio.loanaccount.serialization;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
import org.mifosplatform.infrastructure.core.serialization.AbstractFromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.serialization.FromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.loanschedule.query.CalculateLoanScheduleQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
/**
* Implementation of {@link FromApiJsonDeserializer} for
* {@link CalculateLoanScheduleQuery}'s.
*/
@Component
public final class CalculateLoanScheduleQueryFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<CalculateLoanScheduleQuery> {
/**
* The parameters supported for this command.
*/
final Set<String> supportedParameters = new HashSet<String>(Arrays.asList("clientId", "groupId", "productId", "externalId", "fundId",
"transactionProcessingStrategyId", "principal", "inArrearsTolerance", "interestRatePerPeriod", "repaymentEvery",
"numberOfRepayments", "loanTermFrequency", "loanTermFrequencyType", "charges", "repaymentFrequencyType",
"interestRateFrequencyType", "amortizationType", "interestType", "interestCalculationPeriodType", "expectedDisbursementDate",
"repaymentsStartingFromDate", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", "locale", "dateFormat",
"loanOfficerId", "id"));
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public CalculateLoanScheduleQueryFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
@Override
public CalculateLoanScheduleQuery commandFromApiJson(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters);
final JsonElement element = fromApiJsonHelper.parse(json);
final Long productId = fromApiJsonHelper.extractLongNamed("productId", element);
final BigDecimal principal = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element);
final BigDecimal interestRatePerPeriod = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element);
final Integer interestRateFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestRateFrequencyType", element);
final Integer interestType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element);
final Integer interestCalculationPeriodType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestCalculationPeriodType",
element);
final Integer repaymentEvery = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element);
final Integer repaymentFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyType", element);
final Integer numberOfRepayments = fromApiJsonHelper.extractIntegerWithLocaleNamed("numberOfRepayments", element);
final Integer amortizationType = fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element);
final Integer loanTermFrequency = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequency", element);
final Integer loanTermFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequencyType", element);
final LocalDate expectedDisbursementDate = fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element);
final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element);
final LocalDate interestChargedFromDate = fromApiJsonHelper.extractLocalDateNamed("interestChargedFromDate", element);
// ///
LoanChargeCommand[] charges = null;
if (element.isJsonObject()) {
final JsonObject topLevelJsonElement = element.getAsJsonObject();
final String dateFormat = fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement);
final Locale locale = fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement);
if (topLevelJsonElement.has("charges") && topLevelJsonElement.get("charges").isJsonArray()) {
final JsonArray array = topLevelJsonElement.get("charges").getAsJsonArray();
charges = new LoanChargeCommand[array.size()];
for (int i = 0; i < array.size(); i++) {
final JsonObject loanChargeElement = array.get(i).getAsJsonObject();
final Set<String> parametersPassedInForChargesCommand = new HashSet<String>();
final Long id = fromApiJsonHelper.extractLongNamed("id", loanChargeElement);
final Long chargeId = fromApiJsonHelper.extractLongNamed("chargeId", loanChargeElement);
final BigDecimal amount = fromApiJsonHelper.extractBigDecimalNamed("amount", loanChargeElement, locale);
final Integer chargeTimeType = fromApiJsonHelper.extractIntegerNamed("chargeTimeType", loanChargeElement, locale);
final Integer chargeCalculationType = fromApiJsonHelper.extractIntegerNamed("chargeCalculationType", loanChargeElement,
locale);
final LocalDate specifiedDueDate = fromApiJsonHelper.extractLocalDateNamed("specifiedDueDate", loanChargeElement,
dateFormat, locale);
charges[i] = new LoanChargeCommand(parametersPassedInForChargesCommand, id, null, chargeId, amount, chargeTimeType,
chargeCalculationType, specifiedDueDate);
}
}
}
// /
return new CalculateLoanScheduleQuery(productId, principal, interestRatePerPeriod, interestRateFrequencyType, interestType,
interestCalculationPeriodType, repaymentEvery, repaymentFrequencyType, numberOfRepayments, amortizationType,
loanTermFrequency, loanTermFrequencyType, expectedDisbursementDate, repaymentsStartingFromDate, interestChargedFromDate,
charges);
}
}
@@ -0,0 +1,142 @@
package org.mifosplatform.portfolio.loanaccount.serialization;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
import org.mifosplatform.infrastructure.core.serialization.AbstractFromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.serialization.FromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.loanaccount.command.LoanApplicationCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
/**
* Implementation of {@link FromApiJsonDeserializer} for
* {@link LoanApplicationCommand}'s.
*/
@Component
public final class LoanApplicationCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<LoanApplicationCommand> {
/**
* The parameters supported for this command.
*/
final Set<String> supportedParameters = new HashSet<String>(Arrays.asList("clientId", "groupId", "productId", "externalId", "fundId",
"transactionProcessingStrategyId", "principal", "inArrearsTolerance", "interestRatePerPeriod", "repaymentEvery",
"numberOfRepayments", "loanTermFrequency", "loanTermFrequencyType", "charges", "repaymentFrequencyType",
"interestRateFrequencyType", "amortizationType", "interestType", "interestCalculationPeriodType", "expectedDisbursementDate",
"repaymentsStartingFromDate", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", "locale", "dateFormat",
"loanOfficerId", "id"));
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public LoanApplicationCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
@Override
public LoanApplicationCommand commandFromApiJson(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters);
final JsonElement element = fromApiJsonHelper.parse(json);
final Long clientId = fromApiJsonHelper.extractLongNamed("clientId", element);
final Long groupId = fromApiJsonHelper.extractLongNamed("groupId", element);
final Long productId = fromApiJsonHelper.extractLongNamed("productId", element);
final String externalId = fromApiJsonHelper.extractStringNamed("externalId", element);
final Long fundId = fromApiJsonHelper.extractLongNamed("fundId", element);
final Long loanOfficerId = fromApiJsonHelper.extractLongNamed("loanOfficerId", element);
final Long transactionProcessingStrategyId = fromApiJsonHelper.extractLongNamed("transactionProcessingStrategyId", element);
final LocalDate submittedOnDate = fromApiJsonHelper.extractLocalDateNamed("submittedOnDate", element);
final String submittedOnNote = fromApiJsonHelper.extractStringNamed("submittedOnNote", element);
final BigDecimal principal = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element);
final BigDecimal interestRatePerPeriod = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element);
final Integer interestRateFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestRateFrequencyType", element);
final Integer interestType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element);
final Integer interestCalculationPeriodType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestCalculationPeriodType",
element);
// final InterestMethod interestMethod =
// InterestMethod.fromInt(interestType);
// final PeriodFrequencyType interestRatePeriodFrequencyType =
// PeriodFrequencyType.fromInt(interestRateFrequencyType);
// final InterestCalculationPeriodMethod interestCalculationPeriodMethod
// = InterestCalculationPeriodMethod
// .fromInt(interestCalculationPeriodType);
final Integer amortizationType = fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element);
final Integer repaymentEvery = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element);
final Integer repaymentFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyType", element);
final Integer numberOfRepayments = fromApiJsonHelper.extractIntegerWithLocaleNamed("numberOfRepayments", element);
// final PeriodFrequencyType repaymentPeriodFrequencyType =
// PeriodFrequencyType.fromInt(repaymentFrequencyType);
// final AmortizationMethod amortizationMethod =
// AmortizationMethod.fromInt(amortizationType);
final Integer loanTermFrequency = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequency", element);
final Integer loanTermFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequencyType", element);
// final PeriodFrequencyType loanTermPeriodFrequencyType =
// PeriodFrequencyType.fromInt(loanTermFrequencyType);
final LocalDate expectedDisbursementDate = fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element);
final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element);
final LocalDate interestChargedFromDate = fromApiJsonHelper.extractLocalDateNamed("interestChargedFromDate", element);
final BigDecimal inArrearsTolerance = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element);
// ///
LoanChargeCommand[] charges = null;
if (element.isJsonObject()) {
final JsonObject topLevelJsonElement = element.getAsJsonObject();
final String dateFormat = fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement);
final Locale locale = fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement);
if (topLevelJsonElement.has("charges") && topLevelJsonElement.get("charges").isJsonArray()) {
final JsonArray array = topLevelJsonElement.get("charges").getAsJsonArray();
charges = new LoanChargeCommand[array.size()];
for (int i = 0; i < array.size(); i++) {
final JsonObject loanChargeElement = array.get(i).getAsJsonObject();
final Set<String> parametersPassedInForChargesCommand = new HashSet<String>();
final Long id = fromApiJsonHelper.extractLongNamed("id", loanChargeElement);
final Long chargeId = fromApiJsonHelper.extractLongNamed("chargeId", loanChargeElement);
final BigDecimal amount = fromApiJsonHelper.extractBigDecimalNamed("amount", loanChargeElement, locale);
final Integer chargeTimeType = fromApiJsonHelper.extractIntegerNamed("chargeTimeType", loanChargeElement, locale);
final Integer chargeCalculationType = fromApiJsonHelper.extractIntegerNamed("chargeCalculationType", loanChargeElement,
locale);
final LocalDate specifiedDueDate = fromApiJsonHelper.extractLocalDateNamed("specifiedDueDate", loanChargeElement,
dateFormat, locale);
charges[i] = new LoanChargeCommand(parametersPassedInForChargesCommand, id, null, chargeId, amount, chargeTimeType,
chargeCalculationType, specifiedDueDate);
}
}
}
return new LoanApplicationCommand(new HashSet<String>(), null, clientId, groupId, productId, externalId, fundId,
transactionProcessingStrategyId, submittedOnDate, submittedOnNote, expectedDisbursementDate, repaymentsStartingFromDate,
interestChargedFromDate, principal, interestRatePerPeriod, interestRateFrequencyType, interestType,
interestCalculationPeriodType, repaymentEvery, repaymentFrequencyType, numberOfRepayments, amortizationType,
loanTermFrequency, loanTermFrequencyType, inArrearsTolerance, charges, loanOfficerId);
}
}
@@ -0,0 +1,61 @@
package org.mifosplatform.portfolio.loanaccount.serialization;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
import org.mifosplatform.infrastructure.core.serialization.AbstractFromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.serialization.FromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.loanaccount.command.LoanStateTransitionCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
/**
* Implementation of {@link FromApiJsonDeserializer} for
* {@link LoanStateTransitionCommand}'s.
*/
@Component
public final class LoanStateTransitionCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<LoanStateTransitionCommand> {
/**
* The parameters supported for this command.
*/
final Set<String> supportedParameters = new HashSet<String>(Arrays.asList("eventDate", "note", "locale", "dateFormat", "status",
"approvedOnDate", "rejectedOnDate", "withdrawnOnDate", "disbursedOnDate", "expectedMaturityDate"));
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public LoanStateTransitionCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
@Override
public LoanStateTransitionCommand commandFromApiJson(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters);
final JsonElement element = fromApiJsonHelper.parse(json);
final LocalDate eventDate = fromApiJsonHelper.extractLocalDateNamed("eventDate", element);
final String note = fromApiJsonHelper.extractStringNamed("note", element);
final LocalDate approvedOnDate = fromApiJsonHelper.extractLocalDateNamed("approvedOnDate", element);
final LocalDate rejectedOnDate = fromApiJsonHelper.extractLocalDateNamed("rejectedOnDate", element);
final LocalDate withdrawnOnDate = fromApiJsonHelper.extractLocalDateNamed("withdrawnOnDate", element);
final LocalDate disbursedOnDate = fromApiJsonHelper.extractLocalDateNamed("disbursedOnDate", element);
return new LoanStateTransitionCommand(eventDate, note, approvedOnDate, rejectedOnDate, withdrawnOnDate, disbursedOnDate);
}
}
@@ -5,7 +5,9 @@ import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.organisation.staff.domain.Staff;
import org.mifosplatform.organisation.staff.domain.StaffRepository;
import org.mifosplatform.organisation.staff.exception.StaffNotFoundException;
@@ -19,207 +21,140 @@ import org.mifosplatform.portfolio.fund.exception.FundNotFoundException;
import org.mifosplatform.portfolio.group.domain.Group;
import org.mifosplatform.portfolio.group.domain.GroupRepository;
import org.mifosplatform.portfolio.group.exception.GroupNotFoundException;
import org.mifosplatform.portfolio.loanaccount.command.LoanApplicationCommand;
import org.mifosplatform.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionProcessingStrategyRepository;
import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionProcessingStrategyNotFoundException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.service.CalculationPlatformService;
import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule;
import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository;
import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.google.gson.JsonElement;
@Service
public class LoanAssembler {
private final LoanProductRepository loanProductRepository;
private final ClientRepository clientRepository;
private final GroupRepository groupRepository;
private final AprCalculator aprCalculator = new AprCalculator();
private final FundRepository fundRepository;
private final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository;
private final StaffRepository staffRepository;
private final CalculationPlatformService calculationPlatformService;
private final LoanChargeAssembler loanChargeAssembler;
@Autowired
public LoanAssembler(
final LoanProductRepository loanProductRepository,
final ClientRepository clientRepository,
final GroupRepository groupRepository,
final FundRepository fundRepository,
final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository,
final StaffRepository staffRepository,
final CalculationPlatformService calculationPlatformService,
final LoanChargeAssembler loanChargeAssembler) {
this.loanProductRepository = loanProductRepository;
this.clientRepository = clientRepository;
this.groupRepository = groupRepository;
this.fundRepository = fundRepository;
this.loanTransactionProcessingStrategyRepository = loanTransactionProcessingStrategyRepository;
this.staffRepository = staffRepository;
this.calculationPlatformService = calculationPlatformService;
this.loanChargeAssembler = loanChargeAssembler;
}
public Loan assembleFrom(final LoanApplicationCommand command) {
LoanProduct loanProduct = this.loanProductRepository.findOne(command.getProductId());
if (loanProduct == null) {
throw new LoanProductNotFoundException(command.getProductId());
}
private final LoanProductRepository loanProductRepository;
private final ClientRepository clientRepository;
private final GroupRepository groupRepository;
private final FundRepository fundRepository;
private final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository;
private final StaffRepository staffRepository;
private final LoanChargeAssembler loanChargeAssembler;
private final LoanScheduleAssembler loanScheduleAssembler;
private final FromJsonHelper fromApiJsonHelper;
Client client = null;
if (command.getClientId() != null) {
client = this.clientRepository.findOne(command.getClientId());
if (client == null || client.isDeleted()) {
throw new ClientNotFoundException(command.getClientId());
}
}
Group group = null;
if (command.getGroupId() != null) {
group = this.groupRepository.findOne(command.getGroupId());
if (group == null || group.isDeleted()) {
throw new GroupNotFoundException(command.getGroupId());
}
}
final MonetaryCurrency currency = loanProduct.getCurrency();
final Integer loanTermFrequency = command.getLoanTermFrequency();
final PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.fromInt(command.getLoanTermFrequencyType());
final LoanProductRelatedDetail loanRepaymentScheduleDetail = assembleLoanProductRelatedDetailFrom(command, currency);
@Autowired
public LoanAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository,
final ClientRepository clientRepository, final GroupRepository groupRepository, final FundRepository fundRepository,
final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository,
final StaffRepository staffRepository, final LoanScheduleAssembler loanScheduleAssembler,
final LoanChargeAssembler loanChargeAssembler) {
this.fromApiJsonHelper = fromApiJsonHelper;
this.loanProductRepository = loanProductRepository;
this.clientRepository = clientRepository;
this.groupRepository = groupRepository;
this.fundRepository = fundRepository;
this.loanTransactionProcessingStrategyRepository = loanTransactionProcessingStrategyRepository;
this.staffRepository = staffRepository;
this.loanScheduleAssembler = loanScheduleAssembler;
this.loanChargeAssembler = loanChargeAssembler;
}
// associating fund with loan product at creation is optional for now.
final Fund fund = findFundByIdIfProvided(command.getFundId());
final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy = findStrategyByIdIfProvided(command.getTransactionProcessingStrategyId());
public Loan assembleFrom(final JsonCommand command) {
final JsonElement element = command.parsedJson();
//optionally associate a loan officer to the loan
final Staff loanOfficer= findLoanOfficerByIdIfProvided(command.getLoanOfficerId());
// optionally, see if charges are associated with loan on creation (through loan product or by being directly added)
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.assembleFrom(command.getCharges(), loanProduct.getCharges(), command.getPrincipal());
final Long clientId = fromApiJsonHelper.extractLongNamed("clientId", element);
final Long groupId = fromApiJsonHelper.extractLongNamed("groupId", element);
Loan loan = null;
if (group != null) {
loan = Loan.createNew(fund,loanOfficer, loanTransactionProcessingStrategy, loanProduct, group, loanRepaymentScheduleDetail, loanCharges);
loan.setExternalId(command.getExternalId());
}
if (client != null) {
loan = Loan.createNew(fund,loanOfficer, loanTransactionProcessingStrategy, loanProduct, client, loanRepaymentScheduleDetail, loanCharges);
loan.setExternalId(command.getExternalId());
}
if (loan == null) {
// FIXME - kw - put in appropriate exception here.
throw new RuntimeException();
}
return assembleApplication(element, clientId, groupId);
}
final LoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanSchedule(command.toCalculateLoanScheduleCommand());
for (LoanSchedulePeriodData scheduledLoanInstallment : loanSchedule.getPeriods()) {
if (scheduledLoanInstallment.isRepaymentPeriod()) {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(
loan,
scheduledLoanInstallment.periodNumber(),
scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(),
scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(),
scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue());
loan.addRepaymentScheduleInstallment(installment);
}
}
private Loan assembleApplication(final JsonElement element, final Long clientId, final Long groupId) {
loan.submitApplication(loanTermFrequency, loanTermFrequencyType,
command.getSubmittedOnDate(), command.getExpectedDisbursementDate(),
command.getRepaymentsStartingFromDate(), command.getInterestChargedFromDate(),
defaultLoanLifecycleStateMachine());
return loan;
}
final Long productId = fromApiJsonHelper.extractLongNamed("productId", element);
final Long fundId = fromApiJsonHelper.extractLongNamed("fundId", element);
final Long loanOfficerId = fromApiJsonHelper.extractLongNamed("loanOfficerId", element);
final Long transactionProcessingStrategyId = fromApiJsonHelper.extractLongNamed("transactionProcessingStrategyId", element);
public LoanProductRelatedDetail assembleLoanProductRelatedDetailFrom(final LoanApplicationCommand command, MonetaryCurrency currency) {
final BigDecimal defaultNominalInterestRatePerPeriod = command.getInterestRatePerPeriod();
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyType());
BigDecimal defaultAnnualNominalInterestRate = aprCalculator.calculateFrom(interestPeriodFrequencyType, command.getInterestRatePerPeriod());
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType());
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodType());
final Integer defaultNumberOfInstallments = command.getNumberOfRepayments();
final Integer repayEvery = command.getRepaymentEvery();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequencyType());
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationType());
LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency,
command.getPrincipal(), defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate,
interestMethod, interestCalculationPeriodMethod,
repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, command.getInArrearsTolerance());
return loanRepaymentScheduleDetail;
}
final LoanProduct loanProduct = this.loanProductRepository.findOne(productId);
if (loanProduct == null) { throw new LoanProductNotFoundException(productId); }
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values());
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
}
public Fund findFundByIdIfProvided(final Long fundId) {
Fund fund = null;
if (fundId != null) {
fund = this.fundRepository.findOne(fundId);
if (fund == null) {
throw new FundNotFoundException(fundId);
}
}
return fund;
}
public Staff findLoanOfficerByIdIfProvided(final Long loanOfficerId) {
Staff staff = null;
if (loanOfficerId != null) {
staff = this.staffRepository.findOne(loanOfficerId);
if (staff == null) {
throw new StaffNotFoundException(loanOfficerId);
} else if (staff.isNotLoanOfficer()) {
throw new StaffRoleException(loanOfficerId,
StaffRoleException.STAFF_ROLE.LOAN_OFFICER);
}
}
return staff;
}
public LoanTransactionProcessingStrategy findStrategyByIdIfProvided(final Long transactionProcessingStrategyId) {
LoanTransactionProcessingStrategy strategy = null;
if (transactionProcessingStrategyId != null) {
strategy = this.loanTransactionProcessingStrategyRepository.findOne(transactionProcessingStrategyId);
if (strategy == null) {
throw new LoanTransactionProcessingStrategyNotFoundException(transactionProcessingStrategyId);
}
}
return strategy;
}
final Fund fund = findFundByIdIfProvided(fundId);
final Staff loanOfficer = findLoanOfficerByIdIfProvided(loanOfficerId);
final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy = findStrategyByIdIfProvided(transactionProcessingStrategyId);
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element);
final BigDecimal inArrearsTolerance = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element);
LoanSchedule loanSchedule = this.loanScheduleAssembler.fromJson(element, inArrearsTolerance);
Loan loanApplication = null;
if (clientId != null) {
final Client client = this.clientRepository.findOne(clientId);
if (client == null || client.isDeleted()) { throw new ClientNotFoundException(clientId); }
loanApplication = Loan.newIndividualLoanApplication(client, loanProduct, fund, loanOfficer, loanTransactionProcessingStrategy,
loanSchedule, loanCharges);
}
if (groupId != null) {
final Group group = this.groupRepository.findOne(groupId);
if (group == null || group.isDeleted()) { throw new GroupNotFoundException(groupId); }
loanApplication = Loan.newGroupLoanApplication(group, loanProduct, fund, loanOfficer, loanTransactionProcessingStrategy,
loanSchedule, loanCharges);
}
final String externalId = fromApiJsonHelper.extractStringNamed("externalId", element);
final LocalDate submittedOnDate = fromApiJsonHelper.extractLocalDateNamed("submittedOnDate", element);
if (loanApplication == null) { throw new IllegalStateException("No loan application exists for either a client or group (or both)."); }
loanApplication.loanApplicationSubmittal(loanSchedule, defaultLoanLifecycleStateMachine(), submittedOnDate, externalId);
return loanApplication;
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values());
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
}
public Fund findFundByIdIfProvided(final Long fundId) {
Fund fund = null;
if (fundId != null) {
fund = this.fundRepository.findOne(fundId);
if (fund == null) { throw new FundNotFoundException(fundId); }
}
return fund;
}
public Staff findLoanOfficerByIdIfProvided(final Long loanOfficerId) {
Staff staff = null;
if (loanOfficerId != null) {
staff = this.staffRepository.findOne(loanOfficerId);
if (staff == null) {
throw new StaffNotFoundException(loanOfficerId);
} else if (staff.isNotLoanOfficer()) { throw new StaffRoleException(loanOfficerId, StaffRoleException.STAFF_ROLE.LOAN_OFFICER); }
}
return staff;
}
public LoanTransactionProcessingStrategy findStrategyByIdIfProvided(final Long transactionProcessingStrategyId) {
LoanTransactionProcessingStrategy strategy = null;
if (transactionProcessingStrategyId != null) {
strategy = this.loanTransactionProcessingStrategyRepository.findOne(transactionProcessingStrategyId);
if (strategy == null) { throw new LoanTransactionProcessingStrategyNotFoundException(transactionProcessingStrategyId); }
}
return strategy;
}
}
@@ -2,81 +2,99 @@ package org.mifosplatform.portfolio.loanaccount.service;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.charge.domain.Charge;
import org.mifosplatform.portfolio.charge.domain.ChargeCalculationType;
import org.mifosplatform.portfolio.charge.domain.ChargeRepository;
import org.mifosplatform.portfolio.charge.domain.ChargeTimeType;
import org.mifosplatform.portfolio.charge.exception.ChargeIsNotActiveException;
import org.mifosplatform.portfolio.charge.exception.ChargeNotFoundException;
import org.mifosplatform.portfolio.charge.exception.LoanChargeNotFoundException;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanChargeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@Service
public class LoanChargeAssembler {
private final ChargeRepository chargeRepository;
private final LoanChargeRepository loanChargeRepository;
private final FromJsonHelper fromApiJsonHelper;
private final ChargeRepository chargeRepository;
private final LoanChargeRepository loanChargeRepository;
@Autowired
public LoanChargeAssembler(final ChargeRepository chargeRepository, final LoanChargeRepository loanChargeRepository) {
this.chargeRepository = chargeRepository;
this.loanChargeRepository = loanChargeRepository;
}
public Set<LoanCharge> assembleFrom(final LoanChargeCommand[] charges, final BigDecimal loanPrincipal) {
final Set<LoanCharge> loanCharges = new HashSet<LoanCharge>();
if (charges != null) {
for (LoanChargeCommand loanChargeCommand : charges) {
if (loanChargeCommand.getId() == null) {
final Long chargeDefinitionId = loanChargeCommand.getChargeId();
final Charge chargeDefinition = this.chargeRepository.findOne(chargeDefinitionId);
if (chargeDefinition == null || chargeDefinition.isDeleted()) {
throw new ChargeNotFoundException(chargeDefinitionId);
}
if (!chargeDefinition.isActive()) {
throw new ChargeIsNotActiveException(chargeDefinitionId, chargeDefinition.getName());
}
final LoanCharge loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, loanChargeCommand, loanPrincipal);
loanCharges.add(loanCharge);
} else {
final Long loanChargeId = loanChargeCommand.getId();
final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeId);
if (loanCharge == null) {
throw new LoanChargeNotFoundException(loanChargeId);
}
loanCharge.update(loanChargeCommand, loanPrincipal);
loanCharges.add(loanCharge);
}
}
}
return loanCharges;
}
@Autowired
public LoanChargeAssembler(final FromJsonHelper fromApiJsonHelper, final ChargeRepository chargeRepository,
final LoanChargeRepository loanChargeRepository) {
this.fromApiJsonHelper = fromApiJsonHelper;
this.chargeRepository = chargeRepository;
this.loanChargeRepository = loanChargeRepository;
}
public Set<LoanCharge> assembleFrom(final LoanChargeCommand[] charges, final Set<Charge> chargesInheritedFromProduct, final BigDecimal loanPrincipal) {
Set<LoanCharge> loanCharges = new HashSet<LoanCharge>();
if (charges != null) {
loanCharges = assembleFrom(charges, loanPrincipal);
}
// TODO - kw - dont inherit charges from product by default, user may remove charges (that were default) during creation - expect
// charges to be passed along in request at time of creation
// else {
// for (Charge productCharge : chargesInheritedFromProduct) {
// loanCharges.add(LoanCharge.createNew(productCharge));
// }
// }
return loanCharges;
}
public Set<LoanCharge> fromParsedJson(final JsonElement element) {
final Set<LoanCharge> loanCharges = new HashSet<LoanCharge>();
final BigDecimal principal = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element);
if (element.isJsonObject()) {
final JsonObject topLevelJsonElement = element.getAsJsonObject();
final String dateFormat = fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement);
final Locale locale = fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement);
if (topLevelJsonElement.has("charges") && topLevelJsonElement.get("charges").isJsonArray()) {
final JsonArray array = topLevelJsonElement.get("charges").getAsJsonArray();
for (int i = 0; i < array.size(); i++) {
final JsonObject loanChargeElement = array.get(i).getAsJsonObject();
final Long id = fromApiJsonHelper.extractLongNamed("id", loanChargeElement);
final Long chargeId = fromApiJsonHelper.extractLongNamed("chargeId", loanChargeElement);
final BigDecimal amount = fromApiJsonHelper.extractBigDecimalNamed("amount", loanChargeElement, locale);
final Integer chargeTimeType = fromApiJsonHelper.extractIntegerNamed("chargeTimeType", loanChargeElement, locale);
final Integer chargeCalculationType = fromApiJsonHelper.extractIntegerNamed("chargeCalculationType", loanChargeElement,
locale);
final LocalDate specifiedDueDate = fromApiJsonHelper.extractLocalDateNamed("specifiedDueDate", loanChargeElement,
dateFormat, locale);
if (id == null) {
final Charge chargeDefinition = this.chargeRepository.findOne(chargeId);
if (chargeDefinition == null || chargeDefinition.isDeleted()) { throw new ChargeNotFoundException(chargeId); }
if (!chargeDefinition.isActive()) { throw new ChargeIsNotActiveException(chargeId, chargeDefinition.getName()); }
ChargeTimeType chargeTime = null;
if (chargeTimeType != null) {
ChargeTimeType.fromInt(chargeTimeType);
}
ChargeCalculationType chargeCalculation = null;
if (chargeCalculationType != null) {
ChargeCalculationType.fromInt(chargeCalculationType);
}
final LoanCharge loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, principal, amount, chargeTime,
chargeCalculation, specifiedDueDate);
loanCharges.add(loanCharge);
} else {
final Long loanChargeId = id;
final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeId);
if (loanCharge == null) { throw new LoanChargeNotFoundException(loanChargeId); }
loanCharge.update(amount, specifiedDueDate, principal);
loanCharges.add(loanCharge);
}
}
} else {
// no charges passed, use existing ones against loan
}
}
return loanCharges;
}
}
@@ -3,8 +3,8 @@ package org.mifosplatform.portfolio.loanaccount.service;
import java.math.BigDecimal;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.portfolio.loanaccount.loanschedule.command.CalculateLoanScheduleCommand;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.query.CalculateLoanScheduleQuery;
import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod;
@@ -18,48 +18,45 @@ import org.springframework.stereotype.Service;
@Service
public class LoanProductRelatedDetailAssembler {
private final LoanProductRepository loanProductRepository;
private final AprCalculator aprCalculator;
@Autowired
public LoanProductRelatedDetailAssembler(
final LoanProductRepository loanProductRepository,
final AprCalculator aprCalculator) {
this.loanProductRepository = loanProductRepository;
this.aprCalculator = aprCalculator;
}
private final LoanProductRepository loanProductRepository;
private final AprCalculator aprCalculator;
public LoanProductRelatedDetail assembleFrom(final CalculateLoanScheduleCommand command) {
final BigDecimal principalAmount = command.getPrincipal();
final BigDecimal defaultNominalInterestRatePerPeriod = command.getInterestRatePerPeriod();
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyType());
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType());
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodType());
final Integer defaultNumberOfInstallments = command.getNumberOfRepayments();
final Integer repayEvery = command.getRepaymentEvery();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequencyType());
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationType());
final BigDecimal defaultAnnualNominalInterestRate = this.aprCalculator.calculateFrom(interestPeriodFrequencyType, defaultNominalInterestRatePerPeriod);
final LoanProduct loanProduct = this.loanProductRepository.findOne(command.getProductId());
if (loanProduct == null) {
throw new LoanProductNotFoundException(command.getProductId());
}
final MonetaryCurrency currency = loanProduct.getCurrency();
// in arrerars tolerance isnt relevant when auto-calculating a loan schedule
final BigDecimal inArrearsTolerance = BigDecimal.ZERO;
return new LoanProductRelatedDetail(currency, principalAmount,
defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate,
interestMethod, interestCalculationPeriodMethod,
repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod,
inArrearsTolerance);
}
@Autowired
public LoanProductRelatedDetailAssembler(final LoanProductRepository loanProductRepository, final AprCalculator aprCalculator) {
this.loanProductRepository = loanProductRepository;
this.aprCalculator = aprCalculator;
}
public LoanProductRelatedDetail assembleFrom(final CalculateLoanScheduleQuery command) {
final BigDecimal principalAmount = command.getPrincipal();
final BigDecimal defaultNominalInterestRatePerPeriod = command.getInterestRatePerPeriod();
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyType());
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType());
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command
.getInterestCalculationPeriodType());
final Integer defaultNumberOfInstallments = command.getNumberOfRepayments();
final Integer repayEvery = command.getRepaymentEvery();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequencyType());
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationType());
final BigDecimal defaultAnnualNominalInterestRate = this.aprCalculator.calculateFrom(interestPeriodFrequencyType,
defaultNominalInterestRatePerPeriod);
final LoanProduct loanProduct = this.loanProductRepository.findOne(command.getProductId());
if (loanProduct == null) { throw new LoanProductNotFoundException(command.getProductId()); }
final MonetaryCurrency currency = loanProduct.getCurrency();
// in arrerars tolerance isnt relevant when auto-calculating a loan
// schedule
final BigDecimal inArrearsTolerance = BigDecimal.ZERO;
return new LoanProductRelatedDetail(currency, principalAmount, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType,
defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, repayEvery, repaymentFrequencyType,
defaultNumberOfInstallments, amortizationMethod, inArrearsTolerance);
}
}
@@ -1,73 +1,62 @@
package org.mifosplatform.portfolio.loanaccount.service;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EntityIdentifier;
import org.mifosplatform.organisation.staff.command.BulkTransferLoanOfficerCommand;
import org.mifosplatform.portfolio.loanaccount.command.AdjustLoanTransactionCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanApplicationCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanStateTransitionCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanTransactionCommand;
import org.mifosplatform.portfolio.loanaccount.command.UndoStateTransitionCommand;
import org.springframework.security.access.prepost.PreAuthorize;
public interface LoanWritePlatformService {
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'CREATE_LOAN', 'CREATEHISTORIC_LOAN')")
EntityIdentifier submitLoanApplication(LoanApplicationCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'UPDATE_LOAN', 'UPDATEHISTORIC_LOAN')")
EntityIdentifier modifyLoanApplication(LoanApplicationCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'DELETE_LOAN')")
EntityIdentifier deleteLoan(Long loanId);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'APPROVE_LOAN', 'APPROVEINPAST_LOAN')")
EntityIdentifier approveLoanApplication(LoanStateTransitionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'APPROVALUNDO_LOAN')")
EntityIdentifier undoLoanApproval(UndoStateTransitionCommand command);
EntityIdentifier submitLoanApplication(JsonCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'REJECT_LOAN', 'REJECTINPAST_LOAN')")
EntityIdentifier rejectLoan(LoanStateTransitionCommand command);
EntityIdentifier modifyLoanApplication(Long loanId, JsonCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'WITHDRAW_LOAN', 'WITHDRAWINPAST_LOAN')")
EntityIdentifier withdrawLoan(LoanStateTransitionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'DISBURSE_LOAN', 'DISBURSEINPAST_LOAN')")
EntityIdentifier disburseLoan(LoanStateTransitionCommand command);
EntityIdentifier deleteLoan(Long loanId);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'DISBURSALUNDO_LOAN')")
public EntityIdentifier undoLoanDisbursal(UndoStateTransitionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'REPAYMENT_LOAN', 'REPAYMENTINPAST_LOAN')")
public EntityIdentifier makeLoanRepayment(LoanTransactionCommand command);
EntityIdentifier approveLoanApplication(Long loanId, JsonCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'ADJUST_LOAN')")
EntityIdentifier adjustLoanTransaction(AdjustLoanTransactionCommand command);
EntityIdentifier undoLoanApplicationApproval(Long loanId, JsonCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'WAIVEINTERESTPORTION_LOAN')")
EntityIdentifier waiveInterestOnLoan(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'WRITEOFF_LOAN')")
EntityIdentifier writeOff(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'CLOSE_LOAN')")
EntityIdentifier closeLoan(LoanTransactionCommand command);
EntityIdentifier rejectLoanApplication(Long loanId, JsonCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'CLOSEASRESCHEDULED_LOAN')")
EntityIdentifier closeAsRescheduled(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'CREATE_LOANCHARGE')")
EntityIdentifier applicantWithdrawsFromLoanApplication(Long loanId, JsonCommand command);
EntityIdentifier disburseLoan(Long loanId, JsonCommand command);
public EntityIdentifier undoLoanDisbursal(Long loanId, JsonCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'REPAYMENT_LOAN', 'REPAYMENTINPAST_LOAN')")
public EntityIdentifier makeLoanRepayment(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'ADJUST_LOAN')")
EntityIdentifier adjustLoanTransaction(AdjustLoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'WAIVEINTERESTPORTION_LOAN')")
EntityIdentifier waiveInterestOnLoan(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'WRITEOFF_LOAN')")
EntityIdentifier writeOff(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'CLOSE_LOAN')")
EntityIdentifier closeLoan(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'CLOSEASRESCHEDULED_LOAN')")
EntityIdentifier closeAsRescheduled(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'CREATE_LOANCHARGE')")
EntityIdentifier addLoanCharge(LoanChargeCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'UPDATE_LOANCHARGE')")
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'UPDATE_LOANCHARGE')")
EntityIdentifier updateLoanCharge(LoanChargeCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'DELETE_LOANCHARGE')")
EntityIdentifier deleteLoanCharge(final Long loanId, final Long loanChargeId);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'WAIVE_LOANCHARGE')")
EntityIdentifier waiveLoanCharge(LoanChargeCommand command);
EntityIdentifier waiveLoanCharge(LoanChargeCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'BULKREASSIGN_LOAN')")
EntityIdentifier bulkLoanReassignment(final BulkTransferLoanOfficerCommand command);
@@ -138,9 +138,9 @@ public class LoanProduct extends AbstractAuditableCustom<AppUser, Long> {
this.charges = charges;
}
public Map<String, Object> update(final JsonCommand command) {
public Map<String, Object> update(final JsonCommand command, final AprCalculator aprCalculator) {
final Map<String, Object> actualChanges = this.loanProductRelatedDetail.update(command);
final Map<String, Object> actualChanges = this.loanProductRelatedDetail.update(command, aprCalculator);
final String nameParamName = "name";
if (command.isChangeInStringParameterNamed(nameParamName, this.name)) {
@@ -14,6 +14,7 @@ import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
/**
* LoanRepaymentScheduleDetail encapsulates all the details of a
@@ -31,6 +32,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
@Column(name = "nominal_interest_rate_per_period", scale = 6, precision = 19, nullable = false)
private BigDecimal nominalInterestRatePerPeriod;
// FIXME - move away form JPA ordinal use for enums using just integer -
// requires sql patch for existing users of software.
@Enumerated(EnumType.ORDINAL)
@Column(name = "interest_period_frequency_enum", nullable = false)
private PeriodFrequencyType interestPeriodFrequencyType;
@@ -38,10 +41,14 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
@Column(name = "annual_nominal_interest_rate", scale = 6, precision = 19, nullable = false)
private BigDecimal annualNominalInterestRate;
// FIXME - move away form JPA ordinal use for enums using just integer -
// requires sql patch for existing users of software.
@Enumerated(EnumType.ORDINAL)
@Column(name = "interest_method_enum", nullable = false)
private InterestMethod interestMethod;
// FIXME - move away form JPA ordinal use for enums using just integer -
// requires sql patch for existing users of software.
@Enumerated(EnumType.ORDINAL)
@Column(name = "interest_calculated_in_period_enum", nullable = false)
private InterestCalculationPeriodMethod interestCalculationPeriodMethod;
@@ -49,6 +56,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
@Column(name = "repay_every", nullable = false)
private Integer repayEvery;
// FIXME - move away form JPA ordinal use for enums using just integer -
// requires sql patch for existing users of software.
@Enumerated(EnumType.ORDINAL)
@Column(name = "repayment_period_frequency_enum", nullable = false)
private PeriodFrequencyType repaymentPeriodFrequencyType;
@@ -56,6 +65,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
@Column(name = "number_of_repayments", nullable = false)
private Integer numberOfRepayments;
// FIXME - move away form JPA ordinal use for enums using just integer -
// requires sql patch for existing users of software.
@Enumerated(EnumType.ORDINAL)
@Column(name = "amortization_method_enum", nullable = false)
private AmortizationMethod amortizationMethod;
@@ -63,6 +74,18 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
@Column(name = "arrearstolerance_amount", scale = 6, precision = 19, nullable = true)
private BigDecimal inArrearsTolerance;
public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal,
final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType,
final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod,
final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Integer repaymentEvery,
final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer numberOfRepayments,
final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance) {
return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType,
nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentPeriodFrequencyType,
numberOfRepayments, amortizationMethod, inArrearsTolerance);
}
protected LoanProductRelatedDetail() {
//
}
@@ -142,7 +165,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
return amortizationMethod;
}
public Map<String, Object> update(final JsonCommand command) {
public Map<String, Object> update(final JsonCommand command, final AprCalculator aprCalculator) {
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>(20);
@@ -168,6 +191,18 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
this.currency = new MonetaryCurrency(currencyCode, digitsAfterDecimal);
}
final Map<String, Object> loanApplicationAttributeChanges = updateLoanApplicationAttributes(command, aprCalculator);
actualChanges.putAll(loanApplicationAttributeChanges);
return actualChanges;
}
public Map<String, Object> updateLoanApplicationAttributes(final JsonCommand command, final AprCalculator aprCalculator) {
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>(20);
final String localeAsInput = command.locale();
final String principalParamName = "principal";
if (command.isChangeInBigDecimalParameterNamed(principalParamName, this.principal)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(principalParamName);
@@ -222,6 +257,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
actualChanges.put(interestRatePerPeriodParamName, newValue);
actualChanges.put("locale", localeAsInput);
this.nominalInterestRatePerPeriod = newValue;
updateInterestRateDerivedFields(aprCalculator);
}
final String interestRateFrequencyTypeParamName = "interestRateFrequencyType";
@@ -230,6 +266,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
actualChanges.put(interestRateFrequencyTypeParamName, newValue);
actualChanges.put("locale", localeAsInput);
this.interestPeriodFrequencyType = PeriodFrequencyType.fromInt(newValue);
updateInterestRateDerivedFields(aprCalculator);
}
final String interestTypeParamName = "interestType";
@@ -251,4 +288,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
return actualChanges;
}
private void updateInterestRateDerivedFields(final AprCalculator aprCalculator) {
this.annualNominalInterestRate = aprCalculator.calculateFrom(this.interestPeriodFrequencyType, this.nominalInterestRatePerPeriod);
}
}
@@ -108,7 +108,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
final LoanProduct product = this.loanProductRepository.findOne(loanProductId);
if (product == null) { throw new LoanProductNotFoundException(loanProductId); }
final Map<String, Object> changes = product.update(command);
final Map<String, Object> changes = product.update(command, this.aprCalculator);
// associating fund with loan product at creation is optional for now.
if (changes.containsKey("fundId")) {