MIFOSX-90: support modification of loan charge, fix issue with repayments paying off loan charges in wrong order.

This commit is contained in:
Keith Woodlock
2012-11-08 14:35:41 +00:00
parent f90802a292
commit 152a70477d
7 changed files with 83 additions and 9 deletions
@@ -493,7 +493,6 @@ public class LoansApiResource {
public String retrieveNewLoanChargeDetails(@Context final UriInfo uriInfo) { public String retrieveNewLoanChargeDetails(@Context final UriInfo uriInfo) {
final Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); final Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
final boolean feeChargesOnly = false; final boolean feeChargesOnly = false;
@@ -503,6 +502,23 @@ public class LoansApiResource {
return this.apiJsonSerializerService.serializeLoanChargeDataToJson(prettyPrint, responseParameters, loanChargeTemplate); return this.apiJsonSerializerService.serializeLoanChargeDataToJson(prettyPrint, responseParameters, loanChargeTemplate);
} }
@GET
@Path("{loanId}/charges/{chargeId}")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public String retrieveLoanCharge(
@PathParam("loanId") final Long loanId,
@PathParam("chargeId") final Long loanChargeId,
@Context final UriInfo uriInfo){
final Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
final LoanChargeData loanCharge = this.chargeReadPlatformService.retrieveLoanChargeDetails(loanChargeId, loanId);
return this.apiJsonSerializerService.serializeLoanChargeDataToJson(prettyPrint, responseParameters, loanCharge);
}
@PUT @PUT
@Path("{loanId}/charges/{chargeId}") @Path("{loanId}/charges/{chargeId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@@ -19,5 +19,7 @@ public interface ChargeReadPlatformService {
Collection<LoanChargeData> retrieveLoanCharges(Long loanId); Collection<LoanChargeData> retrieveLoanCharges(Long loanId);
LoanChargeData retrieveLoanChargeDetails(Long loanChargeId, Long loanId);
ChargeData retrieveLoanChargeTemplate(); ChargeData retrieveLoanChargeTemplate();
} }
@@ -101,12 +101,24 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService
final LoanChargeMapper rm = new LoanChargeMapper(); final LoanChargeMapper rm = new LoanChargeMapper();
// TODO - KW - check to see if its important that 'c.is_deleted=0' is required here?
final String sql = "select " + rm.loanChargeSchema() + " where c.is_deleted=0 and lc.loan_id=? " + final String sql = "select " + rm.loanChargeSchema() + " where c.is_deleted=0 and lc.loan_id=? " +
" order by lc.charge_time_enum ASC, lc.due_for_collection_as_of_date ASC, lc.is_penalty ASC"; " order by lc.charge_time_enum ASC, lc.due_for_collection_as_of_date ASC, lc.is_penalty ASC";
return this.jdbcTemplate.query(sql, rm, new Object[] {loanId}); return this.jdbcTemplate.query(sql, rm, new Object[] {loanId});
} }
@Override
public LoanChargeData retrieveLoanChargeDetails(final Long id, final Long loanId) {
this.context.authenticatedUser();
final LoanChargeMapper rm = new LoanChargeMapper();
final String sql = "select " + rm.loanChargeSchema() + " where lc.id=? and lc.loan_id=?";
return this.jdbcTemplate.queryForObject(sql, rm, new Object[] {id, loanId});
}
@Override @Override
public Collection<ChargeData> retrieveLoanApplicableCharges(final boolean feeChargesOnly) { public Collection<ChargeData> retrieveLoanApplicableCharges(final boolean feeChargesOnly) {
this.context.authenticatedUser(); this.context.authenticatedUser();
@@ -125,13 +125,29 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
Money amountRemaining = feeCharges; Money amountRemaining = feeCharges;
for (LoanCharge loanCharge : charges) { for (LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) { if (!loanCharge.isDueAtDisbursement()) {
if (loanCharge.isPenaltyCharge() && loanCharge.isNotFullyPaid() && amountRemaining.isGreaterThanZero()) {
amountRemaining = loanCharge.updatePaidAmountBy(amountRemaining); if (loanCharge.isPenaltyCharge() && amountRemaining.isGreaterThanZero()) {
final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges);
amountRemaining = unpaidCharge.updatePaidAmountBy(amountRemaining);
} }
} }
} }
} }
private LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(Set<LoanCharge> charges) {
LoanCharge earliestUnpaidCharge = null;
for (LoanCharge loanCharge : charges) {
if (loanCharge.isNotFullyPaid() && !loanCharge.isDueAtDisbursement()) {
if (earliestUnpaidCharge == null || loanCharge.getDueForCollectionAsOfLocalDate().isBefore(earliestUnpaidCharge.getDueForCollectionAsOfLocalDate())) {
earliestUnpaidCharge = loanCharge;
}
}
}
return earliestUnpaidCharge;
}
@Override @Override
public void handleWriteOff( public void handleWriteOff(
final LoanTransaction loanTransaction, final LoanTransaction loanTransaction,
@@ -370,10 +370,22 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
public void updateLoanCharge(final LoanCharge loanCharge, final LoanChargeCommand command) { public void updateLoanCharge(final LoanCharge loanCharge, final LoanChargeCommand command) {
validateLoanIsNotClosed(loanCharge);
if (this.charges.contains(loanCharge)) { if (this.charges.contains(loanCharge)) {
loanCharge.update(command, getPrincpal().getAmount()); loanCharge.update(command, getPrincpal().getAmount());
updateTotalChargesDueAtDisbursement(); updateTotalChargesDueAtDisbursement();
} }
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy);
if (!loanCharge.isDueAtDisbursement()) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges);
} else {
// reprocess loan schedule based on charge been waived.
LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, this.charges);
}
} }
public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine) { public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
@@ -398,15 +410,15 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
final LoanTransaction waiveLoanChargeTransaction = LoanTransaction.waiveLoanCharge(this, amountWaived, transactionDate,feeChargesWaived, penaltyChargesWaived); final LoanTransaction waiveLoanChargeTransaction = LoanTransaction.waiveLoanCharge(this, amountWaived, transactionDate,feeChargesWaived, penaltyChargesWaived);
// reprocess loan schedule based on charge been added.
LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, this.charges);
// Waive of charges whose due date falls after latest 'repayment' transaction dont require entire loan schedule to be reprocessed. // Waive of charges whose due date falls after latest 'repayment' transaction dont require entire loan schedule to be reprocessed.
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy);
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) { if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement(); final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges); loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges);
} else {
// reprocess loan schedule based on charge been waived.
LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, this.charges);
} }
doPostLoanTransactionChecks(waiveLoanChargeTransaction.getTransactionDate(), loanLifecycleStateMachine); doPostLoanTransactionChecks(waiveLoanChargeTransaction.getTransactionDate(), loanLifecycleStateMachine);
@@ -256,6 +256,7 @@ public class LoanCharge extends AbstractPersistable<Long> {
public Money waive(final MonetaryCurrency currency) { public Money waive(final MonetaryCurrency currency) {
this.amountWaived = this.amount; this.amountWaived = this.amount;
this.amountOutstanding = calculateAmountOutstanding(currency); this.amountOutstanding = calculateAmountOutstanding(currency);
this.paid = determineIfFullyPaid();
return getAmountWaived(currency); return getAmountWaived(currency);
} }
@@ -349,7 +350,19 @@ public class LoanCharge extends AbstractPersistable<Long> {
amountPaidLocal = this.amountPaid; amountPaidLocal = this.amountPaid;
} }
return this.amount.subtract(amountPaidLocal); BigDecimal amountWaivedLocal = BigDecimal.ZERO;
if (this.amountWaived != null) {
amountWaivedLocal = this.amountWaived;
}
BigDecimal amountWrittenOffLocal = BigDecimal.ZERO;
if (this.amountWrittenOff != null) {
amountWrittenOffLocal = this.amountWrittenOff;
}
final BigDecimal totalAccountedFor = amountPaidLocal.add(amountWaivedLocal).add(amountWrittenOffLocal);
return this.amount.subtract(totalAccountedFor);
} }
private BigDecimal percentageOf(final BigDecimal value, final BigDecimal percentage) { private BigDecimal percentageOf(final BigDecimal value, final BigDecimal percentage) {
@@ -452,6 +465,8 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.amountOutstanding = amountExpected.minus(amountPaidToDate).getAmount(); this.amountOutstanding = amountExpected.minus(amountPaidToDate).getAmount();
} }
this.paid = determineIfFullyPaid();
return incrementBy.minus(amountPaidOnThisCharge); return incrementBy.minus(amountPaidOnThisCharge);
} }
@@ -662,7 +662,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
loan.updateLoanCharge(loanCharge, command); loan.updateLoanCharge(loanCharge, command);
this.loanRepository.saveAndFlush(loan);
this.loanRepository.save(loan);
return new EntityIdentifier(loanCharge.getId()); return new EntityIdentifier(loanCharge.getId());
} }