From ece7c9a60095af1c6389f229f037029ebaf2d64d Mon Sep 17 00:00:00 2001 From: andrew-dzak Date: Tue, 22 Apr 2014 11:55:49 +0200 Subject: [PATCH] mifosx-281 done --- api-docs/apiLive.htm | 70 +++++++ ...columns_for_writtenOff_loans_recovered.sql | 31 +++ .../common/AccountingConstants.java | 10 +- .../journalentry/data/LoanTransactionDTO.java | 8 +- .../service/AccountingProcessorHelper.java | 3 +- .../CashBasedAccountingProcessorForLoan.java | 45 ++++- ...AccountMappingFromApiJsonDeserializer.java | 5 + .../LoanProductToGLAccountMappingHelper.java | 24 ++- ...AccountMappingReadPlatformServiceImpl.java | 4 + ...ccountMappingWritePlatformServiceImpl.java | 6 + .../commands/domain/CommandWrapper.java | 3 + .../service/CommandWrapperBuilder.java | 9 + .../SynchronousCommandProcessingService.java | 8 +- .../api/LoanTransactionsApiResource.java | 28 ++- .../portfolio/loanaccount/domain/Loan.java | 189 +++++++++++++----- .../domain/LoanAccountDomainService.java | 7 +- .../domain/LoanAccountDomainServiceJpa.java | 94 ++++++++- .../loanaccount/domain/LoanEvent.java | 1 + .../loanaccount/domain/LoanTransaction.java | 41 ++-- ...RepaymentScheduleTransactionProcessor.java | 7 +- .../LoanRecoveryPaymentCommandHandler.java | 31 +++ .../service/LoanWritePlatformService.java | 2 + ...WritePlatformServiceJpaRepositoryImpl.java | 74 +++---- .../LoanProductDataValidator.java | 12 +- 24 files changed, 567 insertions(+), 145 deletions(-) create mode 100755 mifosng-db/migrations/core_db/V159_1__added_columns_for_writtenOff_loans_recovered.sql mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanTransactionsApiResource.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRecoveryPaymentCommandHandler.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java mode change 100644 => 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 8c3076e07..63da497cc 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -707,6 +707,14 @@ + + + + loans/{loanId}/transactions?command=recoverypayment + Make a Recovery Payment + + + @@ -6971,7 +6979,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" + } +} + +
+
+ + +  
@@ -24105,6 +24167,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/V159_1__added_columns_for_writtenOff_loans_recovered.sql b/mifosng-db/migrations/core_db/V159_1__added_columns_for_writtenOff_loans_recovered.sql new file mode 100755 index 000000000..911f638fd --- /dev/null +++ b/mifosng-db/migrations/core_db/V159_1__added_columns_for_writtenOff_loans_recovered.sql @@ -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; + 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/data/LoanTransactionDTO.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java index 73ea17ecf..b302744ff 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java @@ -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 feePayments, final List 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; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java index 81874904c..9e928ae64 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java @@ -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); 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..28350a922 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,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 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 6db12f1d7..7c0befd97 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -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"); 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 b43031f3c..762d542d5 --- 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 b6030e6de..9b8754f25 --- 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 { @@ -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); 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 00731bffa..988dbff98 --- 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") @@ -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); } 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 44d5a8b80..e157a183a --- 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 @@ -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 { @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, @@ -992,6 +958,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()); @@ -2172,6 +2141,26 @@ public class Loan extends AbstractPersistable { return changedTransactionDetail; } + public ChangedTransactionDetail makeRecoveryRepayment(final LoanTransaction repaymentTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, + final List existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List 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 existingTransactionIds, final List existingReversedTransactionIds, @@ -2232,6 +2221,77 @@ public class Loan extends AbstractPersistable { 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 allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement(); + changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), + allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges()); + for (final Map.Entry 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 { 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 { 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 { 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 { 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; } 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 0e2f1d9de..7a73a0472 --- 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 @@ -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); 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 a3465a1f5..0d3ce963d --- 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 @@ -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 existingTransactionIds = new ArrayList(); + final List existingReversedTransactionIds = new ArrayList(); + + 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 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 dataValidationErrors = new ArrayList(); + 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 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; + } } \ No newline at end of file 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..457fbe3d9 --- 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_WRITTEN_OFF, LOAN_DISBURSED, // LOAN_DISBURSAL_UNDO, // LOAN_REPAYMENT_OR_WAIVER, // 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..6054e3091 --- 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 @@ -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 { @OneToMany(cascade = CascadeType.ALL, mappedBy = "loanTransaction", orphanRemoval = true) private Set loanChargesPaid = new HashSet(); + @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 { 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 { 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 { 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 { 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()); 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..c864c30f7 --- 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 @@ -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); } 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..2ddf493d0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRecoveryPaymentCommandHandler.java @@ -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); + } +} 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 ae037d044..32bb881d9 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 @@ -30,6 +30,8 @@ public interface LoanWritePlatformService { CommandProcessingResult makeLoanRepayment(Long loanId, JsonCommand command); + CommandProcessingResult makeLoanRecoveryPayment(Long loanId, JsonCommand command); + Map makeLoanBulkRepayment(CollectionSheetBulkRepaymentCommand bulkRepaymentCommand); CommandProcessingResult adjustLoanTransaction(Long loanId, Long transactionId, JsonCommand command); 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 old mode 100644 new mode 100755 index 65933448d..d87c2d5d5 --- 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 @@ -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 changes = new LinkedHashMap(); + 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(); + } } \ 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)