diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index a9ac53a8c..0b138189b 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -365,6 +365,22 @@ + + + accounttransfers/templateRefundByTransfer + + Retrieve Refund of an Active Loan by Transfer Template + + + + + + accounttransfers/refundByTransfer + Refund an active loan by transfer + + + + @@ -1062,6 +1078,14 @@ + + loans/{loanId}/transactions?command=refundByCash + Refund an Active Loan by Cash + + + + + Loan Charges loans/{loanId}/charges Add a Loan Charge @@ -8914,6 +8938,11 @@ No Request Body: outstanding. The amount that can be waived is limited to the loan's inArrearsTolerance +
+ 'refundbycash'
"date" is set to the current date
+ "total" "amount" is set + to the total amount paid in advance. +

Example Request:

loans/1/transactions/template?command=repayment
@@ -9000,6 +9029,45 @@ Request Body: + +   +
+
+

Make a Refund of an Active Loan by Cash

+
+
+ +POST https://DomainName/api/v1/loans/{loanId}/transactions?command=refundByCash + + +POST loans/5/transactions?command=refundByCash +Content-Type: application/json +Request Body: +{ + "dateFormat": "dd MMMM yyyy", + "locale": "en", + "transactionDate": "14 May 2013", + "transactionAmount": "500.00", + "paymentTypeId": "12" +} + + +{ + "officeId": 1, + "clientId": 1, + "loanId": 5, + "resourceId": 564, + "changes": { + "transactionDate": "14 May 2013", + "transactionAmount": "500.00", + "locale": "en", + "dateFormat": "dd MMMM yyyy + } +} + +
+
+  
@@ -9259,7 +9327,7 @@ Request Body:
- +   @@ -25621,6 +25689,285 @@ No Request Body: "id": 1, "accountNo": "000000001" } +} + + + + +   +
+
+

Retrieve Refund of an Active Loan by Transfer Template:

+

Example Requests :

+
accounttransfers/templateRefundByTransfer?fromAccountId=2&fromAccountType=1& + toAccountId=1&toAccountType=2&toClientId=1&toOfficeId=1
+
+
+
+ GET https://DomainName/api/v1/accounttransfers/templateRefundByTransfer + +{ +   "currency": { +      "code": "USD", +      "name": "US Dollar", +      "decimalPlaces": 0, +      "inMultiplesOf": 0, +      "displaySymbol": "$", +      "nameCode": "currency.USD", +      "displayLabel": "US Dollar ($)" +   }, +   "transferAmount": 130, +   "transferDate": [ +      2014, +      11, +      1 +   ], +   "fromOffice": { +      "id": 1, +      "name": "Head Office", +      "nameDecorated": "Head Office", +      "externalId": "1", +      "openingDate": [ +         2009, +         1, +         1 +      ], +      "hierarchy": "." +   }, +   "fromClient": { +      "id": 1, +      "accountNo": "000000001", +      "status": { +         "id": 300, +         "code": "clientStatusType.active", +         "value": "Active" +      }, +      "active": true, +      "activationDate": [ +         2012, +         2, +         1 +      ], +      "firstname": "Daniel", +      "lastname": "Owusu", +      "displayName": "Daniel Owusu", +      "gender": {}, +      "clientType": {}, +      "clientClassification": {}, +      "officeId": 1, +      "officeName": "Head Office", +      "timeline": { +         "submittedOnDate": [ +            2012, +            2, +            1 +         ], +         "submittedByUsername": "mifos", +         "submittedByFirstname": "App", +         "submittedByLastname": "Administrator", +         "activatedOnDate": [ +            2012, +            2, +            1 +         ], +         "activatedByUsername": "mifos", +         "activatedByFirstname": "App", +         "activatedByLastname": "Administrator" +      }, +      "groups": [] +   }, +   "fromAccountType": { +      "id": 1, +      "code": "accountType.loan", +      "value": "Loan Account" +   }, +   "fromAccount": { +      "id": 2, +      "accountNo": "000000002", +      "clientId": 1, +      "clientName": "Daniel Owusu", +      "productId": 1, +      "productName": "CTRL", +      "fieldOfficerId": 0, +      "currency": { +         "code": "USD", +         "name": "US Dollar", +         "decimalPlaces": 0, +         "inMultiplesOf": 0, +         "displaySymbol": "$", +         "nameCode": "currency.USD", +         "displayLabel": "US Dollar ($)" +      }, +      "amtForTransfer": 130 +   }, +   "toOffice": { +      "id": 1, +      "name": "Head Office", +      "nameDecorated": "Head Office", +      "externalId": "1", +      "openingDate": [ +         2009, +         1, +         1 +      ], +      "hierarchy": "." +   }, +   "toClient": { +      "id": 1, +      "displayName": "Daniel Owusu", +      "officeId": 1, +      "officeName": "Head Office" +   }, +   "toAccountType": { +      "id": 2, +      "code": "accountType.savings", +      "value": "Savings Account" +   }, +   "toAccount": { +      "id": 1, +      "accountNo": "000000001", +      "clientId": 1, +      "clientName": "Daniel Owusu", +      "productId": 1, +      "productName": "TEST", +      "fieldOfficerId": 0, +      "currency": { +         "code": "USD", +         "name": "US Dollar", +         "decimalPlaces": 0, +         "inMultiplesOf": 0, +         "displaySymbol": "$", +         "nameCode": "currency.USD", +         "displayLabel": "US Dollar ($)" +      } +   }, +   "fromOfficeOptions": [ +      { +         "id": 1, +         "name": "Head Office", +         "nameDecorated": "Head Office" +      } +   ], +   "fromClientOptions": [ +      { +         "id": 1, +         "displayName": "Daniel Owusu", +         "officeId": 1, +         "officeName": "Head Office" +      } +   ], +   "fromAccountTypeOptions": [ +      { +         "id": 2, +         "code": "accountType.savings", +         "value": "Savings Account" +      }, +      { +         "id": 1, +         "code": "accountType.loan", +         "value": "Loan Account" +      } +   ], +   "fromAccountOptions": [ +      { +         "id": 2, +         "accountNo": "000000002", +         "clientId": 1, +         "clientName": "Daniel Owusu", +         "productId": 1, +         "productName": "CTRL", +         "fieldOfficerId": 0, +         "currency": { +            "code": "USD", +            "name": "US Dollar", +            "decimalPlaces": 0, +            "inMultiplesOf": 0, +            "displaySymbol": "$", +            "nameCode": "currency.USD", +            "displayLabel": "US Dollar ($)" +         } +      } +   ], +   "toOfficeOptions": [ +      { +         "id": 1, +         "name": "Head Office", +         "nameDecorated": "Head Office" +      } +   ], +   "toClientOptions": [ +      { +         "id": 1, +         "displayName": "Daniel Owusu", +         "officeId": 1, +         "officeName": "Head Office" +      } +   ], +   "toAccountTypeOptions": [ +      { +         "id": 2, +         "code": "accountType.savings", +         "value": "Savings Account" +      } +   ], +   "toAccountOptions": [ +      { +         "id": 1, +         "accountNo": "000000001", +         "clientId": 1, +         "clientName": "Daniel Owusu", +         "productId": 1, +         "productName": "TEST", +         "fieldOfficerId": 0, +         "currency": { +            "code": "USD", +            "name": "US Dollar", +            "decimalPlaces": 0, +            "inMultiplesOf": 0, +            "displaySymbol": "$", +            "nameCode": "currency.USD", +            "displayLabel": "US Dollar ($)" +         } +      } +   ] +} + +
+
+ + +   +
+
+

Refund of an Active Loan by Transfer

+

Ability to refund an active loan by transferring to a savings account.

