diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 811f5dfed..614339622 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -876,6 +876,14 @@ + + + loans/{loanId}?command=disburseToSavings + Disburse Loan To Savings Account + + + + loans/{loanId}?command=undoDisbursal @@ -6927,6 +6935,74 @@ Request Body: +   +
+
+

Disburse Loan To Savings Account

+ + + + + + + +
Mandatory Fields
actualDisbursementDate
+
+ + + + + + + +
Optional Fields
transactionAmount,fixedEmiAmount
+
+
+ +POST https://DomainName/api/v1/loans/{loanId}?command=disburseToSavings + + +POST loans/1?command=disburse +Content-Type: application/json +Request Body: +{ + "dateFormat": "dd MMMM yyyy", + "locale": "en", + "transactionAmount":10000, + "fixedEmiAmount""1100, + "actualDisbursementDate": "14 May 2013", + "note": "" + + +{ + "officeId": 1, + "clientId": 1, + "loanId": 1, + "resourceId": 1, + "changes": { + "status": { + "id": 300, + "code": "loanStatusType.active", + "value": "Active", + "pendingApproval": false, + "waitingForDisbursal": false, + "active": true, + "closedObligationsMet": false, + "closedWrittenOff": false, + "closedRescheduled": false, + "closed": false, + "overpaid": false + }, + "locale": "en", + "dateFormat": "dd MMMM yyyy", + "actualDisbursementDate": "14 May 2013", + "transactionAmount":10000 + } +} + +
+
+  
@@ -28905,6 +28981,14 @@ No Request Body: + + + loans/{loanId}?command=disburseToSavings + Disburse Loan To Savings Account + + + + loans/{loanId}?command=undoDisbursal diff --git a/mifosng-db/migrations/core_db/V165__added_permission_for_disburse_to_saving_account.sql b/mifosng-db/migrations/core_db/V165__added_permission_for_disburse_to_saving_account.sql new file mode 100755 index 000000000..4eb6cf1b2 --- /dev/null +++ b/mifosng-db/migrations/core_db/V165__added_permission_for_disburse_to_saving_account.sql @@ -0,0 +1 @@ +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES ('transaction_loan', 'DISBURSETOSAVINGS_LOAN', 'LOAN', 'DISBURSETOSAVINGS', 0); \ No newline at end of file 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 d7b860849..48e6e1055 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java @@ -56,8 +56,8 @@ public class ClientLoanIntegrationTest { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); - final Integer loanProductID = createLoanProduct(); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, null); + final Integer loanProductID = createLoanProduct(false); + final Integer loanID = applyForLoanApplication(clientID, loanProductID, null, null, "12,000.00"); final ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID); verifyLoanRepaymentSchedule(loanSchedule); @@ -70,7 +70,7 @@ public class ClientLoanIntegrationTest { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); - final Integer loanProductID = createLoanProduct(); + final Integer loanProductID = createLoanProduct(false); List charges = new ArrayList(); Integer flatDisbursement = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanDisbursementJSON()); @@ -85,7 +85,7 @@ public class ClientLoanIntegrationTest { ChargesHelper.getLoanDisbursementJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_PERCENTAGE_INTEREST, "1")); addCharges(charges, interestPercentage, "1", null); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, charges); + final Integer loanID = applyForLoanApplication(clientID, loanProductID, charges, null, "12,000.00"); Assert.assertNotNull(loanID); HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); @@ -177,7 +177,7 @@ public class ClientLoanIntegrationTest { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); - final Integer loanProductID = createLoanProduct(); + final Integer loanProductID = createLoanProduct(false); List charges = new ArrayList(); Integer flat = ChargesHelper.createCharges(requestSpec, responseSpec, @@ -196,7 +196,7 @@ public class ClientLoanIntegrationTest { ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_PERCENTAGE_INTEREST, "1", false)); addCharges(charges, interestPercentage, "1", "29 September 2011"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, charges); + final Integer loanID = applyForLoanApplication(clientID, loanProductID, charges, null, "12,000.00"); Assert.assertNotNull(loanID); HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); @@ -336,7 +336,7 @@ public class ClientLoanIntegrationTest { 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 loanProductID = createLoanProduct(); + final Integer loanProductID = createLoanProduct(false); List charges = new ArrayList(); Integer flat = ChargesHelper.createCharges(requestSpec, responseSpec, @@ -354,7 +354,7 @@ public class ClientLoanIntegrationTest { ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_PERCENTAGE_INTEREST, "1", false)); addCharges(charges, interestPercentage, "1", "29 September 2011"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, charges); + final Integer loanID = applyForLoanApplication(clientID, loanProductID, charges, null, "12,000.00"); Assert.assertNotNull(loanID); HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); @@ -594,6 +594,148 @@ public class ClientLoanIntegrationTest { } + @Test + public void testLoanCharges_DISBURSEMENT_TO_SAVINGS() { + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + SavingsAccountHelper savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + final Integer loanProductID = createLoanProduct(false); + + final Integer savingsId = createSavings(clientID); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID, null, savingsId.toString(), "12,000.00"); + Assert.assertNotNull(loanID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoan("20 September 2011", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + HashMap summary = savingsAccountHelper.getSavingsSummary(savingsId); + Float balance = new Float(MINIMUM_OPENING_BALANCE); + assertEquals("Verifying opening Balance", balance, summary.get("accountBalance")); + + // DISBURSE + loanStatusHashMap = this.loanTransactionHelper.disburseLoanToSavings(SavingsAccountHelper.TRANSACTION_DATE, loanID); + System.out.println("DISBURSE " + loanStatusHashMap); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + summary = savingsAccountHelper.getSavingsSummary(savingsId); + balance = new Float(MINIMUM_OPENING_BALANCE) + new Float("12000"); + assertEquals("Verifying opening Balance", balance, summary.get("accountBalance")); + + loanStatusHashMap = this.loanTransactionHelper.undoDisbursal(loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + summary = savingsAccountHelper.getSavingsSummary(savingsId); + balance = new Float(MINIMUM_OPENING_BALANCE); + assertEquals("Verifying opening Balance", balance, summary.get("accountBalance")); + + } + + @Test + public void testLoanCharges_DISBURSEMENT_WITH_TRANCHES() { + 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 loanProductID = createLoanProduct(true); + + List tranches = new ArrayList(); + tranches.add(createTrancheDetail("1 March 2014", "25000")); + tranches.add(createTrancheDetail("23 April 2014", "20000")); + + final Integer loanID = applyForLoanApplicationWithTranches(clientID, loanProductID, null, null, "45,000.00", tranches); + Assert.assertNotNull(loanID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoan("1 March 2014", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + // DISBURSE first Tranche + loanStatusHashMap = this.loanTransactionHelper.disburseLoan("1 March 2014", loanID); + System.out.println("DISBURSE " + loanStatusHashMap); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + // DISBURSE Second Tranche + loanStatusHashMap = this.loanTransactionHelper.disburseLoan("23 April 2014", loanID); + System.out.println("DISBURSE " + loanStatusHashMap); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.undoDisbursal(loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + } + + @Test + public void testLoanCharges_DISBURSEMENT_TO_SAVINGS_WITH_TRANCHES() { + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + SavingsAccountHelper savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + final Integer loanProductID = createLoanProduct(true); + + final Integer savingsId = createSavings(clientID); + + List tranches = new ArrayList(); + tranches.add(createTrancheDetail("1 March 2014", "25000")); + tranches.add(createTrancheDetail("23 April 2014", "20000")); + + final Integer loanID = applyForLoanApplicationWithTranches(clientID, loanProductID, null, savingsId.toString(), "45,000.00", + tranches); + Assert.assertNotNull(loanID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------"); + loanStatusHashMap = this.loanTransactionHelper.approveLoan("1 March 2014", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + HashMap summary = savingsAccountHelper.getSavingsSummary(savingsId); + Float balance = new Float(MINIMUM_OPENING_BALANCE); + assertEquals("Verifying opening Balance", balance, summary.get("accountBalance")); + + // DISBURSE first Tranche + loanStatusHashMap = this.loanTransactionHelper.disburseLoanToSavings("1 March 2014", loanID); + System.out.println("DISBURSE " + loanStatusHashMap); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + summary = savingsAccountHelper.getSavingsSummary(savingsId); + balance = new Float(MINIMUM_OPENING_BALANCE) + new Float("25000"); + assertEquals("Verifying opening Balance", balance, summary.get("accountBalance")); + + // DISBURSE Second Tranche + loanStatusHashMap = this.loanTransactionHelper.disburseLoanToSavings("23 April 2014", loanID); + System.out.println("DISBURSE " + loanStatusHashMap); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + summary = savingsAccountHelper.getSavingsSummary(savingsId); + balance = new Float(MINIMUM_OPENING_BALANCE) + new Float("25000") + new Float("20000"); + assertEquals("Verifying opening Balance", balance, summary.get("accountBalance")); + + loanStatusHashMap = this.loanTransactionHelper.undoDisbursal(loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + summary = savingsAccountHelper.getSavingsSummary(savingsId); + balance = new Float(MINIMUM_OPENING_BALANCE); + assertEquals("Verifying opening Balance", balance, summary.get("accountBalance")); + + } + private void validateCharge(Integer amountPercentage, final List loanCharges, final String amount, final String outstanding, String amountPaid, String amountWaived) { HashMap chargeDetail = getloanCharge(amountPercentage, loanCharges); @@ -626,7 +768,7 @@ public class ClientLoanIntegrationTest { Assert.assertTrue(new Float(twoDForm.format(new Float(val))).compareTo(new Float(twoDForm.format(new Float(val2)))) == 0); } - private Integer createLoanProduct() { + private Integer createLoanProduct(boolean multiDisburseLoan) { System.out.println("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); final String loanProductJSON = new LoanProductTestBuilder() // .withPrincipal("12,000.00") // @@ -637,14 +779,16 @@ public class ClientLoanIntegrationTest { .withInterestRateFrequencyTypeAsMonths() // .withAmortizationTypeAsEqualInstallments() // .withInterestTypeAsDecliningBalance() // + .withTranches(multiDisburseLoan) // .build(null); return this.loanTransactionHelper.getLoanProductId(loanProductJSON); } - private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, List charges) { + private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, List charges, + final String savingsId, String principal) { System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); final String loanApplicationJSON = new LoanApplicationTestBuilder() // - .withPrincipal("12,000.00") // + .withPrincipal(principal) // .withLoanTermFrequency("4") // .withLoanTermFrequencyAsMonths() // .withNumberOfRepayments("4") // @@ -656,7 +800,29 @@ public class ClientLoanIntegrationTest { .withInterestCalculationPeriodTypeSameAsRepaymentPeriod() // .withExpectedDisbursementDate("20 September 2011") // .withSubmittedOnDate("20 September 2011") // - .withCharges(charges).build(clientID.toString(), loanProductID.toString(), null); + .withCharges(charges).build(clientID.toString(), loanProductID.toString(), savingsId); + return this.loanTransactionHelper.getLoanId(loanApplicationJSON); + } + + private Integer applyForLoanApplicationWithTranches(final Integer clientID, final Integer loanProductID, List charges, + final String savingsId, String principal, List tranches) { + System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); + final String loanApplicationJSON = new LoanApplicationTestBuilder() // + .withPrincipal(principal) // + .withLoanTermFrequency("4") // + .withLoanTermFrequencyAsMonths() // + .withNumberOfRepayments("4") // + .withRepaymentEveryAfter("1") // + .withRepaymentFrequencyTypeAsMonths() // + .withInterestRatePerPeriod("2") // + .withAmortizationTypeAsEqualInstallments() // + .withInterestTypeAsDecliningBalance() // + .withInterestCalculationPeriodTypeSameAsRepaymentPeriod() // + .withExpectedDisbursementDate("1 March 2014") // + .withTranches(tranches) // + .withSubmittedOnDate("1 March 2014") // + + .withCharges(charges).build(clientID.toString(), loanProductID.toString(), savingsId); return this.loanTransactionHelper.getLoanId(loanApplicationJSON); } @@ -784,4 +950,12 @@ public class ClientLoanIntegrationTest { return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); } + private HashMap createTrancheDetail(final String date, final String amount) { + HashMap detail = new HashMap(); + detail.put("expectedDisbursementDate", date); + detail.put("principal", amount); + + return detail; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/charges/ChargesHelper.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/charges/ChargesHelper.java index bcae88e8a..36be16947 100755 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/charges/ChargesHelper.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/charges/ChargesHelper.java @@ -119,9 +119,17 @@ public class ChargesHelper { } public static String getLoanDisbursementJSON(final Integer chargeCalculationType, final String amount) { + return getLoanDisbursementJSON(chargeCalculationType, amount, ChargesHelper.CHARGE_PAYMENT_MODE_REGULAR); + } + + public static String getLoanDisbursementAccountTransferJSON(final Integer chargeCalculationType, final String amount) { + return getLoanDisbursementJSON(chargeCalculationType, amount, ChargesHelper.CHARGE_PAYMENT_MODE_ACCOUNT_TRANSFER); + } + + public static String getLoanDisbursementJSON(final Integer chargeCalculationType, final String amount, final Integer paymentmode) { final HashMap map = populateDefaultsForLoan(); map.put("chargeTimeType", CHARGE_DISBURSEMENT_FEE); - map.put("chargePaymentMode", ChargesHelper.CHARGE_PAYMENT_MODE_REGULAR); + map.put("chargePaymentMode", paymentmode); map.put("amount", amount); map.put("chargeCalculationType", chargeCalculationType); String chargesCreateJson = new Gson().toJson(map); diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java index 32e1308cd..3c3a5fe03 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java @@ -3,7 +3,6 @@ package org.mifosplatform.integrationtests.common.loans; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; import com.google.gson.Gson; @@ -35,6 +34,9 @@ public class LoanApplicationTestBuilder { private String expectedDisbursmentDate = ""; private String submittedOnDate = ""; private String loanType = "individual"; + private String fixedEmiAmount = "10000"; + private String maxOutstandingLoanBalance = "36000"; + private List disbursementData = null; private List charges = new ArrayList(); public String build(final String ID, final String loanProductId, final String savingsID) { @@ -68,6 +70,14 @@ public class LoanApplicationTestBuilder { if (savingsID != null) { map.put("linkAccountId", savingsID); } + + if (disbursementData != null) { + map.put("disbursementData", disbursementData); + map.put("fixedEmiAmount", fixedEmiAmount); + map.put("maxOutstandingLoanBalance", maxOutstandingLoanBalance); + + } + return new Gson().toJson(map); } @@ -181,4 +191,8 @@ public class LoanApplicationTestBuilder { return this; } + public LoanApplicationTestBuilder withTranches(final List disbursementData) { + this.disbursementData = disbursementData; + return this; + } } diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java index 8be2b9abf..40ff5517d 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java @@ -53,6 +53,10 @@ public class LoanProductTestBuilder { private String maxPrincipal = "100000.00"; private Account[] accountList = null; + private Boolean multiDisburseLoan = false; + private String outstandingLoanBalance = "35000"; + private String maxTrancheCount = "35000"; + public String build(final String chargeId) { final HashMap map = new HashMap(); @@ -83,6 +87,11 @@ public class LoanProductTestBuilder { map.put("minPrincipal", this.minPrincipal); map.put("maxPrincipal", this.maxPrincipal); map.put("overdueDaysForNPA", this.overdueDaysForNPA); + if (multiDisburseLoan) { + map.put("multiDisburseLoan", this.multiDisburseLoan); + map.put("maxTrancheCount", this.maxTrancheCount); + map.put("outstandingLoanBalance", this.outstandingLoanBalance); + } if (this.accountingRule.equals(ACCRUAL_UPFRONT) || this.accountingRule.equals(ACCRUAL_PERIODIC)) { map.putAll(getAccountMappingForAccrualBased()); @@ -215,6 +224,11 @@ public class LoanProductTestBuilder { return this; } + public LoanProductTestBuilder withTranches(boolean multiDisburseLoan) { + this.multiDisburseLoan = multiDisburseLoan; + return this; + } + private Map getAccountMappingForCashBased() { final Map map = new HashMap(); for (int i = 0; i < this.accountList.length; i++) { 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 831e74b50..71a198d0d 100644 --- 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 @@ -22,6 +22,7 @@ public class LoanTransactionHelper { private static final String APPROVE_LOAN_COMMAND = "approve"; private static final String UNDO_APPROVAL_LOAN_COMMAND = "undoApproval"; private static final String DISBURSE_LOAN_COMMAND = "disburse"; + private static final String DISBURSE_LOAN_TO_SAVINGS_COMMAND = "disburseToSavings"; private static final String UNDO_DISBURSE_LOAN_COMMAND = "undoDisbursal"; private static final String WRITE_OFF_LOAN_COMMAND = "writeoff"; private static final String WAIVE_INTEREST_COMMAND = "waiveinterest"; @@ -82,6 +83,10 @@ public class LoanTransactionHelper { public HashMap disburseLoan(final String date, final Integer loanID) { return performLoanTransaction(createLoanOperationURL(DISBURSE_LOAN_COMMAND, loanID), getDisburseLoanAsJSON(date)); } + + public HashMap disburseLoanToSavings(final String date, final Integer loanID) { + return performLoanTransaction(createLoanOperationURL(DISBURSE_LOAN_TO_SAVINGS_COMMAND, loanID), getDisburseLoanAsJSON(date)); + } public HashMap undoDisbursal(final Integer loanID) { final String undoDisburseJson = "{'note' : 'UNDO DISBURSAL'}"; 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 6db12f1d7..e83ef7bf5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -373,6 +373,10 @@ public class CommandWrapper { public boolean isDisbursementOfLoan() { return this.actionName.equalsIgnoreCase("DISBURSE") && this.entityName.equalsIgnoreCase("LOAN"); } + + public boolean isDisbursementOfLoanToSavings() { + return this.actionName.equalsIgnoreCase("DISBURSETOSAVINGS") && this.entityName.equalsIgnoreCase("LOAN"); + } public boolean isUndoDisbursementOfLoan() { return this.actionName.equalsIgnoreCase("DISBURSALUNDO") && this.entityName.equalsIgnoreCase("LOAN"); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java index b43031f3c..8ff5e24c5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -701,6 +701,15 @@ public class CommandWrapperBuilder { this.href = "/loans/" + loanId; return this; } + + public CommandWrapperBuilder disburseLoanToSavingsApplication(final Long loanId) { + this.actionName = "DISBURSETOSAVINGS"; + this.entityName = "LOAN"; + this.entityId = loanId; + this.loanId = loanId; + this.href = "/loans/" + loanId; + return this; + } public CommandWrapperBuilder undoLoanApplicationApproval(final Long loanId) { this.actionName = "APPROVALUNDO"; 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 b6030e6de..95e97644f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -335,6 +335,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer handler = this.applicationContext.getBean("loanApplicationRejectedCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isDisbursementOfLoan()) { handler = this.applicationContext.getBean("disburseLoanCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isDisbursementOfLoanToSavings()) { + handler = this.applicationContext.getBean("disburseLoanToSavingsCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isUndoDisbursementOfLoan()) { handler = this.applicationContext.getBean("undoDisbursalLoanCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isLoanRepayment()) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/data/AccountTransferDTO.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/data/AccountTransferDTO.java index 4e9c87237..91529a09b 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/data/AccountTransferDTO.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/data/AccountTransferDTO.java @@ -7,6 +7,7 @@ import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.portfolio.account.PortfolioAccountType; import org.mifosplatform.portfolio.account.domain.AccountTransferDetails; +import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail; public class AccountTransferDTO { @@ -27,12 +28,16 @@ public class AccountTransferDTO { private final Integer loanInstallmentNumber; private final Integer transferType; private final AccountTransferDetails accountTransferDetails; + private final String noteText; + private final String txnExternalId; + private final Loan loan; public AccountTransferDTO(final LocalDate transactionDate, final BigDecimal transactionAmount, final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType, final Long fromAccountId, final Long toAccountId, final String description, final Locale locale, final DateTimeFormatter fmt, final PaymentDetail paymentDetail, final Integer fromTransferType, final Integer toTransferType, final Long chargeId, - Integer loanInstallmentNumber, Integer transferType, final AccountTransferDetails accountTransferDetails) { + Integer loanInstallmentNumber, Integer transferType, final AccountTransferDetails accountTransferDetails,final String noteText, + final String txnExternalId,final Loan loan) { this.transactionDate = transactionDate; this.transactionAmount = transactionAmount; this.fromAccountType = fromAccountType; @@ -49,6 +54,9 @@ public class AccountTransferDTO { this.loanInstallmentNumber = loanInstallmentNumber; this.transferType = transferType; this.accountTransferDetails = accountTransferDetails; + this.noteText = noteText; + this.txnExternalId = txnExternalId; + this.loan = loan; } public LocalDate getTransactionDate() { @@ -107,14 +115,25 @@ public class AccountTransferDTO { return this.loanInstallmentNumber; } - public Integer getTransferType() { return this.transferType; } - public AccountTransferDetails getAccountTransferDetails() { return this.accountTransferDetails; } + public String getNoteText() { + return this.noteText; + } + + public String getTxnExternalId() { + return this.txnExternalId; + } + + + public Loan getLoan() { + return this.loan; + } + } 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 eeed14fde..6d3db96c8 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 @@ -34,6 +34,7 @@ import org.mifosplatform.portfolio.account.domain.AccountTransferType; 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.service.LoanAssembler; import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail; import org.mifosplatform.portfolio.savings.domain.SavingsAccount; @@ -292,7 +293,11 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer Loan fromLoanAccount = null; SavingsAccount toSavingsAccount = null; if (accountTransferDetails == null) { - fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); + if (accountTransferDTO.getLoan() == null) { + fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); + } else { + fromLoanAccount = accountTransferDTO.getLoan(); + } toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); } else { fromLoanAccount = accountTransferDetails.fromLoanAccount(); @@ -300,15 +305,23 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer toSavingsAccount = accountTransferDetails.toSavingsAccount(); this.savingsAccountAssembler.setHelpers(toSavingsAccount); } - final LoanTransaction loanRefundTransaction = this.loanAccountDomainService.makeRefund(accountTransferDTO.getFromAccountId(), - new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), - accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), null, null); + LoanTransaction loanTransaction = null; + if (LoanTransactionType.DISBURSEMENT.getValue().equals(accountTransferDTO.getFromTransferType())) { + loanTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(), + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), accountTransferDTO.getNoteText(), accountTransferDTO.getTxnExternalId()); + } else { + loanTransaction = this.loanAccountDomainService.makeRefund(accountTransferDTO.getFromAccountId(), + new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), + accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), accountTransferDTO.getNoteText(), + accountTransferDTO.getTxnExternalId()); + } final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail()); accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(accountTransferDTO, fromLoanAccount, - toSavingsAccount, deposit, loanRefundTransaction); + toSavingsAccount, deposit, loanTransaction); this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); transferTransactionId = accountTransferDetails.getId(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java index 39ce11143..90c5cea08 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java @@ -217,7 +217,7 @@ public class StandingInstructionWritePlatformServiceImpl implements StandingInst AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, data.fromAccountType(), data.toAccountType(), data.fromAccount().accountId(), data.toAccount().accountId(), data.name() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null, data - .transferType().getValue(), accountTransferDetails); + .transferType().getValue(), accountTransferDetails, null, null, null); transferAmount(sb, accountTransferDTO, data.getId()); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/collectionsheet/service/CollectionSheetWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/collectionsheet/service/CollectionSheetWritePlatformServiceJpaRepositoryImpl.java index 58c453b9a..53761d5e6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/collectionsheet/service/CollectionSheetWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/collectionsheet/service/CollectionSheetWritePlatformServiceJpaRepositoryImpl.java @@ -83,7 +83,7 @@ public class CollectionSheetWritePlatformServiceJpaRepositoryImpl implements Col final Map changes = new HashMap(); final CollectionSheetBulkDisbursalCommand bulkDisbursalCommand = this.bulkDisbursalCommandFromApiJsonDeserializer .commandFromApiJson(command.json()); - changes.putAll(this.loanWritePlatformService.bulkLoanDisbursal(command, bulkDisbursalCommand)); + changes.putAll(this.loanWritePlatformService.bulkLoanDisbursal(command, bulkDisbursalCommand, false)); return changes; } } 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 00731bffa..d0e1bd487 100644 --- 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,8 +96,11 @@ public class LoanTransactionsApiResource { } else if (is(commandParam, "close")) { transactionData = this.loanReadPlatformService.retrieveNewClosureDetails(); } else if (is(commandParam, "disburse")) { - transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId); - } else { + transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, true); + } else if(is(commandParam, "disburseToSavings")){ + transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, false); + } + else { 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 279f0bdc0..2f228d65a 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 @@ -551,6 +551,9 @@ public class LoansApiResource { } else if (is(commandParam, "disburse")) { final CommandWrapper commandRequest = builder.disburseLoanApplication(loanId).build(); result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "disburseToSavings")) { + final CommandWrapper commandRequest = builder.disburseLoanToSavingsApplication(loanId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } if (is(commandParam, "undoapproval")) { 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 d512fb99f..f38919628 100644 --- 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 @@ -94,7 +94,6 @@ import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations; -import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail; import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.data.jpa.domain.AbstractPersistable; @@ -1687,17 +1686,74 @@ public class Loan extends AbstractPersistable { public void disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, final JsonCommand command, final ApplicationCurrency currency, final List existingTransactionIds, final List existingReversedTransactionIds, - final Map actualChanges, final PaymentDetail paymentDetail, - final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List holidays, - final WorkingDays workingDays, final boolean allowTransactionsOnHoliday, final boolean allowTransactionsOnNonWorkingDay) { - - validateAccountStatus(LoanEvent.LOAN_DISBURSED); - Date lastDusburseDate = this.actualDisbursementDate; + final Map actualChanges, final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, + final List holidays, final WorkingDays workingDays, final boolean allowTransactionsOnHoliday, + final boolean allowTransactionsOnNonWorkingDay, final boolean recalculateSchedule) { final LoanStatus statusEnum = this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, LoanStatus.fromInt(this.loanStatus)); - boolean isMultiTrancheDisburse = false; + final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); + + existingTransactionIds.addAll(findExistingTransactionIds()); + existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); + + this.loanStatus = statusEnum.getValue(); + actualChanges.put("status", LoanEnumerations.status(this.loanStatus)); + + this.disbursedBy = currentUser; + updateLoanScheduleDependentDerivedFields(); + + actualChanges.put("locale", command.locale()); + actualChanges.put("dateFormat", command.dateFormat()); + actualChanges.put("actualDisbursementDate", command.stringValueOfParameterNamed("actualDisbursementDate")); + + // validate if disbursement date is a holiday or a non-working day + validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay); + validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays); + + handleDisbursementTransaction(actualDisbursementDate); + BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName); + boolean isEmiAmountChanged = false; + if (this.loanProduct.isMultiDisburseLoan() && emiAmount != null && emiAmount.compareTo(retriveLastEmiAmount()) != 0) { + LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.EMI_AMOUNT.getValue(), + actualDisbursementDate.toDate(), emiAmount, this); + this.loanTermVariations.add(loanVariationTerms); + isEmiAmountChanged = true; + } + if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate) || recalculateSchedule || isEmiAmountChanged) { + regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, + workingDays); + updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); + } else { + updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); + updateLoanSummaryDerivedFields(); + } + + final Money interestApplied = Money.of(getCurrency(), this.summary.getTotalInterestCharged()); + + /** + * Add an interest applied transaction of the interest is accrued + * upfront (Up front accrual) + **/ + + if (isUpfrontAccrualAccountingEnabledOnLoanProduct()) { + final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(getOffice(), this, interestApplied, + actualDisbursementDate); + this.loanTransactions.add(interestAppliedTransaction); + } + + // changedTransactionDetail = + // reprocessTransactionForDisbursement(changedTransactionDetail); + + } + + public boolean canDisburse(final LocalDate actualDisbursementDate) { + Date lastDusburseDate = this.actualDisbursementDate; + final LoanStatus statusEnum = this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, + LoanStatus.fromInt(this.loanStatus)); + + boolean isMultiTrancheDisburse = false; if (LoanStatus.fromInt(this.loanStatus).isActive() && isAllTranchesNotDisbursed()) { LoanDisbursementDetails details = fetchLastDisburseDetail(); @@ -1711,97 +1767,46 @@ public class Loan extends AbstractPersistable { } isMultiTrancheDisburse = true; } - if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus)) || isMultiTrancheDisburse) { + return (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus)) || isMultiTrancheDisburse); + } - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - this.loanStatus = statusEnum.getValue(); - actualChanges.put("status", LoanEnumerations.status(this.loanStatus)); - - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); - Money disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal().zero(); - BigDecimal principalDisbursed = command.bigDecimalValueOfParameterNamed(LoanApiConstants.principalDisbursedParameterName); - if (this.actualDisbursementDate == null) { - this.actualDisbursementDate = actualDisbursementDate.toDate(); - } - BigDecimal diff = BigDecimal.ZERO; - Collection details = fetchUndisbursedDetail(); - if (principalDisbursed == null) { - disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal(); - if (!details.isEmpty()) { - disburseAmount = disburseAmount.zero(); - for (LoanDisbursementDetails disbursementDetails : details) { - disbursementDetails.updateActualDisbursementDate(actualDisbursementDate.toDate()); - disburseAmount = disburseAmount.plus(disbursementDetails.principal()); - } - } - } else { - disburseAmount = disburseAmount.plus(principalDisbursed); - - if (details.isEmpty()) { - diff = this.loanRepaymentScheduleDetail.getPrincipal().minus(principalDisbursed).getAmount(); - } else { - for (LoanDisbursementDetails disbursementDetails : details) { - disbursementDetails.updateActualDisbursementDate(actualDisbursementDate.toDate()); - diff = diff.add(disbursementDetails.principal().subtract(principalDisbursed)); - disbursementDetails.updatePrincipal(principalDisbursed); - } - } - this.loanRepaymentScheduleDetail.setPrincipal(this.loanRepaymentScheduleDetail.getPrincipal().minus(diff).getAmount()); - if (diff.compareTo(BigDecimal.ZERO) == -1) { - final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved amount "; - throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.amount", principalDisbursed, - this.loanRepaymentScheduleDetail.getPrincipal().getAmount()); - } - } - this.disbursedBy = currentUser; - updateLoanScheduleDependentDerivedFields(); - - actualChanges.put("locale", command.locale()); - actualChanges.put("dateFormat", command.dateFormat()); - actualChanges.put("actualDisbursementDate", command.stringValueOfParameterNamed("actualDisbursementDate")); - - // validate if disbursement date is a holiday or a non-working day - validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay); - validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays); - - handleDisbursementTransaction(paymentDetail, actualDisbursementDate, txnExternalId, disburseAmount); - BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName); - boolean isEmiAmountChanged = false; - if (this.loanProduct.isMultiDisburseLoan() && emiAmount != null && emiAmount.compareTo(retriveLastEmiAmount()) != 0) { - LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.EMI_AMOUNT.getValue(), - actualDisbursementDate.toDate(), emiAmount, this); - this.loanTermVariations.add(loanVariationTerms); - isEmiAmountChanged = true; - } - if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate) || diff.compareTo(BigDecimal.ZERO) != 0 - || isEmiAmountChanged) { - regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, - holidays, workingDays); - updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); - } else { - updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); - updateLoanSummaryDerivedFields(); - } - - final Money interestApplied = Money.of(getCurrency(), this.summary.getTotalInterestCharged()); - - /** - * Add an interest applied transaction of the interest is accrued - * upfront (Up front accrual) - **/ - - if (isUpfrontAccrualAccountingEnabledOnLoanProduct()) { - final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(getOffice(), this, interestApplied, - actualDisbursementDate); - this.loanTransactions.add(interestAppliedTransaction); - } - - // changedTransactionDetail = - // reprocessTransactionForDisbursement(changedTransactionDetail); + public Money adjustDisburseAmount(final JsonCommand command, final LocalDate actualDisbursementDate) { + Money disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal().zero(); + BigDecimal principalDisbursed = command.bigDecimalValueOfParameterNamed(LoanApiConstants.principalDisbursedParameterName); + if (this.actualDisbursementDate == null) { + this.actualDisbursementDate = actualDisbursementDate.toDate(); } + BigDecimal diff = BigDecimal.ZERO; + Collection details = fetchUndisbursedDetail(); + if (principalDisbursed == null) { + disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal(); + if (!details.isEmpty()) { + disburseAmount = disburseAmount.zero(); + for (LoanDisbursementDetails disbursementDetails : details) { + disbursementDetails.updateActualDisbursementDate(actualDisbursementDate.toDate()); + disburseAmount = disburseAmount.plus(disbursementDetails.principal()); + } + } + } else { + disburseAmount = disburseAmount.plus(principalDisbursed); + if (details.isEmpty()) { + diff = this.loanRepaymentScheduleDetail.getPrincipal().minus(principalDisbursed).getAmount(); + } else { + for (LoanDisbursementDetails disbursementDetails : details) { + disbursementDetails.updateActualDisbursementDate(actualDisbursementDate.toDate()); + diff = diff.add(disbursementDetails.principal().subtract(principalDisbursed)); + disbursementDetails.updatePrincipal(principalDisbursed); + } + } + this.loanRepaymentScheduleDetail.setPrincipal(this.loanRepaymentScheduleDetail.getPrincipal().minus(diff).getAmount()); + if (diff.compareTo(BigDecimal.ZERO) == -1) { + final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved amount "; + throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.amount", principalDisbursed, + this.loanRepaymentScheduleDetail.getPrincipal().getAmount()); + } + } + return disburseAmount; } public ChangedTransactionDetail reprocessTransactionForDisbursement() { @@ -1961,13 +1966,7 @@ public class Loan extends AbstractPersistable { updateLoanSchedule(loanSchedule); } - private LoanTransaction handleDisbursementTransaction(final PaymentDetail paymentDetail, final LocalDate disbursedOn, - final String txnExternalId, Money disburseAmt) { - // track disbursement transaction - final LoanTransaction disbursementTransaction = LoanTransaction.disbursement(getOffice(), disburseAmt, paymentDetail, disbursedOn, - txnExternalId); - disbursementTransaction.updateLoan(this); - this.loanTransactions.add(disbursementTransaction); + private void handleDisbursementTransaction(final LocalDate disbursedOn) { // add repayment transaction to track incoming money from client to mfi // for (charges due at time of disbursement) @@ -2036,7 +2035,6 @@ public class Loan extends AbstractPersistable { throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn); } - return disbursementTransaction; } public LoanTransaction handlePayDisbursementTransaction(final Long chargeId, final LoanTransaction chargesPayment, @@ -3574,7 +3572,7 @@ public class Loan extends AbstractPersistable { return loanChargePerInstallments; } - private void validateAccountStatus(final LoanEvent event) { + public void validateAccountStatus(final LoanEvent event) { final List dataValidationErrors = new ArrayList(); 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 0e2f1d9de..e80e8eaff 100644 --- 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 @@ -23,4 +23,7 @@ public interface LoanAccountDomainService { LoanTransaction makeChargePayment(Loan loan, Long chargeId, LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId, Integer transactionType, Integer installmentNumber); + + LoanTransaction makeDisburseTransaction(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount, + PaymentDetail paymentDetail, String noteText, String txnExternalId); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index a3465a1f5..efc89bde8 100644 --- 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 @@ -270,6 +270,45 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { return newRefundTransaction; } + @Transactional + @Override + public LoanTransaction makeDisburseTransaction(final Long loanId, final LocalDate transactionDate, final BigDecimal transactionAmount, + final PaymentDetail paymentDetail, final String noteText, final String txnExternalId) { + final Loan loan = this.loanAccountAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + + final List existingTransactionIds = new ArrayList(); + final List existingReversedTransactionIds = new ArrayList(); + final Money amount = Money.of(loan.getCurrency(), transactionAmount); + LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amount, paymentDetail, transactionDate, + txnExternalId); + disbursementTransaction.updateLoan(loan); + loan.getLoanTransactions().add(disbursementTransaction); + try { + this.loanTransactionRepository.save(disbursementTransaction); + } catch (DataIntegrityViolationException e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { + baseDataValidator.reset().parameter("externalId").value(disbursementTransaction.getExternalId()) + .failWithCode("value.must.be.unique"); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } + this.loanRepository.saveAndFlush(loan); + + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.loanTransactionNote(loan, disbursementTransaction, noteText); + this.noteRepository.save(note); + } + + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + + return disbursementTransaction; + } + @Override public void reverseTransfer(final LoanTransaction loanTransaction) { loanTransaction.reverse(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/DisburseLoanCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/DisburseLoanCommandHandler.java index 7ea6239e7..0e3ba84ca 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/DisburseLoanCommandHandler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/DisburseLoanCommandHandler.java @@ -27,6 +27,6 @@ public class DisburseLoanCommandHandler implements NewCommandSourceHandler { @Override public CommandProcessingResult processCommand(final JsonCommand command) { - return this.writePlatformService.disburseLoan(command.entityId(), command); + return this.writePlatformService.disburseLoan(command.entityId(), command, false); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/DisburseLoanToSavingsCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/DisburseLoanToSavingsCommandHandler.java new file mode 100755 index 000000000..defefbd59 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/DisburseLoanToSavingsCommandHandler.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 DisburseLoanToSavingsCommandHandler implements NewCommandSourceHandler { + + private final LoanWritePlatformService writePlatformService; + + @Autowired + public DisburseLoanToSavingsCommandHandler(final LoanWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.disburseLoan(command.entityId(), command, true); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index ddcaa4cf9..fc0f91d4c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -205,7 +205,8 @@ public final class LoanApplicationCommandFromApiJsonHelper { final Integer graceOnArrearsAgeing = this.fromApiJsonHelper.extractIntegerWithLocaleNamed( LoanProductConstants.graceOnArrearsAgeingParameterName, element); - baseDataValidator.reset().parameter(LoanProductConstants.graceOnArrearsAgeingParameterName).value(graceOnArrearsAgeing).zeroOrPositiveAmount(); + baseDataValidator.reset().parameter(LoanProductConstants.graceOnArrearsAgeingParameterName).value(graceOnArrearsAgeing) + .zeroOrPositiveAmount(); final String interestChargedFromDateParameterName = "interestChargedFromDate"; if (this.fromApiJsonHelper.parameterExists(interestChargedFromDateParameterName, element)) { @@ -814,7 +815,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(LoanApiConstants.disbursementDateParameterName) .failWithCode(LoanApiConstants.DISBURSEMENT_DATE_START_WITH_ERROR); } - if (!tatalDisbursement.equals(totalPrincipal)) { + if (tatalDisbursement.compareTo(totalPrincipal) != 0) { baseDataValidator.reset().parameter(LoanApiConstants.disbursementPrincipalParameterName) .failWithCode(LoanApiConstants.PRINCIPAL_AMOUNT_SHOULD_BE_SAME); } 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 f1163c18e..86bf85663 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 @@ -48,13 +48,20 @@ public final class LoanEventApiJsonValidator { "Validation errors exist.", dataValidationErrors); } } - public void validateDisbursement(final String json) { + public void validateDisbursement(final String json, boolean isAccountTransfer) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } - final Set disbursementParameters = new HashSet(Arrays.asList("actualDisbursementDate", "externalId", "note", - "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", - LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.emiAmountParameterName)); + Set disbursementParameters = null; + + if (isAccountTransfer) { + disbursementParameters = new HashSet(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", + "dateFormat", LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.emiAmountParameterName)); + } else { + disbursementParameters = new HashSet(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", + "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", + LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.emiAmountParameterName)); + } final Type typeOfMap = new TypeToken>() {}.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); 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 34d2dacce..498e390b4 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 @@ -41,7 +41,7 @@ public interface LoanReadPlatformService { LoanTransactionData retrieveNewClosureDetails(); - LoanTransactionData retrieveDisbursalTemplate(Long loanId); + LoanTransactionData retrieveDisbursalTemplate(Long loanId, boolean paymentDetailsRequired); LoanAccountData retrieveTemplateWithCompleteGroupAndProductDetails(Long groupId, Long productId); 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 383ff8e59..b54c57c19 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 @@ -388,12 +388,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { } @Override - public LoanTransactionData retrieveDisbursalTemplate(final Long loanId) { + public LoanTransactionData retrieveDisbursalTemplate(final Long loanId, boolean paymentDetailsRequired) { final Loan loan = this.loanRepository.findOne(loanId); if (loan == null) { throw new LoanNotFoundException(loanId); } final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.DISBURSEMENT); - final Collection paymentOptions = this.codeValueReadPlatformService - .retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName); + Collection paymentOptions = null; + if (paymentDetailsRequired) { + paymentOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName); + } return new LoanTransactionData(null, null, null, transactionType, null, null, loan.getExpectedDisbursedOnLocalDateForTemplate(), loan.getDisburseAmountForTemplate(), null, null, null, null, null, paymentOptions, null, null, loan.retriveLastEmiAmount()); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java index ae037d044..33acc53b7 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 @@ -22,9 +22,9 @@ import java.util.Map; public interface LoanWritePlatformService { - CommandProcessingResult disburseLoan(Long loanId, JsonCommand command); + CommandProcessingResult disburseLoan(Long loanId, JsonCommand command, Boolean isAccountTransfer); - Map bulkLoanDisbursal(JsonCommand command, CollectionSheetBulkDisbursalCommand bulkDisbursalCommand); + Map bulkLoanDisbursal(JsonCommand command, CollectionSheetBulkDisbursalCommand bulkDisbursalCommand, Boolean isAccountTransfer); CommandProcessingResult undoLoanDisbursal(Long loanId, JsonCommand command); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index fffa789c5..e350c6a50 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -92,6 +92,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanChargeRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanDisbursementDetails; +import org.mifosplatform.portfolio.loanaccount.domain.LoanEvent; import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge; @@ -217,11 +218,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf @Transactional @Override - public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand command) { + public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand command, Boolean isAccountTransfer) { final AppUser currentUser = this.context.authenticatedUser(); - this.loanEventApiJsonValidator.validateDisbursement(command.json()); + this.loanEventApiJsonValidator.validateDisbursement(command.json(), isAccountTransfer); final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); @@ -259,11 +260,26 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); updateLoanCounters(loan, actualDisbursementDate); + Money amountBeforeAdjust = loan.getPrincpal(); + loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED); + boolean canDisburse = loan.canDisburse(actualDisbursementDate); + if (canDisburse) { + Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); + boolean recalcualteSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincpal()); + final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + if (isAccountTransfer) { + disburseLoanToSavings(loan, command, disburseAmount, paymentDetail); + } else { + LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail, + actualDisbursementDate, txnExternalId); + disbursementTransaction.updateLoan(loan); + loan.getLoanTransactions().add(disbursementTransaction); + } - loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, - existingReversedTransactionIds, changes, paymentDetail, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, - workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay); - + loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, + existingReversedTransactionIds, changes, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, + allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule); + } if (!changes.isEmpty()) { saveAndFlushLoanWithDataIntegrityViolationChecks(loan); @@ -306,7 +322,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.accountId(), loanId, "Loan Charge Payment", locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), entrySet.getKey(), null, - AccountTransferType.CHARGE_PAYMENT.getValue(), null); + AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } @@ -371,7 +387,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf *****/ @Transactional @Override - public Map bulkLoanDisbursal(final JsonCommand command, final CollectionSheetBulkDisbursalCommand bulkDisbursalCommand) { + public Map bulkLoanDisbursal(final JsonCommand command, final CollectionSheetBulkDisbursalCommand bulkDisbursalCommand, + Boolean isAccountTransfer) { final AppUser currentUser = this.context.authenticatedUser(); final SingleDisbursalCommand[] disbursalCommand = bulkDisbursalCommand.getDisburseTransactions(); @@ -405,12 +422,26 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final WorkingDays workingDays = this.workingDaysRepository.findOne(); final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); + loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED); updateLoanCounters(loan, actualDisbursementDate); - - loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, - existingReversedTransactionIds, changes, paymentDetail, firstRepaymentOnDate, isHolidayEnabled, holidays, workingDays, - allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay); - + boolean canDisburse = loan.canDisburse(actualDisbursementDate); + if (canDisburse) { + Money amountBeforeAdjust = loan.getPrincpal(); + Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); + boolean recalcualteSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincpal()); + final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + if (isAccountTransfer) { + disburseLoanToSavings(loan, command, disburseAmount, paymentDetail); + } else { + LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail, + actualDisbursementDate, txnExternalId); + disbursementTransaction.updateLoan(loan); + loan.getLoanTransactions().add(disbursementTransaction); + } + loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, + existingReversedTransactionIds, changes, firstRepaymentOnDate, isHolidayEnabled, holidays, workingDays, + allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule); + } if (!changes.isEmpty()) { try { this.loanRepository.save(loan); @@ -463,7 +494,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.accountId(), loan.getId(), "Loan Charge Payment", locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), - entrySet.getKey(), null, AccountTransferType.CHARGE_PAYMENT.getValue(), null); + entrySet.getKey(), null, AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } } @@ -1222,7 +1253,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, portfolioAccountData.accountId(), loanId, "Loan Charge Payment", locale, fmt, null, null, LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber, - AccountTransferType.CHARGE_PAYMENT.getValue(), null); + AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); return new CommandProcessingResultBuilder() // @@ -1235,6 +1266,27 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .withSavingsId(portfolioAccountData.accountId()).build(); } + public void disburseLoanToSavings(final Loan loan, final JsonCommand command, final Money amount, final PaymentDetail paymentDetail) { + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); + final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); + final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanAssociation(loan.getId()); + if (portfolioAccountData == null) { + final String errorMessage = "Disburse Loan with id:" + loan.getId() + " requires linked savings account for payment"; + throw new LinkedAccountRequiredException("loan.disburse.to.savings", errorMessage, loan.getId()); + } + + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount.getAmount(), + PortfolioAccountType.LOAN, PortfolioAccountType.SAVINGS, loan.getId(), portfolioAccountData.accountId(), + "Loan Disbursement", locale, fmt, paymentDetail, LoanTransactionType.DISBURSEMENT.getValue(), null, null, null, + AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, txnExternalId, loan); + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + + } + @Override @CronTarget(jobName = JobName.TRANSFER_FEE_CHARGE_FOR_LOANS) public void transferFeeCharges() throws JobExecutionException { @@ -1257,7 +1309,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf installmentChargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null, LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(), - installmentChargeData.getInstallmentNumber(), AccountTransferType.CHARGE_PAYMENT.getValue(), null); + installmentChargeData.getInstallmentNumber(), AccountTransferType.CHARGE_PAYMENT.getValue(), null, + null, null, null); transferFeeCharge(sb, accountTransferDTO); } } @@ -1268,7 +1321,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf chargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null, LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(), null, - AccountTransferType.CHARGE_PAYMENT.getValue(), null); + AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null); transferFeeCharge(sb, accountTransferDTO); } } @@ -1919,6 +1972,73 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } + public LoanTransaction disburseLoanAmountToSavings(final Long loanId, Long loanChargeId, final JsonCommand command, + final boolean isChargeIdIncludedInJson) { + + LoanTransaction transaction = null; + + this.loanEventApiJsonValidator.validateChargePaymentTransaction(command.json(), isChargeIdIncludedInJson); + if (isChargeIdIncludedInJson) { + loanChargeId = command.longValueOfParameterNamed("chargeId"); + } + final Loan loan = this.loanAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + + // Charges may be waived only when the loan associated with them are + // active + if (!loan.status().isActive()) { throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.LOAN_INACTIVE, + loanCharge.getId()); } + + // validate loan charge is not already paid or waived + if (loanCharge.isWaived()) { + throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId()); + } else if (loanCharge.isPaid()) { throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID, + loanCharge.getId()); } + + if (!loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { throw new LoanChargeCannotBePayedException( + LOAN_CHARGE_CANNOT_BE_PAYED_REASON.CHARGE_NOT_ACCOUNT_TRANSFER, loanCharge.getId()); } + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); + Integer loanInstallmentNumber = null; + BigDecimal amount = loanCharge.amountOutstanding(); + if (loanCharge.isInstalmentFee()) { + LoanInstallmentCharge chargePerInstallment = null; + final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); + final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); + if (dueDate != null) { + chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate); + } else if (installmentNumber != null) { + chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); + } + if (chargePerInstallment == null) { + chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge(); + } + if (chargePerInstallment.isWaived()) { + throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId()); + } else if (chargePerInstallment.isPaid()) { throw new LoanChargeCannotBePayedException( + LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID, loanCharge.getId()); } + loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber(); + amount = chargePerInstallment.getAmountOutstanding(); + } + + final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanAssociation(loanId); + if (portfolioAccountData == null) { + final String errorMessage = "Charge with id:" + loanChargeId + " requires linked savings account for payment"; + throw new LinkedAccountRequiredException("loanCharge.pay", errorMessage, loanChargeId); + } + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.SAVINGS, + PortfolioAccountType.LOAN, portfolioAccountData.accountId(), loanId, "Loan Charge Payment", locale, fmt, null, null, + LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber, + AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null); + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + + return transaction; + } + private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId); if (transferTransaction != null) {