MIFOSX-172: WIP support loan writeoff, closure scenarios with accounting.

This commit is contained in:
Keith Woodlock
2013-02-19 12:59:51 +00:00
parent 87eb0a6fe8
commit 524206f0ff
25 changed files with 240 additions and 372 deletions
@@ -7,7 +7,6 @@ package org.mifosplatform.accounting.journalentry.service;
import java.util.Map;
import org.mifosplatform.accounting.glaccount.data.LoanDTO;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
@@ -18,7 +17,4 @@ public interface JournalEntryWritePlatformService {
CommandProcessingResult revertJournalEntry(JsonCommand command);
void createJournalEntriesForLoan(Map<String, Object> accountingBridgeData);
void createJournalEntriesForLoan(LoanDTO loanDTO);
}
@@ -140,17 +140,6 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
return new CommandProcessingResultBuilder().withTransactionId(reversalTransactionId).build();
}
@Transactional
@Override
public void createJournalEntriesForLoan(final LoanDTO loanDTO) {
// shouldn't be before an accounting closure
if (loanDTO.isCashBasedAccountingEnabled()) {
createJournalEntriesUsingCashRules(loanDTO);
} else if (loanDTO.isAccrualBasedAccountingEnabled()) {
// TODO Vishwas: add accrual based accounting rules
}
}
@Transactional
@Override
public void createJournalEntriesForLoan(final Map<String, Object> accountingBridgeData) {
@@ -33,10 +33,11 @@ public class ApiRequestJsonSerializationSettings {
public static ApiRequestJsonSerializationSettings from(final boolean prettyPrint, final Set<String> parametersForPartialResponse,
final boolean template, final Long commandId, final boolean makerCheckerable, final boolean includeJson) {
// FIXME - kw - rather than always creating new objects for this could
// FIXME - KW - rather than always creating new objects for this could
// just send by common ones like, prettyprint=false, empty response
// parameters
return new ApiRequestJsonSerializationSettings(prettyPrint, parametersForPartialResponse, template, commandId, makerCheckerable, includeJson);
return new ApiRequestJsonSerializationSettings(prettyPrint, parametersForPartialResponse, template, commandId, makerCheckerable,
includeJson);
}
public boolean isPrettyPrint() {
@@ -50,7 +51,7 @@ public class ApiRequestJsonSerializationSettings {
public boolean isMakerCheckerable() {
return this.makerCheckerable;
}
public boolean isIncludeJson() {
return this.includeJson;
}
@@ -27,7 +27,7 @@ import org.springframework.stereotype.Service;
import com.sun.rowset.CachedRowSetImpl;
//TODO - Performance Item - most items (code values etc) can be cached but not doing that yet
//TODO - JW - Performance Item - most items (code values etc) can be cached but not doing that yet
@Service
public class GenericDataServiceImpl implements GenericDataService {
@@ -41,8 +41,8 @@ public class GenericDataServiceImpl implements GenericDataService {
@Override
public CachedRowSet getCachedResultSet(final String sql, final String sqlErrorMsg) {
// FIXME - Need to reimplement this away from Sun library - could be
// mixture of Lists and jdbcTemplate.query
// FIXME - KW - Need to reimplement this away from Sun library - could
// be mixture of Lists and jdbcTemplate.query
// long startTime = System.currentTimeMillis();
Connection db_connection = null;
@@ -195,7 +195,7 @@ public class GenericDataServiceImpl implements GenericDataService {
writer.append("[" + localDate.getYear() + ", " + localDate.getMonthOfYear() + ", " + localDate.getDayOfMonth()
+ "]");
} else
writer.append(doubleQuote + replace(currVal, doubleQuote,slashDoubleQuote) + doubleQuote);
writer.append(doubleQuote + replace(currVal, doubleQuote, slashDoubleQuote) + doubleQuote);
}
} else
writer.append("null");
@@ -82,7 +82,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
return new StreamingOutput() {
@Override
public void write(OutputStream out) {
public void write(final OutputStream out) {
try {
GenericResultsetData result = retrieveGenericResultset(name, type, queryParams);
@@ -108,7 +108,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
}
private static StringBuffer generateCsvFileBuffer(GenericResultsetData result) {
private static StringBuffer generateCsvFileBuffer(final GenericResultsetData result) {
StringBuffer writer = new StringBuffer();
List<ResultsetColumnHeader> columnHeaders = result.getColumnHeaders();
@@ -206,7 +206,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
}
private String getSql(String name, String type) {
private String getSql(final String name, final String type) {
String inputSql = "select " + type + "_sql as the_sql from stretchy_" + type + " where " + type + "_name = '" + name + "'";
inputSql = genericDataService.wrapSQL(inputSql);
@@ -226,7 +226,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
}
@Override
public String getReportType(String reportName) {
public String getReportType(final String reportName) {
String sql = "SELECT ifnull(report_type,'') as report_type FROM `stretchy_report` where report_name = '" + reportName + "'";
sql = genericDataService.wrapSQL(sql);
@@ -246,7 +246,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
}
@Override
public Response processPentahoRequest(String reportName, String outputTypeParam, Map<String, String> queryParams) {
public Response processPentahoRequest(final String reportName, final String outputTypeParam, final Map<String, String> queryParams) {
String outputType = "HTML";
if (StringUtils.isNotBlank(outputTypeParam)) outputType = outputTypeParam;
@@ -258,7 +258,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
if (noPentaho)
throw new PlatformDataIntegrityException("error.msg.no.pentaho", "Pentaho is not enabled", "Pentaho is not enabled");
// TODO - use pentaho location finder like Pawel does in Mifos
// TODO - JW - use pentaho location finder like Pawel does in Mifos
// String reportPath =
// "C:\\dev\\apache-tomcat-7.0.25\\webapps\\ROOT\\PentahoReports\\"
// + reportName + ".prpt";
@@ -313,7 +313,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
}
private void addParametersToReport(MasterReport report, Map<String, String> queryParams) {
private void addParametersToReport(final MasterReport report, final Map<String, String> queryParams) {
try {
ReportParameterValues rptParamValues = report.getParameterValues();
@@ -103,7 +103,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
String updatePermissionChecker = "'UPDATE_" + datatable + "_CHECKER'";
String deletePermission = "'DELETE_" + datatable + "'";
String deletePermissionChecker = "'DELETE_" + datatable + "_CHECKER'";
// TODO - put in batch command later
// TODO - JW - put in batch command later
String sql = "insert into x_registered_table (registered_table_name, application_table_name) values ('" + datatable + "', '"
+ appTable + "')";
@@ -125,7 +125,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
@Override
public void deregisterDatatable(final String datatable) {
// TODO - put in batch command later
// TODO - JW - put in batch command later
String permissionList = "('CREATE_" + datatable + "', 'CREATE_" + datatable + "_CHECKER', 'READ_" + datatable + "', 'UPDATE_"
+ datatable + "', 'UPDATE_" + datatable + "_CHECKER', 'DELETE_" + datatable + "', 'DELETE_" + datatable + "_CHECKER')";
@@ -356,7 +356,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
}
private String getApplicationTableName(final String datatable) {
// TODO - only used for update... can probably remove this after as its
// TODO - JW - only used for update... can probably remove this after as
// its
// a reread
String sql = "SELECT application_table_name FROM x_registered_table where registered_table_name = '" + datatable + "'";
@@ -37,7 +37,7 @@ import com.sun.jersey.core.header.FormDataContentDisposition;
import com.sun.jersey.multipart.FormDataBodyPart;
import com.sun.jersey.multipart.FormDataParam;
// FIXME - kw- image should be images to keep consitent with rest of API - even if we only support one image per client.
// FIXME - KW - image should be images to keep consistent with rest of API - even if we only support one image per client.
@Path("/clients/{clientId}/image")
@Component
@Scope("singleton")
@@ -63,9 +63,9 @@ public class ClientImagesApiResource {
@POST
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@Produces({ MediaType.APPLICATION_JSON })
public String addNewClientImage(@PathParam("clientId") final Long clientId, @HeaderParam("Content-Length") Long fileSize,
@FormDataParam("file") InputStream inputStream, @FormDataParam("file") FormDataContentDisposition fileDetails,
@FormDataParam("file") FormDataBodyPart bodyPart) {
public String addNewClientImage(@PathParam("clientId") final Long clientId, @HeaderParam("Content-Length") final Long fileSize,
@FormDataParam("file") final InputStream inputStream, @FormDataParam("file") final FormDataContentDisposition fileDetails,
@FormDataParam("file") final FormDataBodyPart bodyPart) {
// TODO: vishwas might need more advances validation (like reading magic
// number) for handling malicious clients
@@ -119,9 +119,9 @@ public class ClientImagesApiResource {
@PUT
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@Produces({ MediaType.APPLICATION_JSON })
public String updateClientImage(@PathParam("clientId") final Long clientId, @HeaderParam("Content-Length") Long fileSize,
@FormDataParam("file") InputStream inputStream, @FormDataParam("file") FormDataContentDisposition fileDetails,
@FormDataParam("file") FormDataBodyPart bodyPart) {
public String updateClientImage(@PathParam("clientId") final Long clientId, @HeaderParam("Content-Length") final Long fileSize,
@FormDataParam("file") final InputStream inputStream, @FormDataParam("file") final FormDataContentDisposition fileDetails,
@FormDataParam("file") final FormDataBodyPart bodyPart) {
return addNewClientImage(clientId, fileSize, inputStream, fileDetails, bodyPart);
}
@@ -50,7 +50,7 @@ public class ClientIdentifierCommand {
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
public void validateForUpdate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
@@ -58,10 +58,10 @@ public class ClientIdentifierCommand {
baseDataValidator.reset().parameter("documentKey").value(this.documentKey).ignoreIfNull().notBlank();
// FIXME - kw
// if (command.isDocumentTypeChanged()) {
// baseDataValidator.reset().parameter("documentTypeId").value(command.getDocumentTypeId()).notNull().integerGreaterThanZero();
// }
// FIXME - KW - add in validation
// if (command.isDocumentTypeChanged()) {
// baseDataValidator.reset().parameter("documentTypeId").value(command.getDocumentTypeId()).notNull().integerGreaterThanZero();
// }
baseDataValidator.reset().anyOfNotNull(this.documentTypeId, this.documentKey);
@@ -96,7 +96,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
}
@Override
public Collection<ClientLookup> retrieveAllIndividualClientsForLookup(String extraCriteria) {
public Collection<ClientLookup> retrieveAllIndividualClientsForLookup(final String extraCriteria) {
this.context.authenticatedUser();
@@ -112,7 +112,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
}
@Override
public Collection<ClientLookup> retrieveAllIndividualClientsForLookupByOfficeId(Long officeId) {
public Collection<ClientLookup> retrieveAllIndividualClientsForLookupByOfficeId(final Long officeId) {
this.context.authenticatedUser();
ClientLookupMapper rm = new ClientLookupMapper();
@@ -290,7 +290,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
}
@Override
public Collection<ClientAccountSummaryData> retrieveClientLoanAccountsByLoanOfficerId(Long clientId, Long loanOfficerId) {
public Collection<ClientAccountSummaryData> retrieveClientLoanAccountsByLoanOfficerId(final Long clientId, final Long loanOfficerId) {
this.context.authenticatedUser();
@@ -357,7 +357,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
}
@Override
public NoteData retrieveClientNote(Long clientId, Long noteId) {
public NoteData retrieveClientNote(final Long clientId, final Long noteId) {
try {
context.authenticatedUser();
@@ -365,7 +365,8 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
// Check if client exists
retrieveIndividualClient(clientId);
// FIXME - use join on sql query to fetch user information for note
// FIXME - KW - use join on sql query to fetch user information for
// note
// rather than fetching users?
Collection<AppUserData> allUsers = this.appUserReadPlatformService.retrieveAllUsers();
@@ -380,14 +381,14 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
}
@Override
public Collection<NoteData> retrieveAllClientNotes(Long clientId) {
public Collection<NoteData> retrieveAllClientNotes(final Long clientId) {
context.authenticatedUser();
// Check if client exists
retrieveIndividualClient(clientId);
// FIXME - use join on sql query to fetch user information for note
// FIXME - KW - use join on sql query to fetch user information for note
// rather than fetching users?
Collection<AppUserData> allUsers = this.appUserReadPlatformService.retrieveAllUsers();
@@ -402,7 +403,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
private final Collection<AppUserData> allUsers;
public NoteMapper(Collection<AppUserData> allUsers) {
public NoteMapper(final Collection<AppUserData> allUsers) {
this.allUsers = allUsers;
}
@@ -36,12 +36,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
// FIXME: - undo disbursal fails when transactions have notes against them
// FIXME: - modifying loan after disbursement
// FIXME: - does not take into account 'actual disbursement' date being different to 'expected'
// FIXME: - the schedule is 'generated' rather than been read from current loan schedule
// FIXME: - should not allow modify loan after disbursal (should be only restructure/reschedule)
// FIXME: - ui problem with default date for disburse dialog on some screens
@Path("/loans/{loanId}/transactions")
@Component
@Scope("singleton")
@@ -1,57 +0,0 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.command;
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;
/**
* Immutable command for loan transactions.
*/
public class LoanTransactionCommand {
private final LocalDate transactionDate;
private final BigDecimal transactionAmount;
private final String note;
public LoanTransactionCommand(final LocalDate paymentDate, final BigDecimal paymentAmount, final String note) {
this.transactionDate = paymentDate;
this.transactionAmount = paymentAmount;
this.note = note;
}
public void validate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
baseDataValidator.reset().parameter("transactionDate").value(this.transactionDate).notNull();
baseDataValidator.reset().parameter("transactionAmount").value(this.transactionAmount).notNull().positiveAmount();
baseDataValidator.reset().parameter("note").value(this.note).notExceedingLengthOf(1000);
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
public void validateNonMonetaryTransaction() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
baseDataValidator.reset().parameter("transactionDate").value(this.transactionDate).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); }
}
}
@@ -144,7 +144,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
}
private LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(Set<LoanCharge> charges) {
private LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set<LoanCharge> charges) {
LoanCharge earliestUnpaidCharge = null;
for (LoanCharge loanCharge : charges) {
@@ -175,6 +175,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (currentInstallment.isNotFullyCompleted()) {
principalPortion = principalPortion.plus(currentInstallment.writeOffOutstandingPrincipal(currency));
interestPortion = interestPortion.plus(currentInstallment.writeOffOutstandingInterest(currency));
feeChargesPortion = feeChargesPortion.plus(currentInstallment.writeOffOutstandingFeeCharges(currency));
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.writeOffOutstandingPenaltyCharges(currency));
}
}
@@ -387,8 +387,6 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor
.determineProcessor(this.transactionProcessingStrategy);
// TODO - only need to reprocess transactions against loan schedule
// if loan charge is added with due date before latest transaction.
if (!loanCharge.isDueAtDisbursement()) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement,
@@ -1351,7 +1349,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
loanTransactionDate, this.getDisbursementDate());
}
if (loanTransactionDate.isAfter(new LocalDate())) {
if (loanTransactionDate.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, loanTransactionDate);
}
@@ -1359,12 +1357,12 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
if (loanTransaction.isInterestWaiver()) {
Money totalInterestOutstandingOnLoan = getTotalInterestOutstandingOnLoan();
if (adjustedTransaction != null) {
totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount());
totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(loanCurrency()));
}
if (loanTransaction.getAmount(loanCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) {
final String errorMessage = "The amount of interest to waive cannot be greater than total interest outstanding on loan.";
throw new InvalidLoanStateTransitionException("waive.interest", "amount.exceeds.total.outstanding.interest", errorMessage,
loanTransaction.getAmount(), totalInterestOutstandingOnLoan.getAmount());
loanTransaction.getAmount(loanCurrency()), totalInterestOutstandingOnLoan.getAmount());
}
}
@@ -1616,53 +1614,61 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
public LoanTransaction closeAsWrittenOff(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final Map<String, Object> changes) {
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING,
LoanStatus.fromInt(this.loanStatus));
LoanTransaction loanTransaction = null;
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
changes.put("status", LoanEnumerations.status(this.loanStatus));
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed("transactionDate");
this.closedOnDate = writtenOffOnLocalDate.toDate();
this.writtenOffOnDate = writtenOffOnLocalDate.toDate();
changes.put("closedOnDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("writtenOffOnDate", command.stringValueOfParameterNamed("transactionDate"));
if (writtenOffOnLocalDate.isBefore(this.getDisbursementDate())) {
final String errorMessage = "The date on which a loan is written off cannot be before the loan disbursement date: "
+ getDisbursementDate().toString();
throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.before.submittal.date", errorMessage,
writtenOffOnLocalDate, getDisbursementDate());
}
if (writtenOffOnLocalDate.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is written off cannot be in the future.";
throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, writtenOffOnLocalDate);
}
loanTransaction = LoanTransaction.writeoff(this, writtenOffOnLocalDate);
final boolean isLastTransaction = isChronologicallyLatestTransaction(loanTransaction, loanTransactions);
if (!isLastTransaction) {
final String errorMessage = "The date of the writeoff transaction must occur on or before previous transactions.";
throw new InvalidLoanStateTransitionException("writeoff", "must.occur.on.or.after.other.transaction.dates", errorMessage,
writtenOffOnLocalDate);
}
this.loanTransactions.add(loanTransaction);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor
.determineProcessor(this.transactionProcessingStrategy);
loanRepaymentScheduleTransactionProcessor.handleWriteOff(loanTransaction, loanCurrency(), this.repaymentScheduleInstallments);
}
final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed("transactionDate");
this.closedOnDate = writtenOffOnLocalDate.toDate();
this.writtenOffOnDate = writtenOffOnLocalDate.toDate();
changes.put("closedOnDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("writtenOffOnDate", command.stringValueOfParameterNamed("transactionDate"));
if (writtenOffOnLocalDate.isBefore(this.getDisbursementDate())) {
final String errorMessage = "The date on which a loan is written off cannot be before the loan disbursement date: "
+ getDisbursementDate().toString();
throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.before.submittal.date", errorMessage,
writtenOffOnLocalDate, getDisbursementDate());
}
if (writtenOffOnLocalDate.isAfter(new LocalDate())) {
final String errorMessage = "The date on which a loan is written off cannot be in the future.";
throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, writtenOffOnLocalDate);
}
final LoanTransaction loanTransaction = LoanTransaction.writeoff(this, writtenOffOnLocalDate);
boolean isLastTransaction = isChronologicallyLatestTransaction(loanTransaction, loanTransactions);
if (!isLastTransaction) {
final String errorMessage = "The date of the writeoff transaction must occur on or before previous transactions.";
throw new InvalidLoanStateTransitionException("writeoff", "must.occur.on.or.after.other.transaction.dates", errorMessage,
writtenOffOnLocalDate);
}
this.loanTransactions.add(loanTransaction);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor
.determineProcessor(this.transactionProcessingStrategy);
loanRepaymentScheduleTransactionProcessor.handleWriteOff(loanTransaction, loanCurrency(), this.repaymentScheduleInstallments);
return loanTransaction;
}
public LoanTransaction close(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final Map<String, Object> changes) {
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final LocalDate closureDate = command.localDateValueOfParameterNamed("transactionDate");
@@ -1676,7 +1682,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
getDisbursementDate());
}
if (closureDate.isAfter(new LocalDate())) {
if (closureDate.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is closed cannot be in the future.";
throw new InvalidLoanStateTransitionException("close", "cannot.be.a.future.date", errorMessage, closureDate);
}
@@ -1712,10 +1718,9 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
if (isOverPaid()) {
final Money totalLoanOverpayment = calculateTotalOverpayment();
// FIXME - kw - use overpaymentTolerance setting when in place on
// loan product settings.
if (totalLoanOverpayment.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) {
// TODO - technically should set somewhere that this loan has
// TODO - KW - technically should set somewhere that this loan
// has
// 'overpaid' amount
updateLoanForClosure(closureDate, loanLifecycleStateMachine, changes);
} else if (totalLoanOverpayment.isGreaterThanZero()) {
@@ -1729,11 +1734,10 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
private Money getTotalOutstanding() {
Money totalOutstanding = Money.zero(loanCurrency());
// 1. check totalOutstanding value on loan
// 2. if null calculate from loan schedule
final LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
totalOutstanding = wrapper.calculateTotalOutstanding(loanCurrency(), this.repaymentScheduleInstallments);
return totalOutstanding;
}
@@ -1751,8 +1755,6 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
/**
* Behaviour added to comply with capability of previous mifos product to
* support easier transition to mifosx platform.
*
* @param changes
*/
public void closeAsMarkedForReschedule(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final Map<String, Object> changes) {
@@ -2013,11 +2015,11 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
private Money getTotalPaidInRepayments() {
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency());
Money cumulativePaid = Money.zero(loanCurrency());
for (LoanTransaction repayment : this.loanTransactions) {
if (repayment.isRepayment()) {
cumulativePaid = cumulativePaid.plus(repayment.getAmount());
cumulativePaid = cumulativePaid.plus(repayment.getAmount(loanCurrency()));
}
}
@@ -2209,20 +2211,15 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return officeId;
}
public boolean isAccountingEnabledOnLoanProduct() {
return this.loanProduct.isAccountingEnabled();
}
public Boolean isCashBasedAccountingEnabledOnLoanProduct() {
private Boolean isCashBasedAccountingEnabledOnLoanProduct() {
return this.loanProduct.isCashBasedAccountingEnabled();
}
public Boolean isAccrualBasedAccountingEnabledOnLoanProduct() {
private Boolean isAccrualBasedAccountingEnabledOnLoanProduct() {
return this.loanProduct.isAccrualBasedAccountingEnabled();
}
// TODO - make private if possible
public Long productId() {
private Long productId() {
return this.loanProduct.getId();
}
@@ -433,6 +433,22 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
return interestDue;
}
public Money writeOffOutstandingFeeCharges(final MonetaryCurrency currency) {
final Money feeChargesOutstanding = getFeeChargesOutstanding(currency);
this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesOutstanding.getAmount());
this.completed = getTotalOutstanding(currency).isZero();
return feeChargesOutstanding;
}
public Money writeOffOutstandingPenaltyCharges(final MonetaryCurrency currency) {
final Money penaltyChargesOutstanding = getPenaltyChargesOutstanding(currency);
this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesOutstanding.getAmount());
this.completed = getTotalOutstanding(currency).isZero();
return penaltyChargesOutstanding;
}
public boolean isOverdueOn(final LocalDate transactionDate) {
return this.getDueDate().isBefore(transactionDate);
}
@@ -33,7 +33,7 @@ public class LoanScheduleWrapper {
}
/**
* FIXME - this is not a side effect free approach so move this into a
* FIXME - KW - this is not a side effect free approach so move this into a
* specific wrapper than makes this obvious from the read-only wrapper.
*/
public void reprocess(final MonetaryCurrency currency, final LocalDate disbursementDate,
@@ -50,16 +50,16 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
private BigDecimal amount;
@Column(name = "principal_portion_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal principalPortion = BigDecimal.ZERO;
private BigDecimal principalPortion;
@Column(name = "interest_portion_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal interestPortion = BigDecimal.ZERO;
private BigDecimal interestPortion;
@Column(name = "fee_charges_portion_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal feeChargesPortion = BigDecimal.ZERO;
private BigDecimal feeChargesPortion;
@Column(name = "penalty_charges_portion_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal penaltyChargesPortion = BigDecimal.ZERO;
private BigDecimal penaltyChargesPortion;
@Column(name = "is_reversed", nullable = false)
private boolean reversed;
@@ -160,30 +160,14 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return Money.of(currency, this.interestPortion);
}
public BigDecimal getInterestPortion() {
return this.interestPortion;
}
public Money getFeeChargesPortion(final MonetaryCurrency currency) {
return Money.of(currency, this.feeChargesPortion);
}
public BigDecimal getFeePortion() {
return this.feeChargesPortion;
}
public Money getPenaltyChargesPortion(final MonetaryCurrency currency) {
return Money.of(currency, this.penaltyChargesPortion);
}
public BigDecimal getPenaltyChargesPortion() {
return this.penaltyChargesPortion;
}
public BigDecimal getAmount() {
return this.amount;
}
public Money getAmount(final MonetaryCurrency currency) {
return Money.of(currency, this.amount);
}
@@ -89,7 +89,7 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper {
baseDataValidator.reset().parameter(repaymentEveryFrequencyTypeParameterName).value(repaymentEveryType).notNull()
.inMinMaxRange(0, 3);
// FIXME - this constraint doesnt really need to be here. should be
// FIXME - KW - 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.
validateSelectedPeriodFrequencyTypeIsTheSame(dataValidationErrors, loanTermFrequency, loanTermFrequencyType, numberOfRepayments,
@@ -207,7 +207,7 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper {
}
private void validateRepaymentsStartingFromDateAndInterestChargedFromDate(final List<ApiParameterError> dataValidationErrors,
final LocalDate expectedDisbursementDate, LocalDate repaymentsStartingFromDate, LocalDate interestChargedFromDate) {
final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate, final LocalDate interestChargedFromDate) {
if (repaymentsStartingFromDate != null && interestChargedFromDate == null) {
ApiParameterError error = ApiParameterError.parameterError(
@@ -228,7 +228,7 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper {
}
private void validateRepaymentsStartingFromDateIsAfterDisbursementDate(final List<ApiParameterError> dataValidationErrors,
final LocalDate expectedDisbursementDate, LocalDate repaymentsStartingFromDate) {
final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate) {
if (expectedDisbursementDate != null) {
if (repaymentsStartingFromDate != null && expectedDisbursementDate.isAfter(repaymentsStartingFromDate)) {
ApiParameterError error = ApiParameterError.parameterError(
@@ -6,6 +6,7 @@
package org.mifosplatform.portfolio.loanaccount.serialization;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -36,6 +37,11 @@ public final class LoanEventApiJsonValidator {
this.fromApiJsonHelper = fromApiJsonHelper;
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
public void validateDisbursement(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
@@ -59,8 +65,50 @@ public final class LoanEventApiJsonValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
public void validateTransaction(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Set<String> disbursementParameters = new HashSet<String>(Arrays.asList("transactionDate", "transactionAmount", "note",
"locale", "dateFormat"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
final JsonElement element = fromApiJsonHelper.parse(json);
final LocalDate transactionDate = fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
final BigDecimal transactionAmount = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
final String note = fromApiJsonHelper.extractStringNamed("note", element);
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateTransactionWithNoAmount(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Set<String> disbursementParameters = new HashSet<String>(Arrays.asList("transactionDate", "note", "locale", "dateFormat"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
final JsonElement element = fromApiJsonHelper.parse(json);
final LocalDate transactionDate = fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
final String note = fromApiJsonHelper.extractStringNamed("note", element);
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
}
@@ -1,63 +0,0 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
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.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.LoanTransactionCommand;
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 LoanTransactionCommand}'s.
*/
@Component
public final class LoanTransactionCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<LoanTransactionCommand> {
/**
* The parameters supported for this command.
*/
final Set<String> supportedParameters = new HashSet<String>(Arrays.asList("transactionDate", "transactionAmount", "note", "dateFormat",
"locale" ,"status", "closedOnDate", "writtenOffOnDate", "rescheduledOnDate"));
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public LoanTransactionCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
@Override
public LoanTransactionCommand 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 transactionDate = fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
final BigDecimal transactionAmount = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
final String note = fromApiJsonHelper.extractStringNamed("note", element);
return new LoanTransactionCommand(transactionDate, transactionAmount, note);
}
}
@@ -248,7 +248,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
context.authenticatedUser();
// TODO - OPTIMIZE - write simple sql query to fetch back date of
// TODO - KW - OPTIMIZE - write simple sql query to fetch back date of
// possible next transaction date.
Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) { throw new LoanNotFoundException(loanId); }
@@ -273,7 +273,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
context.authenticatedUser();
// TODO - OPTIMIZE - write simple sql query to fetch back overdue
// TODO - KW -OPTIMIZE - write simple sql query to fetch back overdue
// interest that can be waived along with the date of repayment period
// interest is overdue.
final Loan loan = this.loanRepository.findOne(loanId);
@@ -290,8 +290,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WAIVE_INTEREST);
return new LoanTransactionData(null, transactionType, currencyData, waiveOfInterest.getTransactionDate(),
waiveOfInterest.getAmount(), null, null, null, null);
final MonetaryCurrency monetaryCurrency = new MonetaryCurrency(currency.getCode(), currency.getDecimalPlaces());
final BigDecimal amount = waiveOfInterest.getAmount(monetaryCurrency).getAmount();
return new LoanTransactionData(null, transactionType, currencyData, waiveOfInterest.getTransactionDate(), amount, null, null, null,
null);
}
@Override
@@ -299,8 +301,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
context.authenticatedUser();
LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WRITEOFF);
return new LoanTransactionData(null, transactionType, null, new LocalDate(), null, null, null, null, null);
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WRITEOFF);
return new LoanTransactionData(null, transactionType, null, DateUtils.getLocalDateOfTenant(), null, null, null, null, null);
}
@Override
@@ -429,7 +431,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final String closedByUsername = rs.getString("closedByUsername");
final String closedByFirstname = rs.getString("closedByFirstname");
final String closedByLastname = rs.getString("closedByLastname");
final LocalDate expectedMaturityDate = JdbcSupport.getLocalDate(rs, "expectedMaturityDate");
final LoanApplicationTimelineData timeline = new LoanApplicationTimelineData(submittedOnDate, submittedByUsername,
@@ -14,14 +14,10 @@ import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.accounting.glaccount.data.LoanDTO;
import org.mifosplatform.accounting.glaccount.data.LoanTransactionDTO;
import org.mifosplatform.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.infrastructure.security.exception.NoAuthorizationException;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepository;
@@ -34,7 +30,6 @@ import org.mifosplatform.portfolio.charge.exception.LoanChargeNotFoundException;
import org.mifosplatform.portfolio.client.domain.Note;
import org.mifosplatform.portfolio.client.domain.NoteRepository;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanTransactionCommand;
import org.mifosplatform.portfolio.loanaccount.command.LoanUpdateCommand;
import org.mifosplatform.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
@@ -51,7 +46,6 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignment
import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
import org.mifosplatform.portfolio.loanaccount.serialization.LoanChargeCommandFromApiJsonDeserializer;
import org.mifosplatform.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
import org.mifosplatform.portfolio.loanaccount.serialization.LoanTransactionCommandFromApiJsonDeserializer;
import org.mifosplatform.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException;
import org.mifosplatform.useradministration.domain.AppUser;
@@ -64,7 +58,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final PlatformSecurityContext context;
private final LoanEventApiJsonValidator loanEventApiJsonValidator;
private final LoanTransactionCommandFromApiJsonDeserializer loanTransactionCommandFromApiJsonDeserializer;
private final LoanChargeCommandFromApiJsonDeserializer loanChargeCommandFromApiJsonDeserializer;
private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer;
private final LoanRepository loanRepository;
@@ -79,7 +72,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Autowired
public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
final LoanEventApiJsonValidator loanEventApiJsonValidator,
final LoanTransactionCommandFromApiJsonDeserializer loanTransactionCommandFromApiJsonDeserializer,
final LoanChargeCommandFromApiJsonDeserializer loanChargeCommandFromApiJsonDeserializer,
final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, final LoanAssembler loanAssembler,
final LoanRepository loanRepository, final LoanTransactionRepository loanTransactionRepository,
@@ -88,7 +80,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final JournalEntryWritePlatformService journalEntryWritePlatformService) {
this.context = context;
this.loanEventApiJsonValidator = loanEventApiJsonValidator;
this.loanTransactionCommandFromApiJsonDeserializer = loanTransactionCommandFromApiJsonDeserializer;
this.loanChargeCommandFromApiJsonDeserializer = loanChargeCommandFromApiJsonDeserializer;
this.loanAssembler = loanAssembler;
this.loanRepository = loanRepository;
@@ -101,10 +92,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.loanUpdateCommandFromApiJsonDeserializer = loanUpdateCommandFromApiJsonDeserializer;
}
private boolean isBeforeToday(final LocalDate date) {
return date.isBefore(DateUtils.getLocalDateOfTenant());
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values());
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
@@ -193,11 +180,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult makeLoanRepayment(final Long loanId, final JsonCommand command) {
final AppUser currentUser = context.authenticatedUser();
context.authenticatedUser();
final LoanTransactionCommand loanTransactionCommand = this.loanTransactionCommandFromApiJsonDeserializer.commandFromApiJson(command
.json());
loanTransactionCommand.validate();
this.loanEventApiJsonValidator.validateTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
@@ -205,12 +190,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Map<String, Object> changes = new LinkedHashMap<String, Object>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final Loan loan = retrieveLoanBy(loanId);
if (this.isBeforeToday(transactionDate) && currentUser.canNotMakeRepaymentOnLoanInPast()) { throw new NoAuthorizationException(
"error.msg.no.permission.to.make.repayment.on.loan.in.past"); }
final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
@@ -226,9 +210,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note);
}
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final ApplicationCurrency currency = this.applicationCurrencyRepository.findOneByCode(loan.getCurrencyCode());
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.toData(), existingTransactionIds,
existingReversedTransactionIds);
@@ -251,9 +232,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
context.authenticatedUser();
final LoanTransactionCommand loanTransactionCommand = this.loanTransactionCommandFromApiJsonDeserializer.commandFromApiJson(command
.json());
loanTransactionCommand.validate();
this.loanEventApiJsonValidator.validateTransaction(command.json());
final Loan loan = retrieveLoanBy(loanId);
@@ -266,6 +245,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Map<String, Object> changes = new LinkedHashMap<String, Object>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
@@ -286,9 +267,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note);
}
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final ApplicationCurrency currency = this.applicationCurrencyRepository.findOneByCode(loan.getCurrencyCode());
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.toData(), existingTransactionIds,
existingReversedTransactionIds);
@@ -311,13 +289,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
context.authenticatedUser();
final LoanTransactionCommand loanTransactionCommand = this.loanTransactionCommandFromApiJsonDeserializer.commandFromApiJson(command
.json());
loanTransactionCommand.validate();
this.loanEventApiJsonValidator.validateTransaction(command.json());
final Map<String, Object> changes = new LinkedHashMap<String, Object>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final Loan loan = retrieveLoanBy(loanId);
@@ -333,9 +311,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note);
}
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(waiveTransaction.getId()) //
@@ -352,16 +327,19 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
public CommandProcessingResult writeOff(final Long loanId, final JsonCommand command) {
context.authenticatedUser();
final LoanTransactionCommand loanTransactionCommand = this.loanTransactionCommandFromApiJsonDeserializer.commandFromApiJson(command
.json());
loanTransactionCommand.validateNonMonetaryTransaction();
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
final Map<String, Object> changes = new LinkedHashMap<String, Object>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final Loan loan = retrieveLoanBy(loanId);
final LoanTransaction writeoff = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine(), changes);
final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
final LoanTransaction writeoff = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine(), changes,
existingTransactionIds, existingReversedTransactionIds);
this.loanTransactionRepository.save(writeoff);
this.loanRepository.save(loan);
@@ -373,14 +351,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note);
}
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
// make a call to accounting
if (loan.isAccountingEnabledOnLoanProduct()) {
LoanDTO loanDTO = populateLoanData(loan, writeoff);
journalEntryWritePlatformService.createJournalEntriesForLoan(loanDTO);
}
final ApplicationCurrency currency = this.applicationCurrencyRepository.findOneByCode(loan.getCurrencyCode());
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.toData(), existingTransactionIds,
existingReversedTransactionIds);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
@@ -399,16 +373,20 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
context.authenticatedUser();
final LoanTransactionCommand loanTransactionCommand = this.loanTransactionCommandFromApiJsonDeserializer.commandFromApiJson(command
.json());
loanTransactionCommand.validateNonMonetaryTransaction();
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
final Loan loan = retrieveLoanBy(loanId);
final Map<String, Object> changes = new LinkedHashMap<String, Object>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final LoanTransaction possibleClosingTransaction = loan.close(command, defaultLoanLifecycleStateMachine(), changes);
final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
final LoanTransaction possibleClosingTransaction = loan.close(command, defaultLoanLifecycleStateMachine(), changes,
existingTransactionIds, existingReversedTransactionIds);
if (possibleClosingTransaction != null) {
this.loanTransactionRepository.save(possibleClosingTransaction);
}
@@ -421,8 +399,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note);
}
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
if (possibleClosingTransaction != null) {
final ApplicationCurrency currency = this.applicationCurrencyRepository.findOneByCode(loan.getCurrencyCode());
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.toData(), existingTransactionIds,
existingReversedTransactionIds);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
CommandProcessingResult result = null;
if (possibleClosingTransaction != null) {
@@ -456,14 +438,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
public CommandProcessingResult closeAsRescheduled(final Long loanId, final JsonCommand command) {
context.authenticatedUser();
final LoanTransactionCommand loanTransactionCommand = this.loanTransactionCommandFromApiJsonDeserializer.commandFromApiJson(command
.json());
loanTransactionCommand.validateNonMonetaryTransaction();
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
final Loan loan = retrieveLoanBy(loanId);
final Map<String, Object> changes = new LinkedHashMap<String, Object>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
loan.closeAsMarkedForReschedule(command, defaultLoanLifecycleStateMachine(), changes);
@@ -476,9 +458,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note);
}
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanId) //
@@ -719,24 +698,4 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.withLoanId(loanId) //
.build();
}
private LoanDTO populateLoanDTO(final Loan loan, final List<LoanTransaction> loanTransactions) {
List<LoanTransactionDTO> loanTransactionDTOs = new ArrayList<LoanTransactionDTO>();
for (LoanTransaction loanTransaction : loanTransactions) {
LoanTransactionDTO loanTransactionDTO = new LoanTransactionDTO(loanTransaction.getId().toString(), loanTransaction.getDateOf(),
loanTransaction.getAmount(), loanTransaction.getPrincipalPortion(), loanTransaction.getInterestPortion(),
loanTransaction.getFeePortion(), loanTransaction.getPenaltyChargesPortion(), loanTransaction.isDisbursement(),
loanTransaction.isRepayment(), loanTransaction.isRepaymentAtDisbursement(), loanTransaction.isReversed(),
loanTransaction.isWriteOff());
loanTransactionDTOs.add(loanTransactionDTO);
}
return new LoanDTO(loan.getId(), loan.productId(), loan.getOfficeId(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isAccrualBasedAccountingEnabledOnLoanProduct(), loanTransactionDTOs);
}
private LoanDTO populateLoanData(final Loan loan, final LoanTransaction loanTransaction) {
List<LoanTransaction> loanTransactions = new ArrayList<LoanTransaction>();
loanTransactions.add(loanTransaction);
return populateLoanDTO(loan, loanTransactions);
}
}
@@ -625,11 +625,10 @@ public class SavingAccount extends AbstractAuditableCustom<AppUser, Long> {
public void postInterest() {
LocalDate lastInterestPostedDate = getLastInterestPostedDate();
@SuppressWarnings("unused")
LocalDate nextInterestPostingDate = getNextInterestPostedDate();
@SuppressWarnings("unused")
SavingInterestCalculationMethod savingInterestCalculationMethod = SavingInterestCalculationMethod
.fromInt(this.interestCalculationMethod);
// LocalDate nextInterestPostingDate = getNextInterestPostedDate();
// SavingInterestCalculationMethod savingInterestCalculationMethod =
// SavingInterestCalculationMethod
// .fromInt(this.interestCalculationMethod);
Integer monthsForInterestCalculation = Months.monthsBetween(lastInterestPostedDate, new LocalDate()).getMonths();
Integer postInterestItereations = monthsForInterestCalculation / this.interestPostEvery;
@@ -644,7 +643,7 @@ public class SavingAccount extends AbstractAuditableCustom<AppUser, Long> {
while (postInterestItereations > 0) {
lastInterestPostedDate = getLastInterestPostedDate();
nextInterestPostingDate = getNextInterestPostedDate();
// nextInterestPostingDate = getNextInterestPostedDate();
LocalDate transactionStartDate = lastInterestPostedDate;
BigDecimal termTotalAmount = BigDecimal.ZERO;
@@ -687,7 +686,7 @@ public class SavingAccount extends AbstractAuditableCustom<AppUser, Long> {
isOutstandingAmountCalculated = true;
}
// TODO- calculate interest on calculation method
// TODO - HUGO - calculate interest on calculation method
// if (savingInterestCalculationMethod.isAverageBalance()) {
averageBalanceForTerm = BigDecimal.valueOf(termTotalAmount.doubleValue() / this.interestPostEvery);
@@ -45,12 +45,12 @@ public class CalculateSavingScheduleImpl implements CalculateSavingSchedule {
}
@Override
public SavingScheduleData calculateSavingSchedule(CalculateSavingScheduleCommand command) {
public SavingScheduleData calculateSavingSchedule(final CalculateSavingScheduleCommand command) {
context.authenticatedUser();
SavingScheduleData savingScheduleData = null;
// FIXME Validations should write here
// FIXME - HUGO - Validations should write here
final SavingProduct savingProduct = this.savingProductRepository.findOne(command.getProductId());
if (savingProduct == null) throw new SavingProductNotFoundException(command.getProductId());
@@ -39,7 +39,7 @@ public class SavingProductEnumerations {
return savingsLockinPeriod(SavingsLockinPeriodEnum.fromInt(id));
}
public static EnumOptionData savingsLockinPeriod(SavingsLockinPeriodEnum type) {
public static EnumOptionData savingsLockinPeriod(final SavingsLockinPeriodEnum type) {
final String codePrefix = "savings.product.";
EnumOptionData optionData = null;
switch (type) {
@@ -67,7 +67,7 @@ public class SavingProductEnumerations {
return optionData;
}
public static EnumOptionData savingInterestCalculationMethod(SavingInterestCalculationMethod type) {
public static EnumOptionData savingInterestCalculationMethod(final SavingInterestCalculationMethod type) {
final String codePrefix = "savings.interestCalculation.method.";
EnumOptionData optionData = null;
switch (type) {
@@ -95,7 +95,7 @@ public class SavingProductEnumerations {
return optionData;
}
public static EnumOptionData savingInterestType(SavingsInterestType type) {
public static EnumOptionData savingInterestType(final SavingsInterestType type) {
final String codePrefix = "savings.interest.type.";
EnumOptionData optionData = null;
switch (type) {
@@ -115,7 +115,7 @@ public class SavingProductEnumerations {
return optionData;
}
public static EnumOptionData interestFrequencyType(SavingFrequencyType type) {
public static EnumOptionData interestFrequencyType(final SavingFrequencyType type) {
final String codePrefix = "saving.product.frequency.period.";
EnumOptionData optionData = null;
switch (type) {
@@ -171,7 +171,6 @@ public class SavingProductEnumerations {
final String codePrefix = "savings.product.";
EnumOptionData optionData = null;
switch (type) {
// FIXME - Madhukar - spelling mistake on 'Recurring'
case RECURRING:
optionData = new EnumOptionData(SavingProductType.RECURRING.getValue().longValue(), codePrefix
+ SavingProductType.RECURRING.getCode(), "Recurring");
@@ -16,11 +16,11 @@ import org.springframework.stereotype.Service;
@Service
public class FixedTermDepositInterestCalculator {
// FIXME - MADHUKAR - Are the unused field here needed?
// FIXME - HUGO - Are the unused field here needed?
public Money calculateInterestOnMaturityFor(final Money deposit, final Integer tenureInMonths, final BigDecimal maturityInterestRate,
final Integer interestCompoundedEvery, final PeriodFrequencyType interestCompoundedFrequencyPeriodType) {
// TODO - Can change 'interest compounding' fields into something
// TODO - HUGO - Can change 'interest compounding' fields into something
// simpler like interestCompoundedFrequency {None (Simple Interest),
// Monthly, Quarterly, Half-Yearly, Annually}
// - the 'interestCompoundedEvery' would then be inferred form the
@@ -28,7 +28,7 @@ public class FixedTermDepositInterestCalculator {
// - the special case of 'simple interest' or 'no compounding' would use
// simple formula rather than 'future value' function.
// FIXME - assuming only monthly compounding period for now
// FIXME - HUGO - assuming only monthly compounding period for now
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
Integer monthsInYear = 12;
@@ -59,8 +59,8 @@ public class FixedTermDepositInterestCalculator {
* payments are due at the beginning of period (false => payments are due at
* end of period)
*/
private static double fv(double ratePeriodCompoundingPeriod, double numberOfCompoundingPeriods, double pmtPerPeriod,
double presentValue, boolean type) {
private static double fv(final double ratePeriodCompoundingPeriod, final double numberOfCompoundingPeriods, final double pmtPerPeriod,
final double presentValue, final boolean type) {
double retval = 0;
if (ratePeriodCompoundingPeriod == 0) {
retval = -1 * (presentValue + (numberOfCompoundingPeriods * pmtPerPeriod));
@@ -72,8 +72,8 @@ public class FixedTermDepositInterestCalculator {
return retval;
}
public Money calculateInterestOnMaturityForSimpleInterest(Money deposit, Integer tenureInMonths, BigDecimal maturityInterestRate,
Integer interestCompoundedEvery, PeriodFrequencyType interestCompoundedFrequencyPeriodType) {
public Money calculateInterestOnMaturityForSimpleInterest(final Money deposit, final Integer tenureInMonths, final BigDecimal maturityInterestRate,
final Integer interestCompoundedEvery, final PeriodFrequencyType interestCompoundedFrequencyPeriodType) {
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
Integer monthsInYear = 12;
@@ -91,7 +91,7 @@ public class FixedTermDepositInterestCalculator {
return Money.of(deposit.getCurrency(), totalAmount);
}
public Money calculateRemainInterest(Money deposit, Integer days, BigDecimal interestRate) {
public Money calculateRemainInterest(final Money deposit, final Integer days, final BigDecimal interestRate) {
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
Integer daysInYear = 365;