MIFOSX-1146: added support for disbursing loan amount to savings account

This commit is contained in:
Pramod Nuthakki
2014-04-28 21:26:57 +05:30
parent f0bfec3459
commit d34dcb1037
27 changed files with 716 additions and 161 deletions
+84
View File
@@ -876,6 +876,14 @@
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>loans/{loanId}?command=disburseToSavings</td>
<td><a href="#loans_disburse_to_savings">Disburse Loan To Savings Account</a></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr class="alt">
<td></td>
<td>loans/{loanId}?command=undoDisbursal</td>
@@ -6927,6 +6935,74 @@ Request Body:
</div>
</div>
<a id="loans_disburse_to_savings" name="loans_disburse_to_savings" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Disburse Loan To Savings Account</h4>
<table class=matrixHeading>
<tr class="matrixHeadingBG">
<td><div class="mifosXHeading2">Mandatory Fields</div></td>
</tr>
<tr class=alt>
<td>actualDisbursementDate</td>
</tr>
</table>
<br/>
<table class=matrixHeading>
<tr class="matrixHeadingBG">
<td><div class="mifosXHeading2">Optional Fields</div></td>
</tr>
<tr class=alt>
<td>transactionAmount,fixedEmiAmount</td>
</tr>
</table>
</div>
<div class="method-example">
<code class="method-declaration">
POST https://DomainName/api/v1/loans/{loanId}?command=disburseToSavings
</code>
<code class="method-request">
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": ""
</code>
<code class="method-response">
{
"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
}
}
</code>
</div>
</div>
<a id="loans_disburse_undo" name="loans_disburse_undo" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
@@ -28905,6 +28981,14 @@ No Request Body:
<td></td>
<td></td>
</tr>
<tr class="alt">
<td></td>
<td>loans/{loanId}?command=disburseToSavings</td>
<td><a href="#loans_disburse_to_savings">Disburse Loan To Savings Account</a></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>loans/{loanId}?command=undoDisbursal</td>
@@ -0,0 +1 @@
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES ('transaction_loan', 'DISBURSETOSAVINGS_LOAN', 'LOAN', 'DISBURSETOSAVINGS', 0);
@@ -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<HashMap> 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<HashMap> charges = new ArrayList<HashMap>();
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<HashMap> charges = new ArrayList<HashMap>();
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<HashMap> charges = new ArrayList<HashMap>();
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<HashMap> tranches = new ArrayList<HashMap>();
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<HashMap> tranches = new ArrayList<HashMap>();
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<HashMap> 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<HashMap> charges) {
private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, List<HashMap> 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<HashMap> charges,
final String savingsId, String principal, List<HashMap> 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;
}
}
@@ -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<String, Object> 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);
@@ -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<HashMap> disbursementData = null;
private List<HashMap> charges = new ArrayList<HashMap>();
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<HashMap> disbursementData) {
this.disbursementData = disbursementData;
return this;
}
}
@@ -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<String, Object> map = new HashMap<String, Object>();
@@ -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<String, String> getAccountMappingForCashBased() {
final Map<String, String> map = new HashMap<String, String>();
for (int i = 0; i < this.accountList.length; i++) {
@@ -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'}";
@@ -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");
@@ -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";
@@ -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()) {
@@ -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;
}
}
@@ -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();
}
@@ -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());
}
}
@@ -83,7 +83,7 @@ public class CollectionSheetWritePlatformServiceJpaRepositoryImpl implements Col
final Map<String, Object> changes = new HashMap<String, Object>();
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;
}
}
@@ -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);
}
@@ -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")) {
@@ -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<Long> {
public void disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, final JsonCommand command,
final ApplicationCurrency currency, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final Map<String, Object> actualChanges, final PaymentDetail paymentDetail,
final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays, final boolean allowTransactionsOnHoliday, final boolean allowTransactionsOnNonWorkingDay) {
validateAccountStatus(LoanEvent.LOAN_DISBURSED);
Date lastDusburseDate = this.actualDisbursementDate;
final Map<String, Object> actualChanges, final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled,
final List<Holiday> 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<Long> {
}
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<LoanDisbursementDetails> 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<LoanDisbursementDetails> 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<Long> {
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<Long> {
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<Long> {
return loanChargePerInstallments;
}
private void validateAccountStatus(final LoanEvent event) {
public void validateAccountStatus(final LoanEvent event) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
@@ -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);
}
@@ -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<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
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<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
if (realCause.getMessage().toLowerCase().contains("external_id_unique")) {
baseDataValidator.reset().parameter("externalId").value(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();
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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<String> disbursementParameters = new HashSet<String>(Arrays.asList("actualDisbursementDate", "externalId", "note",
"locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber",
LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.emiAmountParameterName));
Set<String> disbursementParameters = null;
if (isAccountTransfer) {
disbursementParameters = new HashSet<String>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale",
"dateFormat", LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.emiAmountParameterName));
} else {
disbursementParameters = new HashSet<String>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale",
"dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber",
LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.emiAmountParameterName));
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
@@ -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);
@@ -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<CodeValueData> paymentOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName);
Collection<CodeValueData> 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());
}
@@ -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<String, Object> bulkLoanDisbursal(JsonCommand command, CollectionSheetBulkDisbursalCommand bulkDisbursalCommand);
Map<String, Object> bulkLoanDisbursal(JsonCommand command, CollectionSheetBulkDisbursalCommand bulkDisbursalCommand, Boolean isAccountTransfer);
CommandProcessingResult undoLoanDisbursal(Long loanId, JsonCommand command);
@@ -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<String, Object> bulkLoanDisbursal(final JsonCommand command, final CollectionSheetBulkDisbursalCommand bulkDisbursalCommand) {
public Map<String, Object> 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) {