cleaning up recovery payments functionality

This commit is contained in:
Vishwas Babu A J
2014-05-04 18:02:59 +05:30
24 changed files with 378 additions and 80 deletions
+70
View File
@@ -923,6 +923,14 @@
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>loans/{loanId}/transactions?command=recoverypayment</td>
<td><a href="#loans_transaction_recovery_payment">Make a Recovery Payment</a></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
@@ -7342,7 +7350,61 @@ Request Body:
</code>
</div>
</div>
<!------recovery payment -->
<a id="loans_transaction_recovery_payment" name="loans_transaction_recovery_payment" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Make Recovery Payment </h4>
<p>only applies to written-off loans</p>
</div>
<div class="method-example">
<code class="method-declaration">
POST https://DomainName/api/v1/loans/{loanId}/transactions?command=recoverypayment
</code>
<code class="method-request">
POST loans/5/transactions?command=recoverypayment
Content-Type: application/json
Request Body:
{
"dateFormat": "dd MMMM yyyy",
"locale": "en",
"transactionDate": "14 May 2013",
"transactionAmount": "500.00",
"paymentTypeId": "12",
"note": "check payment",
"accountNumber": "acc123",
"checkNumber": "che123",
"routingCode": "rou123",
"receiptNumber": "rec123",
"bankNumber": "ban123"
}
</code>
<code class="method-response">
{
"officeId": 1,
"clientId": 1,
"loanId": 5,
"resourceId": 564,
"changes": {
"transactionDate": "14 May 2013",
"transactionAmount": "500.00",
"locale": "en",
"dateFormat": "dd MMMM yyyy",
"note": "check payment",
"accountNumber": "acc123",
"checkNumber": "che123",
"routingCode": "rou123",
"receiptNumber": "rec123",
"bankNumber": "ban123"
}
}
</code>
</div>
</div>
<!--- end of recovery payment -->
<a id="loans_transaction_undo_write-off_loan" name="loans_transaction_undo_write-off_loan" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
@@ -29138,6 +29200,14 @@ No Request Body:
<td></td>
<td></td>
</tr>
</tr><tr>
<td></td>
<td>loans/{loanId}/transactions?command=recoverypayment</td>
<td><a href="#loans_transaction_recovery_payment">Make Recovery Payment</a></td>
<td></td>
<td></td>
<td></td>
</tr>
</tr><tr>
<td></td>
<td>loans/{loanId}/transactions?command=undowriteoff</td>
@@ -0,0 +1,28 @@
INSERT INTO `m_permission` (
`id` ,
`grouping` ,
`code` ,
`entity_name` ,
`action_name` ,
`can_maker_checker`
)
VALUES (
NULL , 'transaction_loan', 'RECOVERYPAYMENT_LOAN', 'LOAN', 'RECOVERYPAYMENT', '0'
);
Alter table m_loan
add column total_recovered_derived Decimal(19,6);
INSERT INTO `acc_gl_account` (`name`, `hierarchy`, `gl_code`,`account_usage`, `classification_enum`,`description`)
select 'Loan Recovery (Temp)', '.', '220002-Temp', 1, 4,'Temporary account to track income from Loan recovery'
FROM m_product_loan WHERE accounting_type != 1
limit 1;
INSERT INTO `acc_product_mapping` (`gl_account_id`,`product_id`,`product_type`,`financial_account_type`)
select (select max(id) from acc_gl_account where classification_enum=4 and account_usage=1 LIMIT 1), mapping.product_id, mapping.product_type, 12
from acc_product_mapping mapping
where mapping.financial_account_type = 4 and mapping.product_type=1
group by mapping.product_id;
@@ -13,7 +13,7 @@ public class AccountingConstants {
/*** Accounting placeholders for cash based accounting for loan products ***/
public static enum CASH_ACCOUNTS_FOR_LOAN {
FUND_SOURCE(1), LOAN_PORTFOLIO(2), INTEREST_ON_LOANS(3), INCOME_FROM_FEES(4), INCOME_FROM_PENALTIES(5), LOSSES_WRITTEN_OFF(6), TRANSFERS_SUSPENSE(
10), OVERPAYMENT(11);
10), OVERPAYMENT(11),INCOME_FROM_RECOVERY(12);
private final Integer value;
@@ -46,7 +46,7 @@ public class AccountingConstants {
/*** Accounting placeholders for accrual based accounting for loan products ***/
public static enum ACCRUAL_ACCOUNTS_FOR_LOAN {
FUND_SOURCE(1), LOAN_PORTFOLIO(2), INTEREST_ON_LOANS(3), INCOME_FROM_FEES(4), INCOME_FROM_PENALTIES(5), LOSSES_WRITTEN_OFF(6), INTEREST_RECEIVABLE(
7), FEES_RECEIVABLE(8), PENALTIES_RECEIVABLE(9), TRANSFERS_SUSPENSE(10), OVERPAYMENT(11);
7), FEES_RECEIVABLE(8), PENALTIES_RECEIVABLE(9), TRANSFERS_SUSPENSE(10), OVERPAYMENT(11),INCOME_FROM_RECOVERY(12);
private final Integer value;
@@ -88,7 +88,8 @@ public class AccountingConstants {
"receivableFeeAccountId"), PENALTIES_RECEIVABLE("receivablePenaltyAccountId"), TRANSFERS_SUSPENSE(
"transfersInSuspenseAccountId"), PAYMENT_CHANNEL_FUND_SOURCE_MAPPING("paymentChannelToFundSourceMappings"), PAYMENT_TYPE(
"paymentTypeId"), FEE_INCOME_ACCOUNT_MAPPING("feeToIncomeAccountMappings"), PENALTY_INCOME_ACCOUNT_MAPPING(
"penaltyToIncomeAccountMappings"), CHARGE_ID("chargeId"), INCOME_ACCOUNT_ID("incomeAccountId");
"penaltyToIncomeAccountMappings"), CHARGE_ID("chargeId"), INCOME_ACCOUNT_ID("incomeAccountId"),
INCOME_FROM_RECOVERY("incomeFromRecoveryAccountId");
private final String value;
@@ -110,7 +111,8 @@ public class AccountingConstants {
FUND_SOURCE("fundSourceAccount"), LOAN_PORTFOLIO("loanPortfolioAccount"), INTEREST_ON_LOANS("interestOnLoanAccount"), INCOME_FROM_FEES(
"incomeFromFeeAccount"), INCOME_FROM_PENALTIES("incomeFromPenaltyAccount"), LOSSES_WRITTEN_OFF("writeOffAccount"), OVERPAYMENT(
"overpaymentLiabilityAccount"), INTEREST_RECEIVABLE("receivableInterestAccount"), FEES_RECEIVABLE("receivableFeeAccount"), PENALTIES_RECEIVABLE(
"receivablePenaltyAccount"), TRANSFERS_SUSPENSE("transfersInSuspenseAccount"), INCOME_ACCOUNT_ID("incomeAccount");
"receivablePenaltyAccount"), TRANSFERS_SUSPENSE("transfersInSuspenseAccount"), INCOME_ACCOUNT_ID("incomeAccount"),
INCOME_FROM_RECOVERY("incomeFromRecoveryAccount");
private final String value;
@@ -56,6 +56,11 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
createJournalEntriesForRepayments(loanDTO, loanTransactionDTO, office);
}
/** Logic for handling recovery payments **/
else if (loanTransactionDTO.getTransactionType().isRecoveryRepayment()) {
createJournalEntriesForRecoveryRepayments(loanDTO, loanTransactionDTO, office);
}
/** Logic for Refunds of Overpayments **/
else if (loanTransactionDTO.getTransactionType().isRefund()) {
createJournalEntriesForRefund(loanDTO, loanTransactionDTO, office);
@@ -202,6 +207,33 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal);
}
/**
* Create a single Debit to fund source and a single credit to
* "Income from Recovery"
*
* In case the loan transaction is a reversal, all debits are turned into
* credits and vice versa
*/
private void createJournalEntriesForRecoveryRepayments(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
final String currencyCode = loanDTO.getCurrencyCode();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final Date transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal amount = loanTransactionDTO.getAmount();
final boolean isReversal = loanTransactionDTO.isReversed();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
this.helper.createCashBasedJournalEntriesAndReversalsForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY, loanProductId, paymentTypeId, loanId, transactionId, transactionDate, amount,
isReversal);
}
/**
* Credit loan Portfolio and Debit Suspense Account for a Transfer
* Initiation. A Transfer acceptance would be treated the opposite i.e Debit
@@ -102,6 +102,11 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue()).value(incomeFromPenaltyId)
.notNull().integerGreaterThanZero();
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue()).value(incomeFromRecoveryAccountId)
.notNull().integerGreaterThanZero();
final Long writeOffAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue()).value(writeOffAccountId)
@@ -121,6 +121,10 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
element);
final Long incomeFromPenaltyId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(), element);
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(), element);
final Long writeOffAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), element);
final Long overPaymentAccountId = this.fromApiJsonHelper.extractLongNamed(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(),
@@ -140,16 +144,16 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
break;
case CASH_BASED:
populateChangesForCashBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId,
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId);
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,incomeFromRecoveryAccountId);
break;
case ACCRUAL_PERIODIC:
populateChangesForAccrualBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId,
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,incomeFromRecoveryAccountId,
receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId);
break;
case ACCRUAL_UPFRONT:
populateChangesForAccrualBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId,
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,incomeFromRecoveryAccountId,
receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId);
break;
}
@@ -159,7 +163,7 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
private void populateChangesForAccrualBasedAccounting(final Map<String, Object> changes, final Long fundAccountId,
final Long loanPortfolioAccountId, final Long incomeFromInterestId, final Long incomeFromFeeId, final Long incomeFromPenaltyId,
final Long writeOffAccountId, final Long overPaymentAccountId, final Long transfersInSuspenseAccountId,
final Long writeOffAccountId, final Long overPaymentAccountId, final Long transfersInSuspenseAccountId,final Long incomeFromRecoveryAccountId,
final Long receivableInterestAccountId, final Long receivableFeeAccountId, final Long receivablePenaltyAccountId) {
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_RECEIVABLE.getValue(), receivableInterestAccountId);
@@ -167,13 +171,13 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTIES_RECEIVABLE.getValue(), receivablePenaltyAccountId);
populateChangesForCashBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId, incomeFromFeeId,
incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId);
incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,incomeFromRecoveryAccountId);
}
private void populateChangesForCashBasedAccounting(final Map<String, Object> changes, final Long fundAccountId,
final Long loanPortfolioAccountId, final Long incomeFromInterestId, final Long incomeFromFeeId, final Long incomeFromPenaltyId,
final Long writeOffAccountId, final Long overPaymentAccountId, final Long transfersInSuspenseAccountId) {
final Long writeOffAccountId, final Long overPaymentAccountId, final Long transfersInSuspenseAccountId,final Long incomeFromRecoveryAccountId) {
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.FUND_SOURCE.getValue(), fundAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOAN_PORTFOLIO.getValue(), loanPortfolioAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_ON_LOANS.getValue(), incomeFromInterestId);
@@ -182,6 +186,8 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), writeOffAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), overPaymentAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(), transfersInSuspenseAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(), incomeFromRecoveryAccountId);
}
/**
@@ -215,6 +221,9 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
mergeLoanToIncomeAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(),
loanProductId, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue(),
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.toString(), changes);
mergeLoanToIncomeAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(),
loanProductId, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY.getValue(),
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY.toString(), changes);
// expenses
mergeLoanToExpenseAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(),
@@ -255,6 +264,9 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
mergeLoanToIncomeAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(),
loanProductId, ACCRUAL_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue(),
ACCRUAL_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.toString(), changes);
mergeLoanToIncomeAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(),
loanProductId, ACCRUAL_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY.getValue(),
ACCRUAL_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY.toString(), changes);
// expenses
mergeLoanToExpenseAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(),
@@ -125,6 +125,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_DATA_PARAMS.LOSSES_WRITTEN_OFF.getValue(), gLAccountData);
} else if (glAccountForLoan.equals(CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT)) {
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_DATA_PARAMS.OVERPAYMENT.getValue(), gLAccountData);
} else if(glAccountForLoan.equals(CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY)) {
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_DATA_PARAMS.INCOME_FROM_RECOVERY.getValue(), gLAccountData);
}
}
} else if (AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingType)
@@ -161,6 +163,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_DATA_PARAMS.FEES_RECEIVABLE.getValue(), gLAccountData);
} else if (glAccountForLoan.equals(ACCRUAL_ACCOUNTS_FOR_LOAN.PENALTIES_RECEIVABLE)) {
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_DATA_PARAMS.PENALTIES_RECEIVABLE.getValue(), gLAccountData);
} else if((glAccountForLoan.equals(ACCRUAL_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY))){
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_DATA_PARAMS.INCOME_FROM_RECOVERY.getValue(), gLAccountData);
}
}
@@ -77,6 +77,9 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element,
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(), loanProductId,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue());
this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element,
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(), loanProductId,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY.getValue());
// expenses
this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element,
@@ -125,6 +128,9 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element,
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(), loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue());
this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element,
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(), loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY.getValue());
// expenses
this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element,
@@ -385,6 +385,9 @@ public class CommandWrapper {
public boolean isLoanRepayment() {
return this.actionName.equalsIgnoreCase("REPAYMENT") && this.entityName.equalsIgnoreCase("LOAN");
}
public boolean isLoanRecoveryPayment() {
return this.actionName.equalsIgnoreCase("RECOVERYPAYMENT") && this.entityName.equalsIgnoreCase("LOAN");
}
public boolean isLoanRepaymentAdjustment() {
return this.actionName.equalsIgnoreCase("ADJUST") && this.entityName.equalsIgnoreCase("LOAN");
@@ -576,6 +576,15 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder loanRecoveryPaymentTransaction(final Long loanId){
this.actionName = "RECOVERYPAYMENT";
this.entityName = "LOAN";
this.entityId = null;
this.loanId = loanId;
this.href = "/loans/" + loanId + "/transactions/template?command=recoverypayment";
return this;
}
public CommandWrapperBuilder waiveInterestPortionTransaction(final Long loanId) {
this.actionName = "WAIVEINTERESTPORTION";
this.entityName = "LOAN";
@@ -5,8 +5,6 @@
*/
package org.mifosplatform.commands.service;
import java.util.Map;
import org.joda.time.DateTime;
import org.mifosplatform.commands.domain.CommandSource;
import org.mifosplatform.commands.domain.CommandSourceRepository;
@@ -26,6 +24,8 @@ import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
@Service
public class SynchronousCommandProcessingService implements CommandProcessingService {
@@ -341,7 +341,9 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
handler = this.applicationContext.getBean("undoDisbursalLoanCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isLoanRepayment()) {
handler = this.applicationContext.getBean("loanRepaymentCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isLoanRepaymentAdjustment()) {
} else if (wrapper.isLoanRecoveryPayment()){
handler = this.applicationContext.getBean("loanRecoveryPaymentCommandHandler", NewCommandSourceHandler.class);
}else if (wrapper.isLoanRepaymentAdjustment()) {
handler = this.applicationContext.getBean("loanRepaymentAdjustmentCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isWaiveInterestPortionOnLoan()) {
handler = this.applicationContext.getBean("waiveInterestPortionOnLoanCommandHandler", NewCommandSourceHandler.class);
@@ -128,8 +128,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
final boolean isRecoveryRepayment = false;
final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeRepayment(toLoanAccount,
new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null);
new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null,
isRecoveryRepayment);
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(command,
fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction);
@@ -253,9 +255,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
accountTransferDTO.getLoanInstallmentNumber());
} else {
final boolean isRecoveryRepayment = false;
loanTransaction = this.loanAccountDomainService.makeRepayment(toLoanAccount, new CommandProcessingResultBuilder(),
accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getPaymentDetail(), null, null);
accountTransferDTO.getPaymentDetail(), null, null, isRecoveryRepayment);
}
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount,
@@ -5,22 +5,6 @@
*/
package org.mifosplatform.portfolio.loanaccount.api;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.commands.domain.CommandWrapper;
import org.mifosplatform.commands.service.CommandWrapperBuilder;
@@ -40,6 +24,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@Path("/loans/{loanId}/transactions")
@Component
@Scope("singleton")
@@ -155,6 +148,9 @@ public class LoanTransactionsApiResource {
} else if (is(commandParam, "undowriteoff")) {
final CommandWrapper commandRequest = builder.undoWriteOffLoanTransaction(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}else if(is(commandParam,"recoverypayment")){
final CommandWrapper commandRequest = builder.loanRecoveryPaymentTransaction(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
if (result == null) { throw new UnrecognizedQueryParamException("command", commandParam); }
@@ -303,6 +303,9 @@ public class Loan extends AbstractPersistable<Long> {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true)
private final Set<LoanTermVariations> loanTermVariations = new HashSet<LoanTermVariations>();
@Column(name = "total_recovered_derived", scale = 6, precision = 19)
private BigDecimal totalRecovered;
public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType,
final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final LoanTransactionProcessingStrategy transactionProcessingStrategy,
@@ -991,6 +994,9 @@ public class Loan extends AbstractPersistable<Long> {
final Money overpaidBy = calculateTotalOverpayment();
this.totalOverpaid = overpaidBy.getAmountDefaultedToNullIfZero();
final Money recoveredAmount = calculateTotalRecoveredPayments();
this.totalRecovered = recoveredAmount.getAmountDefaultedToNullIfZero();
final Money principal = this.loanRepaymentScheduleDetail.getPrincipal();
this.summary.updateSummary(loanCurrency(), principal, this.repaymentScheduleInstallments, this.loanSummaryWrapper,
isDisbursed());
@@ -2148,7 +2154,7 @@ public class Loan extends AbstractPersistable<Long> {
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final ChangedTransactionDetail changedTransactionDetail = handleRepaymentOrWaiverTransaction(waiveInterestTransaction,
final ChangedTransactionDetail changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(waiveInterestTransaction,
loanLifecycleStateMachine, null);
return changedTransactionDetail;
@@ -2157,10 +2163,17 @@ public class Loan extends AbstractPersistable<Long> {
public ChangedTransactionDetail makeRepayment(final LoanTransaction repaymentTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List<Holiday> holidays,
final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) {
final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay, boolean isRecoveryRepayment) {
validateAccountStatus(LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, repaymentTransaction.getTransactionDate());
LoanEvent event = null;
if (isRecoveryRepayment) {
event = LoanEvent.LOAN_RECOVERY_PAYMENT;
} else {
event = LoanEvent.LOAN_REPAYMENT_OR_WAIVER;
}
validateAccountStatus(event);
validateActivityNotBeforeClientOrGroupTransferDate(event, repaymentTransaction.getTransactionDate());
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays);
validateRepaymentDateIsOnNonWorkingDay(repaymentTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay);
@@ -2168,7 +2181,7 @@ public class Loan extends AbstractPersistable<Long> {
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final ChangedTransactionDetail changedTransactionDetail = handleRepaymentOrWaiverTransaction(repaymentTransaction,
final ChangedTransactionDetail changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction,
loanLifecycleStateMachine, null);
return changedTransactionDetail;
@@ -2233,13 +2246,19 @@ public class Loan extends AbstractPersistable<Long> {
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
}
private ChangedTransactionDetail handleRepaymentOrWaiverTransaction(final LoanTransaction loanTransaction,
private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction) {
ChangedTransactionDetail changedTransactionDetail = null;
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
LoanStatus.fromInt(this.loanStatus));
LoanStatus statusEnum = null;
if (loanTransaction.isRecoveryRepayment()) {
statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_RECOVERY_PAYMENT, LoanStatus.fromInt(this.loanStatus));
} else {
statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, LoanStatus.fromInt(this.loanStatus));
}
this.loanStatus = statusEnum.getValue();
loanTransaction.updateLoan(this);
@@ -2250,9 +2269,10 @@ public class Loan extends AbstractPersistable<Long> {
this.loanTransactions.add(loanTransaction);
}
if (loanTransaction.isNotRepayment() && loanTransaction.isNotWaiver()) {
final String errorMessage = "A transaction of type repayment or waiver was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.repayment.or.waiver.transaction", errorMessage);
if (loanTransaction.isNotRepayment() && loanTransaction.isNotWaiver() && loanTransaction.isNotRecoveryRepayment()) {
final String errorMessage = "A transaction of type repayment or recovery repayment or waiver was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.repayment.or.waiver.or.recovery.transaction",
errorMessage);
}
final LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
@@ -2311,7 +2331,13 @@ public class Loan extends AbstractPersistable<Long> {
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
/**
* FIXME: Vishwas, skipping post loan transaction checks for Loan
* recoveries
**/
if (loanTransaction.isNotRecoveryRepayment()) {
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
}
return changedTransactionDetail;
}
@@ -2520,7 +2546,7 @@ public class Loan extends AbstractPersistable<Long> {
}
if (newTransactionDetail.isRepayment() || newTransactionDetail.isInterestWaiver()) {
changedTransactionDetail = handleRepaymentOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine,
changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine,
transactionForAdjustment);
}
@@ -2589,6 +2615,13 @@ public class Loan extends AbstractPersistable<Long> {
return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments);
}
public Money calculateTotalRecoveredPayments() {
Money totalRecoveredPayments = getTotalRecoveredPayments();
// in case logic for reversing recovered payment is implemented handle
// subtraction from totalRecoveredPayments
return totalRecoveredPayments;
}
private MonetaryCurrency loanCurrency() {
return this.loanRepaymentScheduleDetail.getCurrency();
}
@@ -2930,6 +2963,14 @@ public class Loan extends AbstractPersistable<Long> {
return disbursementDate;
}
public LocalDate getWrittenOffDate() {
LocalDate writtenOffDate = null;
if (this.writtenOffOnDate != null) {
writtenOffDate = new LocalDate(this.writtenOffOnDate);
}
return writtenOffDate;
}
public LocalDate getExpectedDisbursedOnLocalDateForTemplate() {
LocalDate expectedDisbursementDate = null;
@@ -2998,6 +3039,17 @@ public class Loan extends AbstractPersistable<Long> {
return cumulativePaid;
}
public Money getTotalRecoveredPayments() {
Money cumulativePaid = Money.zero(loanCurrency());
for (final LoanTransaction recoveredPayment : this.loanTransactions) {
if (recoveredPayment.isRecoveryRepayment()) {
cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(loanCurrency()));
}
}
return cumulativePaid;
}
private Money getTotalInterestOutstandingOnLoan() {
Money cumulativeInterest = Money.zero(loanCurrency());
@@ -3682,6 +3734,14 @@ public class Loan extends AbstractPersistable<Long> {
dataValidationErrors.add(error);
}
break;
case LOAN_RECOVERY_PAYMENT:
if (!isClosedWrittenOff()) {
final String defaultUserMessage = "Recovery repayments may only be made on loans which are written off";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.account.is.not.written.off",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
default:
break;
}
@@ -14,7 +14,8 @@ import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail;
public interface LoanAccountDomainService {
LoanTransaction makeRepayment(Loan loan, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId);
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
final boolean isRecoveryRepayment);
LoanTransaction makeRefund(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId);
@@ -82,7 +82,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@Override
public LoanTransaction makeRepayment(final Loan loan, final CommandProcessingResultBuilder builderResult,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText,
final String txnExternalId) {
final String txnExternalId, final boolean isRecoveryRepayment) {
checkClientOrGroupActive(loan);
@@ -103,8 +103,15 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
final Money repaymentAmount = Money.of(loan.getCurrency(), transactionAmount);
final LoanTransaction newRepaymentTransaction = LoanTransaction.repayment(loan.getOffice(), repaymentAmount, paymentDetail,
transactionDate, txnExternalId);
LoanTransaction newRepaymentTransaction = null;
if (isRecoveryRepayment) {
newRepaymentTransaction = LoanTransaction.recoveryRepayment(loan.getOffice(), repaymentAmount, paymentDetail, transactionDate,
txnExternalId);
} else {
newRepaymentTransaction = LoanTransaction.repayment(loan.getOffice(), repaymentAmount, paymentDetail, transactionDate,
txnExternalId);
}
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(),
transactionDate.toDate(), HolidayStatusType.ACTIVE.getValue());
@@ -113,7 +120,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction,
defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, allowTransactionsOnHoliday,
holidays, workingDays, allowTransactionsOnNonWorkingDay);
holidays, workingDays, allowTransactionsOnNonWorkingDay, isRecoveryRepayment);
try {
this.loanTransactionRepository.save(newRepaymentTransaction);
@@ -15,6 +15,7 @@ public enum LoanEvent {
LOAN_WITHDRAWN, //
LOAN_APPROVED, //
LOAN_APPROVAL_UNDO, //
LOAN_RECOVERY_PAYMENT, //
LOAN_DISBURSED, //
LOAN_DISBURSAL_UNDO, //
LOAN_REPAYMENT_OR_WAIVER, //
@@ -23,5 +24,8 @@ public enum LoanEvent {
WRITE_OFF_OUTSTANDING_UNDO, //
LOAN_RESCHEDULE, //
INTERST_REBATE_OWED, //
LOAN_OVERPAYMENT, LOAN_CHARGE_PAYMENT, LOAN_CLOSED, LOAN_EDIT_MULTI_DISBURSE_DATE;
LOAN_OVERPAYMENT, //
LOAN_CHARGE_PAYMENT, //
LOAN_CLOSED, //
LOAN_EDIT_MULTI_DISBURSE_DATE;
}
@@ -112,6 +112,12 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return new LoanTransaction(null, office, LoanTransactionType.REPAYMENT, paymentDetail, amount.getAmount(), paymentDate, externalId);
}
public static LoanTransaction recoveryRepayment(final Office office, final Money amount, final PaymentDetail paymentDetail,
final LocalDate paymentDate, final String externalId) {
return new LoanTransaction(null, office, LoanTransactionType.RECOVERY_REPAYMENT, paymentDetail, amount.getAmount(), paymentDate,
externalId);
}
public static LoanTransaction loanPayment(final Loan loan, final Office office, final Money amount, final PaymentDetail paymentDetail,
final LocalDate paymentDate, final String externalId, final LoanTransactionType transactionType) {
return new LoanTransaction(loan, office, transactionType, paymentDetail, amount.getAmount(), paymentDate, externalId);
@@ -364,6 +370,10 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.equals(getTypeOf()) && isNotReversed();
}
public boolean isNotRecoveryRepayment() {
return !isRecoveryRepayment();
}
public boolean isRecoveryRepayment() {
return LoanTransactionType.RECOVERY_REPAYMENT.equals(getTypeOf()) && isNotReversed();
}
@@ -75,14 +75,14 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
for (final LoanChargePaidBy chargePaidBy : chargePaidBies) {
LoanCharge loanCharge = chargePaidBy.getLoanCharge();
transferCharges.add(loanCharge);
if(loanCharge.isInstalmentFee()){
if (loanCharge.isInstalmentFee()) {
LoanRepaymentScheduleInstallment installment = null;
if(loanCharge.isFeeCharge()){
if (loanCharge.isFeeCharge()) {
installment = loanCharge.fetchRepaymentInstallment(loanTransaction.getFeeChargesPortion(currency));
}else{
} else {
installment = loanCharge.fetchRepaymentInstallment(loanTransaction.getPenaltyChargesPortion(currency));
}
if(installment !=null){
if (installment != null) {
chargePaymentInstallments.add(installment);
}
}
@@ -106,7 +106,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
for (final LoanTransaction loanTransaction : transactionstoBeProcessed) {
if (loanTransaction.isRepayment() || loanTransaction.isInterestWaiver()) {
if (loanTransaction.isRepayment() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
// pass through for new transactions
if (loanTransaction.getId() == null) {
loanTransaction.resetDerivedComponents();
@@ -186,19 +186,19 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
Integer installmentNumber = null;
if(loanTransaction.isChargePayment() && installments.size() == 1){
if (loanTransaction.isChargePayment() && installments.size() == 1) {
installmentNumber = installments.get(0).getInstallmentNumber();
}
if (loanTransaction.isNotWaiver()) {
final Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
if (feeCharges.isGreaterThanZero()) {
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees,installmentNumber);
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber);
}
final Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
if (penaltyCharges.isGreaterThanZero()) {
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties,installmentNumber);
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber);
}
}
@@ -228,16 +228,17 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return penaltyCharges;
}
private void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges,final Integer installmentNumber) {
private void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges,
final Integer installmentNumber) {
Money amountRemaining = feeCharges;
while(amountRemaining.isGreaterThanZero()) {
while (amountRemaining.isGreaterThanZero()) {
final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges);
Money feeAmount = feeCharges.zero();
if(loanTransaction.isChargePayment()){
Money feeAmount = feeCharges.zero();
if (loanTransaction.isChargePayment()) {
feeAmount = feeCharges;
}
final Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining,installmentNumber, feeAmount);
final Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining, installmentNumber, feeAmount);
if (!amountPaidTowardsCharge.isZero()) {
if (!loanTransaction.isChargePayment()) {
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, unpaidCharge,
@@ -247,7 +248,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
amountRemaining = amountRemaining.minus(amountPaidTowardsCharge);
}
}
}
private LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set<LoanCharge> charges) {
@@ -256,18 +257,22 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
LoanInstallmentCharge chargePerInstallment = null;
for (final LoanCharge loanCharge : charges) {
if (loanCharge.isNotFullyPaid() && !loanCharge.isDueAtDisbursement()) {
if(loanCharge.isInstalmentFee()){
LoanInstallmentCharge unpaidLoanChargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge();
if(chargePerInstallment == null || chargePerInstallment.getRepaymentInstallment().getDueDate().isAfter(unpaidLoanChargePerInstallment.getRepaymentInstallment().getDueDate())){
if (loanCharge.isInstalmentFee()) {
LoanInstallmentCharge unpaidLoanChargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge();
if (chargePerInstallment == null
|| chargePerInstallment.getRepaymentInstallment().getDueDate()
.isAfter(unpaidLoanChargePerInstallment.getRepaymentInstallment().getDueDate())) {
installemntCharge = loanCharge;
chargePerInstallment = unpaidLoanChargePerInstallment;
}
}else if (earliestUnpaidCharge == null || loanCharge.getDueLocalDate().isBefore(earliestUnpaidCharge.getDueLocalDate())) {
} else if (earliestUnpaidCharge == null || loanCharge.getDueLocalDate().isBefore(earliestUnpaidCharge.getDueLocalDate())) {
earliestUnpaidCharge = loanCharge;
}
}
}
if(earliestUnpaidCharge == null || (chargePerInstallment!=null && earliestUnpaidCharge.getDueLocalDate().isAfter(chargePerInstallment.getRepaymentInstallment().getDueDate()))){
if (earliestUnpaidCharge == null
|| (chargePerInstallment != null && earliestUnpaidCharge.getDueLocalDate().isAfter(
chargePerInstallment.getRepaymentInstallment().getDueDate()))) {
earliestUnpaidCharge = installemntCharge;
}
@@ -0,0 +1,25 @@
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.CommandProcessingResult;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LoanRecoveryPaymentCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public LoanRecoveryPaymentCommandHandler(LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
final boolean isRecoveryRepayment = true;
return writePlatformService.makeLoanRepayment(command.getLoanId(), command, isRecoveryRepayment);
}
}
@@ -26,7 +26,7 @@ public class LoanRepaymentCommandHandler implements NewCommandSourceHandler {
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.makeLoanRepayment(command.getLoanId(), command);
boolean isRecoveryRepayment = false;
return this.writePlatformService.makeLoanRepayment(command.getLoanId(), command, isRecoveryRepayment);
}
}
@@ -5,6 +5,9 @@
*/
package org.mifosplatform.portfolio.loanaccount.service;
import java.util.Collection;
import java.util.Map;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
@@ -17,18 +20,16 @@ import org.mifosplatform.portfolio.collectionsheet.command.CollectionSheetBulkDi
import org.mifosplatform.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import java.util.Collection;
import java.util.Map;
public interface LoanWritePlatformService {
CommandProcessingResult disburseLoan(Long loanId, JsonCommand command, Boolean isAccountTransfer);
Map<String, Object> bulkLoanDisbursal(JsonCommand command, CollectionSheetBulkDisbursalCommand bulkDisbursalCommand, Boolean isAccountTransfer);
Map<String, Object> bulkLoanDisbursal(JsonCommand command, CollectionSheetBulkDisbursalCommand bulkDisbursalCommand,
Boolean isAccountTransfer);
CommandProcessingResult undoLoanDisbursal(Long loanId, JsonCommand command);
CommandProcessingResult makeLoanRepayment(Long loanId, JsonCommand command);
CommandProcessingResult makeLoanRepayment(Long loanId, JsonCommand command, boolean isRecoveryRepayment);
Map<String, Object> makeLoanBulkRepayment(CollectionSheetBulkRepaymentCommand bulkRepaymentCommand);
@@ -560,7 +560,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional
@Override
public CommandProcessingResult makeLoanRepayment(final Long loanId, final JsonCommand command) {
public CommandProcessingResult makeLoanRepayment(final Long loanId, final JsonCommand command, final boolean isRecoveryRepayment) {
this.context.authenticatedUser();
@@ -585,7 +585,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
this.loanAccountDomainService.makeRepayment(loan, commandProcessingResultBuilder, transactionDate, transactionAmount,
paymentDetail, noteText, txnExternalId);
paymentDetail, noteText, txnExternalId, isRecoveryRepayment);
return commandProcessingResultBuilder.withCommandId(command.commandId()) //
.withLoanId(loanId) //
@@ -600,6 +600,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.context.authenticatedUser();
final SingleRepaymentCommand[] repaymentCommand = bulkRepaymentCommand.getLoanTransactions();
final Map<String, Object> changes = new LinkedHashMap<String, Object>();
final boolean isRecoveryRepayment = false;
if (repaymentCommand == null) { return changes; }
@@ -612,7 +613,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final PaymentDetail paymentDetail = null;
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
this.loanAccountDomainService.makeRepayment(loan, commandProcessingResultBuilder, bulkRepaymentCommand.getTransactionDate(),
singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail, bulkRepaymentCommand.getNote(), null);
singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail, bulkRepaymentCommand.getNote(), null,
isRecoveryRepayment);
changes.put("bulkTransations", singleLoanRepaymentCommand);
}
@@ -2046,4 +2048,5 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransferRepository.save(transferTransaction);
}
}
}
@@ -54,7 +54,7 @@ public final class LoanProductDataValidator {
LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTIES_RECEIVABLE.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue(),LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), LoanProductConstants.useBorrowerCycleParameterName,
LoanProductConstants.principalVariationsForBorrowerCycleParameterName,
LoanProductConstants.interestRateVariationsForBorrowerCycleParameterName,
@@ -299,6 +299,11 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue()).value(incomeFromPenaltyId)
.notNull().integerGreaterThanZero();
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue()).value(incomeFromRecoveryAccountId)
.notNull().integerGreaterThanZero();
final Long writeOffAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue()).value(writeOffAccountId)
@@ -600,6 +605,11 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue()).value(incomeFromPenaltyId)
.ignoreIfNull().integerGreaterThanZero();
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_RECOVERY.getValue()).value(incomeFromRecoveryAccountId)
.notNull().integerGreaterThanZero();
final Long writeOffAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue()).value(writeOffAccountId)