diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResource.java index cd6dac151..eb2cc6458 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResource.java @@ -104,7 +104,7 @@ public class JournalEntriesApiResource { + "\n" + "journalentries?orderBy=transactionId&sortOrder=DESC\n" + "\n" + "journalentries?runningBalance=true\n" + "\n" + "journalentries?transactionDetails=true\n" + "\n" + "journalentries?loanId=12\n" + "\n" + "journalentries?savingsId=24") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = JournalEntryData.class)))) }) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = JournalEntriesApiResourceSwagger.GetJournalEntriesTransactionIdResponse.class)))) }) public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("officeId") @Parameter(description = "officeId") final Long officeId, @QueryParam("glAccountId") @Parameter(description = "glAccountId") final Long glAccountId, @@ -166,7 +166,7 @@ public class JournalEntriesApiResource { + "journalentries/1?fields=officeName,glAccountId,entryType,amount\n" + "\n" + "journalentries/1?runningBalance=true\n" + "\n" + "journalentries/1?transactionDetails=true") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = JournalEntryData.class))) }) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = JournalEntriesApiResourceSwagger.JournalEntryTransactionItem.class))) }) public String retrieveJournalEntryById( @PathParam("journalEntryId") @Parameter(description = "journalEntryId") final Long journalEntryId, @Context final UriInfo uriInfo, diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java index a1cea3d0b..9c7bf43a2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java @@ -19,6 +19,12 @@ package org.apache.fineract.accounting.journalentry.api; import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import org.apache.fineract.organisation.monetary.api.CurrenciesApiResourceSwagger.CurrencyItem; +import org.apache.fineract.portfolio.note.api.NotesApiResourceSwagger.GetResourceTypeResourceIdNotesResponse; +import org.apache.fineract.portfolio.paymenttype.api.PaymentTypeApiResourceSwagger.GetPaymentTypesResponse; /** * Created by sanyam on 25/7/17. @@ -62,4 +68,112 @@ final class JournalEntriesApiResourceSwagger { @Schema(description = "1") public Long officeId; } + + static final class EnumOptionType { + + private EnumOptionType() {} + + @Schema(example = "2") + public Long id; + @Schema(example = "accountType.asset") + public String code; + @Schema(example = "ASSET") + public String value; + } + + static final class JournalEntryTransactionItem { + + private JournalEntryTransactionItem() {} + + static final class PaymentDetailData { + + private PaymentDetailData() {} + + @Schema(example = "62") + public Long id; + public GetPaymentTypesResponse paymentType; + @Schema(example = "acc123") + public String accountNumber; + @Schema(example = "che123") + public String checkNumber; + @Schema(example = "rou123") + public String routingCode; + @Schema(example = "rec123") + public String receiptNumber; + @Schema(example = "ban123") + public String bankNumber; + } + + static final class TransactionDetails { + + private TransactionDetails() {} + + @Schema(example = "2") + public Long transactionId; + public EnumOptionType transactionType; + public GetResourceTypeResourceIdNotesResponse noteData; + public PaymentDetailData paymentDetails; + } + + @Schema(example = "1") + public Long id; + @Schema(example = "L12") + public String transactionId; + @Schema(example = "1") + public Long entityId; + @Schema(example = "1") + public Long officeId; + @Schema(example = "Head Office") + public String officeName; + @Schema(example = "10") + public Long glAccountId; + @Schema(example = "Cash Account") + public String glAccountName; + @Schema(example = "0123-4567") + public String glAccountCode; + @Schema(example = "[2022, 07, 01]") + public LocalDate transactionDate; + @Schema(example = "[2022, 07, 01]") + public LocalDate submittedOnDate; + + @Schema(example = "100.000000") + public Double amount; + @Schema(example = "false") + public boolean reversed; + @Schema(example = "false") + public boolean manualEntry; + @Schema(example = "Manual entry") + public String comments; + @Schema(example = "QWERTY") + public String referenceNumber; + @Schema(example = "1234.56") + public BigDecimal officeRunningBalance; + @Schema(example = "1234.56") + public BigDecimal organizationRunningBalance; + @Schema(example = "false") + public boolean runningBalanceComputed; + @Schema(example = "1") + public Long createdByUserId; + @Schema(example = "mifos") + public String createdByUserName; + @Schema(example = "[2022, 07, 01]") + public LocalDate createdDate; + + public CurrencyItem currency; + public EnumOptionType glAccountType; + public EnumOptionType entryType; + public EnumOptionType entityType; + public TransactionDetails transactionDetails; + } + + @Schema(description = "GetJournalEntriesTransactionIdResponse") + public static final class GetJournalEntriesTransactionIdResponse { + + private GetJournalEntriesTransactionIdResponse() {} + + @Schema(example = "2") + public Long totalFilteredRecords; + public List pageItems; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java index 4a59beda4..549a0ffd6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java @@ -25,12 +25,32 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; /** * Created by sanyam on 14/8/17. */ -final class CurrenciesApiResourceSwagger { +public final class CurrenciesApiResourceSwagger { private CurrenciesApiResourceSwagger() { } + public static final class CurrencyItem { + + private CurrencyItem() {} + + @Schema(example = "USD") + public String code; + @Schema(example = "US Dollar") + public String name; + @Schema(example = "2") + public Integer decimalPlaces; + @Schema(example = "100") + public Integer inMultiplesOf; + @Schema(example = "$") + public String displaySymbol; + @Schema(example = "currency.USD") + public String nameCode; + @Schema(example = "US Dollar ($)") + public String displayLabel; + } + @Schema(description = "GetCurrenciesResponse") public static final class GetCurrenciesResponse { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index bfe7e7bf4..e9ccab2fd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -3447,10 +3447,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } } if (isAllChargesPaid) { - loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this); - this.closedOnDate = transactionDate; this.actualMaturityDate = transactionDate; + loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this); + } else if (LoanStatus.fromInt(this.loanStatus).isOverpaid()) { if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) { this.overpaidOnDate = null; @@ -3506,6 +3506,39 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { updateLoanOutstandingBalances(); } + public void applyIncomeAccrualTransaction(LocalDate closedDate) { + ExternalId externalId = ExternalId.empty(); + boolean isExternalIdAutoGenerationEnabled = TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled(); + if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + List updatedAccrualTransactions = retrieveListOfAccrualTransactions(); + LocalDate lastAccruedDate = this.getDisbursementDate(); + if (!updatedAccrualTransactions.isEmpty()) { + lastAccruedDate = updatedAccrualTransactions.get(updatedAccrualTransactions.size() - 1).getTransactionDate(); + } + HashMap feeDetails = new HashMap<>(); + determineFeeDetails(lastAccruedDate, closedDate, feeDetails); + if (isExternalIdAutoGenerationEnabled) { + externalId = ExternalId.generate(); + } + BigDecimal fee = (BigDecimal) feeDetails.get(FEE); + if (fee == null) { + fee = BigDecimal.ZERO; + } + BigDecimal penalty = (BigDecimal) feeDetails.get(PENALTIES); + if (penalty == null) { + penalty = BigDecimal.ZERO; + } + BigDecimal total = fee.add(penalty); + // TODO: calculate interest? + if (total.compareTo(BigDecimal.ZERO) > 0) { + LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(this, this.getOffice(), closedDate, total, null, fee, + penalty, externalId); + updateLoanChargesPaidBy(accrualTransaction, feeDetails, null); + addLoanTransaction(accrualTransaction); + } + } + } + private void determineCumulativeIncomeFromInstallments(HashMap cumulativeIncomeFromInstallments) { BigDecimal interest = BigDecimal.ZERO; BigDecimal fee = BigDecimal.ZERO; @@ -3797,6 +3830,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING); + final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); + this.closedOnDate = writtenOffOnLocalDate; + this.writtenOffOnDate = writtenOffOnLocalDate; + this.closedBy = currentUser; final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.WRITE_OFF_OUTSTANDING, this); LoanTransaction loanTransaction = null; @@ -3807,7 +3844,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { existingTransactionIds.addAll(findExistingTransactionIds()); existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(EXTERNAL_ID); ExternalId externalId = ExternalIdFactory.produce(txnExternalId); @@ -3816,9 +3852,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { externalId = ExternalId.generate(); } - this.closedOnDate = writtenOffOnLocalDate; - this.writtenOffOnDate = writtenOffOnLocalDate; - this.closedBy = currentUser; changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); changes.put(WRITTEN_OFF_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); changes.put("externalId", externalId); @@ -3934,12 +3967,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final Money totalOutstanding = this.summary.getTotalOutstanding(loanCurrency()); if (totalOutstanding.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalOutstanding)) { + this.closedOnDate = closureDate; final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.REPAID_IN_FULL, this); if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) { loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this); changes.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); } - this.closedOnDate = closureDate; changes.put("externalId", externalId); loanTransaction = LoanTransaction.writeoff(this, getOffice(), closureDate, externalId); final boolean isLastTransaction = isChronologicallyLatestTransaction(loanTransaction, getLoanTransactions()); @@ -3965,14 +3998,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final Money totalLoanOverpayment = calculateTotalOverpayment(); if (totalLoanOverpayment.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) { // TODO - KW - technically should set somewhere that this loan - // has - // 'overpaid' amount + // has 'overpaid' amount + this.closedOnDate = closureDate; final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.REPAID_IN_FULL, this); if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) { loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this); changes.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); } - this.closedOnDate = closureDate; } else if (totalLoanOverpayment.isGreaterThanZero()) { final String errorMessage = "The loan is marked as 'Overpaid' and cannot be moved to 'Closed (obligations met)."; throw new InvalidLoanStateTransitionException("close", "loan.is.overpaid", errorMessage, totalLoanOverpayment.toString()); @@ -3995,13 +4027,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LocalDate rescheduledOn = command.localDateValueOfParameterNamed(TRANSACTION_DATE); + this.closedOnDate = rescheduledOn; final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_RESCHEDULE, this); if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) { loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, this); changes.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); } - this.closedOnDate = rescheduledOn; this.rescheduledOnDate = rescheduledOn; changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); changes.put("rescheduledOnDate", command.stringValueOfParameterNamed(TRANSACTION_DATE)); @@ -6121,6 +6153,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) { this.overpaidOnDate = null; + this.closedOnDate = newCreditBalanceRefundTransaction.getTransactionDate(); defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, this); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformService.java new file mode 100644 index 000000000..fc737fec8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformService.java @@ -0,0 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +public interface LoanStatusChangePlatformService {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java new file mode 100644 index 000000000..3f620fc38 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.event.business.BusinessEventListener; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.springframework.stereotype.Service; + +@Slf4j +@RequiredArgsConstructor +@Service +public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService { + + private final BusinessEventNotifierService businessEventNotifierService; + + @PostConstruct + public void addListeners() { + businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class, new LoanStatusChangedListener()); + } + + private static class LoanStatusChangedListener implements BusinessEventListener { + + @Override + public void onBusinessEvent(LoanStatusChangedBusinessEvent event) { + final Loan loan = event.get(); + log.debug("Loan Status change for loan {}", loan.getId()); + // Apply common actions on Loan account closed + if (loan.isClosed()) { + log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId()); + loan.applyIncomeAccrualTransaction(loan.getClosedOnDate()); + } + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index d16740cad..b5ebb1e31 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -1039,110 +1039,30 @@ final class LoanProductsApiResourceSwagger { private GetLoanAccountingMappings() {} - static final class GetLoanFundSourceAccount { + static final class GetGlAccountMapping { - private GetLoanFundSourceAccount() {} - - @Schema(example = "1") - public Long id; - @Schema(example = "fund source") - public String name; - @Schema(example = "01") - public Integer glCode; - } - - static final class GetLoanPortfolioAccount { - - private GetLoanPortfolioAccount() {} - - @Schema(example = "2") - public Long id; - @Schema(example = "Loan portfolio") - public String name; - @Schema(example = "02") - public Integer glCode; - } - - static final class GetLoanTransfersInSuspenseAccount { - - private GetLoanTransfersInSuspenseAccount() {} - - @Schema(example = "3") - public Long id; - @Schema(example = "transfers") - public String name; - @Schema(example = "03") - public Integer glCode; - } - - static final class GetLoanInterestOnLoanAccount { - - private GetLoanInterestOnLoanAccount() {} - - @Schema(example = "4") - public Long id; - @Schema(example = "income from interest") - public String name; - @Schema(example = "04") - public Integer glCode; - } - - static final class GetLoanIncomeFromFeeAccount { - - private GetLoanIncomeFromFeeAccount() {} - - @Schema(example = "8") - public Long id; - @Schema(example = "income from fees 2") - public String name; - @Schema(example = "10") - public Integer glCode; - } - - static final class GetLoanIncomeFromPenaltyAccount { - - private GetLoanIncomeFromPenaltyAccount() {} - - @Schema(example = "9") - public Long id; - @Schema(example = "income from penalities 2") - public String name; - @Schema(example = "11") - public Integer glCode; - } - - static final class GetLoanWriteOffAccount { - - private GetLoanWriteOffAccount() {} + private GetGlAccountMapping() {} @Schema(example = "10") public Long id; - @Schema(example = "loans written off 2") + @Schema(example = "Cash Account") public String name; - @Schema(example = "12") - public Integer glCode; + @Schema(example = "012-34-65") + public String glCode; } - static final class GetLoanOverpaymentLiabilityAccount { - - private GetLoanOverpaymentLiabilityAccount() {} - - @Schema(example = "11") - public Long id; - @Schema(example = "over payment") - public String name; - @Schema(example = "13") - public Integer glCode; - } - - public GetLoanFundSourceAccount fundSourceAccount; - public GetLoanPortfolioAccount loanPortfolioAccount; - public GetLoanTransfersInSuspenseAccount transfersInSuspenseAccount; - public GetLoanInterestOnLoanAccount interestOnLoanAccount; - public GetLoanIncomeFromFeeAccount incomeFromFeeAccount; - public GetLoanIncomeFromPenaltyAccount incomeFromPenaltyAccount; - public GetLoanWriteOffAccount writeOffAccount; - public GetLoanOverpaymentLiabilityAccount overpaymentLiabilityAccount; + public GetGlAccountMapping fundSourceAccount; + public GetGlAccountMapping loanPortfolioAccount; + public GetGlAccountMapping transfersInSuspenseAccount; + public GetGlAccountMapping receivableInterestAccount; + public GetGlAccountMapping receivablePenaltyAccount; + public GetGlAccountMapping interestOnLoanAccount; + public GetGlAccountMapping incomeFromFeeAccount; + public GetGlAccountMapping incomeFromPenaltyAccount; + public GetGlAccountMapping incomeFromRecoveryAccount; + public GetGlAccountMapping writeOffAccount; + public GetGlAccountMapping goodwillCreditAccount; + public GetGlAccountMapping overpaymentLiabilityAccount; } static final class GetLoanPaymentChannelToFundSourceMappings { @@ -1174,7 +1094,7 @@ final class LoanProductsApiResourceSwagger { } public GetLoanCharge charge; - public GetLoanAccountingMappings.GetLoanIncomeFromFeeAccount incomeAccount; + public GetLoanAccountingMappings.GetGlAccountMapping incomeAccount; @Schema(example = "10") public Long chargeId; @Schema(example = "39") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/api/NotesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/api/NotesApiResourceSwagger.java index a0524d59d..e6d2b71e7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/api/NotesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/api/NotesApiResourceSwagger.java @@ -24,7 +24,7 @@ import java.time.ZonedDateTime; /** * Created by Chirag Gupta on 12/29/17. */ -final class NotesApiResourceSwagger { +public final class NotesApiResourceSwagger { private NotesApiResourceSwagger() {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceSwagger.java index 8813ec286..a2d065944 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceSwagger.java @@ -23,7 +23,7 @@ import io.swagger.v3.oas.annotations.media.Schema; /** * Created by Chirag Gupta on 01/01/18. */ -final class PaymentTypeApiResourceSwagger { +public final class PaymentTypeApiResourceSwagger { private PaymentTypeApiResourceSwagger() {} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java index 7b8e599ec..02393b488 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.integrationtests; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import io.restassured.builder.RequestSpecBuilder; @@ -27,15 +28,25 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.time.LocalDate; import java.util.HashMap; +import java.util.List; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse; import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.JournalEntryTransactionItem; import org.apache.fineract.client.models.PostChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.accounting.AccountHelper; +import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper; +import org.apache.fineract.integrationtests.common.accounting.PeriodicAccrualAccountingHelper; import org.apache.fineract.integrationtests.common.charges.ChargesHelper; import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; @@ -49,6 +60,10 @@ public class LoanChargeSpecificDueDateTest { private ResponseSpecification responseSpec; private RequestSpecification requestSpec; private LoanTransactionHelper loanTransactionHelper; + private PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper; + private AccountHelper accountHelper; + private JournalEntryHelper journalEntryHelper; + private static final String principalAmount = "1000.00"; @BeforeEach @@ -60,6 +75,9 @@ public class LoanChargeSpecificDueDateTest { responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(this.requestSpec, this.responseSpec); + accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); + journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); } @Test @@ -79,7 +97,7 @@ public class LoanChargeSpecificDueDateTest { // Create Loan Account final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), - getLoanProductsProductResponse.getId().toString(), operationDate); + getLoanProductsProductResponse.getId().toString(), operationDate, "12", "0"); // Get loan details GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -132,7 +150,7 @@ public class LoanChargeSpecificDueDateTest { // Create Loan Account final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), - getLoanProductsProductResponse.getId().toString(), operationDate); + getLoanProductsProductResponse.getId().toString(), operationDate, "12", "0"); // Get loan details GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -187,7 +205,7 @@ public class LoanChargeSpecificDueDateTest { // Create Loan Account final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), - getLoanProductsProductResponse.getId().toString(), operationDate); + getLoanProductsProductResponse.getId().toString(), operationDate, "12", "0"); // Get loan details GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -233,6 +251,132 @@ public class LoanChargeSpecificDueDateTest { } + @Test + public void testApplyFeeAccrualOnClosedDate() { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, todaysDate); + + // Client and Loan account creation + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012"); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithPeriodicAccrual(loanTransactionHelper, + null); + assertNotNull(getLoanProductsProductResponse); + + LocalDate transactionDate = LocalDate.of(Utils.getLocalDateOfTenant().getYear(), 1, 1); + String operationDate = Utils.dateFormatter.format(transactionDate); + log.info("Disbursement date {}", transactionDate); + + // Create Loan Account + final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), + getLoanProductsProductResponse.getId().toString(), operationDate, "1", "0"); + + // Get loan details + GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("0.00"), true); + + // Apply Loan Charge with specific due date + String feeAmount = "10.00"; + String payloadJSON = ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, feeAmount, true); + final PostChargesResponse postChargesResponse = ChargesHelper.createLoanCharge(requestSpec, responseSpec, payloadJSON); + assertNotNull(postChargesResponse); + final Long chargeId = postChargesResponse.getResourceId(); + assertNotNull(chargeId); + + // First Loan Charge + transactionDate = transactionDate.plusDays(1); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, transactionDate); + operationDate = Utils.dateFormatter.format(transactionDate); + log.info("Operation date {}", transactionDate); + payloadJSON = LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(chargeId.toString(), operationDate, feeAmount); + PostLoansLoanIdChargesResponse postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON, + responseSpec); + assertNotNull(postLoansLoanIdChargesResponse); + final Long loanChargeId01 = postLoansLoanIdChargesResponse.getResourceId(); + assertNotNull(loanChargeId01); + + // Run Accruals + log.info("Running Periodic Accrual for date {}", transactionDate); + periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(operationDate); + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + loanTransactionHelper.evaluateLoanTransactionData(getLoansLoanIdResponse, "loanTransactionType.accrual", Double.valueOf("10.00")); + + // Repay the first charge fully, 10 + Float amount = Float.valueOf("10.00"); + transactionDate = transactionDate.plusDays(40); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, transactionDate); + operationDate = Utils.dateFormatter.format(transactionDate); + log.info("Operation date {}", transactionDate); + PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, + loanId); + assertNotNull(loanIdTransactionsResponse); + log.info("Loan Transaction Id: {} {}", loanId, loanIdTransactionsResponse.getResourceId()); + + // Second Loan Charge + feeAmount = "15.00"; + transactionDate = transactionDate.plusDays(1); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, transactionDate); + operationDate = Utils.dateFormatter.format(transactionDate); + log.info("Operation date {}", transactionDate); + payloadJSON = LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(chargeId.toString(), operationDate, feeAmount); + postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON, responseSpec); + assertNotNull(postLoansLoanIdChargesResponse); + final Long loanChargeId02 = postLoansLoanIdChargesResponse.getResourceId(); + assertNotNull(loanChargeId02); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("15.00"), true); + + // Run Accruals + log.info("Running Periodic Accrual for date {}", transactionDate); + periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(operationDate); + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + loanTransactionHelper.evaluateLoanTransactionData(getLoansLoanIdResponse, "loanTransactionType.accrual", Double.valueOf("25.00")); + + // Third Loan Charge + feeAmount = "25.00"; + transactionDate = transactionDate.plusDays(1); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, transactionDate); + operationDate = Utils.dateFormatter.format(transactionDate); + log.info("Operation date {}", transactionDate); + payloadJSON = LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(chargeId.toString(), operationDate, feeAmount); + postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON, responseSpec); + assertNotNull(postLoansLoanIdChargesResponse); + final Long loanChargeId03 = postLoansLoanIdChargesResponse.getResourceId(); + assertNotNull(loanChargeId03); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("40.00"), true); + loanTransactionHelper.evaluateLoanTransactionData(getLoansLoanIdResponse, "loanTransactionType.accrual", Double.valueOf("25.00")); + + amount = Float.valueOf("1040.00"); + loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); + assertNotNull(loanIdTransactionsResponse); + log.info("Loan Transaction Id: {} {}", loanId, loanIdTransactionsResponse.getResourceId()); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + assertNotNull(getLoansLoanIdResponse); + loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met"); + loanTransactionHelper.evaluateLoanTransactionData(getLoansLoanIdResponse, "loanTransactionType.accrual", Double.valueOf("50.00")); + + final Long transactionId = loanTransactionHelper.evaluateLastLoanTransactionData(getLoansLoanIdResponse, + "loanTransactionType.accrual", operationDate, Double.valueOf("25.00")); + assertNotNull(transactionId); + log.info("transactionId {}", transactionId); + + final GetJournalEntriesTransactionIdResponse journalEntriesResponse = journalEntryHelper + .getJournalEntries("L" + transactionId.toString()); + assertNotNull(journalEntriesResponse); + final List journalEntries = journalEntriesResponse.getPageItems(); + assertEquals(2, journalEntries.size()); + assertEquals(25, journalEntries.get(0).getAmount()); + assertEquals(25, journalEntries.get(1).getAmount()); + assertEquals(transactionDate, journalEntries.get(0).getTransactionDate()); + assertEquals(transactionDate, journalEntries.get(1).getTransactionDate()); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper, final Integer delinquencyBucketId) { final HashMap loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucketId); @@ -240,12 +384,27 @@ public class LoanChargeSpecificDueDateTest { return loanTransactionHelper.getLoanProduct(loanProductId); } + private GetLoanProductsProductIdResponse createLoanProductWithPeriodicAccrual(final LoanTransactionHelper loanTransactionHelper, + final Integer delinquencyBucketId) { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account assetFeeAndPenaltyAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final HashMap loanProductMap = new LoanProductTestBuilder() + .withAccountingRulePeriodicAccrual(new Account[] { assetAccount, incomeAccount, expenseAccount, overpaymentAccount }) + .build(null, delinquencyBucketId); + final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); + return loanTransactionHelper.getLoanProduct(loanProductId); + } + private Integer createLoanAccount(final LoanTransactionHelper loanTransactionHelper, final String clientId, final String loanProductId, - final String operationDate) { - final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount).withLoanTermFrequency("12") - .withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1") + final String operationDate, final String repayments, final String interestRate) { + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount).withLoanTermFrequency(repayments) + .withLoanTermFrequencyAsMonths().withNumberOfRepayments(repayments).withRepaymentEveryAfter("1") .withRepaymentFrequencyTypeAsMonths() // - .withInterestRatePerPeriod("0") // + .withInterestRatePerPeriod(interestRate) // .withExpectedDisbursementDate(operationDate) // .withInterestTypeAsDecliningBalance() // .withSubmittedOnDate(operationDate) // diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java index 838afcd39..155586294 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java @@ -20,18 +20,24 @@ package org.apache.fineract.integrationtests.common.accounting; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.gson.Gson; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.util.ArrayList; import java.util.HashMap; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse; +import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.common.Utils; import org.junit.jupiter.api.Assertions; +@Slf4j @SuppressWarnings("rawtypes") public class JournalEntryHelper { private final RequestSpecification requestSpec; private final ResponseSpecification responseSpec; + private static final Gson GSON = new JSON().getGson(); public JournalEntryHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { this.requestSpec = requestSpec; @@ -104,4 +110,12 @@ public class JournalEntryHelper { + "&orderBy=id&sortOrder=desc&locale=en&dateFormat=dd MMMM yyyy"); } + public GetJournalEntriesTransactionIdResponse getJournalEntries(final String transactionId) { + log.info("Getting GL Journal entries for transaction id {}", transactionId); + final String url = createURLForGettingAccountEntriesByTransactionId(transactionId); + final String response = Utils.performServerGet(this.requestSpec, this.responseSpec, url, null); + log.info("response {}", response); + return GSON.fromJson(response, GetJournalEntriesTransactionIdResponse.class); + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 2b9045de5..d445462cb 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -55,6 +55,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdSummary; +import org.apache.fineract.client.models.GetLoansLoanIdTransactions; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.GetPaymentTypesResponse; @@ -1580,6 +1581,37 @@ public class LoanTransactionHelper extends IntegrationTest { } } + public void evaluateLoanTransactionData(GetLoansLoanIdResponse getLoansLoanIdResponse, String transactionType, Double amountExpected) { + List transactions = getLoansLoanIdResponse.getTransactions(); + log.info("Loan with {} transactions", transactions.size()); + Double transactionsAmount = 0.0; + for (GetLoansLoanIdTransactions transaction : transactions) { + log.info(" Id {} code {} date {} amount {}", transaction.getId(), transaction.getType().getCode(), transaction.getDate(), + transaction.getAmount()); + if (transactionType.equals(transaction.getType().getCode())) { + transactionsAmount += transaction.getAmount(); + } + } + assertEquals(amountExpected, transactionsAmount); + } + + public Long evaluateLastLoanTransactionData(GetLoansLoanIdResponse getLoansLoanIdResponse, String transactionType, + String transactionExpected, Double amountExpected) { + List transactions = getLoansLoanIdResponse.getTransactions(); + log.info("Loan with {} transactions", transactions.size()); + GetLoansLoanIdTransactions lastTransaction = null; + for (GetLoansLoanIdTransactions transaction : transactions) { + log.info(" Id {} code {} date {} amount {}", transaction.getId(), transaction.getType().getCode(), transaction.getDate(), + transaction.getAmount()); + if (transactionType.equals(transaction.getType().getCode())) { + lastTransaction = transaction; + } + } + assertEquals(transactionExpected, Utils.dateFormatter.format(lastTransaction.getDate())); + assertEquals(amountExpected, lastTransaction.getAmount()); + return lastTransaction.getId(); + } + public void validateLoanStatus(GetLoansLoanIdResponse getLoansLoanIdResponse, final String statusCodeExpected) { final String statusCode = getLoansLoanIdResponse.getStatus().getCode(); log.info("Loan with Id {} is with Status {}", getLoansLoanIdResponse.getId(), statusCode);