+
+
+ POST https://Domain Name/api/v1/accounttransfers + POST refundByTransfer/ +Content-Type: application/json +No Request Body: +{ + "fromAccountId": "2", + "fromAccountType": 1, + "toOfficeId": 1, + "toClientId": 1, + "toAccountType": 2, + "toAccountId": 1, + "transferAmount": 130, + "transferDate": "31 October 2014", + "transferDescription": "Transfer refund to my savings account", + "locale": "en", + "dateFormat": "dd MMMM yyyy", + "fromClientId": 1, + "fromOfficeId": 1 + +} + + +{ + "savingsId": 1, + "resourceId": 1 }
diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java index 98126146c..93afd772e 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java @@ -35,7 +35,10 @@ import org.mifosplatform.integrationtests.common.loans.LoanApplicationTestBuilde import org.mifosplatform.integrationtests.common.loans.LoanProductTestBuilder; import org.mifosplatform.integrationtests.common.loans.LoanStatusChecker; import org.mifosplatform.integrationtests.common.loans.LoanTransactionHelper; +import org.mifosplatform.integrationtests.common.savings.AccountTransferHelper; import org.mifosplatform.integrationtests.common.savings.SavingsAccountHelper; +import org.mifosplatform.integrationtests.common.savings.SavingsProductHelper; +import org.mifosplatform.integrationtests.common.savings.SavingsStatusChecker; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.builder.ResponseSpecBuilder; @@ -66,6 +69,8 @@ public class ClientLoanIntegrationTest { private AccountHelper accountHelper; private SchedulerJobHelper schedulerJobHelper; private PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper; + private SavingsAccountHelper savingsAccountHelper; + private AccountTransferHelper accountTransferHelper; @Before public void setup() { @@ -891,6 +896,23 @@ public class ClientLoanIntegrationTest { return this.loanTransactionHelper.getLoanProductId(loanProductJSON); } + private Integer createLoanProduct(final String inMultiplesOf, final String digitsAfterDecimal, final String repaymentStrategy, + final String accountingRule, final Account... accounts) { + System.out.println("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); + final String loanProductJSON = new LoanProductTestBuilder() // + .withPrincipal("10000000.00") // + .withNumberOfRepayments("24") // + .withRepaymentAfterEvery("1") // + .withRepaymentTypeAsMonth() // + .withinterestRatePerPeriod("2") // + .withInterestRateFrequencyTypeAsMonths() // + .withRepaymentStrategy(repaymentStrategy) // + .withAmortizationTypeAsEqualPrincipalPayment() // + .withInterestTypeAsDecliningBalance() // + .currencyDetails(digitsAfterDecimal, inMultiplesOf).withAccounting(accountingRule, accounts).build(null); + return this.loanTransactionHelper.getLoanProductId(loanProductJSON); + } + private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, String graceOnPrincipalPayment) { System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); final String loanApplicationJSON = new LoanApplicationTestBuilder() // @@ -992,6 +1014,32 @@ public class ClientLoanIntegrationTest { return this.loanTransactionHelper.getLoanId(loanApplicationJSON); } + private Integer applyForLoanApplicationWithPaymentStrategyAndPastMonth(final Integer clientID, final Integer loanProductID, + List charges, final String savingsId, String principal, final String repaymentStrategy, final int month) { + System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); + + Calendar fourMonthsfromNowCalendar = Calendar.getInstance(); + fourMonthsfromNowCalendar.add(Calendar.MONTH, month); + + String fourMonthsfromNow = new SimpleDateFormat("dd MMMMMM yyyy").format(fourMonthsfromNowCalendar.getTime()); + final String loanApplicationJSON = new LoanApplicationTestBuilder() // + .withPrincipal(principal) // + .withLoanTermFrequency("6") // + .withLoanTermFrequencyAsMonths() // + .withNumberOfRepayments("6") // + .withRepaymentEveryAfter("1") // + .withRepaymentFrequencyTypeAsMonths() // + .withInterestRatePerPeriod("2") // + .withAmortizationTypeAsEqualInstallments() // + .withInterestTypeAsFlatBalance() // + .withInterestCalculationPeriodTypeSameAsRepaymentPeriod() // + .withExpectedDisbursementDate(fourMonthsfromNow) // + .withSubmittedOnDate(fourMonthsfromNow) // + .withwithRepaymentStrategy(repaymentStrategy) // + .withCharges(charges).build(clientID.toString(), loanProductID.toString(), savingsId); + return this.loanTransactionHelper.getLoanId(loanApplicationJSON); + } + private void verifyLoanRepaymentSchedule(final ArrayList loanSchedule) { System.out.println("--------------------VERIFYING THE PRINCIPAL DUES,INTEREST DUE AND DUE DATE--------------------------"); @@ -3820,4 +3868,539 @@ public class ClientLoanIntegrationTest { index++; } } + + /*** + * Test case to verify Mifos Style payment strategy + */ + @Test + public void testLoanRefundByCashCashBasedAccounting() { + this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); + + Calendar fourMonthsfromNowCalendar = Calendar.getInstance(); + fourMonthsfromNowCalendar.add(Calendar.MONTH, -4); + + String fourMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + + /*** + * Create loan product with MIFOS STYLE strategy + */ + + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProduct("0", "0", LoanProductTestBuilder.MIFOS_STANDARD_STRATEGY, CASH_BASED, assetAccount, + incomeAccount, expenseAccount, overpaymentAccount); + Assert.assertNotNull(loanProductID); + + /*** + * Apply for loan application and verify loan status + */ + final String savingsId = null; + final String principal = "12,000.00"; + + // Add charges with payment mode regular + List charges = new ArrayList<>(); + + Integer flatInstallmentFee = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); + addCharges(charges, flatInstallmentFee, "50", null); + + final Integer loanID = applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, charges, savingsId, + principal, LoanApplicationTestBuilder.MIFOS_STANDARD_STRATEGY, -4); + Assert.assertNotNull(loanID); + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoan(fourMonthsfromNow, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.disburseLoan(fourMonthsfromNow, loanID); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + final JournalEntry[] assetAccountInitialEntry = { new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.CREDIT), + new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.DEBIT) }; + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, fourMonthsfromNow, assetAccountInitialEntry); + + ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + HashMap firstInstallment = loanSchedule.get(1); + validateNumberForEqual("2290", String.valueOf(firstInstallment.get("totalOutstandingForPeriod"))); + + // Make payment for installment #1 + + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + + final String threeMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRepayment(threeMonthsfromNow, Float.valueOf("2290"), loanID); + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + firstInstallment = loanSchedule.get(1); + validateNumberForEqual("0.00", String.valueOf(firstInstallment.get("totalOutstandingForPeriod"))); + + // Make payment for installment #2 + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + + final String twoMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRepayment(twoMonthsfromNow, Float.valueOf("2290"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, twoMonthsfromNow, new JournalEntry(Float.valueOf("2290"), + JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("2000"), JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, twoMonthsfromNow, new JournalEntry(Float.valueOf("50"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("240"), JournalEntry.TransactionType.CREDIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + Map secondInstallment = loanSchedule.get(2); + validateNumberForEqual("0.00", String.valueOf(secondInstallment.get("totalOutstandingForPeriod"))); + + // Make payment for installment #3 + // Pay 2290 more than expected + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + + final String oneMonthfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRepayment(oneMonthfromNow, Float.valueOf("4580"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, oneMonthfromNow, new JournalEntry(Float.valueOf("4580"), + JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("4000"), JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, oneMonthfromNow, new JournalEntry(Float.valueOf("100"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("480"), JournalEntry.TransactionType.CREDIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + HashMap thirdInstallment = loanSchedule.get(3); + validateNumberForEqual("0.00", String.valueOf(thirdInstallment.get("totalOutstandingForPeriod"))); + + // Make refund of 20 + // max 2290 to refund. Pay 20 means only principal + // mifos style refund order(principal, interest, fees and penalties + // paid: principal 2000, interest 240, fees 50, penalty 0 + // refund 20 means paid: principal 1980, interest 240, fees 50, penalty + // 0 + + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + final String now = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRefundByCash(now, Float.valueOf("20"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now, new JournalEntry(Float.valueOf("20"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("20"), JournalEntry.TransactionType.DEBIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + HashMap fourthInstallment = loanSchedule.get(4); + validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod"))); + validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("principalOutstanding"))); + validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("interestOutstanding"))); + validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding"))); + + // Make refund of 2000 + // max 2270 to refund. Pay 2000 means only principal + // paid: principal 1980, interest 240, fees 50, penalty 0 + // refund 2000 means paid: principal 0, interest 220, fees 50, penalty 0 + + this.loanTransactionHelper.makeRefundByCash(now, Float.valueOf("2000"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now, new JournalEntry(Float.valueOf("2000"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("1980"), JournalEntry.TransactionType.DEBIT)); + + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, now, new JournalEntry(Float.valueOf("20"), + JournalEntry.TransactionType.DEBIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + fourthInstallment = loanSchedule.get(4); + validateNumberForEqual("2020.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod"))); + validateNumberForEqual("2000.00", String.valueOf(fourthInstallment.get("principalOutstanding"))); + validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("interestOutstanding"))); + validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding"))); + + } + + /*** + * Test case to verify Mifos style payment strategy + */ + @Test + public void testLoanRefundByCashAccrualBasedAccounting() { + this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); + + Calendar fourMonthsfromNowCalendar = Calendar.getInstance(); + fourMonthsfromNowCalendar.add(Calendar.MONTH, -4); + + String fourMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + + /*** + * Create loan product with MIFOS STYLE strategy + */ + + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProduct("0", "0", LoanProductTestBuilder.MIFOS_STANDARD_STRATEGY, ACCRUAL_UPFRONT, + assetAccount, incomeAccount, expenseAccount, overpaymentAccount);// , + // LoanProductTestBuilder.EQUAL_INSTALLMENTS, + // LoanProductTestBuilder.FLAT_BALANCE); + Assert.assertNotNull(loanProductID); + + /*** + * Apply for loan application and verify loan status + */ + final String savingsId = null; + final String principal = "12,000.00"; + + // Add charges with payment mode regular + List charges = new ArrayList<>(); + + Integer flatInstallmentFee = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); + addCharges(charges, flatInstallmentFee, "50", null); + + final Integer loanID = applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, charges, savingsId, + principal, LoanApplicationTestBuilder.MIFOS_STANDARD_STRATEGY, -4); + Assert.assertNotNull(loanID); + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoan(fourMonthsfromNow, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.disburseLoan(fourMonthsfromNow, loanID); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + final JournalEntry[] assetAccountInitialEntry = { new JournalEntry(Float.valueOf("1440"), JournalEntry.TransactionType.DEBIT), + new JournalEntry(Float.valueOf("300.00"), JournalEntry.TransactionType.DEBIT), + new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.CREDIT), + new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.DEBIT) }; + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, fourMonthsfromNow, assetAccountInitialEntry); + + ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + HashMap firstInstallment = loanSchedule.get(1); + validateNumberForEqual("2290", String.valueOf(firstInstallment.get("totalOutstandingForPeriod"))); + + // Make payment for installment #1 + + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + + final String threeMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRepayment(threeMonthsfromNow, Float.valueOf("2290"), loanID); + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + firstInstallment = loanSchedule.get(1); + validateNumberForEqual("0.00", String.valueOf(firstInstallment.get("totalOutstandingForPeriod"))); + + // Make payment for installment #2 + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + + final String twoMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRepayment(twoMonthsfromNow, Float.valueOf("2290"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, twoMonthsfromNow, new JournalEntry(Float.valueOf("2290"), + JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("50"), JournalEntry.TransactionType.CREDIT), + new JournalEntry(Float.valueOf("240"), JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("2000"), + JournalEntry.TransactionType.CREDIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + Map secondInstallment = loanSchedule.get(2); + validateNumberForEqual("0.00", String.valueOf(secondInstallment.get("totalOutstandingForPeriod"))); + + // Make payment for installment #3 + // Pay 2290 more than expected + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + + final String oneMonthfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRepayment(oneMonthfromNow, Float.valueOf("4580"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, oneMonthfromNow, new JournalEntry(Float.valueOf("4580"), + JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("100"), JournalEntry.TransactionType.CREDIT), + new JournalEntry(Float.valueOf("480"), JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("4000"), + JournalEntry.TransactionType.CREDIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + HashMap thirdInstallment = loanSchedule.get(3); + validateNumberForEqual("0.00", String.valueOf(thirdInstallment.get("totalOutstandingForPeriod"))); + + // Make refund of 20 + // max 2290 to refund. Pay 20 means only principal + // mifos style refund order(principal, interest, fees and penalties + // paid: principal 2000, interest 240, fees 50, penalty 0 + // refund 20 means paid: principal 1980, interest 240, fees 50, penalty + // 0 + + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + final String now = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRefundByCash(now, Float.valueOf("20"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now, new JournalEntry(Float.valueOf("20"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("20"), JournalEntry.TransactionType.DEBIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + HashMap fourthInstallment = loanSchedule.get(4); + validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod"))); + validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("principalOutstanding"))); + validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("interestOutstanding"))); + validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding"))); + + // Make refund of 2000 + // max 2270 to refund. Pay 2000 means only principal + // paid: principal 1980, interest 240, fees 50, penalty 0 + // refund 2000 means paid: principal 0, interest 220, fees 50, penalty 0 + + this.loanTransactionHelper.makeRefundByCash(now, Float.valueOf("2000"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now, new JournalEntry(Float.valueOf("2000"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("1980"), JournalEntry.TransactionType.DEBIT)); + + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, now, new JournalEntry(Float.valueOf("20"), + JournalEntry.TransactionType.DEBIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + fourthInstallment = loanSchedule.get(4); + validateNumberForEqual("2020.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod"))); + validateNumberForEqual("2000.00", String.valueOf(fourthInstallment.get("principalOutstanding"))); + validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("interestOutstanding"))); + validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding"))); + + } + + @Test + public void testLoanRefundByTransferCashBasedAccounting() { + this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); + this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + this.accountTransferHelper = new AccountTransferHelper(this.requestSpec, this.responseSpec); + + Calendar fourMonthsfromNowCalendar = Calendar.getInstance(); + fourMonthsfromNowCalendar.add(Calendar.MONTH, -4); + + String fourMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + + final Integer savingsProductID = createSavingsProduct(this.requestSpec, this.responseSpec, MINIMUM_OPENING_BALANCE); + Assert.assertNotNull(savingsProductID); + + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL); + Assert.assertNotNull(savingsProductID); + + HashMap modifications = this.savingsAccountHelper.updateSavingsAccount(clientID, savingsProductID, savingsId, + ACCOUNT_TYPE_INDIVIDUAL); + Assert.assertTrue(modifications.containsKey("submittedOnDate")); + + HashMap savingsStatusHashMap = SavingsStatusChecker.getStatusOfSavings(this.requestSpec, this.responseSpec, savingsId); + SavingsStatusChecker.verifySavingsIsPending(savingsStatusHashMap); + + savingsStatusHashMap = this.savingsAccountHelper.approveSavings(savingsId); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + + savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsId); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + + /*** + * Create loan product with MIFOS STYLE strategy + */ + + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProduct("0", "0", LoanProductTestBuilder.MIFOS_STANDARD_STRATEGY, CASH_BASED, assetAccount, + incomeAccount, expenseAccount, overpaymentAccount); + Assert.assertNotNull(loanProductID); + + /*** + * Apply for loan application and verify loan status + */ + + final String principal = "12,000.00"; + + // Add charges with payment mode regular + List charges = new ArrayList<>(); + + Integer flatInstallmentFee = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); + addCharges(charges, flatInstallmentFee, "50", null); + + final Integer loanID = applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, charges, null, principal, + LoanApplicationTestBuilder.MIFOS_STANDARD_STRATEGY, -4); + Assert.assertNotNull(loanID); + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoan(fourMonthsfromNow, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.disburseLoan(fourMonthsfromNow, loanID); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + final JournalEntry[] assetAccountInitialEntry = { new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.CREDIT), + new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.DEBIT) }; + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, fourMonthsfromNow, assetAccountInitialEntry); + + ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + HashMap firstInstallment = loanSchedule.get(1); + validateNumberForEqual("2290", String.valueOf(firstInstallment.get("totalOutstandingForPeriod"))); + + // Make payment for installment #1 + + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + + final String threeMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRepayment(threeMonthsfromNow, Float.valueOf("2290"), loanID); + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + firstInstallment = loanSchedule.get(1); + validateNumberForEqual("0.00", String.valueOf(firstInstallment.get("totalOutstandingForPeriod"))); + + // Make payment for installment #2 + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + + final String twoMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRepayment(twoMonthsfromNow, Float.valueOf("2290"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, twoMonthsfromNow, new JournalEntry(Float.valueOf("2290"), + JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("2000"), JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, twoMonthsfromNow, new JournalEntry(Float.valueOf("50"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("240"), JournalEntry.TransactionType.CREDIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + Map secondInstallment = loanSchedule.get(2); + validateNumberForEqual("0.00", String.valueOf(secondInstallment.get("totalOutstandingForPeriod"))); + + // Make payment for installment #3 + // Pay 2290 more than expected + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + + final String oneMonthfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + this.loanTransactionHelper.makeRepayment(oneMonthfromNow, Float.valueOf("4580"), loanID); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, oneMonthfromNow, new JournalEntry(Float.valueOf("4580"), + JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("4000"), JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, oneMonthfromNow, new JournalEntry(Float.valueOf("100"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("480"), JournalEntry.TransactionType.CREDIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + HashMap thirdInstallment = loanSchedule.get(3); + validateNumberForEqual("0.00", String.valueOf(thirdInstallment.get("totalOutstandingForPeriod"))); + + // Make refund of 20 + // max 2290 to refund. Pay 20 means only principal + // mifos style refund order(principal, interest, fees and penalties + // paid: principal 2000, interest 240, fees 50, penalty 0 + // refund 20 means paid: principal 1980, interest 240, fees 50, penalty + // 0 + + Float TRANSFER_AMOUNT = 20f; + + fourMonthsfromNowCalendar.add(Calendar.MONTH, 1); + final String now = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar); + + final String FROM_LOAN_ACCOUNT_TYPE = "1"; + final String TO_SAVINGS_ACCOUNT_TYPE = "2"; + + this.accountTransferHelper.refundLoanByTransfer(now, clientID, loanID, clientID, savingsId, FROM_LOAN_ACCOUNT_TYPE, + TO_SAVINGS_ACCOUNT_TYPE, TRANSFER_AMOUNT.toString()); + + Float toSavingsBalance = new Float(MINIMUM_OPENING_BALANCE); + + HashMap toSavingsSummaryAfter = this.savingsAccountHelper.getSavingsSummary(savingsId); + + toSavingsBalance += TRANSFER_AMOUNT; + + // Verifying toSavings Account Balance after Account Transfer + assertEquals("Verifying From Savings Account Balance after Account Transfer", toSavingsBalance, + toSavingsSummaryAfter.get("accountBalance")); + + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now, new JournalEntry(Float.valueOf("20"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("20"), JournalEntry.TransactionType.DEBIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + HashMap fourthInstallment = loanSchedule.get(4); + validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod"))); + validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("principalOutstanding"))); + validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("interestOutstanding"))); + validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding"))); + + // Make refund of 2000 + // max 2270 to refund. Pay 2000 means only principal + // paid: principal 1980, interest 240, fees 50, penalty 0 + // refund 2000 means paid: principal 0, interest 220, fees 50, penalty 0 + // final String now = Utils.convertDate(fourMonthsfromNowCalendar); + + TRANSFER_AMOUNT = 2000f; + + this.accountTransferHelper.refundLoanByTransfer(now, clientID, loanID, clientID, savingsId, FROM_LOAN_ACCOUNT_TYPE, + TO_SAVINGS_ACCOUNT_TYPE, TRANSFER_AMOUNT.toString()); + + toSavingsSummaryAfter = this.savingsAccountHelper.getSavingsSummary(savingsId); + + toSavingsBalance += TRANSFER_AMOUNT; + + // Verifying toSavings Account Balance after Account Transfer + assertEquals("Verifying From Savings Account Balance after Account Transfer", toSavingsBalance, + toSavingsSummaryAfter.get("accountBalance")); + + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now, new JournalEntry(Float.valueOf("2000"), + JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("1980"), JournalEntry.TransactionType.DEBIT)); + + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, now, new JournalEntry(Float.valueOf("20"), + JournalEntry.TransactionType.DEBIT)); + + loanSchedule.clear(); + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); + fourthInstallment = loanSchedule.get(4); + validateNumberForEqual("2020.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod"))); + validateNumberForEqual("2000.00", String.valueOf(fourthInstallment.get("principalOutstanding"))); + validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("interestOutstanding"))); + validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding"))); + + } + + private Integer createSavingsProduct(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String minOpenningBalance) { + System.out.println("------------------------------CREATING NEW SAVINGS PRODUCT ---------------------------------------"); + SavingsProductHelper savingsProductHelper = new SavingsProductHelper(); + + final String savingsProductJSON = savingsProductHelper + // + .withInterestCompoundingPeriodTypeAsDaily() + // + .withInterestPostingPeriodTypeAsMonthly() + // + .withInterestCalculationPeriodTypeAsDailyBalance() + + .withMinimumOpenningBalance(minOpenningBalance).build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + } \ No newline at end of file diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/Utils.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/Utils.java index ba84a7506..ae7e31429 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/Utils.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/Utils.java @@ -13,6 +13,7 @@ import static org.junit.Assert.fail; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Locale; import java.util.Random; @@ -115,5 +116,9 @@ public class Utils { public static String randomNameGenerator(final String prefix, final int lenOfRandomSuffix) { return randomStringGenerator(prefix, lenOfRandomSuffix); } + + public static String convertDateToURLFormat(final Calendar dateToBeConvert) { + return new SimpleDateFormat("dd MMMMMM yyyy").format(dateToBeConvert.getTime()); + } } diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java index b55ecde4b..e52b0349e 100755 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java @@ -36,6 +36,7 @@ public class LoanTransactionHelper { private static final String MAKE_REPAYMENT_COMMAND = "repayment"; private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = "withdrawnByApplicant"; private static final String RECOVER_FROM_GUARANTORS_COMMAND = "recoverGuarantees"; + private static final String MAKE_REFUND_BY_CASH_COMMAND = "refundByCash"; public LoanTransactionHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { this.requestSpec = requestSpec; @@ -90,9 +91,9 @@ public class LoanTransactionHelper { final String URL = "/mifosng-provider/api/v1/loans/" + loanID + "?associations=all&" + Utils.TENANT_IDENTIFIER; return Utils.performServerGet(requestSpec, responseSpec, URL, param); } - - public Object getLoanProductDetail(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final Integer loanProductId, - final String jsonAttributeToGetBack) { + + public Object getLoanProductDetail(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final Integer loanProductId, final String jsonAttributeToGetBack) { final String URL = "/mifosng-provider/api/v1/loanproducts/" + loanProductId + "?associations=all&" + Utils.TENANT_IDENTIFIER; return Utils.performServerGet(requestSpec, responseSpec, URL, jsonAttributeToGetBack); } @@ -421,6 +422,10 @@ public class LoanTransactionHelper { return response; } + private String createLoanRefundTransferURL() { + return "/mifosng-provider/api/v1/accounttransfers/refundByTransfer?tenantIdentifier=default"; + } + public void verifyRepaymentScheduleEntryFor(final int repaymentNumber, final float expectedPrincipalOutstanding, final Integer loanID) { System.out.println("---------------------------GETTING LOAN REPAYMENT SCHEDULE--------------------------------"); final ArrayList repaymentPeriods = getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); @@ -457,4 +462,44 @@ public class LoanTransactionHelper { assertTrue("No Accrual entries are posted", isTransactionFound); } + + public HashMap makeRefundByCash(final String date, final Float amountToBeRefunded, final Integer loanID) { + return performLoanTransaction(createLoanTransactionURL(MAKE_REFUND_BY_CASH_COMMAND, loanID), + getRefundByCashBodyAsJSON(date, amountToBeRefunded)); + } + + public HashMap makeRefundByTransfer(final Integer fromAccountId, final Integer toClientId, final Integer toAccountId, + final Integer fromClientId, final String date, final Float amountToBeRefunded) { + return performLoanTransaction(createLoanRefundTransferURL(), + getRefundByTransferBodyAsJSON(fromAccountId, toClientId, toAccountId, fromClientId, date, amountToBeRefunded)); + } + + private String getRefundByCashBodyAsJSON(final String transactionDate, final Float transactionAmount) { + final HashMap map = new HashMap<>(); + map.put("locale", "en"); + map.put("dateFormat", "dd MMMM yyyy"); + map.put("transactionDate", transactionDate); + map.put("transactionAmount", transactionAmount.toString()); + map.put("note", "Refund Made!!!"); + return new Gson().toJson(map); + } + + private String getRefundByTransferBodyAsJSON(final Integer fromAccountId, final Integer toClientId, final Integer toAccountId, + final Integer fromClientId, final String transactionDate, final Float transactionAmount) { + final HashMap map = new HashMap<>(); + map.put("fromAccountId", fromAccountId.toString()); + map.put("fromAccountType", "1"); + map.put("toOfficeId", "1"); + map.put("toClientId", toClientId.toString()); + map.put("toAccountType", "2"); + map.put("toAccountId", toAccountId.toString()); + map.put("dateFormat", "dd MMMM yyyy"); + map.put("transferDate", transactionDate); + map.put("transferAmount", transactionAmount.toString()); + map.put("transferDescription", "Refund Made!!!"); + map.put("fromClientId", fromClientId.toString()); + map.put("fromOfficeId", "1"); + map.put("locale", "en"); + return new Gson().toJson(map); + } } \ No newline at end of file diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/savings/AccountTransferHelper.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/savings/AccountTransferHelper.java index 830469371..4e58d39d8 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/savings/AccountTransferHelper.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/savings/AccountTransferHelper.java @@ -16,6 +16,7 @@ import com.jayway.restassured.specification.ResponseSpecification; public class AccountTransferHelper { private static final String ACCOUNT_TRANSFER_URL = "/mifosng-provider/api/v1/accounttransfers"; + private static final String LOAN_REFUND_BY_TRANSFER_URL = "/mifosng-provider/api/v1/accounttransfers/refundByTransfer"; private static final String LOCALE = "en_GB"; private static final String OFFICE_ID = "1"; private static final String TRANSFER_DESCRIPTION = "Transfer"; @@ -70,4 +71,15 @@ public class AccountTransferHelper { return Utils.performServerPost(this.requestSpec, this.responseSpec, ACCOUNT_TRANSFER_URL + "?" + Utils.TENANT_IDENTIFIER, accountTransferJSON, "savingsId"); } + + public Integer refundLoanByTransfer(final String date, final Integer fromClientId, final Integer fromAccountId, final Integer toClientId, + final Integer toAccountId, final String fromAccountType, final String toAccountType, final String transferAmount) { + System.out.println("--------------------------------ACCOUNT TRANSFER--------------------------------"); + final String accountTransferJSON = new AccountTransferHelper(this.requestSpec, this.responseSpec) // + .withTransferOnDate(date) // + .build(fromAccountId.toString(), fromClientId.toString(), toAccountId.toString(), toClientId.toString(), fromAccountType, + toAccountType, transferAmount); + return Utils.performServerPost(this.requestSpec, this.responseSpec, LOAN_REFUND_BY_TRANSFER_URL + "?" + Utils.TENANT_IDENTIFIER, + accountTransferJSON, "savingsId"); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java index 4f0b1f7e4..104af8f42 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java @@ -6,12 +6,15 @@ package org.mifosplatform.accounting.journalentry.service; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import org.mifosplatform.accounting.closure.domain.GLClosure; import org.mifosplatform.accounting.common.AccountingConstants.ACCRUAL_ACCOUNTS_FOR_LOAN; import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN; import org.mifosplatform.accounting.common.AccountingConstants.FINANCIAL_ACTIVITY; +import org.mifosplatform.accounting.journalentry.data.ChargePaymentDTO; import org.mifosplatform.accounting.journalentry.data.LoanDTO; import org.mifosplatform.accounting.journalentry.data.LoanTransactionDTO; import org.mifosplatform.organisation.office.domain.Office; @@ -72,6 +75,11 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess .getTransactionType().isWaiveCharges())) { createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, true, false); } + + /** Logic for Refunds of Active Loans **/ + else if (loanTransactionDTO.getTransactionType().isRefundForActiveLoans()) { + createJournalEntriesForRefundForActiveLoan(loanDTO, loanTransactionDTO, office); + } } } @@ -339,4 +347,75 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess paymentTypeId, loanId, transactionId, transactionDate, refundAmount, isReversal); } } + private void createJournalEntriesForRefundForActiveLoan(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { + // TODO Auto-generated method stub + // 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 principalAmount = loanTransactionDTO.getPrincipal(); + final BigDecimal interestAmount = loanTransactionDTO.getInterest(); + final BigDecimal feesAmount = loanTransactionDTO.getFees(); + final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); + final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment(); + final boolean isReversal = loanTransactionDTO.isReversed(); + final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO, + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalAmount, !isReversal); + } + + if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.INTEREST_ON_LOANS, + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, interestAmount, !isReversal); + } + + if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + + List chargePaymentDTOs = new ArrayList<>(); + + for(ChargePaymentDTO chargePaymentDTO : loanTransactionDTO.getFeePayments()) { + chargePaymentDTOs.add(new ChargePaymentDTO(chargePaymentDTO.getChargeId(), chargePaymentDTO.getLoanChargeId(), + chargePaymentDTO.getAmount().floatValue() < 0 ? chargePaymentDTO.getAmount().multiply(new BigDecimal(-1)):chargePaymentDTO.getAmount() )); + } + this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode, + CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES.getValue(), loanProductId, loanId, transactionId, transactionDate, feesAmount, + !isReversal, chargePaymentDTOs); + } + + if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + List chargePaymentDTOs = new ArrayList<>(); + + for(ChargePaymentDTO chargePaymentDTO : loanTransactionDTO.getPenaltyPayments()) { + chargePaymentDTOs.add(new ChargePaymentDTO(chargePaymentDTO.getChargeId(), chargePaymentDTO.getLoanChargeId(), + chargePaymentDTO.getAmount().floatValue() < 0 ? chargePaymentDTO.getAmount().multiply(new BigDecimal(-1)):chargePaymentDTO.getAmount() )); + } + + this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode, + CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue(), loanProductId, loanId, transactionId, transactionDate, + penaltiesAmount, !isReversal, chargePaymentDTOs); + } + + if (overPaymentAmount != null && !(overPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT, loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, overPaymentAmount, !isReversal); + } + + /*** create a single debit entry (or reversal) for the entire amount **/ + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE.getValue(), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, !isReversal); + + } } 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 8bc5f26cc..e0afee8ff 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 @@ -6,11 +6,14 @@ package org.mifosplatform.accounting.journalentry.service; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import org.mifosplatform.accounting.closure.domain.GLClosure; import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN; import org.mifosplatform.accounting.common.AccountingConstants.FINANCIAL_ACTIVITY; +import org.mifosplatform.accounting.journalentry.data.ChargePaymentDTO; import org.mifosplatform.accounting.journalentry.data.LoanDTO; import org.mifosplatform.accounting.journalentry.data.LoanTransactionDTO; import org.mifosplatform.organisation.office.domain.Office; @@ -85,6 +88,10 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF || loanTransactionDTO.getTransactionType().isWithdrawTransfer()) { createJournalEntriesForTransfers(loanDTO, loanTransactionDTO, office); } + /** Logic for Refunds of Active Loans **/ + else if (loanTransactionDTO.getTransactionType().isRefundForActiveLoans()) { + createJournalEntriesForRefundForActiveLoan(loanDTO, loanTransactionDTO, office); + } } } @@ -293,4 +300,76 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF null, loanId, transactionId, transactionDate, principalAmount, isReversal); } } + + private void createJournalEntriesForRefundForActiveLoan(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { + // TODO Auto-generated method stub + // 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 principalAmount = loanTransactionDTO.getPrincipal(); + final BigDecimal interestAmount = loanTransactionDTO.getInterest(); + final BigDecimal feesAmount = loanTransactionDTO.getFees(); + final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); + final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment(); + final boolean isReversal = loanTransactionDTO.isReversed(); + final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO, + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalAmount, !isReversal); + } + + if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.INTEREST_ON_LOANS, + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, interestAmount, !isReversal); + } + + if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + + List chargePaymentDTOs = new ArrayList<>(); + + for(ChargePaymentDTO chargePaymentDTO : loanTransactionDTO.getFeePayments()) { + chargePaymentDTOs.add(new ChargePaymentDTO(chargePaymentDTO.getChargeId(), chargePaymentDTO.getLoanChargeId(), + chargePaymentDTO.getAmount().floatValue() < 0 ? chargePaymentDTO.getAmount().multiply(new BigDecimal(-1)):chargePaymentDTO.getAmount() )); + } + this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode, + CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES.getValue(), loanProductId, loanId, transactionId, transactionDate, feesAmount, + !isReversal, chargePaymentDTOs); + } + + if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + List chargePaymentDTOs = new ArrayList<>(); + + for(ChargePaymentDTO chargePaymentDTO : loanTransactionDTO.getPenaltyPayments()) { + chargePaymentDTOs.add(new ChargePaymentDTO(chargePaymentDTO.getChargeId(), chargePaymentDTO.getLoanChargeId(), + chargePaymentDTO.getAmount().floatValue() < 0 ? chargePaymentDTO.getAmount().multiply(new BigDecimal(-1)):chargePaymentDTO.getAmount() )); + } + + this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode, + CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue(), loanProductId, loanId, transactionId, transactionDate, + penaltiesAmount, !isReversal, chargePaymentDTOs); + } + + if (overPaymentAmount != null && !(overPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT, loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, overPaymentAmount, !isReversal); + } + + /*** create a single debit entry (or reversal) for the entire amount **/ + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE.getValue(), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, !isReversal); + + } } 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 index 3c03d8b67..4fc6d0ce0 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -943,5 +943,17 @@ public class CommandWrapper { public boolean isInactivateSavingsAccountCharge() { return this.actionName.equalsIgnoreCase("INACTIVATE") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNTCHARGE"); } + + public boolean isRefundByTransfer() { + return this.actionName.equalsIgnoreCase("REFUNDBYTRANSFER"); + } + + public boolean isLoanRefundByCash() { + return this.actionName.equalsIgnoreCase("REFUNDBYCASH") && this.entityName.equalsIgnoreCase("LOAN"); + } + + public boolean isUndoLoanRefund() { + return this.actionName.equalsIgnoreCase("UNDOREFUND") && this.entityName.equalsIgnoreCase("LOAN"); + } } \ No newline at end of file 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 index 4b4ee4583..016767793 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -697,6 +697,15 @@ public class CommandWrapperBuilder { this.href = "/loans/" + loanId + "/transactions/" + transactionId; return this; } + + public CommandWrapperBuilder refundLoanTransactionByCash(final Long loanId) { + this.actionName = "REFUNDBYCASH"; + this.entityName = "LOAN"; + this.entityId = null; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/transactions/template?command=refundbycash"; + return this; + } public CommandWrapperBuilder createLoanApplication() { this.actionName = "CREATE"; @@ -2185,4 +2194,12 @@ public class CommandWrapperBuilder { this.json = "{}"; return this; } + + public CommandWrapperBuilder refundByTransfer() { + this.actionName = "REFUNDBYTRANSFER"; + this.entityName = "ACCOUNTTRANSFER"; + this.entityId = null; + this.href = "/refundByTransfer"; + return this; + } } \ No newline at end of file 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 index b65a1ddb8..d3669f048 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -130,6 +130,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer if (wrapper.isAccountTransferResource()) { if (wrapper.isCreate()) { handler = this.applicationContext.getBean("createAccountTransferCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isRefundByTransfer()) { + handler = this.applicationContext.getBean("refundByTransferCommandHandler", NewCommandSourceHandler.class); } else { throw new UnsupportedCommandException(wrapper.commandName()); } @@ -396,6 +398,10 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer handler = this.applicationContext.getBean("undoWriteOffLoanCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isGuaranteeRecovery()) { handler = this.applicationContext.getBean("recoverFromGuarantorCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isLoanRefundByCash()) { + handler = this.applicationContext.getBean("loanRefundByCashCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isUndoLoanRefund()) { + handler = this.applicationContext.getBean("loanRefundAdjustmentCommandHandler", NewCommandSourceHandler.class); } else { throw new UnsupportedCommandException(wrapper.commandName()); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/api/AccountTransfersApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/api/AccountTransfersApiResource.java index c69979f33..b064007be 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/api/AccountTransfersApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/api/AccountTransfersApiResource.java @@ -119,4 +119,36 @@ public class AccountTransfersApiResource { final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.toApiJsonSerializer.serialize(settings, transfer, AccountTransfersApiConstants.RESPONSE_DATA_PARAMETERS); } + + @GET + @Path("templateRefundByTransfer") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String templateRefundByTransfer(@QueryParam("fromOfficeId") final Long fromOfficeId, @QueryParam("fromClientId") final Long fromClientId, + @QueryParam("fromAccountId") final Long fromAccountId, @QueryParam("fromAccountType") final Integer fromAccountType, + @QueryParam("toOfficeId") final Long toOfficeId, @QueryParam("toClientId") final Long toClientId, + @QueryParam("toAccountId") final Long toAccountId, @QueryParam("toAccountType") final Integer toAccountType, + @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(AccountTransfersApiConstants.ACCOUNT_TRANSFER_RESOURCE_NAME); + + final AccountTransferData transferData = this.accountTransfersReadPlatformService.retrieveRefundByTransferTemplate(fromOfficeId, fromClientId, + fromAccountId, fromAccountType, toOfficeId, toClientId, toAccountId, toAccountType); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, transferData, AccountTransfersApiConstants.RESPONSE_DATA_PARAMETERS); + } + + @POST + @Path("refundByTransfer") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String templateRefundByTransferPost(final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().refundByTransfer().withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/handler/RefundByTransferCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/handler/RefundByTransferCommandHandler.java new file mode 100644 index 000000000..720f57a4c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/handler/RefundByTransferCommandHandler.java @@ -0,0 +1,32 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.account.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.account.service.AccountTransfersWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class RefundByTransferCommandHandler implements NewCommandSourceHandler { + + private final AccountTransfersWritePlatformService writePlatformService; + + @Autowired + public RefundByTransferCommandHandler(final AccountTransfersWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.refundByTransfer(command); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformService.java index 48cc28301..4753f56ae 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformService.java @@ -26,4 +26,7 @@ public interface AccountTransfersReadPlatformService { Page retrieveByStandingInstruction(Long id, SearchParameters searchParameters); Collection fetchPostInterestTransactionIds(Long accountId); + + AccountTransferData retrieveRefundByTransferTemplate(Long fromOfficeId, Long fromClientId, Long fromAccountId, Integer fromAccountType, + Long toOfficeId, Long toClientId, Long toAccountId, Integer toAccountType); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java index f5b69e1f2..948db0dc6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java @@ -411,4 +411,114 @@ public class AccountTransfersReadPlatformServiceImpl implements AccountTransfers this.accountTransfersMapper); } + @Override + public AccountTransferData retrieveRefundByTransferTemplate(final Long fromOfficeId, final Long fromClientId, final Long fromAccountId, + final Integer fromAccountType, final Long toOfficeId, final Long toClientId, final Long toAccountId, final Integer toAccountType) { + // TODO Auto-generated method stub + final EnumOptionData loanAccountType = AccountTransferEnumerations.accountType(PortfolioAccountType.LOAN); + final EnumOptionData savingsAccountType = AccountTransferEnumerations.accountType(PortfolioAccountType.SAVINGS); + + final Integer mostRelevantFromAccountType = fromAccountType; + final Collection fromAccountTypeOptions = Arrays.asList(savingsAccountType, loanAccountType); + final Collection toAccountTypeOptions; + if (mostRelevantFromAccountType == 1) { + // overpaid loan amt transfer to savings account + toAccountTypeOptions = Arrays.asList(savingsAccountType); + } else { + toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType); + } + final Integer mostRelevantToAccountType = toAccountType; + + final EnumOptionData fromAccountTypeData = AccountTransferEnumerations.accountType(mostRelevantFromAccountType); + final EnumOptionData toAccountTypeData = AccountTransferEnumerations.accountType(mostRelevantToAccountType); + + // from settings + OfficeData fromOffice = null; + ClientData fromClient = null; + PortfolioAccountData fromAccount = null; + + OfficeData toOffice = null; + ClientData toClient = null; + PortfolioAccountData toAccount = null; + + // template + Collection fromAccountOptions = null; + Collection toAccountOptions = null; + + Long mostRelevantFromOfficeId = fromOfficeId; + Long mostRelevantFromClientId = fromClientId; + + Long mostRelevantToOfficeId = toOfficeId; + Long mostRelevantToClientId = toClientId; + + if (fromAccountId != null) { + Integer accountType; + if (mostRelevantFromAccountType == 1) { + accountType = PortfolioAccountType.LOAN.getValue(); + } else { + accountType = PortfolioAccountType.SAVINGS.getValue(); + } + fromAccount = this.portfolioAccountReadPlatformService.retrieveOneByPaidInAdvance(fromAccountId, accountType); + + // override provided fromClient with client of account + mostRelevantFromClientId = fromAccount.clientId(); + } + + if (mostRelevantFromClientId != null) { + fromClient = this.clientReadPlatformService.retrieveOne(mostRelevantFromClientId); + mostRelevantFromOfficeId = fromClient.officeId(); + long[] loanStatus = null; + if (mostRelevantFromAccountType == 1) { + loanStatus = new long[] { 300, 700 }; + } + PortfolioAccountDTO portfolioAccountDTO = new PortfolioAccountDTO(mostRelevantFromAccountType, mostRelevantFromClientId, + loanStatus); + fromAccountOptions = this.portfolioAccountReadPlatformService.retrieveAllForLookup(portfolioAccountDTO); + } + + Collection fromOfficeOptions = null; + Collection fromClientOptions = null; + if (mostRelevantFromOfficeId != null) { + fromOffice = this.officeReadPlatformService.retrieveOffice(mostRelevantFromOfficeId); + fromOfficeOptions = this.officeReadPlatformService.retrieveAllOfficesForDropdown(); + fromClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantFromOfficeId); + } + + // defaults + final LocalDate transferDate = DateUtils.getLocalDateOfTenant(); + Collection toOfficeOptions = fromOfficeOptions; + Collection toClientOptions = null; + + if (toAccountId != null && fromAccount != null) { + toAccount = this.portfolioAccountReadPlatformService.retrieveOne(toAccountId, mostRelevantToAccountType, + fromAccount.currencyCode()); + mostRelevantToClientId = toAccount.clientId(); + } + + if (mostRelevantToClientId != null) { + toClient = this.clientReadPlatformService.retrieveOne(mostRelevantToClientId); + mostRelevantToOfficeId = toClient.officeId(); + + toClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantToOfficeId); + + toAccountOptions = retrieveToAccounts(fromAccount, mostRelevantToAccountType, mostRelevantToClientId); + } + + if (mostRelevantToOfficeId != null) { + toOffice = this.officeReadPlatformService.retrieveOffice(mostRelevantToOfficeId); + toOfficeOptions = this.officeReadPlatformService.retrieveAllOfficesForDropdown(); + + toClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantToOfficeId); + if (toClientOptions != null && toClientOptions.size() == 1) { + toClient = new ArrayList<>(toClientOptions).get(0); + + toAccountOptions = retrieveToAccounts(fromAccount, mostRelevantToAccountType, mostRelevantToClientId); + } + } + + return AccountTransferData.template(fromOffice, fromClient, fromAccountTypeData, fromAccount, transferDate, toOffice, toClient, + toAccountTypeData, toAccount, fromOfficeOptions, fromClientOptions, fromAccountTypeOptions, fromAccountOptions, + toOfficeOptions, toClientOptions, toAccountTypeOptions, toAccountOptions); + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformService.java index 68d043e3d..4f2ce4154 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformService.java @@ -22,4 +22,6 @@ public interface AccountTransfersWritePlatformService { void reverseAllTransactions(Long accountId, PortfolioAccountType accountTypeId); void updateLoanTransaction(Long loanTransactionId, LoanTransaction newLoanTransaction); + + CommandProcessingResult refundByTransfer(JsonCommand command); } \ No newline at end of file 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 0059f4ec5..310c2ff5b 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 @@ -13,6 +13,7 @@ import static org.mifosplatform.portfolio.account.api.AccountTransfersApiConstan import static org.mifosplatform.portfolio.account.api.AccountTransfersApiConstants.transferDateParamName; import java.math.BigDecimal; +import java.util.Date; import java.util.List; import java.util.Locale; @@ -35,7 +36,10 @@ import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType; +import org.mifosplatform.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException; +import org.mifosplatform.portfolio.loanaccount.exception.InvalidRefundDateException; import org.mifosplatform.portfolio.loanaccount.service.LoanAssembler; +import org.mifosplatform.portfolio.loanaccount.service.LoanReadPlatformService; import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail; import org.mifosplatform.portfolio.savings.SavingsTransactionBooleanValues; import org.mifosplatform.portfolio.savings.domain.SavingsAccount; @@ -59,6 +63,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer private final LoanAccountDomainService loanAccountDomainService; private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; private final AccountTransferDetailRepository accountTransferDetailRepository; + private final LoanReadPlatformService loanReadPlatformService; @Autowired public AccountTransfersWritePlatformServiceImpl(final AccountTransfersDataValidator accountTransfersDataValidator, @@ -66,7 +71,8 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer final SavingsAccountAssembler savingsAccountAssembler, final SavingsAccountDomainService savingsAccountDomainService, final LoanAssembler loanAssembler, final LoanAccountDomainService loanAccountDomainService, final SavingsAccountWritePlatformService savingsAccountWritePlatformService, - final AccountTransferDetailRepository accountTransferDetailRepository) { + final AccountTransferDetailRepository accountTransferDetailRepository, + final LoanReadPlatformService loanReadPlatformService) { this.accountTransfersDataValidator = accountTransfersDataValidator; this.accountTransferAssembler = accountTransferAssembler; this.accountTransferRepository = accountTransferRepository; @@ -76,6 +82,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer this.loanAccountDomainService = loanAccountDomainService; this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; this.accountTransferDetailRepository = accountTransferDetailRepository; + this.loanReadPlatformService = loanReadPlatformService; } @Transactional @@ -398,5 +405,54 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount(); } + + @Override + @Transactional + public CommandProcessingResult refundByTransfer(JsonCommand command) { + // TODO Auto-generated method stub + this.accountTransfersDataValidator.validate(command); + final LocalDate transactionDate = command.localDateValueOfParameterNamed(transferDateParamName); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(transferAmountParamName); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); + + final PaymentDetail paymentDetail = null; + Long transferTransactionId = null; + + final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); + + BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(fromLoanAccountId).getPaidInAdvance(); + + if (overpaid == null || overpaid.equals(BigDecimal.ZERO) || transactionAmount.floatValue() > overpaid.floatValue()) { + if(overpaid == null) + overpaid = BigDecimal.ZERO; + throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString()); + } + + final LoanTransaction loanRefundTransaction = this.loanAccountDomainService.makeRefundForActiveLoan(fromLoanAccountId, + new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null); + + final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId); + + final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, + transactionAmount, paymentDetail, true, true); + + final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command, + fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferTransactionId = accountTransferDetails.getId(); + + final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(transferTransactionId); + + // if (fromAccountType.isSavingsAccount()) { + + builder.withSavingsId(toSavingsAccountId); + // } + + return builder.build(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/PortfolioAccountReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/PortfolioAccountReadPlatformService.java index a89259ea1..9db0c6a8e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/PortfolioAccountReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/PortfolioAccountReadPlatformService.java @@ -17,4 +17,6 @@ public interface PortfolioAccountReadPlatformService { PortfolioAccountData retrieveOne(Long accountId, Integer accountTypeId, String currencyCode); Collection retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO); + + PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/PortfolioAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/PortfolioAccountReadPlatformServiceImpl.java index af99ac8b2..962f905da 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/PortfolioAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/PortfolioAccountReadPlatformServiceImpl.java @@ -33,12 +33,14 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount // mapper private final PortfolioSavingsAccountMapper savingsAccountMapper; private final PortfolioLoanAccountMapper loanAccountMapper; + private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper; @Autowired public PortfolioAccountReadPlatformServiceImpl(final RoutingDataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.savingsAccountMapper = new PortfolioSavingsAccountMapper(); this.loanAccountMapper = new PortfolioLoanAccountMapper(); + this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(); } @Override @@ -265,4 +267,107 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount fieldOfficerId, fieldOfficerName, currency, amtForTransfer); } } + + private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper { + + private final String schemaSql; + + public PortfolioLoanAccountRefundByTransferMapper() { + + final StringBuilder amountQueryString = new StringBuilder(400); + amountQueryString.append("(select (SUM(ifnull(mr.principal_completed_derived, 0)) +"); + amountQueryString.append("SUM(ifnull(mr.interest_completed_derived, 0)) + "); + amountQueryString.append("SUM(ifnull(mr.fee_charges_completed_derived, 0)) + "); + amountQueryString.append(" SUM(ifnull(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived"); + amountQueryString.append(" from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id"); + amountQueryString.append(" where ml.id=? and ml.loan_status_id = 300"); + amountQueryString.append(" and mr.duedate >= CURDATE() group by ml.id having"); + amountQueryString.append(" (SUM(ifnull(mr.principal_completed_derived, 0)) + "); + amountQueryString.append(" SUM(ifnull(mr.interest_completed_derived, 0)) + "); + amountQueryString.append("SUM(ifnull(mr.fee_charges_completed_derived, 0)) + "); + amountQueryString.append("SUM(ifnull(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid "); + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, "); + sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); + sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); + sqlBuilder.append("lp.id as productId, lp.name as productName, "); + sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); + sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,"); + sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, "); + sqlBuilder.append(amountQueryString.toString()); + sqlBuilder.append(", "); + sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); + sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); + sqlBuilder.append("from m_loan la "); + sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id "); + sqlBuilder.append("join m_currency curr on curr.code = la.currency_code "); + sqlBuilder.append("left join m_client c ON c.id = la.client_id "); + sqlBuilder.append("left join m_group g ON g.id = la.group_id "); + sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final String accountNo = rs.getString("accountNo"); + final String externalId = rs.getString("externalId"); + + final Long groupId = JdbcSupport.getLong(rs, "groupId"); + final String groupName = rs.getString("groupName"); + final Long clientId = JdbcSupport.getLong(rs, "clientId"); + final String clientName = rs.getString("clientName"); + + final Long productId = rs.getLong("productId"); + final String productName = rs.getString("productName"); + + final Long fieldOfficerId = rs.getLong("fieldOfficerId"); + final String fieldOfficerName = rs.getString("fieldOfficerName"); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, + currencyDisplaySymbol, currencyNameCode); + + return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, + fieldOfficerId, fieldOfficerName, currency, amtForTransfer); + } + } + + @Override + public PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId) { + // TODO Auto-generated method stub + Object[] sqlParams = new Object[] { accountId , accountId}; + PortfolioAccountData accountData = null; + //String currencyCode = null; + try { + String sql = null; + //final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId); + + sql = "select " + this.accountRefundByTransferMapper.schema() + " where la.id = ?"; + /* if (currencyCode != null) { + sql += " and la.currency_code = ?"; + sqlParams = new Object[] {accountId , accountId,currencyCode }; + }*/ + + accountData = this.jdbcTemplate.queryForObject(sql, this.accountRefundByTransferMapper, sqlParams); + + } catch (final EmptyResultDataAccessException e) { + throw new AccountTransferNotFoundException(accountId); + } + + return accountData; + } } \ No newline at end of file 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 index ce53e9599..bc9e1bf88 100755 --- 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 @@ -96,7 +96,14 @@ public class LoanTransactionsApiResource { transactionData = this.loanReadPlatformService.retrieveRecoveryPaymentTemplate(loanId); } else if (is(commandParam, "prepayLoan")) { transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(loanId); - } else { + } + else if (is(commandParam, "refundbycash")) { + transactionData = this.loanReadPlatformService.retrieveRefundByCashTemplate(loanId); + } + else if (is(commandParam, "refundbytransfer")) { + transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, true); + } + else { throw new UnrecognizedQueryParamException("command", commandParam); } @@ -155,6 +162,10 @@ public class LoanTransactionsApiResource { final CommandWrapper commandRequest = builder.loanRecoveryPaymentTransaction(loanId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } + else if (is(commandParam, "refundByCash")) { + final CommandWrapper commandRequest = builder.refundLoanTransactionByCash(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/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java index 85dd143fa..357958e01 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java @@ -70,6 +70,7 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanApprovalData; import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData; import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData; +import org.mifosplatform.portfolio.loanaccount.data.PaidInAdvanceData; import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType; import org.mifosplatform.portfolio.loanaccount.exception.LoanTemplateTypeRequiredException; @@ -465,6 +466,7 @@ public class LoansApiResource { Collection loanCollateralOptions = null; Collection calendarOptions = null; Collection accountLinkingOptions = null; + PaidInAdvanceData paidInAdvanceTemplate = null; final boolean template = ApiParameterHelper.template(uriInfo.getQueryParameters()); if (template) { @@ -510,13 +512,15 @@ public class LoansApiResource { Collection overdueCharges = this.chargeReadPlatformService.retrieveLoanProductCharges(loanBasicDetails.loanProductId(), ChargeTimeType.OVERDUE_INSTALLMENT); + + paidInAdvanceTemplate = this.loanReadPlatformService.retrieveTotalPaidInAdvance(loanId); final LoanAccountData loanAccount = LoanAccountData.associationsAndTemplate(loanBasicDetails, repaymentSchedule, loanRepayments, charges, collateral, guarantors, meeting, productOptions, loanTermFrequencyTypeOptions, repaymentFrequencyTypeOptions, null, null, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, allowedLoanOfficers, loanPurposeOptions, loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData, emiAmountVariations, - overdueCharges); + overdueCharges, paidInAdvanceTemplate); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), mandatoryResponseParameters); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java index 34fa39785..87ce1d5ee 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java @@ -177,6 +177,9 @@ public class LoanAccountData { private final LoanInterestRecalculationData interestRecalculationData; private final Boolean createStandingInstructionAtDisbursement; + // Paid In Advance + private final PaidInAdvanceData paidInAdvance; + /** * Used to produce a {@link LoanAccountData} with only collateral options * for now. @@ -275,6 +278,7 @@ public class LoanAccountData { final LoanInterestRecalculationData interestRecalculationData = null; final LoanScheduleData originalSchedule = null; final Boolean createStandingInstructionAtDisbursement = null; + final PaidInAdvanceData paidInAdvance = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, @@ -291,7 +295,8 @@ public class LoanAccountData { syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmount, maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, - isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement); + isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, + paidInAdvance); } /** @@ -392,6 +397,7 @@ public class LoanAccountData { final LoanInterestRecalculationData interestRecalculationData = null; final LoanScheduleData originalSchedule = null; final Boolean createStandingInstructionAtDisbursement = null; + final PaidInAdvanceData paidInAdvance = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, @@ -408,8 +414,8 @@ public class LoanAccountData { syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmount, maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, - isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement); - + isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, + paidInAdvance); } public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) { @@ -433,7 +439,7 @@ public class LoanAccountData { acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, - acc.originalSchedule, acc.createStandingInstructionAtDisbursement); + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance); } /** @@ -535,6 +541,7 @@ public class LoanAccountData { final LoanInterestRecalculationData interestRecalculationData = null; final LoanScheduleData originalSchedule = null; final Boolean createStandingInstructionAtDisbursement = null; + final PaidInAdvanceData paidInAdvance = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, @@ -551,7 +558,7 @@ public class LoanAccountData { syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmount, maxOutstandingBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, - interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement); + interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance); } public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) { @@ -575,7 +582,7 @@ public class LoanAccountData { acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, - acc.originalSchedule, acc.createStandingInstructionAtDisbursement); + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance); } public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product, @@ -691,6 +698,7 @@ public class LoanAccountData { final Boolean isNPA = null; final LoanScheduleData originalSchedule = null; final Boolean createStandingInstructionAtDisbursement = null; + final PaidInAdvanceData paidInAdvance = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), loanPurposeId, @@ -710,7 +718,7 @@ public class LoanAccountData { disbursementData, product.getMultiDisburseLoan(), fixedEmi, product.getOutstandingLoanBalance(), emiAmountVariations, memberVariations, product, inArrears, product.getGraceOnArrearsAgeing(), product.overdueFeeCharges(), isNPA, product.getDaysInMonthType(), product.getDaysInYearType(), product.isInterestRecalculationEnabled(), - product.toLoanInterestRecalculationData(), originalSchedule, createStandingInstructionAtDisbursement); + product.toLoanInterestRecalculationData(), originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance); } public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { @@ -736,6 +744,7 @@ public class LoanAccountData { final Collection loanPurposeOptions = null; final Collection loanCollateralOptions = null; final Collection calendarOptions = null; + final PaidInAdvanceData paidInAdvance = null; final Integer termFrequency = product.getNumberOfRepayments() * product.getRepaymentEvery(); final EnumOptionData termPeriodFrequencyType = product.getRepaymentFrequencyType(); @@ -764,7 +773,7 @@ public class LoanAccountData { product.getOutstandingLoanBalance(), acc.emiAmountVariations, acc.memberVariations, product, acc.inArrears, product.getGraceOnArrearsAgeing(), product.overdueFeeCharges(), acc.isNPA, product.getDaysInMonthType(), product.getDaysInYearType(), product.isInterestRecalculationEnabled(), product.toLoanInterestRecalculationData(), - acc.originalSchedule, acc.createStandingInstructionAtDisbursement); + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, paidInAdvance); } /* @@ -823,6 +832,7 @@ public class LoanAccountData { final LoanProductData product = null; final Collection overdueCharges = null; final LoanScheduleData originalSchedule = null; + final PaidInAdvanceData paidInAdvance = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, @@ -839,7 +849,7 @@ public class LoanAccountData { syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmont, outstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, - interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement); + interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance); } /* @@ -861,7 +871,8 @@ public class LoanAccountData { final Collection loanCollateralOptions, final Collection calendarOptions, final Collection notes, final Collection accountLinkingOptions, final PortfolioAccountData linkedAccount, final Collection disbursementDetails, - final Collection emiAmountVariations, final Collection overdueCharges) { + final Collection emiAmountVariations, final Collection overdueCharges, + final PaidInAdvanceData paidInAdvance) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, @@ -881,7 +892,7 @@ public class LoanAccountData { disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, - acc.createStandingInstructionAtDisbursement); + acc.createStandingInstructionAtDisbursement, paidInAdvance); } public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, @@ -893,7 +904,7 @@ public class LoanAccountData { acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, allowedLoanOfficers, acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions, acc.notes, accountLinkingOptions, - acc.linkedAccount, acc.disbursementDetails, acc.emiAmountVariations, acc.overdueCharges); + acc.linkedAccount, acc.disbursementDetails, acc.emiAmountVariations, acc.overdueCharges, acc.paidInAdvance); } public static LoanAccountData associateGroup(final LoanAccountData acc, final GroupGeneralData group) { @@ -916,7 +927,7 @@ public class LoanAccountData { acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, - acc.originalSchedule, acc.createStandingInstructionAtDisbursement); + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance); } public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map memberLoanCycle) { @@ -976,7 +987,7 @@ public class LoanAccountData { acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, acc.emiAmountVariations, memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, - acc.originalSchedule, acc.createStandingInstructionAtDisbursement); + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance); } public static LoanAccountData withInterestRecalculationCalendarData(final LoanAccountData acc, final CalendarData calendarData) { @@ -1002,7 +1013,7 @@ public class LoanAccountData { acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, interestRecalculationData, - acc.originalSchedule, acc.createStandingInstructionAtDisbursement); + acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance); } public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) { @@ -1026,7 +1037,7 @@ public class LoanAccountData { acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance, acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, - originalSchedule, acc.createStandingInstructionAtDisbursement); + originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance); } private LoanAccountData( @@ -1087,8 +1098,7 @@ public class LoanAccountData { final Collection overdueCharges, final Boolean isNPA, final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType, final boolean isInterestRecalculationEnabled, final LoanInterestRecalculationData interestRecalculationData, final LoanScheduleData originalSchedule, - final Boolean createStandingInstructionAtDisbursement) { - + final Boolean createStandingInstructionAtDisbursement, final PaidInAdvanceData paidInAdvance) { this.id = id; this.accountNo = accountNo; this.status = status; @@ -1239,6 +1249,7 @@ public class LoanAccountData { this.interestRecalculationData = interestRecalculationData; this.originalSchedule = originalSchedule; this.createStandingInstructionAtDisbursement = createStandingInstructionAtDisbursement; + this.paidInAdvance = paidInAdvance; } public RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionEnumData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionEnumData.java index 9cd4b074e..5a9fa932e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionEnumData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionEnumData.java @@ -30,6 +30,7 @@ public class LoanTransactionEnumData { private final boolean rejectTransfer; private final boolean chargePayment; private final boolean refund; + private final boolean refundForActiveLoans; public LoanTransactionEnumData(final Long id, final String code, final String value) { this.id = id; @@ -50,6 +51,7 @@ public class LoanTransactionEnumData { this.rejectTransfer = Long.valueOf(15).equals(this.id); this.refund = Long.valueOf(16).equals(this.id); this.chargePayment = Long.valueOf(17).equals(this.id); + this.refundForActiveLoans = Long.valueOf(18).equals(this.id); } public Long id() { @@ -130,5 +132,9 @@ public class LoanTransactionEnumData { public boolean isRefund() { return this.refund; } + + public boolean isRefundForActiveLoans() { + return this.refundForActiveLoans; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/PaidInAdvanceData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/PaidInAdvanceData.java new file mode 100644 index 000000000..eca64d3da --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/PaidInAdvanceData.java @@ -0,0 +1,23 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.data; + +import java.math.BigDecimal; + +public class PaidInAdvanceData { + + private final BigDecimal paidInAdvance; + + public PaidInAdvanceData(final BigDecimal paidInAdvance) { + this.paidInAdvance = paidInAdvance; + } + + public BigDecimal getPaidInAdvance() { + return paidInAdvance; + } + + +} 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 index e79e54826..95b32fd03 100755 --- 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 @@ -85,6 +85,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanR 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.InvalidRefundDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanDisbursalException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException; @@ -1195,9 +1196,9 @@ public class Loan extends AbstractPersistable { if (command.isChangeInBooleanParameterNamed(createSiAtDisbursementParameterName, shouldCreateStandingInstructionAtDisbursement())) { final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(createSiAtDisbursementParameterName); actualChanges.put(createSiAtDisbursementParameterName, valueAsInput); - this.createStandingInstructionAtDisbursement = valueAsInput; + this.createStandingInstructionAtDisbursement = valueAsInput; } - + final String externalIdParamName = "externalId"; if (command.isChangeInStringParameterNamed(externalIdParamName, this.externalId)) { final String newValue = command.stringValueOfParameterNamed(externalIdParamName); @@ -3073,7 +3074,7 @@ public class Loan extends AbstractPersistable { } for (final LoanTransaction loanTransaction : this.loanTransactions) { - if (loanTransaction.isRefund() && !loanTransaction.isReversed()) { + if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) && !loanTransaction.isReversed()) { totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency)); } } @@ -3853,15 +3854,13 @@ public class Loan extends AbstractPersistable { public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule, final boolean isHolidayEnabled, final List holidays, final WorkingDays workingDays, final Boolean reschedulebasedOnMeetingDates, final LocalDate presentMeetingDate, final LocalDate newMeetingDate) { - + // first repayment's from date is same as disbursement date. /* - * meetingStartDate is used as seedDate Capture the seedDate from - * user and use the seedDate as meetingStart date + * meetingStartDate is used as seedDate Capture the seedDate from user + * and use the seedDate as meetingStart date */ - - LocalDate tmpFromDate = getDisbursementDate(); final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery(); @@ -3869,38 +3868,37 @@ public class Loan extends AbstractPersistable { LocalDate newRepaymentDate = null; Boolean isFirstTime = true; - + for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) { - + LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); - - if ( oldDueDate.isEqual(presentMeetingDate) || oldDueDate.isAfter(presentMeetingDate) ) { - - - if(isFirstTime){ - + + if (oldDueDate.isEqual(presentMeetingDate) || oldDueDate.isAfter(presentMeetingDate)) { + + if (isFirstTime) { + isFirstTime = false; newRepaymentDate = newMeetingDate; - - }else{ + + } else { // tmpFromDate.plusDays(1) is done to make sure // getNewRepaymentMeetingDate method returns next meeting // date and not the same as tmpFromDate newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, tmpFromDate, tmpFromDate.plusDays(1), loanRepaymentInterval, frequency, workingDays); } - + if (isHolidayEnabled) { newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); } loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); // reset from date to get actual daysInPeriod - - if(!isFirstTime){ + + if (!isFirstTime) { loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); } - + tmpFromDate = newRepaymentDate;// update with new repayment // date } else { @@ -3911,7 +3909,7 @@ public class Loan extends AbstractPersistable { public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule, final boolean isHolidayEnabled, final List holidays, final WorkingDays workingDays) { - + // first repayment's from date is same as disbursement date. LocalDate tmpFromDate = getDisbursementDate(); final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); @@ -3920,18 +3918,17 @@ public class Loan extends AbstractPersistable { LocalDate newRepaymentDate = null; LocalDate seedDate = meetingStartDate; - + for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) { - + LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); - + // FIXME: AA this won't update repayment dates before current date. - - - if ( oldDueDate.isAfter(seedDate) && oldDueDate.isAfter(DateUtils.getLocalDateOfTenant()) ) { - - newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, seedDate, oldDueDate, - loanRepaymentInterval, frequency, workingDays); + + if (oldDueDate.isAfter(seedDate) && oldDueDate.isAfter(DateUtils.getLocalDateOfTenant())) { + + newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, seedDate, oldDueDate, loanRepaymentInterval, + frequency, workingDays); final LocalDate maxDateLimitForNewRepayment = getMaxDateLimitForNewRepayment(repaymentPeriodFrequencyType, loanRepaymentInterval, tmpFromDate); @@ -4167,6 +4164,11 @@ public class Loan extends AbstractPersistable { action = "charge.payment"; postfix = "cannot.be.made.before.client.transfer.date"; break; + case LOAN_REFUND: + errorMessage = "The date on which a refund is made cannot be earlier than client's transfer date to this office"; + action = "refund"; + postfix = "cannot.be.made.before.client.transfer.date"; + break; default: break; } @@ -4372,6 +4374,14 @@ public class Loan extends AbstractPersistable { dataValidationErrors.add(error); } break; + case LOAN_REFUND: + if (!isOpen()) { + final String defaultUserMessage = "Loan Refund is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.refund.account.is.not.active", + defaultUserMessage); + dataValidationErrors.add(error); + } + break; default: break; } @@ -4979,4 +4989,128 @@ public class Loan extends AbstractPersistable { public BigDecimal getGuaranteeAmount() { return this.guaranteeAmountDerived == null ? BigDecimal.ZERO : this.guaranteeAmountDerived; } + + public ChangedTransactionDetail makeRefundForActiveLoan(final LoanTransaction loanTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, + final List existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List holidays, + final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) { + + validateAccountStatus(LoanEvent.LOAN_REFUND); + validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REFUND, loanTransaction.getTransactionDate()); + + validateRefundDateIsAfterLastRepayment(loanTransaction.getTransactionDate()); + + validateRepaymentDateIsOnHoliday(loanTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays); + validateRepaymentDateIsOnNonWorkingDay(loanTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay); + + existingTransactionIds.addAll(findExistingTransactionIds()); + existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); + + final ChangedTransactionDetail changedTransactionDetail = handleRefundTransaction(loanTransaction, loanLifecycleStateMachine, null); + + return changedTransactionDetail; + + } + + private void validateRefundDateIsAfterLastRepayment(final LocalDate refundTransactionDate) { + final LocalDate possibleNextRefundDate = possibleNextRefundDate(); + + if (possibleNextRefundDate == null || refundTransactionDate.isBefore(possibleNextRefundDate)) { throw new InvalidRefundDateException( + refundTransactionDate.toString()); } + + } + + private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction loanTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction) { + + ChangedTransactionDetail changedTransactionDetail = null; + + final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REFUND, LoanStatus.fromInt(this.loanStatus)); + this.loanStatus = statusEnum.getValue(); + + loanTransaction.updateLoan(this); + + // final boolean isTransactionChronologicallyLatest = + // isChronologicallyLatestRefund(loanTransaction, + // this.loanTransactions); + + if (status().isOverpaid() || status().isClosed()) { + + final String errorMessage = "This refund option is only for active loans "; + throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage, this.totalOverpaid, + loanTransaction.getAmount(getCurrency()).getAmount()); + + } else if (this.getTotalPaidInRepayments().isZero()) { + final String errorMessage = "Cannot refund when no payment has been made"; + throw new InvalidLoanStateTransitionException("transaction", "no.payment.yet.made.for.loan", errorMessage); + } + + if (loanTransaction.isNotZero(loanCurrency())) { + this.loanTransactions.add(loanTransaction); + } + + if (loanTransaction.isNotRefundForActiveLoan()) { + final String errorMessage = "A transaction of type refund was expected but not received."; + throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.refund.transaction", errorMessage); + } + + final LocalDate loanTransactionDate = loanTransaction.getTransactionDate(); + if (loanTransactionDate.isBefore(getDisbursementDate())) { + final String errorMessage = "The transaction date cannot be before the loan disbursement date: " + + getApprovedOnDate().toString(); + throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage, + loanTransactionDate, getDisbursementDate()); + } + + 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 is a refund + if (adjustedTransaction == null) { + loanRepaymentScheduleTransactionProcessor.handleRefund(loanTransaction, getCurrency(), this.repaymentScheduleInstallments, + charges()); + } else { + final List allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement(); + changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), + allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges(), null); + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + mapEntry.getValue().updateLoan(this); + } + + } + + updateLoanSummaryDerivedFields(); + + doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); + + return changedTransactionDetail; + } + + public LocalDate possibleNextRefundDate() { + + final LocalDate now = new LocalDate(); + + LocalDate lastTransactionDate = null; + for (final LoanTransaction transaction : this.loanTransactions) { + if ((transaction.isRepayment() || transaction.isRefundForActiveLoan()) && transaction.isNonZero()) { + lastTransactionDate = transaction.getTransactionDate(); + } + } + + return lastTransactionDate == null ? now : lastTransactionDate; + } } 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 index 94e972b5f..165d0bde9 100755 --- 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 @@ -31,6 +31,10 @@ public interface LoanAccountDomainService { PaymentDetail paymentDetail, String noteText, String txnExternalId); LocalDate getCalculatedRepaymentsStartingFromDate(LocalDate actualDisbursementDate, Loan loan, CalendarInstance calendarInstance); + + LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult, + LocalDate transactionDate, BigDecimal transactionAmount, + PaymentDetail paymentDetail, String noteText, String txnExternalId); /** * This method is to recalculate and accrue the income till the last accrued 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 index 317ce9881..6efcb7e72 100755 --- 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 @@ -548,4 +548,45 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { return user; } + @Override + public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, + BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId) { + final Loan loan = this.loanAccountAssembler.assembleFrom(accountId); + checkClientOrGroupActive(loan); + + final List existingTransactionIds = new ArrayList(); + final List existingReversedTransactionIds = new ArrayList(); + AppUser currentUser = getAppUserIfPresent(); + + final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount); + final LoanTransaction newRefundTransaction = LoanTransaction.refundForActiveLoan(loan.getOffice(), refundAmount, paymentDetail, + transactionDate, txnExternalId, DateUtils.getLocalDateTimeOfTenant(), currentUser); + 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.makeRefundForActiveLoan(newRefundTransaction, + defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, allowTransactionsOnHoliday, + holidays, workingDays, allowTransactionsOnNonWorkingDay); + + this.loanTransactionRepository.save(newRefundTransaction); + this.loanRepository.save(loan); + + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.loanTransactionNote(loan, newRefundTransaction, noteText); + this.noteRepository.save(note); + } + + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); + recalculateAccruals(loan); + + builderResult.withEntityId(newRefundTransaction.getId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()); // + + return newRefundTransaction; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java index a1f38f63c..b985f4748 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java @@ -883,4 +883,48 @@ public class LoanCharge extends AbstractPersistable { public LoanOverdueInstallmentCharge getOverdueInstallmentCharge() { return this.overdueInstallmentCharge; } + + public Money undoPaidOrPartiallyAmountBy(final Money incrementBy,final Integer installmentNumber, final Money feeAmount) { + Money processAmount = Money.zero(incrementBy.getCurrency()); + if(isInstalmentFee()){ + if(installmentNumber == null){ + processAmount = getLastPaidOrPartiallyPaidInstallmentLoanCharge(incrementBy.getCurrency()).undoPaidAmountBy(incrementBy, feeAmount); + }else{ + processAmount = getInstallmentLoanCharge(installmentNumber).undoPaidAmountBy(incrementBy, feeAmount); + } + }else{ + processAmount = incrementBy; + } + Money amountPaidToDate = Money.of(processAmount.getCurrency(), this.amountPaid); + + Money amountDeductedOnThisCharge = Money.zero(processAmount.getCurrency()); + if (processAmount.isGreaterThanOrEqualTo(amountPaidToDate)) { + amountDeductedOnThisCharge = amountPaidToDate; + amountPaidToDate = Money.zero(processAmount.getCurrency()); + this.amountPaid = amountPaidToDate.getAmount(); + this.amountOutstanding = this.amount; + this.paid = false; + + } else { + amountDeductedOnThisCharge = processAmount; + amountPaidToDate = amountPaidToDate.minus(processAmount); + this.amountPaid = amountPaidToDate.getAmount(); + this.amountOutstanding = calculateAmountOutstanding(incrementBy.getCurrency()); + } + return amountDeductedOnThisCharge; + } + + public LoanInstallmentCharge getLastPaidOrPartiallyPaidInstallmentLoanCharge(MonetaryCurrency currency) { + LoanInstallmentCharge paidChargePerInstallment = null; + for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { + Money outstanding = Money.of(currency, loanChargePerInstallment.getAmountOutstanding()); + final boolean partiallyPaid = outstanding.isGreaterThanZero() && outstanding.isLessThan(loanChargePerInstallment.getAmount(currency)); + if ((partiallyPaid || loanChargePerInstallment.isPaid()) + && (paidChargePerInstallment == null || paidChargePerInstallment.getRepaymentInstallment().getDueDate() + .isBefore(loanChargePerInstallment.getRepaymentInstallment().getDueDate()))) { + paidChargePerInstallment = loanChargePerInstallment; + } + } + return paidChargePerInstallment; + } } \ 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 index dfe6c4237..cf1c92673 100755 --- 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 @@ -27,5 +27,6 @@ public enum LoanEvent { LOAN_OVERPAYMENT, // LOAN_CHARGE_PAYMENT, // LOAN_CLOSED, // - LOAN_EDIT_MULTI_DISBURSE_DATE; + LOAN_EDIT_MULTI_DISBURSE_DATE, // + LOAN_REFUND; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java index d0240e9c8..88fd223d1 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java @@ -252,5 +252,27 @@ public class LoanInstallmentCharge extends AbstractPersistable { public void updateInstallment(LoanRepaymentScheduleInstallment installment) { this.installment = installment; } + + public Money undoPaidAmountBy(final Money incrementBy, final Money feeAmount) { + + Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid); + + Money amountToDeductOnThisCharge = Money.zero(incrementBy.getCurrency()); + if (incrementBy.isGreaterThanOrEqualTo(amountPaidToDate)) { + amountToDeductOnThisCharge = amountPaidToDate; + amountPaidToDate = Money.zero(incrementBy.getCurrency()); + this.amountPaid = amountPaidToDate.getAmount(); + this.amountOutstanding = this.amount; + } else { + amountToDeductOnThisCharge = incrementBy; + amountPaidToDate = amountPaidToDate.minus(incrementBy); + this.amountPaid = amountPaidToDate.getAmount(); + this.amountOutstanding = calculateAmountOutstanding(incrementBy.getCurrency()); + } + this.amountThroughChargePayment = feeAmount.getAmount(); + this.paid = determineIfFullyPaid(); + + return amountToDeductOnThisCharge; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index f96e4e9b4..27dfe919b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -563,6 +563,9 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus if (this.obligationsMet) { this.obligationsMetOnDate = transactionDate.toDate(); } + else { + this.obligationsMetOnDate = null; + } } public void updateDueDate(final LocalDate newDueDate) { @@ -645,4 +648,113 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus return obligationsMetOnDate; } + + /********** UNPAY COMPONENTS ****/ + + public Money unpayPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { + + final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); + Money penaltyPortionOfTransactionDeducted = Money.zero(currency); + + final Money penaltyChargesCompleted = getPenaltyChargesPaid(currency); + if (transactionAmountRemaining.isGreaterThanOrEqualTo(penaltyChargesCompleted)) { + this.penaltyChargesPaid = Money.zero(currency).getAmount(); + penaltyPortionOfTransactionDeducted = penaltyChargesCompleted; + } else { + this.penaltyChargesPaid = penaltyChargesCompleted.minus(transactionAmountRemaining).getAmount(); + penaltyPortionOfTransactionDeducted = transactionAmountRemaining; + } + + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); + + return penaltyPortionOfTransactionDeducted; + } + + public Money unpayFeeChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { + + final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); + Money feePortionOfTransactionDeducted = Money.zero(currency); + + final Money feeChargesCompleted = getFeeChargesPaid(currency); + if (transactionAmountRemaining.isGreaterThanOrEqualTo(feeChargesCompleted)) { + this.feeChargesPaid = Money.zero(currency).getAmount(); + feePortionOfTransactionDeducted = feeChargesCompleted; + } else { + this.feeChargesPaid = feeChargesCompleted.minus(transactionAmountRemaining).getAmount(); + feePortionOfTransactionDeducted = transactionAmountRemaining; + } + + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); + + reduceAdvanceAndLateTotalsForRepaymentPeriod(transactionDate, currency, feePortionOfTransactionDeducted); + + return feePortionOfTransactionDeducted; + } + + public Money unpayInterestComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { + + final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); + Money interestPortionOfTransactionDeducted = Money.zero(currency); + + final Money interestCompleted = getInterestPaid(currency); + if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestCompleted)) { + this.interestPaid = Money.zero(currency).getAmount(); + interestPortionOfTransactionDeducted = interestCompleted; + } else { + this.interestPaid = interestCompleted.minus(transactionAmountRemaining).getAmount(); + interestPortionOfTransactionDeducted = transactionAmountRemaining; + } + + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); + + reduceAdvanceAndLateTotalsForRepaymentPeriod(transactionDate, currency, interestPortionOfTransactionDeducted); + + return interestPortionOfTransactionDeducted; + } + + public Money unpayPrincipalComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { + + final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); + Money principalPortionOfTransactionDeducted = Money.zero(currency); + + final Money principalCompleted = getPrincipalCompleted(currency); + if (transactionAmountRemaining.isGreaterThanOrEqualTo(principalCompleted)) { + this.principalCompleted = Money.zero(currency).getAmount(); + principalPortionOfTransactionDeducted = principalCompleted; + } else { + this.principalCompleted = principalCompleted.minus(transactionAmountRemaining).getAmount(); + principalPortionOfTransactionDeducted = transactionAmountRemaining; + } + + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); + + reduceAdvanceAndLateTotalsForRepaymentPeriod(transactionDate, currency, principalPortionOfTransactionDeducted); + + return principalPortionOfTransactionDeducted; + } + + private void reduceAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency, + final Money amountDeductedInRepaymentPeriod) { + + + if (isInAdvance(transactionDate)) { + Money mTotalPaidInAdvance = Money.of(currency,this.totalPaidInAdvance); + + if(mTotalPaidInAdvance.isLessThan(amountDeductedInRepaymentPeriod) || mTotalPaidInAdvance.isEqualTo(amountDeductedInRepaymentPeriod)) + this.totalPaidInAdvance = Money.zero(currency).getAmount(); + else + this.totalPaidInAdvance = mTotalPaidInAdvance.minus(amountDeductedInRepaymentPeriod).getAmount(); + } else if (isLatePayment(transactionDate)) { + Money mTotalPaidLate = Money.of(currency,this.totalPaidLate); + + if(mTotalPaidLate.isLessThan(amountDeductedInRepaymentPeriod) || mTotalPaidLate.isEqualTo(amountDeductedInRepaymentPeriod)) + this.totalPaidLate = Money.zero(currency).getAmount(); + else + this.totalPaidLate = mTotalPaidLate.minus(amountDeductedInRepaymentPeriod).getAmount(); + } + } + + public Money getDue(MonetaryCurrency currency) { + return getPrincipal(currency).plus(getInterestCharged(currency)).plus(getFeeChargesCharged(currency)).plus(getPenaltyChargesCharged(currency)); + } } \ No newline at end of file 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 index b9b1d3be6..782bef329 100755 --- 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 @@ -229,6 +229,12 @@ public final class LoanTransaction extends AbstractPersistable { return applyCharge; } + public static LoanTransaction refundForActiveLoan(final Office office, final Money amount, final PaymentDetail paymentDetail, + final LocalDate paymentDate, final String externalId, final LocalDateTime createdDate, final AppUser appUser) { + return new LoanTransaction(null, office, LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, paymentDetail, amount.getAmount(), + paymentDate, externalId, createdDate, appUser); + } + public static boolean transactionAmountsMatch(final MonetaryCurrency currency, final LoanTransaction loanTransaction, final LoanTransaction newLoanTransaction) { if (loanTransaction.getAmount(currency).isEqualTo(newLoanTransaction.getAmount(currency)) @@ -610,6 +616,15 @@ public final class LoanTransaction extends AbstractPersistable { this.outstandingLoanBalance = outstandingLoanBalance; } + public boolean isNotRefundForActiveLoan() { + // TODO Auto-generated method stub + return !isRefundForActiveLoan(); + } + + public boolean isRefundForActiveLoan() { + return LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.equals(getTypeOf()) && isNotReversed(); + } + public boolean isManuallyAdjustedOrReversed() { return this.manuallyAdjustedOrReversed; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransactionType.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransactionType.java index 5337d056b..632e83038 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransactionType.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransactionType.java @@ -37,7 +37,8 @@ public enum LoanTransactionType { WITHDRAW_TRANSFER(14, "loanTransactionType.withdrawTransfer"), // REJECT_TRANSFER(15, "loanTransactionType.rejectTransfer"), // REFUND(16, "loanTransactionType.refund"), // - CHARGE_PAYMENT(17, "loanTransactionType.chargePayment"); + CHARGE_PAYMENT(17, "loanTransactionType.chargePayment"), // + REFUND_FOR_ACTIVE_LOAN(18, "loanTransactionType.refund"); private final Integer value; private final String code; @@ -109,6 +110,9 @@ public enum LoanTransactionType { case 17: loanTransactionType = LoanTransactionType.CHARGE_PAYMENT; break; + case 18: + loanTransactionType = LoanTransactionType.REFUND_FOR_ACTIVE_LOAN; + break; default: loanTransactionType = LoanTransactionType.INVALID; break; @@ -151,4 +155,8 @@ public enum LoanTransactionType { public boolean isChargePayment() { return this.value.equals(LoanTransactionType.CHARGE_PAYMENT.getValue()); } + + public boolean isRefundForActiveLoan() { + return this.value.equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.getValue()); + } } \ No newline at end of file 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 index 9a90a6743..e538c674e 100755 --- 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 @@ -5,7 +5,10 @@ */ package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor; +import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -22,6 +25,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; @@ -136,6 +140,16 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } for (final LoanTransaction loanTransaction : transactionstoBeProcessed) { + + if(!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) { + final Comparator byDate = new Comparator() { + @Override + public int compare(LoanRepaymentScheduleInstallment ord1, LoanRepaymentScheduleInstallment ord2) { + return ord1.getDueDate().compareTo(ord2.getDueDate()); + } + }; + Collections.sort(installments,byDate); + } if (loanTransaction.isRepayment() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { // pass through for new transactions @@ -170,6 +184,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen loanTransaction.resetDerivedComponents(); handleWriteOff(loanTransaction, currency, installments); } + else if(loanTransaction.isRefundForActiveLoan()) { + loanTransaction.resetDerivedComponents(); + + handleRefund(loanTransaction, currency, installments,charges); + } } return changedTransactionDetail; } @@ -510,6 +529,104 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } } + + @Override + public void handleRefund(LoanTransaction loanTransaction, + MonetaryCurrency currency, + List installments, + final Set charges) { + // TODO Auto-generated method stub + + final Comparator byDate = new Comparator() { + @Override + public int compare(LoanRepaymentScheduleInstallment ord1, + LoanRepaymentScheduleInstallment ord2) { + return ord1.getDueDate().compareTo(ord2.getDueDate()); + } + }; + Collections.sort(installments, Collections.reverseOrder(byDate)); + Money transactionAmountUnprocessed = loanTransaction + .getAmount(currency); + + for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { + Money outstanding = currentInstallment + .getTotalOutstanding(currency); + Money due = currentInstallment.getDue(currency); + + if (outstanding.isLessThan(due)) { + transactionAmountUnprocessed = handleRefundTransactionPaymentOfInstallment( + currentInstallment, loanTransaction, + transactionAmountUnprocessed); + + } + + if (transactionAmountUnprocessed.isZero()) + break; + + } + + final Set loanFees = extractFeeCharges(charges); + final Set loanPenalties = extractPenaltyCharges(charges); + Integer installmentNumber = null; + + final Money feeCharges = loanTransaction.getFeeChargesPortion(currency); + if (feeCharges.isGreaterThanZero()) { + undoChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, + installmentNumber); + } + + final Money penaltyCharges = loanTransaction + .getPenaltyChargesPortion(currency); + if (penaltyCharges.isGreaterThanZero()) { + undoChargesPaidAmountBy(loanTransaction, penaltyCharges, + loanPenalties, installmentNumber); + } + + } + + /** + * Invoked when a there is a refund of an active loan or undo of an active + * loan + * + * Undoes principal, interest, fees and charges of this transaction based on + * the repayment strategy + * + */ + protected abstract Money handleRefundTransactionPaymentOfInstallment( + final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, + final Money transactionAmountUnprocessed); + + private void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, + final Money feeCharges, final Set charges, + final Integer installmentNumber) { + + Money amountRemaining = feeCharges; + while (amountRemaining.isGreaterThanZero()) { + final LoanCharge paidCharge = findLatestPaidChargeFromUnOrderedSet( + charges, feeCharges.getCurrency()); + + if (paidCharge != null) { + Money feeAmount = feeCharges.zero(); + + final Money amountDeductedTowardsCharge = paidCharge + .undoPaidOrPartiallyAmountBy(amountRemaining, + installmentNumber, feeAmount); + if (amountDeductedTowardsCharge.isGreaterThanZero()) { + + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy( + loanTransaction, paidCharge, + amountDeductedTowardsCharge.getAmount().multiply( + new BigDecimal(-1)), null); + loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); + + amountRemaining = amountRemaining + .minus(amountDeductedTowardsCharge); + } + } + } + + } @Override public ChangedTransactionDetail populateDerivedFeildsWithoutReprocess(final LocalDate disbursementDate, @@ -519,5 +636,48 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen return handleTransaction(disbursementDate, transactionsPostDisbursement, currency, installments, charges, recalculateChargesFrom, reprocessCharges); } + + private LoanCharge findLatestPaidChargeFromUnOrderedSet( + final Set charges, MonetaryCurrency currency) { + LoanCharge latestPaidCharge = null; + LoanCharge installemntCharge = null; + LoanInstallmentCharge chargePerInstallment = null; + for (final LoanCharge loanCharge : charges) { + boolean isPaidOrPartiallyPaid = loanCharge + .isPaidOrPartiallyPaid(currency); + if (isPaidOrPartiallyPaid && !loanCharge.isDueAtDisbursement()) { + if (loanCharge.isInstalmentFee()) { + LoanInstallmentCharge paidLoanChargePerInstallment = loanCharge + .getLastPaidOrPartiallyPaidInstallmentLoanCharge(currency); + if (chargePerInstallment == null + || (paidLoanChargePerInstallment != null && chargePerInstallment + .getRepaymentInstallment() + .getDueDate() + .isBefore( + paidLoanChargePerInstallment + .getRepaymentInstallment() + .getDueDate()))) { + installemntCharge = loanCharge; + chargePerInstallment = paidLoanChargePerInstallment; + } + } else if (latestPaidCharge == null + || (loanCharge.isPaidOrPartiallyPaid(currency)) + && loanCharge.getDueLocalDate().isAfter( + latestPaidCharge.getDueLocalDate())) { + latestPaidCharge = loanCharge; + } + } + } + if (latestPaidCharge == null + || (chargePerInstallment != null && latestPaidCharge + .getDueLocalDate().isAfter( + chargePerInstallment.getRepaymentInstallment() + .getDueDate()))) { + latestPaidCharge = installemntCharge; + } + + return latestPaidCharge; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java index 234295414..6280133fa 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java @@ -45,5 +45,10 @@ public interface LoanRepaymentScheduleTransactionProcessor { ChangedTransactionDetail populateDerivedFeildsWithoutReprocess(LocalDate disbursementDate, List transactionsPostDisbursement, MonetaryCurrency currency, List installments, Set charges, LocalDate recalculateChargesFrom); + + void handleRefund(LoanTransaction loanTransaction, + MonetaryCurrency currency, + List installments, + final Set charges); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java index f974adc5f..592218e1f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java @@ -129,4 +129,37 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractL protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { // dont do anything for with loan over-payment } + + @Override + protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + + if(transactionAmountRemaining.isGreaterThanZero()) { + interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } + + if(transactionAmountRemaining.isGreaterThanZero()) { + feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + + if(transactionAmountRemaining.isGreaterThanZero()) { + penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + + return transactionAmountRemaining; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java index 41a22d847..45f8b73e0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java @@ -137,5 +137,42 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr return transactionAmountRemaining; } + + @Override + protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (transactionAmountRemaining.isGreaterThanZero()) { + feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + + return transactionAmountRemaining; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java index 68662c0d8..c769378d1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java @@ -181,4 +181,41 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst @Override protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {} + + @Override + protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (transactionAmountRemaining.isGreaterThanZero()) { + penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + } + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + + return transactionAmountRemaining; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java index 431f33cc0..6f02d69fc 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java @@ -104,4 +104,40 @@ public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionPr return transactionAmountRemaining; } + + @Override + protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (transactionAmountRemaining.isGreaterThanZero()) { + feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + + return transactionAmountRemaining; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/MifosStyleLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/MifosStyleLoanRepaymentScheduleTransactionProcessor.java index f574654ac..ddb9c7347 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/MifosStyleLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/MifosStyleLoanRepaymentScheduleTransactionProcessor.java @@ -123,4 +123,38 @@ public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends Abstrac protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { // TODO - KW - dont do anything with loan over-payment for now } + + @Override + protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + + if (transactionAmountRemaining.isGreaterThanZero()) { + interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + + return transactionAmountRemaining; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java index e2d692a31..531f81f29 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java @@ -104,4 +104,42 @@ public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionPr return transactionAmountRemaining; } + + @Override + protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (transactionAmountRemaining.isGreaterThanZero()) { + feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + } + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + + return transactionAmountRemaining; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java index b98e6eefb..95616049e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java @@ -209,4 +209,41 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe public boolean isInterestFirstRepaymentScheduleTransactionProcessor() { return true; } + + @Override + protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (transactionAmountRemaining.isGreaterThanZero()) { + principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + + return transactionAmountRemaining; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/InvalidPaidInAdvanceAmountException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/InvalidPaidInAdvanceAmountException.java new file mode 100644 index 000000000..263d55fd4 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/InvalidPaidInAdvanceAmountException.java @@ -0,0 +1,21 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * {@link AbstractPlatformDomainRuleException} thrown an action to transition a + * loan from one state to another violates a domain rule. + */ +public class InvalidPaidInAdvanceAmountException extends AbstractPlatformDomainRuleException { + + public InvalidPaidInAdvanceAmountException(final String refundAmountString) { + super("error.msg.loan.refund.amount.invalid", "The refund amount `" + refundAmountString + "`" + + "` is invalid or loan is not paid in advance.", new Object[] { refundAmountString}); + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/InvalidRefundDateException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/InvalidRefundDateException.java new file mode 100644 index 000000000..53a01edc4 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/InvalidRefundDateException.java @@ -0,0 +1,20 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class InvalidRefundDateException extends AbstractPlatformDomainRuleException { + + public InvalidRefundDateException(final String refundDateAsString) { + super("error.msg.loan.refund.failed", "The refund date`" + refundDateAsString + "`" + + "` cannot be before the smallest repayment transaction date", new Object[] { refundDateAsString}); + } + + public InvalidRefundDateException(final String defaultUserMessage,final String entity,final Object... defaultUserMessageArgs) { + super("error.msg.loan." + entity , defaultUserMessage, defaultUserMessageArgs); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRefundByCashCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRefundByCashCommandHandler.java new file mode 100644 index 000000000..6f13457ae --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/LoanRefundByCashCommandHandler.java @@ -0,0 +1,32 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +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; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class LoanRefundByCashCommandHandler implements NewCommandSourceHandler { + + private final LoanWritePlatformService writePlatformService; + + @Autowired + public LoanRefundByCashCommandHandler(final LoanWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.makeLoanRefund(command.getLoanId(), command); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index c0d595446..67cac314f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -388,5 +388,33 @@ public final class LoanEventApiJsonValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + + public void validateNewRefundTransaction(final String json) { + + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Set transactionParameters = new HashSet(Arrays.asList("transactionDate", "transactionAmount", "externalId", + "note", "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", + "bankNumber")); + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); + baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); + + final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); + baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount(); + + final String note = this.fromApiJsonHelper.extractStringNamed("note", element); + baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); + + validatePaymentDetails(baseDataValidator, element); + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java index d741dac42..ed5927d43 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -6,18 +6,20 @@ package org.mifosplatform.portfolio.loanaccount.service; import java.util.Collection; +import java.util.Date; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.service.Page; +import org.mifosplatform.infrastructure.core.service.SearchParameters; import org.mifosplatform.organisation.staff.data.StaffData; import org.mifosplatform.portfolio.calendar.data.CalendarData; -import org.mifosplatform.infrastructure.core.service.SearchParameters; import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData; import org.mifosplatform.portfolio.loanaccount.data.LoanApprovalData; import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData; +import org.mifosplatform.portfolio.loanaccount.data.PaidInAdvanceData; import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; @@ -45,7 +47,7 @@ public interface LoanReadPlatformService { LoanTransactionData retrieveNewClosureDetails(); LoanTransactionData retrieveDisbursalTemplate(Long loanId, boolean paymentDetailsRequired); - + LoanApprovalData retrieveApprovalTemplate(Long loanId); LoanAccountData retrieveTemplateWithCompleteGroupAndProductDetails(Long groupId, Long productId); @@ -101,4 +103,10 @@ public interface LoanReadPlatformService { Collection fetchWaiverInterestRepaymentData(Long loanId); boolean isGuaranteeRequired(Long loanId); + + Date retrieveMinimumDateOfRepaymentTransaction(Long loanId); + + PaidInAdvanceData retrieveTotalPaidInAdvance(Long loanId); + + LoanTransactionData retrieveRefundByCashTemplate(Long loanId); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 01c87a48c..7d75991aa 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -11,6 +11,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,6 +30,7 @@ import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.infrastructure.core.service.PaginationHelper; import org.mifosplatform.infrastructure.core.service.RoutingDataSource; +import org.mifosplatform.infrastructure.core.service.SearchParameters; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; @@ -56,7 +58,6 @@ import org.mifosplatform.portfolio.fund.service.FundReadPlatformService; import org.mifosplatform.portfolio.group.data.GroupGeneralData; import org.mifosplatform.portfolio.group.data.GroupRoleData; import org.mifosplatform.portfolio.group.service.GroupReadPlatformService; -import org.mifosplatform.infrastructure.core.service.SearchParameters; import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData; import org.mifosplatform.portfolio.loanaccount.data.LoanApplicationTimelineData; @@ -68,6 +69,7 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanSummaryData; import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData; +import org.mifosplatform.portfolio.loanaccount.data.PaidInAdvanceData; import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; @@ -328,8 +330,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final Collection calendarsData = null; final CalendarData collectionMeetingCalendar = null; final Collection groupRoles = null; - groupAccount = GroupGeneralData.withAssocations(groupAccount, membersOfGroup, activeClientMembers, - groupRoles, calendarsData, collectionMeetingCalendar); + groupAccount = GroupGeneralData.withAssocations(groupAccount, membersOfGroup, activeClientMembers, groupRoles, calendarsData, + collectionMeetingCalendar); } final LocalDate expectedDisbursementDate = DateUtils.getLocalDateOfTenant(); @@ -418,8 +420,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); final CurrencyData currencyData = applicationCurrency.toData(); - final LoanTransaction waiveOfInterest = loan.deriveDefaultInterestWaiverTransaction( - DateUtils.getLocalDateTimeOfTenant(), currentUser); + final LoanTransaction waiveOfInterest = loan.deriveDefaultInterestWaiverTransaction(DateUtils.getLocalDateTimeOfTenant(), + currentUser); final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WAIVE_INTEREST); @@ -448,7 +450,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final Loan loan = this.loanRepository.findOne(loanId); if (loan == null) { throw new LoanNotFoundException(loanId); } - return new LoanApprovalData(loan.getProposedPrincipal(),DateUtils.getLocalDateOfTenant()); + return new LoanApprovalData(loan.getProposedPrincipal(), DateUtils.getLocalDateOfTenant()); } @@ -827,8 +829,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, groupData, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, - loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, approvedPrincipal, totalOverpaid, inArrearsTolerance, - termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, + loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, approvedPrincipal, totalOverpaid, + inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, repaymentFrequencyNthDayType, repaymentFrequencyDayOfWeekType, transactionStrategyId, transactionStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, @@ -1322,8 +1324,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final Collection calendarsData = null; final CalendarData collectionMeetingCalendar = null; final Collection groupRoles = null; - groupAccount = GroupGeneralData.withAssocations(groupAccount, membersOfGroup, activeClientMembers, - groupRoles, calendarsData, collectionMeetingCalendar); + groupAccount = GroupGeneralData.withAssocations(groupAccount, membersOfGroup, activeClientMembers, groupRoles, calendarsData, + collectionMeetingCalendar); } return LoanAccountData.groupDefaults(groupAccount, expectedDisbursementDate); @@ -1785,4 +1787,59 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { } } + @Override + public Date retrieveMinimumDateOfRepaymentTransaction(Long loanId) { + // TODO Auto-generated method stub + Date date = this.jdbcTemplate.queryForObject( + "select min(transaction_date) from m_loan_transaction where loan_id=? and transaction_type_enum=2", + new Object[] { loanId }, Date.class); + + return date; + } + + @Override + public PaidInAdvanceData retrieveTotalPaidInAdvance(Long loanId) { + // TODO Auto-generated method stub + try { + final String sql = " select (SUM(ifnull(mr.principal_completed_derived, 0)) +" + + " + SUM(ifnull(mr.interest_completed_derived, 0)) " + " + SUM(ifnull(mr.fee_charges_completed_derived, 0)) " + + " + SUM(ifnull(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived " + + " from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id " + + " where ml.id=? and mr.duedate >= CURDATE() group by ml.id having " + + " (SUM(ifnull(mr.principal_completed_derived, 0)) " + " + SUM(ifnull(mr.interest_completed_derived, 0)) " + + " + SUM(ifnull(mr.fee_charges_completed_derived, 0)) " + + "+ SUM(ifnull(mr.penalty_charges_completed_derived, 0))) > 0"; + BigDecimal bigDecimal = this.jdbcTemplate.queryForObject(sql, BigDecimal.class, loanId); + return new PaidInAdvanceData(bigDecimal); + } catch (DataAccessException e) { + // TODO Auto-generated catch block + return new PaidInAdvanceData(new BigDecimal(0)); + } + } + + @Override + public LoanTransactionData retrieveRefundByCashTemplate(Long loanId) { + // TODO Auto-generated method stub + this.context.authenticatedUser(); + + // TODO - KW - OPTIMIZE - write simple sql query to fetch back date of + // possible next transaction date. + final Loan loan = this.loanRepository.findOne(loanId); + if (loan == null) { throw new LoanNotFoundException(loanId); } + + final MonetaryCurrency currency = loan.getCurrency(); + final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + + final CurrencyData currencyData = applicationCurrency.toData(); + + final LocalDate earliestUnpaidInstallmentDate = new LocalDate(); + + final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN); + final Collection paymentOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName); + return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate, + retrieveTotalPaidInAdvance(loan.getId()).getPaidInAdvance(), null, null, null, null, null, null, paymentOptions, null, + null, null, null, false); + } + } \ 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 0b30c0ffb..850c43afa 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 @@ -86,4 +86,6 @@ public interface LoanWritePlatformService { void applyMeetingDateChanges(Calendar calendar, Collection loanCalendarInstances); + CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command); + } \ No newline at end of file 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 7097228c9..2e2fb9967 100755 --- 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 @@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -122,6 +123,8 @@ 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.exception.InvalidPaidInAdvanceAmountException; +import org.mifosplatform.portfolio.loanaccount.exception.InvalidRefundDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanDisbursalException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException; @@ -2705,6 +2708,55 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } return installments; } + + @Override + @Transactional + public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) { + // TODO Auto-generated method stub + + this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json()); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + + //checkRefundDateIsAfterAtLeastOneRepayment(loanId, transactionDate); + + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + checkIfLoanIsPaidInAdvance(loanId, transactionAmount); + + 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()); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + changes.put("note", noteText); + } + + final PaymentDetail paymentDetail = null; + + final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder(); + + this.loanAccountDomainService.makeRefundForActiveLoan(loanId, commandProcessingResultBuilder, transactionDate, transactionAmount, + paymentDetail, noteText, null); + + return commandProcessingResultBuilder.withCommandId(command.commandId()) // + .withLoanId(loanId) // + .with(changes) // + .build(); + + } + + private void checkIfLoanIsPaidInAdvance(final Long loanId,final BigDecimal transactionAmount) { + BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(loanId).getPaidInAdvance(); + + if(overpaid == null || overpaid.equals(new BigDecimal(0)) || transactionAmount.floatValue() > overpaid.floatValue()) { + if(overpaid == null) + overpaid = BigDecimal.ZERO; + throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString()); + } + } private AppUser getAppUserIfPresent() { AppUser user = null; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java index 3f33b33a8..080a70f7a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java @@ -364,6 +364,10 @@ public class LoanEnumerations { optionData = new LoanTransactionEnumData(LoanTransactionType.CHARGE_PAYMENT.getValue().longValue(), LoanTransactionType.CHARGE_PAYMENT.getCode(), "Charge Payment"); break; + case REFUND_FOR_ACTIVE_LOAN: + optionData = new LoanTransactionEnumData(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.getValue().longValue(), + LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.getCode(), "Refund"); + break; default: break; } diff --git a/mifosng-provider/src/main/resources/sql/migrations/core_db/V227__loan-refund-permissions.sql b/mifosng-provider/src/main/resources/sql/migrations/core_db/V227__loan-refund-permissions.sql new file mode 100644 index 000000000..174d1a366 --- /dev/null +++ b/mifosng-provider/src/main/resources/sql/migrations/core_db/V227__loan-refund-permissions.sql @@ -0,0 +1,16 @@ +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES + ('transaction_savings', 'REFUNDBYTRANSFER_ACCOUNTTRANSFER_CHECKER', 'ACCOUNTTRANSFER', 'REFUNDBYTRANSFER', 0); + + INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES + ( 'transaction_savings', 'REFUNDBYTRANSFER_ACCOUNTTRANSFER', 'ACCOUNTTRANSFER', 'REFUNDBYTRANSFER', 1); + + INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES + ('transaction_loan', 'REFUNDBYCASH_LOAN', 'LOAN', 'REFUNDBYCASH', 1); + +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES + ('transaction_loan', 'REFUNDBYCASH_LOAN_CHECKER', 'LOAN', 'REFUNDBYCASH', 0); +