diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index e1a276cda..2948cc98a 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -923,6 +923,14 @@ + + + + loans/{loanId}/transactions?command=recoverypayment + Make a Recovery Payment + + + @@ -7342,7 +7350,61 @@ Request Body: + +   +
+
+

Make Recovery Payment

+

only applies to written-off loans

+
+
+ +POST https://DomainName/api/v1/loans/{loanId}/transactions?command=recoverypayment + + +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" +} + + +{ + "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" + } +} + +
+
+ + +  
@@ -29138,6 +29200,14 @@ No Request Body: + + + loans/{loanId}/transactions?command=recoverypayment + Make Recovery Payment + + + + loans/{loanId}/transactions?command=undowriteoff diff --git a/mifosng-db/migrations/core_db/V167_added_columns_for_writtenOff_loans_recovered.sql b/mifosng-db/migrations/core_db/V167_added_columns_for_writtenOff_loans_recovered.sql new file mode 100644 index 000000000..8d4d7429c --- /dev/null +++ b/mifosng-db/migrations/core_db/V167_added_columns_for_writtenOff_loans_recovered.sql @@ -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; + diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/common/AccountingConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/common/AccountingConstants.java index dd61d5cd6..6991e696c 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/common/AccountingConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/common/AccountingConstants.java @@ -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; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java index e38bab18a..374f3634b 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java @@ -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 diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java index 4c120aaf6..f323d743d 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java @@ -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) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java index 294b8770d..27d2666ec 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java @@ -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 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 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(), diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java index 185031aea..ae5ff9be3 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java @@ -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); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java index d75a28dad..55e28825a 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java @@ -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, diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java old mode 100644 new mode 100755 index e83ef7bf5..71d145d92 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -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"); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java old mode 100644 new mode 100755 index 8ff5e24c5..66a314e8d --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -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"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java old mode 100644 new mode 100755 index 95e97644f..206307db8 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -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); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java index c6e81e0e5..e02036e23 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java @@ -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, diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanTransactionsApiResource.java old mode 100644 new mode 100755 index d0e1bd487..28d51d600 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -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); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java old mode 100644 new mode 100755 index de6ef69b9..6fbaeb74a --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -303,6 +303,9 @@ public class Loan extends AbstractPersistable { @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true) private final Set loanTermVariations = new HashSet(); + @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 { 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 { 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 { public ChangedTransactionDetail makeRepayment(final LoanTransaction repaymentTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, final List existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List 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 { 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 { 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 { 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 { 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 { } if (newTransactionDetail.isRepayment() || newTransactionDetail.isInterestWaiver()) { - changedTransactionDetail = handleRepaymentOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine, + changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment); } @@ -2589,6 +2615,13 @@ public class Loan extends AbstractPersistable { 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 { 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 { 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 { 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; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java old mode 100644 new mode 100755 index e80e8eaff..2ece441cf --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java @@ -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); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java old mode 100644 new mode 100755 index efc89bde8..78383b97e --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -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 existingReversedTransactionIds = new ArrayList(); 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 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); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java old mode 100644 new mode 100755 index 58a5a7ca7..dfe6c4237 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java @@ -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; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java old mode 100644 new mode 100755 index 4cc87f9c5..0d8f16514 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java @@ -112,6 +112,12 @@ public final class LoanTransaction extends AbstractPersistable { 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 { return LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.equals(getTypeOf()) && isNotReversed(); } + public boolean isNotRecoveryRepayment() { + return !isRecoveryRepayment(); + } + public boolean isRecoveryRepayment() { return LoanTransactionType.RECOVERY_REPAYMENT.equals(getTypeOf()) && isNotReversed(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java old mode 100644 new mode 100755 index 0e7c1bd80..35149ed3c --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -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 loanFees = extractFeeCharges(charges); final Set 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 charges,final Integer installmentNumber) { + private void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set 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 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; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRecoveryPaymentCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRecoveryPaymentCommandHandler.java new file mode 100755 index 000000000..0875beb22 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRecoveryPaymentCommandHandler.java @@ -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); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRepaymentCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRepaymentCommandHandler.java index 6b5b9ec46..513e91cce 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRepaymentCommandHandler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRepaymentCommandHandler.java @@ -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); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java index 33acc53b7..79408718d 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -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 bulkLoanDisbursal(JsonCommand command, CollectionSheetBulkDisbursalCommand bulkDisbursalCommand, Boolean isAccountTransfer); + Map 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 makeLoanBulkRepayment(CollectionSheetBulkRepaymentCommand bulkRepaymentCommand); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 5e6e814b1..994294b9b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -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 changes = new LinkedHashMap(); + 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); } } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java old mode 100644 new mode 100755 index 85b713e2e..1f1e38f71 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -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)