mifosx-281 done

This commit is contained in:
andrew-dzak
2014-04-22 11:55:49 +02:00
parent e60c4ec40d
commit ece7c9a600
24 changed files with 567 additions and 145 deletions
+70
View File
@@ -707,6 +707,14 @@
<td></td>
<td></td>
<td></td>
</tr>
<tr class="alt">
<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 class="alt">
<td></td>
@@ -6971,7 +6979,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">
@@ -24105,6 +24167,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,31 @@
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);
Alter table m_loan_transaction
add column recovered_portion_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;
@@ -28,6 +28,7 @@ public class LoanTransactionDTO {
private final BigDecimal fees;
private final BigDecimal penalties;
private final BigDecimal overPayment;
private final BigDecimal recoveryPayment;
/*** Boolean values determines if the transaction is reversed ***/
private final boolean reversed;
@@ -38,7 +39,7 @@ public class LoanTransactionDTO {
public LoanTransactionDTO(final Long officeId, final Long paymentTypeId, final String transactionId, final Date transactionDate,
final LoanTransactionEnumData transactionType, final BigDecimal amount, final BigDecimal principal, final BigDecimal interest,
final BigDecimal fees, final BigDecimal penalties, final BigDecimal overPayment, final boolean reversed,
final BigDecimal fees, final BigDecimal penalties, final BigDecimal overPayment, final BigDecimal recoveryPayment, final boolean reversed,
final List<ChargePaymentDTO> feePayments, final List<ChargePaymentDTO> penaltyPayments) {
this.paymentTypeId = paymentTypeId;
this.transactionId = transactionId;
@@ -53,6 +54,7 @@ public class LoanTransactionDTO {
this.feePayments = feePayments;
this.penaltyPayments = penaltyPayments;
this.overPayment = overPayment;
this.recoveryPayment = recoveryPayment;
this.officeId = officeId;
}
@@ -96,6 +98,10 @@ public class LoanTransactionDTO {
return this.overPayment;
}
public BigDecimal getRecoveryPayment() {
return this.recoveryPayment;
}
public boolean isReversed() {
return this.reversed;
}
@@ -96,6 +96,7 @@ public class AccountingProcessorHelper {
final BigDecimal fees = (BigDecimal) map.get("feeChargesPortion");
final BigDecimal penalties = (BigDecimal) map.get("penaltyChargesPortion");
final BigDecimal overPayments = (BigDecimal) map.get("overPaymentPortion");
final BigDecimal recoveryPayments = (BigDecimal) map.get("recoveryPaymentPortion");
final boolean reversed = (Boolean) map.get("reversed");
final Long paymentTypeId = (Long) map.get("paymentTypeId");
@@ -120,7 +121,7 @@ public class AccountingProcessorHelper {
}
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId,
transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed,
transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, recoveryPayments, reversed,
feePaymentDetails, penaltyPaymentDetails);
newLoanTransactions.add(transaction);
@@ -56,6 +56,12 @@ 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);
@@ -142,7 +148,7 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
* (loan portfolio for principal repayments, Interest on loans for interest
* repayments, Income from fees for fees payment and Income from penalties
* for penalty payment)
*
*
* In case the loan transaction is a reversal, all debits are turned into
* credits and vice versa
*/
@@ -202,6 +208,43 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal);
}
/**
* Create a single Debit to fund source and multiple credits if applicable
* (loan portfolio for principal repayments, Interest on loans for interest
* repayments, Income from fees for fees payment and Income from penalties
* for penalty payment)
*
* 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 recoveryPaymentAmount = loanTransactionDTO.getRecoveryPayment();
final boolean isReversal = loanTransactionDTO.isReversed();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
BigDecimal totalDebitAmount = new BigDecimal(0);
if (recoveryPaymentAmount != null && !(recoveryPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(recoveryPaymentAmount);
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_RECOVERY, loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, recoveryPaymentAmount, isReversal);
}
/*** create a single debit entry (or reversal) for the entire amount **/
this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE, loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, 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,
@@ -381,6 +381,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 {
@@ -339,7 +339,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);
@@ -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")
@@ -152,6 +145,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); }
@@ -5,36 +5,9 @@
*/
package org.mifosplatform.portfolio.loanaccount.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.annotations.LazyCollection;
@@ -74,21 +47,8 @@ import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.exception.ExceedingTrancheCountException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanDisbursalException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException;
import org.mifosplatform.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.mifosplatform.portfolio.loanaccount.exception.*;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.*;
import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
@@ -98,9 +58,12 @@ import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail;
import org.mifosplatform.useradministration.domain.AppUser;
import org.springframework.data.jpa.domain.AbstractPersistable;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import javax.persistence.*;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.*;
@Entity
@Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "loan_account_no_UNIQUE"),
@@ -304,6 +267,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,
@@ -992,6 +958,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());
@@ -2172,6 +2141,26 @@ public class Loan extends AbstractPersistable<Long> {
return changedTransactionDetail;
}
public ChangedTransactionDetail makeRecoveryRepayment(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) {
validateAccountStatus(LoanEvent.LOAN_WRITTEN_OFF);
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.WRITE_OFF_OUTSTANDING, repaymentTransaction.getTransactionDate());
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays);
validateRepaymentDateIsOnNonWorkingDay(repaymentTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay);
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final ChangedTransactionDetail changedTransactionDetail = handleRecoveryRepayment(repaymentTransaction,
loanLifecycleStateMachine, null);
return changedTransactionDetail;
}
public void makeChargePayment(final Long chargeId, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
@@ -2232,6 +2221,77 @@ public class Loan extends AbstractPersistable<Long> {
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
}
private ChangedTransactionDetail handleRecoveryRepayment(final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction){
ChangedTransactionDetail changedTransactionDetail = null;
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WRITTEN_OFF,
LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
loanTransaction.updateLoan(this);
final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction, this.loanTransactions);
if (loanTransaction.isNotZero(loanCurrency())) {
this.loanTransactions.add(loanTransaction);
}
if (!loanTransaction.isRecoveryRepayment()) {
final String errorMessage = "A transaction of type recovery repayment was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.recovery.repayment.transaction", errorMessage);
}
final LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
if (loanTransactionDate.isBefore(getWrittenOffDate())) {
final String errorMessage = "The transaction date cannot be before the loan written-off date: "
+ getWrittenOffDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.written-off.date", errorMessage,
loanTransactionDate, getWrittenOffDate());
}
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);
}
if (this.loanProduct.isMultiDisburseLoan() && adjustedTransaction == null) {
BigDecimal totalDisbursed = getDisbursedAmount();
if (totalDisbursed.compareTo(this.summary.getTotalPrincipalRepaid()) < 0) {
final String errorMessage = "The transaction cannot be done before the loan disbursement: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.done.before.disbursement", errorMessage);
}
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
if (isTransactionChronologicallyLatest && adjustedTransaction == null) {
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments,
charges());
} else {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
/***
* Commented since throwing exception if external id present for one
* of the transactions. for this need to save the reversed
* transactions first and then new transactions.
*/
// this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
}
updateLoanSummaryDerivedFields();
// doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
return changedTransactionDetail;
}
private ChangedTransactionDetail handleRepaymentOrWaiverTransaction(final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction) {
@@ -2588,6 +2648,12 @@ 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();
}
@@ -2929,6 +2995,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;
@@ -2997,6 +3071,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.getRecoveredPaymentPortion(loanCurrency())) ;
}
}
return cumulativePaid;
}
private Money getTotalInterestOutstandingOnLoan() {
Money cumulativeInterest = Money.zero(loanCurrency());
@@ -3681,6 +3766,14 @@ public class Loan extends AbstractPersistable<Long> {
dataValidationErrors.add(error);
}
break;
case LOAN_WRITTEN_OFF:
if(!isClosedWrittenOff()){
final String defaultUserMessage = "Active Loan Account is not allowed for recovery repayment. Loan Account is Active.";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.close.account.is.not.active",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
default:
break;
}
@@ -5,17 +5,20 @@
*/
package org.mifosplatform.portfolio.loanaccount.domain;
import java.math.BigDecimal;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail;
import java.math.BigDecimal;
public interface LoanAccountDomainService {
LoanTransaction makeRepayment(Loan loan, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId);
LoanTransaction makeLoanRecoveryRepayment(Loan loan, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId);
LoanTransaction makeRefund(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId);
@@ -5,12 +5,6 @@
*/
package org.mifosplatform.portfolio.loanaccount.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.accounting.journalentry.service.JournalEntryWritePlatformService;
@@ -43,6 +37,12 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Service
public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@@ -284,4 +284,86 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
}
@Override
@Transactional
public LoanTransaction makeLoanRecoveryRepayment(Loan loan, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId) {
checkClientOrGroupActive(loan);
// TODO: Is it required to validate transaction date with meeting dates
// if repayments is synced with meeting?
/*
* if(loan.isSyncDisbursementWithMeeting()){ // validate actual
* disbursement date against meeting date CalendarInstance
* calendarInstance =
* this.calendarInstanceRepository.findCalendarInstaneByLoanId
* (loan.getId(), CalendarEntityType.LOANS.getValue());
* this.loanEventApiJsonValidator
* .validateRepaymentDateWithMeetingDate(transactionDate,
* calendarInstance); }
*/
final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
final Money repaymentAmount = Money.of(loan.getCurrency(), transactionAmount);
final LoanTransaction newRepaymentTransaction = LoanTransaction.recoveryRepayment(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());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
final ChangedTransactionDetail changedTransactionDetail = loan.makeRecoveryRepayment(newRepaymentTransaction,
defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, allowTransactionsOnHoliday,
holidays, workingDays, allowTransactionsOnNonWorkingDay);
try {
this.loanTransactionRepository.save(newRepaymentTransaction);
} catch (DataIntegrityViolationException e) {
final Throwable realCause = e.getCause();
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
if (realCause.getMessage().toLowerCase().contains("external_id_unique")) {
baseDataValidator.reset().parameter("externalId").value(newRepaymentTransaction.getExternalId())
.failWithCode("value.must.be.unique");
}
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
/***
* TODO Vishwas Batch save is giving me a
* HibernateOptimisticLockingFailureException, looping and saving for
* the time being, not a major issue for now as this loop is entered
* only in edge cases (when a payment is made before the latest payment
* recorded against the loan)
***/
this.loanRepository.saveAndFlush(loan);
if (changedTransactionDetail != null) {
for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
// update loan with references to the newly created transactions
loan.getLoanTransactions().add(mapEntry.getValue());
updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanTransactionNote(loan, newRepaymentTransaction, noteText);
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
builderResult.withEntityId(newRepaymentTransaction.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()); //
return newRepaymentTransaction;
}
}
@@ -15,6 +15,7 @@ public enum LoanEvent {
LOAN_WITHDRAWN, //
LOAN_APPROVED, //
LOAN_APPROVAL_UNDO, //
LOAN_WRITTEN_OFF,
LOAN_DISBURSED, //
LOAN_DISBURSAL_UNDO, //
LOAN_REPAYMENT_OR_WAIVER, //
@@ -5,26 +5,6 @@
*/
package org.mifosplatform.portfolio.loanaccount.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.UniqueConstraint;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.joda.time.LocalDate;
@@ -40,6 +20,10 @@ import org.mifosplatform.portfolio.paymentdetail.data.PaymentDetailData;
import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail;
import org.springframework.data.jpa.domain.AbstractPersistable;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.*;
/**
* All monetary transactions against a loan are modelled through this entity.
* Disbursements, Repayments, Waivers, Write-off etc
@@ -95,6 +79,9 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loanTransaction", orphanRemoval = true)
private Set<LoanChargePaidBy> loanChargesPaid = new HashSet<LoanChargePaidBy>();
@Column(name = "recovered_portion_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal recoveredPaymentPortion;
protected LoanTransaction() {
this.loan = null;
this.dateOf = null;
@@ -111,6 +98,10 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
final LocalDate paymentDate, final String externalId) {
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) {
@@ -296,6 +287,11 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
this.overPaymentPortion = defaultToNullIfZero(getOverPaymentPortion(currency).plus(overPayment).getAmount());
}
public void updateRecoveredPayments(final Money recoveredPayment){
final MonetaryCurrency currency = recoveredPayment.getCurrency();
this.recoveredPaymentPortion = defaultToNullIfZero(getRecoveredPaymentPortion(currency).plus(recoveredPayment).getAmount());
}
public Money getPrincipalPortion(final MonetaryCurrency currency) {
return Money.of(currency, this.principalPortion);
}
@@ -320,6 +316,10 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return Money.of(currency, this.overPaymentPortion);
}
public Money getRecoveredPaymentPortion(final MonetaryCurrency currency){
return Money.of(currency,this.recoveredPaymentPortion) ;
}
public Money getAmount(final MonetaryCurrency currency) {
return Money.of(currency, this.amount);
}
@@ -471,6 +471,7 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
thisTransactionData.put("feeChargesPortion", this.feeChargesPortion);
thisTransactionData.put("penaltyChargesPortion", this.penaltyChargesPortion);
thisTransactionData.put("overPaymentPortion", this.overPaymentPortion);
thisTransactionData.put("recoveryPaymentPortion", this.recoveredPaymentPortion);
if (this.paymentDetail != null) {
thisTransactionData.put("paymentTypeId", this.paymentDetail.getPaymentType().getId());
@@ -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();
@@ -202,7 +202,10 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
}
if (transactionAmountUnprocessed.isGreaterThanZero()) {
if(loanTransaction.isRecoveryRepayment()){
loanTransaction.updateRecoveredPayments(transactionAmountUnprocessed);
System.out.println(loanTransaction.getRecoveredPaymentPortion(currency)) ;
}else if(transactionAmountUnprocessed.isGreaterThanZero() && !loanTransaction.isRecoveryRepayment()) {
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
loanTransaction.updateOverPayments(transactionAmountUnprocessed);
}
@@ -0,0 +1,31 @@
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;
/**
* Created with IntelliJ IDEA.
* User: andrew
* Date: 2-4-14
* Time: 15:21
* To change this template use File | Settings | File Templates.
*/
@Service
public class LoanRecoveryPaymentCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public LoanRecoveryPaymentCommandHandler(LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
return writePlatformService.makeLoanRecoveryPayment(command.getLoanId(), command);
}
}
@@ -30,6 +30,8 @@ public interface LoanWritePlatformService {
CommandProcessingResult makeLoanRepayment(Long loanId, JsonCommand command);
CommandProcessingResult makeLoanRecoveryPayment(Long loanId, JsonCommand command);
Map<String, Object> makeLoanBulkRepayment(CollectionSheetBulkRepaymentCommand bulkRepaymentCommand);
CommandProcessingResult adjustLoanTransaction(Long loanId, Long transactionId, JsonCommand command);
@@ -5,17 +5,7 @@
*/
package org.mifosplatform.portfolio.loanaccount.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import com.google.gson.JsonElement;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
@@ -63,16 +53,11 @@ import org.mifosplatform.portfolio.calendar.service.CalendarUtils;
import org.mifosplatform.portfolio.charge.domain.Charge;
import org.mifosplatform.portfolio.charge.domain.ChargePaymentMode;
import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeAddedException;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeDeletedException;
import org.mifosplatform.portfolio.charge.exception.*;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeDeletedException.LOAN_CHARGE_CANNOT_BE_DELETED_REASON;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBePayedException;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBePayedException.LOAN_CHARGE_CANNOT_BE_PAYED_REASON;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeUpdatedException;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeUpdatedException.LOAN_CHARGE_CANNOT_BE_UPDATED_REASON;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeWaivedException;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeWaivedException.LOAN_CHARGE_CANNOT_BE_WAIVED_REASON;
import org.mifosplatform.portfolio.charge.exception.LoanChargeNotFoundException;
import org.mifosplatform.portfolio.client.domain.Client;
import org.mifosplatform.portfolio.client.exception.ClientNotActiveException;
import org.mifosplatform.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand;
@@ -85,22 +70,7 @@ import org.mifosplatform.portfolio.group.exception.GroupNotActiveException;
import org.mifosplatform.portfolio.loanaccount.command.LoanUpdateCommand;
import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData;
import org.mifosplatform.portfolio.loanaccount.data.LoanInstallmentChargeData;
import org.mifosplatform.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.mifosplatform.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanChargeRepository;
import org.mifosplatform.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.mifosplatform.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository;
import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType;
import org.mifosplatform.portfolio.loanaccount.domain.*;
import org.mifosplatform.portfolio.loanaccount.exception.LoanDisbursalException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException;
@@ -130,7 +100,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import com.google.gson.JsonElement;
import java.math.BigDecimal;
import java.util.*;
@Service
public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService {
@@ -1951,4 +1922,39 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransferRepository.save(transferTransaction);
}
}
@Override
public CommandProcessingResult makeLoanRecoveryPayment(Long loanId, JsonCommand command) {
this.context.authenticatedUser();
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
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());
changes.put("paymentTypeId",command.stringValueOfParameterNamed("paymentTypeId"));
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
}
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
this.loanAccountDomainService.makeLoanRecoveryRepayment(loan, commandProcessingResultBuilder, transactionDate, transactionAmount,
paymentDetail, noteText, txnExternalId);
return commandProcessingResultBuilder.withCommandId(command.commandId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
}
@@ -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)