diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java index edf889588..686924583 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java @@ -157,6 +157,9 @@ public class CommandSourceService { } if ("GLACCOUNT".equalsIgnoreCase(commandSource.getEntityName()) || "CBSBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName()) + || "LOANBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName()) + || "DELINQUENCY_BUCKET".equalsIgnoreCase(commandSource.getEntityName()) + || "DELINQUENCY_RANGE".equalsIgnoreCase(commandSource.getEntityName()) || "CHARGE".equalsIgnoreCase(commandSource.getEntityName()) || "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName()) || "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName()) diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index d88124708..9e491dbdf 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -2460,9 +2460,9 @@ public class CommandWrapperBuilder { return this; } - public CommandWrapperBuilder createCbsLoanBaseInterestRate() { + public CommandWrapperBuilder createLoanBaseInterestRate() { this.actionName = "CREATE"; - this.entityName = "CBSLOANBASEINTERESTRATE"; + this.entityName = "LOANBASEINTERESTRATE"; this.entityId = null; this.href = "/loanproducts/baseinterestrates"; return this; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java index 03f96300e..26fbb4a42 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java @@ -295,6 +295,175 @@ public class LoanProductAdvanceExt { @Column(name = "cheque_block_lien_released") private Boolean chequeBlockLienReleased; + // ---- Loans A/C (EMI) wizard fields ------------------------------------- + @Column(name = "loan_security_mandatory") + private String securityMandatory; + + @Column(name = "loan_guarantors_required") + private Integer guarantorsRequired; + + @Column(name = "loan_co_applicant") + private String coApplicant; + + @Column(name = "loan_third_party_security") + private String thirdPartySecurity; + + @Column(name = "loan_legal_opinion_required") + private Boolean legalOpinionRequired; + + @Column(name = "loan_property_insurance_required") + private Boolean propertyInsuranceRequired; + + @Column(name = "loan_register_charge") + private Boolean registerCharge; + + @Column(name = "loan_release_security_on_closure") + private Boolean releaseSecurityOnClosure; + + @Column(name = "loan_allow_multiple_securities") + private Boolean allowMultipleSecurities; + + @Column(name = "loan_allow_third_party_deposit") + private Boolean allowThirdPartyDeposit; + + @Column(name = "loan_min_age") + private Integer minAge; + + @Column(name = "loan_max_age_at_maturity") + private Integer maxAgeAtMaturity; + + @Column(name = "loan_min_cibil") + private Integer minCibil; + + @Column(name = "loan_min_monthly_income") + private BigDecimal minMonthlyIncome; + + @Column(name = "loan_max_foir") + private BigDecimal maxFoir; + + @Column(name = "loan_max_loans_per_customer") + private Integer maxLoansPerCustomer; + + @Column(name = "loan_disbursement_type") + private String disbursementType; + + @Column(name = "loan_disbursement_mode") + private String disbursementMode; + + @Column(name = "loan_undisbursed_interest") + private String undisbursedInterest; + + @Column(name = "loan_pre_emi_interest") + private Boolean preEmiInterest; + + @Column(name = "loan_emi_due_day") + private String emiDueDay; + + @Column(name = "loan_holiday_rule") + private String holidayRule; + + @Column(name = "loan_moratorium_months") + private Integer moratoriumMonths; + + @Column(name = "loan_moratorium_interest") + private String moratoriumInterest; + + @Column(name = "loan_variable_installments") + private Boolean variableInstallments; + + @Column(name = "loan_repayment_mode") + private String repaymentMode; + + @Column(name = "loan_linked_account_type") + private String linkedAccountType; + + @Column(name = "loan_pdc_collection") + private String pdcCollection; + + @Column(name = "loan_mandate_required") + private Boolean mandateRequired; + + @Column(name = "loan_advance_emi") + private String advanceEmi; + + @Column(name = "loan_rate_type") + private String rateType; + + @Column(name = "loan_register_particular") + private String registerParticular; + + @Column(name = "loan_apply_cibil_adjustment") + private Boolean applyCibilAdjustment; + + @Column(name = "loan_rate_resolved_at") + private String rateResolvedAt; + + @Column(name = "loan_calc_period") + private String calcPeriod; + + @Column(name = "loan_interest_starts_from") + private String interestStartsFrom; + + @Column(name = "loan_compounding") + private String compounding; + + @Column(name = "loan_benchmark") + private String benchmark; + + @Column(name = "loan_reset_frequency") + private String resetFrequency; + + @Column(name = "loan_reset_change") + private String resetChange; + + @Column(name = "loan_part_prepayment_allowed") + private Boolean partPrepaymentAllowed; + + @Column(name = "loan_min_part_payment") + private BigDecimal minPartPayment; + + @Column(name = "loan_allowed_on") + private String allowedOn; + + @Column(name = "loan_part_payments_per_year") + private String partPaymentsPerYear; + + @Column(name = "loan_part_prepayment_mode") + private String partPrepaymentMode; + + @Column(name = "loan_foreclosure_floating_individual") + private BigDecimal foreclosureFloatingIndividual; + + @Column(name = "loan_foreclosure_fixed_individual") + private BigDecimal foreclosureFixedIndividual; + + @Column(name = "loan_foreclosure_non_individual") + private BigDecimal foreclosureNonIndividual; + + @Column(name = "loan_foreclosure_takeover") + private BigDecimal foreclosureTakeover; + + @Column(name = "loan_foreclosure_quote_valid_days") + private Integer foreclosureQuoteValidDays; + + @Column(name = "loan_auto_close_on_zero_balance") + private Boolean autoCloseOnZeroBalance; + + @Column(name = "loan_noc_letter") + private String nocLetter; + + @Column(name = "loan_restructuring") + private String restructuring; + + @Column(name = "loan_release_collateral_on_closure") + private Boolean releaseCollateralOnClosure; + + @Column(name = "loan_top_up_allowed") + private Boolean topUpAllowed; + + @Column(name = "loan_balance_transfer_out_allowed") + private Boolean balanceTransferOutAllowed; + public LoanProductAdvanceExt(final LoanProduct loanProduct) { this.loanProduct = loanProduct; this.loanProductId = loanProduct.getId(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterProduct.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterProduct.java new file mode 100644 index 000000000..8b82d5739 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterProduct.java @@ -0,0 +1,52 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * 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.apache.fineract.portfolio.loanproduct.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * A "particular" (loan type) row of the Loan Interest Rate Register chart. The GL account is a + * property of the particular, so it lives here rather than on each rate-register version. Kept as a + * plain {@code gl_account_id} column (no relation) to avoid pulling the accounting module's entity + * graph into the loan module. + */ +@Entity +@Table(name = "m_loan_rate_register_product") +@Getter +@Setter +@NoArgsConstructor +public class LoanRateRegisterProduct { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "sr_no", nullable = false) + private Integer srNo; + + @Column(name = "name", length = 200, nullable = false) + private String name; + + @Column(name = "gl_account_id") + private Long glAccountId; + + @Column(name = "is_active", nullable = false) + private Boolean isActive; + + @Column(name = "created_date") + private LocalDateTime createdDate; +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterProductRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterProductRepository.java new file mode 100644 index 000000000..0024d1f94 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterProductRepository.java @@ -0,0 +1,17 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * 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.apache.fineract.portfolio.loanproduct.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LoanRateRegisterProductRepository extends JpaRepository { + + /** Active particulars in chart order. */ + List findByIsActiveTrueOrderBySrNoAsc(); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterSlabHistory.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterSlabHistory.java index 95d40e024..7ffde2a32 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterSlabHistory.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRateRegisterSlabHistory.java @@ -41,6 +41,10 @@ public class LoanRateRegisterSlabHistory { private Long effectiveId; // ---- product dimension ---------------------------------------------- + /** Stable link to the master particular (and its GL). Null only for rows written before the master existed. */ + @Column(name = "product_id") + private Long productId; + @Column(name = "product_sr_no", nullable = false) private Integer productSrNo; @@ -73,11 +77,12 @@ public class LoanRateRegisterSlabHistory { @Column(name = "created_date") private LocalDateTime createdDate; - public static LoanRateRegisterSlabHistory create(final Long effectiveId, final Integer productSrNo, final String productName, - final Integer slabOrder, final String slabLabel, final BigDecimal amountUpto, final BigDecimal rateValue, - final String rateFormula, final boolean isFormula, final boolean isNa) { + public static LoanRateRegisterSlabHistory create(final Long effectiveId, final Long productId, final Integer productSrNo, + final String productName, final Integer slabOrder, final String slabLabel, final BigDecimal amountUpto, + final BigDecimal rateValue, final String rateFormula, final boolean isFormula, final boolean isNa) { final LoanRateRegisterSlabHistory s = new LoanRateRegisterSlabHistory(); s.effectiveId = effectiveId; + s.productId = productId; s.productSrNo = productSrNo; s.productName = productName; s.slabOrder = slabOrder; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index c4ec760a5..48158729a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -37,10 +37,15 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.UriInfo; +import java.time.LocalDate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -58,6 +63,7 @@ import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.infrastructure.codes.data.CodeValueData; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.ApiFacingEnum; @@ -222,11 +228,54 @@ public class LoanProductsApiResource { return this.productMixDataApiJsonSerializer.serialize(settings, productMixes, PRODUCT_MIX_DATA_PARAMETERS); } - final Collection products = this.loanProductReadPlatformService.retrieveAllLoanProducts(); + final Collection products = enrichAndFilterProducts( + this.loanProductReadPlatformService.retrieveAllLoanProducts(), uriInfo); return this.toApiJsonSerializer.serialize(settings, products, LOAN_PRODUCT_DATA_PARAMETERS); } + /** + * Adds each product's category and type ("Advances A/C" or "Loans A/C" and its sub type) to the list, and applies + * the optional filters {@code activeOnly}, {@code productTypeGroup} and {@code productTypeSubGroup}. With none of + * them given the list holds the same products as before, each now carrying its classification. + */ + private Collection enrichAndFilterProducts(final Collection products, final UriInfo uriInfo) { + final MultivaluedMap query = uriInfo.getQueryParameters(); + final boolean activeOnly = Boolean.parseBoolean(query.getFirst("activeOnly")); + final String group = trimToNull(query.getFirst("productTypeGroup")); + final String subGroup = trimToNull(query.getFirst("productTypeSubGroup")); + final Map> extensions = this.loanProductAdvanceExtService.retrieveAllAsMap(); + final LocalDate today = DateUtils.getBusinessLocalDate(); + + final List result = new ArrayList<>(); + for (final LoanProductData product : products) { + final Map ext = extensions.getOrDefault(product.getId(), Collections.emptyMap()); + if (activeOnly && !isActiveOn(product, today)) { + continue; + } + if (group != null && !group.equalsIgnoreCase(String.valueOf(ext.get("productTypeGroup")))) { + continue; + } + if (subGroup != null && !subGroup.equalsIgnoreCase(String.valueOf(ext.get("productTypeSubGroup")))) { + continue; + } + product.setAdvanceExtensions(new LinkedHashMap<>(ext)); + result.add(product); + } + return result; + } + + /** A product is active from its start date until its close date; either end may be open. */ + private static boolean isActiveOn(final LoanProductData product, final LocalDate date) { + final LocalDate start = product.getStartDate(); + final LocalDate close = product.getCloseDate(); + return (start == null || !start.isAfter(date)) && (close == null || !close.isBefore(date)); + } + + private static String trimToNull(final String value) { + return value == null || value.isBlank() ? null : value.trim(); + } + @GET @Path("template") @Consumes({ MediaType.APPLICATION_JSON }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsBaseInterestRatesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsBaseInterestRatesApiResource.java index 0d47cb3e7..1382b267b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsBaseInterestRatesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsBaseInterestRatesApiResource.java @@ -44,7 +44,7 @@ import org.springframework.stereotype.Component; * a POST that goes through Fineract's maker-checker pipeline. * * Reads are gated by the pre-existing {@code LOANPRODUCT} resource; writes by the two seeded - * permissions {@code CREATE_CBSLOANBASEINTERESTRATE} (maker) and {@code CREATE_CBSLOANBASEINTERESTRATE_CHECKER}. + * permissions {@code CREATE_LOANBASEINTERESTRATE} (maker) and {@code CREATE_LOANBASEINTERESTRATE_CHECKER}. */ @Path("/v1/loanproducts/baseinterestrates") @Component @@ -76,6 +76,18 @@ public class LoanProductsBaseInterestRatesApiResource { return this.gson.toJson(data); } + /** Master list of particulars (loan types) that make up the chart rows. Literal path wins over {id}. */ + @GET + @Path("products") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Get Loan Interest Rate Register particulars", description = "Returns the master list of loan types (chart rows) with their mapped GL account id, in chart order.") + @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) + public String retrieveProducts() { + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + return this.gson.toJson(this.readService.retrieveProducts()); + } + /** The currently in-force version, with its full chart + CIBIL. */ @GET @Path("active") @@ -103,7 +115,7 @@ public class LoanProductsBaseInterestRatesApiResource { } /** - * Submit a new version. If {@code CREATE_CBSLOANBASEINTERESTRATE.can_maker_checker=true}, this + * Submit a new version. If {@code CREATE_LOANBASEINTERESTRATE.can_maker_checker=true}, this * returns {@code rollbackTransaction=true} and the row sits in {@code m_portfolio_command_source} * until a checker approves it from the Approvals tab. Otherwise it commits immediately. */ @@ -114,7 +126,7 @@ public class LoanProductsBaseInterestRatesApiResource { @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) public CommandProcessingResult submitRateRegister(@Parameter(hidden = true) final String apiRequestBodyAsJson) { final CommandWrapper commandRequest = new CommandWrapperBuilder() - .createCbsLoanBaseInterestRate() + .createLoanBaseInterestRate() .withJson(apiRequestBodyAsJson) .build(); return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanRateRegisterSaveRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanRateRegisterSaveRequest.java index 175f68476..7d1a913d3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanRateRegisterSaveRequest.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanRateRegisterSaveRequest.java @@ -30,9 +30,23 @@ public class LoanRateRegisterSaveRequest { private String fallbackBelowMin; private String note; + /** + * Particulars (chart rows) with their GL. A row whose srNo is not yet in the master is created on + * approval and must carry a glAccountId; for an existing row a changed glAccountId/name updates the master. + */ + private List products; private List slabs; private List cibilSlabs; + @Getter + @Setter + @NoArgsConstructor + public static class ProductRequest { + private Integer srNo; + private String name; + private Long glAccountId; + } + @Getter @Setter @NoArgsConstructor diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanRateRegisterProductData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanRateRegisterProductData.java new file mode 100644 index 000000000..96cabe402 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanRateRegisterProductData.java @@ -0,0 +1,24 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * 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.apache.fineract.portfolio.loanproduct.data; + +import lombok.Builder; +import lombok.Getter; + +/** Response DTO for one particular (loan type) of the Loan Interest Rate Register. */ +@Getter +@Builder +public class LoanRateRegisterProductData { + + private Long id; + private Integer srNo; + private String name; + /** Null until the bank maps the particular to a GL account. */ + private Long glAccountId; + private Boolean isActive; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanRateRegisterSlabData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanRateRegisterSlabData.java index 016f98b16..e09b2ffcc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanRateRegisterSlabData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanRateRegisterSlabData.java @@ -20,6 +20,7 @@ import lombok.Getter; public class LoanRateRegisterSlabData { private Long id; + private Long productId; private Integer productSrNo; private String productName; private Integer slabOrder; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateCbsLoanBaseInterestRateCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanBaseInterestRateCommandHandler.java similarity index 81% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateCbsLoanBaseInterestRateCommandHandler.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanBaseInterestRateCommandHandler.java index 0e6f761b6..0146122b9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateCbsLoanBaseInterestRateCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanBaseInterestRateCommandHandler.java @@ -17,15 +17,15 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** - * Dispatched by Fineract when a {@code CBSLOANBASEINTERESTRATE} / {@code CREATE} command is executed — + * Dispatched by Fineract when a {@code LOANBASEINTERESTRATE} / {@code CREATE} command is executed — * on first submit (maker-checker off) or on checker approval (maker-checker on). The {@code @CommandType} - * entity string here MUST match the one registered by {@code CommandWrapperBuilder.createCbsLoanBaseInterestRate()} + * entity string here MUST match the one registered by {@code CommandWrapperBuilder.createLoanBaseInterestRate()} * and the permission codes seeded by {@code 0581_enable_loan_rate_register_maker_checker.xml}. */ @Service -@CommandType(entity = "CBSLOANBASEINTERESTRATE", action = "CREATE") +@CommandType(entity = "LOANBASEINTERESTRATE", action = "CREATE") @RequiredArgsConstructor -public class CreateCbsLoanBaseInterestRateCommandHandler implements NewCommandSourceHandler { +public class CreateLoanBaseInterestRateCommandHandler implements NewCommandSourceHandler { private final LoanRateRegisterWritePlatformService writePlatformService; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java index 9308653d1..bc233bc74 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java @@ -61,7 +61,22 @@ public class LoanProductAdvanceExtService { "chequeBookSizes", "chequeMaxBooksOutstanding", "chequeReissueWhenLeaves", "chequeFirstBookFree", "chequeFreeBooksPerYear", "chequeNamePrinted", "chequePrintDescription", "chequeStopPaymentAllowed", "chequeMaintainStockRegister", "chequeAllowWithdrawalSlip", "chequeBlockNpa", "chequeBlockLimitExpired", - "chequeBlockOverDp", "chequeBlockFrozen", "chequeBlockLienReleased" }; + "chequeBlockOverDp", "chequeBlockFrozen", "chequeBlockLienReleased", + // Loans A/C (EMI) wizard fields + "securityMandatory", "guarantorsRequired", "coApplicant", "thirdPartySecurity", + "legalOpinionRequired", "propertyInsuranceRequired", "registerCharge", "releaseSecurityOnClosure", + "allowMultipleSecurities", "allowThirdPartyDeposit", "minAge", "maxAgeAtMaturity", + "minCibil", "minMonthlyIncome", "maxFoir", "maxLoansPerCustomer", + "disbursementType", "disbursementMode", "undisbursedInterest", "preEmiInterest", + "emiDueDay", "holidayRule", "moratoriumMonths", "moratoriumInterest", + "variableInstallments", "repaymentMode", "linkedAccountType", "pdcCollection", + "mandateRequired", "advanceEmi", "rateType", "registerParticular", + "applyCibilAdjustment", "rateResolvedAt", "calcPeriod", "interestStartsFrom", + "compounding", "benchmark", "resetFrequency", "resetChange", + "partPrepaymentAllowed", "minPartPayment", "allowedOn", "partPaymentsPerYear", + "partPrepaymentMode", "foreclosureFloatingIndividual", "foreclosureFixedIndividual", "foreclosureNonIndividual", + "foreclosureTakeover", "foreclosureQuoteValidDays", "autoCloseOnZeroBalance", "nocLetter", + "restructuring", "releaseCollateralOnClosure", "topUpAllowed", "balanceTransferOutAllowed" }; /** * Insert or partial-update the satellite for this product. Returns the fields that actually changed so the @@ -84,6 +99,18 @@ public class LoanProductAdvanceExtService { .orElseGet(LinkedHashMap::new); } + /** + * Every product's satellite in one query, keyed by loan product id. Used by the product list so a screen can + * show and filter products by category and type without one extra call per product. + */ + public Map> retrieveAllAsMap() { + final Map> byProduct = new LinkedHashMap<>(); + for (final LoanProductAdvanceExt ext : this.repository.findAll()) { + byProduct.put(ext.getLoanProductId(), toMap(ext)); + } + return byProduct; + } + private boolean hasAnyAdvanceParameter(final JsonCommand command) { for (final String param : ADVANCE_PARAMETERS) { if (command.parameterExists(param)) { @@ -205,6 +232,63 @@ public class LoanProductAdvanceExtService { setBoolean(command, "chequeBlockFrozen", ext::getChequeBlockFrozen, ext::setChequeBlockFrozen, changes); setBoolean(command, "chequeBlockLienReleased", ext::getChequeBlockLienReleased, ext::setChequeBlockLienReleased, changes); + // Loans A/C (EMI) wizard fields + setString(command, "securityMandatory", ext::getSecurityMandatory, ext::setSecurityMandatory, changes); + setInteger(command, "guarantorsRequired", ext::getGuarantorsRequired, ext::setGuarantorsRequired, changes); + setString(command, "coApplicant", ext::getCoApplicant, ext::setCoApplicant, changes); + setString(command, "thirdPartySecurity", ext::getThirdPartySecurity, ext::setThirdPartySecurity, changes); + setBoolean(command, "legalOpinionRequired", ext::getLegalOpinionRequired, ext::setLegalOpinionRequired, changes); + setBoolean(command, "propertyInsuranceRequired", ext::getPropertyInsuranceRequired, ext::setPropertyInsuranceRequired, changes); + setBoolean(command, "registerCharge", ext::getRegisterCharge, ext::setRegisterCharge, changes); + setBoolean(command, "releaseSecurityOnClosure", ext::getReleaseSecurityOnClosure, ext::setReleaseSecurityOnClosure, changes); + setBoolean(command, "allowMultipleSecurities", ext::getAllowMultipleSecurities, ext::setAllowMultipleSecurities, changes); + setBoolean(command, "allowThirdPartyDeposit", ext::getAllowThirdPartyDeposit, ext::setAllowThirdPartyDeposit, changes); + setInteger(command, "minAge", ext::getMinAge, ext::setMinAge, changes); + setInteger(command, "maxAgeAtMaturity", ext::getMaxAgeAtMaturity, ext::setMaxAgeAtMaturity, changes); + setInteger(command, "minCibil", ext::getMinCibil, ext::setMinCibil, changes); + setDecimal(command, "minMonthlyIncome", ext::getMinMonthlyIncome, ext::setMinMonthlyIncome, changes); + setDecimal(command, "maxFoir", ext::getMaxFoir, ext::setMaxFoir, changes); + setInteger(command, "maxLoansPerCustomer", ext::getMaxLoansPerCustomer, ext::setMaxLoansPerCustomer, changes); + setString(command, "disbursementType", ext::getDisbursementType, ext::setDisbursementType, changes); + setString(command, "disbursementMode", ext::getDisbursementMode, ext::setDisbursementMode, changes); + setString(command, "undisbursedInterest", ext::getUndisbursedInterest, ext::setUndisbursedInterest, changes); + setBoolean(command, "preEmiInterest", ext::getPreEmiInterest, ext::setPreEmiInterest, changes); + setString(command, "emiDueDay", ext::getEmiDueDay, ext::setEmiDueDay, changes); + setString(command, "holidayRule", ext::getHolidayRule, ext::setHolidayRule, changes); + setInteger(command, "moratoriumMonths", ext::getMoratoriumMonths, ext::setMoratoriumMonths, changes); + setString(command, "moratoriumInterest", ext::getMoratoriumInterest, ext::setMoratoriumInterest, changes); + setBoolean(command, "variableInstallments", ext::getVariableInstallments, ext::setVariableInstallments, changes); + setString(command, "repaymentMode", ext::getRepaymentMode, ext::setRepaymentMode, changes); + setString(command, "linkedAccountType", ext::getLinkedAccountType, ext::setLinkedAccountType, changes); + setString(command, "pdcCollection", ext::getPdcCollection, ext::setPdcCollection, changes); + setBoolean(command, "mandateRequired", ext::getMandateRequired, ext::setMandateRequired, changes); + setString(command, "advanceEmi", ext::getAdvanceEmi, ext::setAdvanceEmi, changes); + setString(command, "rateType", ext::getRateType, ext::setRateType, changes); + setString(command, "registerParticular", ext::getRegisterParticular, ext::setRegisterParticular, changes); + setBoolean(command, "applyCibilAdjustment", ext::getApplyCibilAdjustment, ext::setApplyCibilAdjustment, changes); + setString(command, "rateResolvedAt", ext::getRateResolvedAt, ext::setRateResolvedAt, changes); + setString(command, "calcPeriod", ext::getCalcPeriod, ext::setCalcPeriod, changes); + setString(command, "interestStartsFrom", ext::getInterestStartsFrom, ext::setInterestStartsFrom, changes); + setString(command, "compounding", ext::getCompounding, ext::setCompounding, changes); + setString(command, "benchmark", ext::getBenchmark, ext::setBenchmark, changes); + setString(command, "resetFrequency", ext::getResetFrequency, ext::setResetFrequency, changes); + setString(command, "resetChange", ext::getResetChange, ext::setResetChange, changes); + setBoolean(command, "partPrepaymentAllowed", ext::getPartPrepaymentAllowed, ext::setPartPrepaymentAllowed, changes); + setDecimal(command, "minPartPayment", ext::getMinPartPayment, ext::setMinPartPayment, changes); + setString(command, "allowedOn", ext::getAllowedOn, ext::setAllowedOn, changes); + setString(command, "partPaymentsPerYear", ext::getPartPaymentsPerYear, ext::setPartPaymentsPerYear, changes); + setString(command, "partPrepaymentMode", ext::getPartPrepaymentMode, ext::setPartPrepaymentMode, changes); + setDecimal(command, "foreclosureFloatingIndividual", ext::getForeclosureFloatingIndividual, ext::setForeclosureFloatingIndividual, changes); + setDecimal(command, "foreclosureFixedIndividual", ext::getForeclosureFixedIndividual, ext::setForeclosureFixedIndividual, changes); + setDecimal(command, "foreclosureNonIndividual", ext::getForeclosureNonIndividual, ext::setForeclosureNonIndividual, changes); + setDecimal(command, "foreclosureTakeover", ext::getForeclosureTakeover, ext::setForeclosureTakeover, changes); + setInteger(command, "foreclosureQuoteValidDays", ext::getForeclosureQuoteValidDays, ext::setForeclosureQuoteValidDays, changes); + setBoolean(command, "autoCloseOnZeroBalance", ext::getAutoCloseOnZeroBalance, ext::setAutoCloseOnZeroBalance, changes); + setString(command, "nocLetter", ext::getNocLetter, ext::setNocLetter, changes); + setString(command, "restructuring", ext::getRestructuring, ext::setRestructuring, changes); + setBoolean(command, "releaseCollateralOnClosure", ext::getReleaseCollateralOnClosure, ext::setReleaseCollateralOnClosure, changes); + setBoolean(command, "topUpAllowed", ext::getTopUpAllowed, ext::setTopUpAllowed, changes); + setBoolean(command, "balanceTransferOutAllowed", ext::getBalanceTransferOutAllowed, ext::setBalanceTransferOutAllowed, changes); } // ---- typed setters: only write params that are present, and only report real changes ---------- @@ -368,6 +452,63 @@ public class LoanProductAdvanceExtService { map.put("chequeBlockOverDp", ext.getChequeBlockOverDp()); map.put("chequeBlockFrozen", ext.getChequeBlockFrozen()); map.put("chequeBlockLienReleased", ext.getChequeBlockLienReleased()); + // Loans A/C (EMI) wizard fields + map.put("securityMandatory", ext.getSecurityMandatory()); + map.put("guarantorsRequired", ext.getGuarantorsRequired()); + map.put("coApplicant", ext.getCoApplicant()); + map.put("thirdPartySecurity", ext.getThirdPartySecurity()); + map.put("legalOpinionRequired", ext.getLegalOpinionRequired()); + map.put("propertyInsuranceRequired", ext.getPropertyInsuranceRequired()); + map.put("registerCharge", ext.getRegisterCharge()); + map.put("releaseSecurityOnClosure", ext.getReleaseSecurityOnClosure()); + map.put("allowMultipleSecurities", ext.getAllowMultipleSecurities()); + map.put("allowThirdPartyDeposit", ext.getAllowThirdPartyDeposit()); + map.put("minAge", ext.getMinAge()); + map.put("maxAgeAtMaturity", ext.getMaxAgeAtMaturity()); + map.put("minCibil", ext.getMinCibil()); + map.put("minMonthlyIncome", ext.getMinMonthlyIncome()); + map.put("maxFoir", ext.getMaxFoir()); + map.put("maxLoansPerCustomer", ext.getMaxLoansPerCustomer()); + map.put("disbursementType", ext.getDisbursementType()); + map.put("disbursementMode", ext.getDisbursementMode()); + map.put("undisbursedInterest", ext.getUndisbursedInterest()); + map.put("preEmiInterest", ext.getPreEmiInterest()); + map.put("emiDueDay", ext.getEmiDueDay()); + map.put("holidayRule", ext.getHolidayRule()); + map.put("moratoriumMonths", ext.getMoratoriumMonths()); + map.put("moratoriumInterest", ext.getMoratoriumInterest()); + map.put("variableInstallments", ext.getVariableInstallments()); + map.put("repaymentMode", ext.getRepaymentMode()); + map.put("linkedAccountType", ext.getLinkedAccountType()); + map.put("pdcCollection", ext.getPdcCollection()); + map.put("mandateRequired", ext.getMandateRequired()); + map.put("advanceEmi", ext.getAdvanceEmi()); + map.put("rateType", ext.getRateType()); + map.put("registerParticular", ext.getRegisterParticular()); + map.put("applyCibilAdjustment", ext.getApplyCibilAdjustment()); + map.put("rateResolvedAt", ext.getRateResolvedAt()); + map.put("calcPeriod", ext.getCalcPeriod()); + map.put("interestStartsFrom", ext.getInterestStartsFrom()); + map.put("compounding", ext.getCompounding()); + map.put("benchmark", ext.getBenchmark()); + map.put("resetFrequency", ext.getResetFrequency()); + map.put("resetChange", ext.getResetChange()); + map.put("partPrepaymentAllowed", ext.getPartPrepaymentAllowed()); + map.put("minPartPayment", ext.getMinPartPayment()); + map.put("allowedOn", ext.getAllowedOn()); + map.put("partPaymentsPerYear", ext.getPartPaymentsPerYear()); + map.put("partPrepaymentMode", ext.getPartPrepaymentMode()); + map.put("foreclosureFloatingIndividual", ext.getForeclosureFloatingIndividual()); + map.put("foreclosureFixedIndividual", ext.getForeclosureFixedIndividual()); + map.put("foreclosureNonIndividual", ext.getForeclosureNonIndividual()); + map.put("foreclosureTakeover", ext.getForeclosureTakeover()); + map.put("foreclosureQuoteValidDays", ext.getForeclosureQuoteValidDays()); + map.put("autoCloseOnZeroBalance", ext.getAutoCloseOnZeroBalance()); + map.put("nocLetter", ext.getNocLetter()); + map.put("restructuring", ext.getRestructuring()); + map.put("releaseCollateralOnClosure", ext.getReleaseCollateralOnClosure()); + map.put("topUpAllowed", ext.getTopUpAllowed()); + map.put("balanceTransferOutAllowed", ext.getBalanceTransferOutAllowed()); return map; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterReadPlatformService.java index dba4416db..9dd0e4287 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterReadPlatformService.java @@ -10,6 +10,7 @@ package org.apache.fineract.portfolio.loanproduct.service; import java.util.List; import java.util.Optional; import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterData; +import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterProductData; public interface LoanRateRegisterReadPlatformService { @@ -21,4 +22,7 @@ public interface LoanRateRegisterReadPlatformService { /** A specific version by id. */ Optional retrieveById(Long id); + + /** Master list of particulars (loan types) that make up the chart rows, in chart order. */ + List retrieveProducts(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterReadPlatformServiceImpl.java index 02c131798..9577935d0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterReadPlatformServiceImpl.java @@ -13,11 +13,14 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterCibilSlabData; import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterData; +import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterProductData; import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterSlabData; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterCibilSlab; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterCibilSlabRepository; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterEffective; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterEffectiveRepository; +import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterProduct; +import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterProductRepository; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterSlabHistory; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterSlabHistoryRepository; import org.springframework.stereotype.Service; @@ -33,6 +36,23 @@ public class LoanRateRegisterReadPlatformServiceImpl implements LoanRateRegister private final LoanRateRegisterEffectiveRepository effectiveRepository; private final LoanRateRegisterSlabHistoryRepository slabRepository; private final LoanRateRegisterCibilSlabRepository cibilRepository; + private final LoanRateRegisterProductRepository productRepository; + + @Override + public List retrieveProducts() { + final List products = this.productRepository.findByIsActiveTrueOrderBySrNoAsc(); + final List out = new ArrayList<>(products.size()); + for (final LoanRateRegisterProduct p : products) { + out.add(LoanRateRegisterProductData.builder() + .id(p.getId()) + .srNo(p.getSrNo()) + .name(p.getName()) + .glAccountId(p.getGlAccountId()) + .isActive(p.getIsActive()) + .build()); + } + return out; + } @Override public List retrieveAll() { @@ -65,6 +85,7 @@ public class LoanRateRegisterReadPlatformServiceImpl implements LoanRateRegister for (final LoanRateRegisterSlabHistory c : cells) { slabData.add(LoanRateRegisterSlabData.builder() .id(c.getId()) + .productId(c.getProductId()) .productSrNo(c.getProductSrNo()) .productName(c.getProductName()) .slabOrder(c.getSlabOrder()) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterWritePlatformServiceImpl.java index dbf58c0e0..0c09e7df9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanRateRegisterWritePlatformServiceImpl.java @@ -10,21 +10,32 @@ package org.apache.fineract.portfolio.loanproduct.service; import com.google.gson.Gson; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest; import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest.CibilSlabRequest; +import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest.ProductRequest; import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest.SlabRequest; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterCibilSlab; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterCibilSlabRepository; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterEffective; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterEffectiveRepository; +import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterProduct; +import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterProductRepository; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterSlabHistory; import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterSlabHistoryRepository; import org.apache.fineract.portfolio.loanproduct.serialization.LoanRateRegisterDataValidator; @@ -45,6 +56,8 @@ public class LoanRateRegisterWritePlatformServiceImpl implements LoanRateRegiste private final LoanRateRegisterEffectiveRepository effectiveRepository; private final LoanRateRegisterSlabHistoryRepository slabRepository; private final LoanRateRegisterCibilSlabRepository cibilRepository; + private final LoanRateRegisterProductRepository productRepository; + private final GLAccountRepositoryWrapper glAccountRepositoryWrapper; private final Gson gson = new Gson(); @Override @@ -56,6 +69,10 @@ public class LoanRateRegisterWritePlatformServiceImpl implements LoanRateRegiste final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()); final LocalDate effectiveFrom = parseDateOrToday(req.getEffectiveFromDate(), today); + // 0) Upsert the particulars master first, so a missing GL on a new product fails the whole + // save before any version row is touched. Returns srNo -> master row for the cells below. + final Map productsBySrNo = upsertProducts(req); + // 1) Deactivate the currently-active version (if any). final List active = this.effectiveRepository.findByIsActiveTrueOrderByEffectiveFromDateDesc(); for (final LoanRateRegisterEffective prior : active) { @@ -65,17 +82,20 @@ public class LoanRateRegisterWritePlatformServiceImpl implements LoanRateRegiste this.effectiveRepository.saveAll(active); } - // 2) Insert the new effective row. + // 2) Insert the new effective row. saveAndFlush ensures Hibernate executes the INSERT + // immediately so the auto-generated id is populated before we use it as the FK on the + // child slab/CIBIL rows (same pattern as FD side's FdBaseInterestRateReadPlatformServiceImpl). final LoanRateRegisterEffective fresh = LoanRateRegisterEffective.create(req.getName(), effectiveFrom, req.getCurrencyCode(), req.getFallbackNoCibilSlab(), req.getFallbackBelowMin(), req.getNote(), createdById); - final LoanRateRegisterEffective saved = this.effectiveRepository.save(fresh); + final LoanRateRegisterEffective saved = this.effectiveRepository.saveAndFlush(fresh); final Long versionId = saved.getId(); // 3) Insert all chart cells for this version. final List cells = new ArrayList<>(req.getSlabs().size()); for (final SlabRequest s : req.getSlabs()) { - cells.add(LoanRateRegisterSlabHistory.create(versionId, s.getProductSrNo(), s.getProductName(), s.getSlabOrder(), - s.getSlabLabel(), s.getAmountUpto(), s.getRateValue(), s.getRateFormula(), + final Long productId = productsBySrNo.get(s.getProductSrNo()).getId(); + cells.add(LoanRateRegisterSlabHistory.create(versionId, productId, s.getProductSrNo(), s.getProductName(), + s.getSlabOrder(), s.getSlabLabel(), s.getAmountUpto(), s.getRateValue(), s.getRateFormula(), Boolean.TRUE.equals(s.getIsFormula()), Boolean.TRUE.equals(s.getIsNa()))); } this.slabRepository.saveAll(cells); @@ -94,6 +114,83 @@ public class LoanRateRegisterWritePlatformServiceImpl implements LoanRateRegiste .build(); } + /** + * Makes sure every particular used by the submitted chart exists in the master. A particular not + * yet in the master is created and must carry a valid GL account; for an existing one a changed + * GL or name is applied. A particular that still has no GL after this (seeded rows the bank has + * not mapped yet) rejects the whole save. + */ + private Map upsertProducts(final LoanRateRegisterSaveRequest req) { + final Map bySrNo = new HashMap<>(); + for (final LoanRateRegisterProduct p : this.productRepository.findAll()) { + bySrNo.put(p.getSrNo(), p); + } + final Map requested = new HashMap<>(); + if (req.getProducts() != null) { + for (final ProductRequest pr : req.getProducts()) { + requested.put(pr.getSrNo(), pr); + } + } + // Every srNo the chart uses, with a label taken from the first cell that mentions it. + final Set used = new LinkedHashSet<>(); + final Map cellLabel = new HashMap<>(); + for (final SlabRequest s : req.getSlabs()) { + used.add(s.getProductSrNo()); + cellLabel.putIfAbsent(s.getProductSrNo(), s.getProductName()); + } + + final List errors = new ArrayList<>(); + final List toSave = new ArrayList<>(); + for (final Integer srNo : used) { + final ProductRequest pr = requested.get(srNo); + LoanRateRegisterProduct existing = bySrNo.get(srNo); + final String name = pr != null && pr.getName() != null && !pr.getName().isBlank() ? pr.getName() : cellLabel.get(srNo); + if (existing == null) { + if (pr == null || pr.getGlAccountId() == null) { + errors.add(ApiParameterError.parameterError("validation.loanRateRegister.product.gl.required", + "New product '" + name + "' (Sr. " + srNo + ") needs a GL account", "products[" + srNo + "].glAccountId")); + continue; + } + this.glAccountRepositoryWrapper.findOneWithNotFoundDetection(pr.getGlAccountId()); + existing = new LoanRateRegisterProduct(); + existing.setSrNo(srNo); + existing.setName(name); + existing.setGlAccountId(pr.getGlAccountId()); + existing.setIsActive(true); + existing.setCreatedDate(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant())); + toSave.add(existing); + } else if (pr != null) { + boolean changed = false; + if (pr.getGlAccountId() != null && !pr.getGlAccountId().equals(existing.getGlAccountId())) { + this.glAccountRepositoryWrapper.findOneWithNotFoundDetection(pr.getGlAccountId()); + existing.setGlAccountId(pr.getGlAccountId()); + changed = true; + } + if (name != null && !name.equals(existing.getName())) { + existing.setName(name); + changed = true; + } + if (changed) { + toSave.add(existing); + } + } + // Every particular in a submitted chart must end up with a GL, including the seeded ones. + if (existing.getGlAccountId() == null) { + errors.add(ApiParameterError.parameterError("validation.loanRateRegister.product.gl.required", + "Product '" + existing.getName() + "' (Sr. " + srNo + ") needs a GL account", "products[" + srNo + "].glAccountId")); + continue; + } + bySrNo.put(srNo, existing); + } + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.loanRateRegister.failed", "Validation errors exist.", errors); + } + for (final LoanRateRegisterProduct p : this.productRepository.saveAllAndFlush(toSave)) { + bySrNo.put(p.getSrNo(), p); + } + return bySrNo; + } + private static LocalDate parseDateOrToday(final String iso, final LocalDate today) { if (iso == null || iso.isBlank()) { return today; diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 1deae024a..5d468f7d2 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -619,6 +619,10 @@ + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0581_enable_loan_rate_register_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0581_enable_loan_rate_register_maker_checker.xml index 6798d45a9..dac63301d 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0581_enable_loan_rate_register_maker_checker.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0581_enable_loan_rate_register_maker_checker.xml @@ -28,29 +28,63 @@ Enable maker-checker on the Loan Interest Rate Register. Mirrors 0470 for the FD side. Two permissions: the maker's gated with can_maker_checker=true so submits land in m_portfolio_command_source as PENDING; the checker approves from the standard Checker Inbox - via /makercheckers/{id}?command=approve, which fires CreateCbsLoanBaseInterestRateCommandHandler. + via /makercheckers/{id}?command=approve, which fires CreateLoanBaseInterestRateCommandHandler. + + Permission codes use the LOANBASEINTERESTRATE entity (no "CBS" prefix) to match the clean + naming convention requested for the Loan module. --> ANY - INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) - SELECT 'portfolio', 'CREATE_CBSLOANBASEINTERESTRATE', 'CBSLOANBASEINTERESTRATE', 'CREATE', true - WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CBSLOANBASEINTERESTRATE'); + -- Rename any previously-seeded CBS-prefixed rows (safe no-op if a fresh DB). + UPDATE m_permission + SET code = 'CREATE_LOANBASEINTERESTRATE', entity_name = 'LOANBASEINTERESTRATE' + WHERE code = 'CREATE_CBSLOANBASEINTERESTRATE'; + + UPDATE m_permission + SET code = 'CREATE_LOANBASEINTERESTRATE_CHECKER', entity_name = 'LOANBASEINTERESTRATE' + WHERE code = 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER'; INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) - SELECT 'portfolio', 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER', 'CBSLOANBASEINTERESTRATE', 'CREATE_CHECKER', false - WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER'); + SELECT 'portfolio', 'CREATE_LOANBASEINTERESTRATE', 'LOANBASEINTERESTRATE', 'CREATE', true + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_LOANBASEINTERESTRATE'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'CREATE_LOANBASEINTERESTRATE_CHECKER', 'LOANBASEINTERESTRATE', 'CREATE_CHECKER', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_LOANBASEINTERESTRATE_CHECKER'); UPDATE m_permission SET can_maker_checker = TRUE - WHERE entity_name = 'CBSLOANBASEINTERESTRATE' + WHERE entity_name = 'LOANBASEINTERESTRATE' AND action_name = 'CREATE'; UPDATE m_permission SET action_name = 'CREATE_CHECKER' - WHERE entity_name = 'CBSLOANBASEINTERESTRATE' - AND code = 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER'; + WHERE entity_name = 'LOANBASEINTERESTRATE' + AND code = 'CREATE_LOANBASEINTERESTRATE_CHECKER'; + + + + + + ANY + + UPDATE m_permission + SET code = 'CREATE_LOANBASEINTERESTRATE', entity_name = 'LOANBASEINTERESTRATE' + WHERE code = 'CREATE_CBSLOANBASEINTERESTRATE'; + + UPDATE m_permission + SET code = 'CREATE_LOANBASEINTERESTRATE_CHECKER', entity_name = 'LOANBASEINTERESTRATE' + WHERE code = 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER'; + + -- Rename any pending command-source rows still carrying the old entity string so + -- the handler picks them up after the restart. + UPDATE m_portfolio_command_source + SET entity_name = 'LOANBASEINTERESTRATE' + WHERE entity_name = 'CBSLOANBASEINTERESTRATE'; @@ -63,7 +97,7 @@ FROM m_role r CROSS JOIN m_permission p WHERE r.name = 'Super user' - AND p.code IN ('CREATE_CBSLOANBASEINTERESTRATE', 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER') + AND p.code IN ('CREATE_LOANBASEINTERESTRATE', 'CREATE_LOANBASEINTERESTRATE_CHECKER') AND NOT EXISTS ( SELECT 1 FROM m_role_permission rp WHERE rp.role_id = r.id AND rp.permission_id = p.id @@ -76,7 +110,7 @@ JOIN m_appuser_role ur ON ur.appuser_id = u.id CROSS JOIN m_permission p WHERE u.username = 'bankadmin' - AND p.code IN ('CREATE_CBSLOANBASEINTERESTRATE', 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER') + AND p.code IN ('CREATE_LOANBASEINTERESTRATE', 'CREATE_LOANBASEINTERESTRATE_CHECKER') AND NOT EXISTS ( SELECT 1 FROM m_role_permission rp WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id @@ -88,7 +122,7 @@ FROM m_role r CROSS JOIN m_permission p WHERE LOWER(r.name) = 'clerk' - AND p.code = 'CREATE_CBSLOANBASEINTERESTRATE' + AND p.code = 'CREATE_LOANBASEINTERESTRATE' AND NOT EXISTS ( SELECT 1 FROM m_role_permission rp WHERE rp.role_id = r.id AND rp.permission_id = p.id diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0594_loan_rate_register_product_master.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0594_loan_rate_register_product_master.xml new file mode 100644 index 000000000..c764e781f --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0594_loan_rate_register_product_master.xml @@ -0,0 +1,199 @@ + + + + + + + + ANY + + + + + + + + + + + + + + + + + + + + + + + + + ANY + + + + + + + + + + + + + + ANY + + UPDATE m_loan_rate_register_slab_history + SET product_id = (SELECT p.id FROM m_loan_rate_register_product p + WHERE p.sr_no = m_loan_rate_register_slab_history.product_sr_no) + WHERE product_id IS NULL; + + + + + ANY + + SELECT COUNT(*) FROM m_loan_rate_register_product + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0595_seed_loan_rate_register_gl_accounts.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0595_seed_loan_rate_register_gl_accounts.xml new file mode 100644 index 000000000..f56f20106 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0595_seed_loan_rate_register_gl_accounts.xml @@ -0,0 +1,95 @@ + + + + + + + ANY + + -- SHORT TERM LOAN (100300) + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'OVERDRAFT', '100304', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100300' AND p.name = 'SHORT TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100304'); + + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'GOLD LOAN', '100305', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100300' AND p.name = 'SHORT TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100305'); + + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'AGRICULTURE LOAN', '100306', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100300' AND p.name = 'SHORT TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100306'); + + -- MEDIUM TERM LOAN (100400) + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'TWO WHEELER LOAN', '100403', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100400' AND p.name = 'MEDIUM TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100403'); + + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'CAR LOAN (NEW)', '100404', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100400' AND p.name = 'MEDIUM TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100404'); + + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'USED CAR / VEHICLE LOAN', '100405', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100400' AND p.name = 'MEDIUM TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100405'); + + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'COMMERCIAL VEHICLE LOAN', '100406', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100400' AND p.name = 'MEDIUM TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100406'); + + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'BUSINESS TERM LOAN', '100407', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100400' AND p.name = 'MEDIUM TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100407'); + + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'MACHINERY & EQUIPMENT LOAN', '100408', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100400' AND p.name = 'MEDIUM TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100408'); + + -- LONG TERM LOAN (100500) + INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) + SELECT 'HOME REPAIR / RENOVATION LOAN', '100502', false, true, 1, 1, p.id FROM acc_gl_account p + WHERE p.gl_code = '100500' AND p.name = 'LONG TERM LOAN' + AND NOT EXISTS (SELECT 1 FROM acc_gl_account WHERE gl_code = '100502'); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0596_enable_delinquency_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0596_enable_delinquency_maker_checker.xml new file mode 100644 index 000000000..72f8d56b9 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0596_enable_delinquency_maker_checker.xml @@ -0,0 +1,111 @@ + + + + + + + + ANY + + UPDATE m_permission + SET can_maker_checker = TRUE + WHERE code IN ('CREATE_DELINQUENCY_RANGE', 'UPDATE_DELINQUENCY_RANGE', 'DELETE_DELINQUENCY_RANGE', + 'CREATE_DELINQUENCY_BUCKET', 'UPDATE_DELINQUENCY_BUCKET', 'DELETE_DELINQUENCY_BUCKET'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'organisation', v.code, v.entity_name, v.action_name, false + FROM ( + SELECT 'CREATE_DELINQUENCY_RANGE_CHECKER' AS code, 'DELINQUENCY_RANGE' AS entity_name, 'CREATE_CHECKER' AS action_name + UNION ALL SELECT 'UPDATE_DELINQUENCY_RANGE_CHECKER', 'DELINQUENCY_RANGE', 'UPDATE_CHECKER' + UNION ALL SELECT 'DELETE_DELINQUENCY_RANGE_CHECKER', 'DELINQUENCY_RANGE', 'DELETE_CHECKER' + UNION ALL SELECT 'CREATE_DELINQUENCY_BUCKET_CHECKER', 'DELINQUENCY_BUCKET', 'CREATE_CHECKER' + UNION ALL SELECT 'UPDATE_DELINQUENCY_BUCKET_CHECKER', 'DELINQUENCY_BUCKET', 'UPDATE_CHECKER' + UNION ALL SELECT 'DELETE_DELINQUENCY_BUCKET_CHECKER', 'DELINQUENCY_BUCKET', 'DELETE_CHECKER' + ) v + WHERE NOT EXISTS (SELECT 1 FROM m_permission p WHERE p.code = v.code); + + + + + ANY + + -- Super user: maker + checker for all six commands. + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE r.name = 'Super user' + AND p.entity_name IN ('DELINQUENCY_RANGE', 'DELINQUENCY_BUCKET') + AND p.action_name IN ('CREATE', 'UPDATE', 'DELETE', 'CREATE_CHECKER', 'UPDATE_CHECKER', 'DELETE_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + -- bankadmin user's role(s): maker + checker for all six commands. + INSERT INTO m_role_permission (role_id, permission_id) + SELECT DISTINCT ur.role_id, p.id + FROM m_appuser u + JOIN m_appuser_role ur ON ur.appuser_id = u.id + CROSS JOIN m_permission p + WHERE u.username = 'bankadmin' + AND p.entity_name IN ('DELINQUENCY_RANGE', 'DELINQUENCY_BUCKET') + AND p.action_name IN ('CREATE', 'UPDATE', 'DELETE', 'CREATE_CHECKER', 'UPDATE_CHECKER', 'DELETE_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id + ); + + -- Every other role loses UPDATE / DELETE on ranges and buckets (and holds no checker permission). + DELETE FROM m_role_permission + WHERE permission_id IN ( + SELECT id FROM m_permission + WHERE entity_name IN ('DELINQUENCY_RANGE', 'DELINQUENCY_BUCKET') + AND action_name IN ('UPDATE', 'DELETE', 'CREATE_CHECKER', 'UPDATE_CHECKER', 'DELETE_CHECKER') + ) + AND role_id NOT IN ( + SELECT id FROM m_role WHERE name = 'Super user' + UNION + SELECT ur.role_id + FROM m_appuser u + JOIN m_appuser_role ur ON ur.appuser_id = u.id + WHERE u.username = 'bankadmin' + ); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_add_loan_product_loans_ext_fields.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_add_loan_product_loans_ext_fields.xml new file mode 100644 index 000000000..a05a790f6 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_add_loan_product_loans_ext_fields.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +