From 43c60223af5a2efece1f0eb4a874d0ecee7a9995 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Tue, 20 Oct 2026 16:44:47 +0530 Subject: [PATCH] feat(fixed-deposits): account-engine FD quote at opening, FD Special schedule and locked Special terms PROBLEM - The account-opening screen quoted FD maturity with a stand-alone calculator (calculate-fd-interest) that counts posting periods from the deposit date. The account engine closes them at calendar period ends (from the financial- year start month), so the quote differed from the account once created, e.g. FD DOUBLE 546,348,939 @ 6.75% for 5 years from 2026-10-21, quarterly: quoted 763,660,633.77, account 763,697,613.00. - FD Special (monthly interest payout) was quoted with compounding, and its product could be saved with Interest Calculation Type "Compounding". CHANGE 1. POST /fixeddepositaccounts/account-preview (FixedDepositAccountPreviewService) Builds the would-be account from the request exactly as submitFDApplication does (DepositAccountAssembler.assembleFrom, updateMaturityDateAndAmount- BeforeAccountActivation, applyCbsFields) inside a transaction that is always rolled back - nothing is saved. Returns the applied rate, maturity date and maturity amount Submit would store, and the interest periods from FixedDepositAccount#projectInterestSchedule (for FD Special: the monthly payout schedule, same engine as the account's Monthly Interest Payout Schedule). Every FD type. Submit's linked-account checks (linkAccountId for FD Special, linkedAccountId for Deposit Account Link) get a stand-in id (-1) when not chosen yet: both are only recorded on the account, never looked up, and the account is never saved. Submit still requires them. 2. FdSpecialProductTermsValidator: an FD Special product must have interestCalculateSelector = Simple, interestCompoundingPeriodType = Monthly and interestPostingPeriodType = Monthly. Checked server-side on create, on update (also the maker-checker approval path) and on submit-for- approval, so Postman / any client gets a 400 validation error per field. Wired through SavingsConfiguration. FILES api/FixedDepositAccountsApiResource.java (account-preview endpoint) service/FixedDepositAccountPreviewService.java (new) data/FdSpecialProductTermsValidator.java (new) service/FixedDepositProductWritePlatformServiceJpaRepositoryImpl.java starter/SavingsConfiguration.java NOT CHANGED How the account calculates interest; calculate-fd-interest still exists but the opening screen no longer uses it. DATA (not in this commit) Existing FD Special products saved as "Compounding" are refused on their next edit until set to Simple (the local tenant's FD SPECIAL was updated). VERIFIED: compileJava. Numbers above reproduced independently. Not yet run end-to-end through the API. Co-Authored-By: Claude Opus 5.5 --- .../api/FixedDepositAccountsApiResource.java | 33 ++++ .../data/FdSpecialProductTermsValidator.java | 119 ++++++++++++++ .../FixedDepositAccountPreviewService.java | 148 ++++++++++++++++++ ...WritePlatformServiceJpaRepositoryImpl.java | 7 + .../savings/starter/SavingsConfiguration.java | 5 +- 5 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/FdSpecialProductTermsValidator.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountPreviewService.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java index 830f6f5a2..1bebaca36 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java @@ -109,6 +109,7 @@ public class FixedDepositAccountsApiResource { private final FixedDepositAccountInterestCalculationService fixedDepositAccountInterestCalculationService; private final SqlValidator sqlValidator; private final DepositAccountWritePlatformService depositAccountWritePlatformService; + private final org.apache.fineract.portfolio.savings.service.FixedDepositAccountPreviewService fixedDepositAccountPreviewService; @GET @Path("template") @@ -315,6 +316,38 @@ public class FixedDepositAccountsApiResource { return toApiJsonSerializer.serializeResult(result); } + /** + * Fixed deposit quote while the account is being opened, for every FD type: takes the same body as Submit + * (clientId, productId, submittedOnDate, depositAmount, depositPeriod, depositPeriodFrequencyId, isSeniorCitizen, + * locale, dateFormat ...) and returns the rate, maturity date and maturity amount Submit would store, plus the + * interest periods (FD Special: the monthly payout schedule). Nothing is saved (see FixedDepositAccountPreviewService). + */ + @POST + @Path("account-preview") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String accountPreview(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); + final JsonElement parsed = this.fromJsonHelper.parse(apiRequestBodyAsJson); + // Submit insists on a linked savings account - "linkAccountId" (FD Special's monthly payout account) and + // "linkedAccountId" (when the product's Deposit Account Link is on) - which the screen asks for after the quote. + // The quote does not depend on which account it is, only that the interest goes out monthly, so a stand-in id is + // used for whichever is not chosen yet. Both are only recorded on the account, never looked up, and the would-be + // account is never saved, so the stand-in goes nowhere. + if (parsed.isJsonObject()) { + for (final String linkParam : new String[] { DepositsApiConstants.linkedAccountParamName, + org.apache.fineract.portfolio.savings.SavingsApiConstants.linkedAccountIdParamName }) { + if (!parsed.getAsJsonObject().has(linkParam) || parsed.getAsJsonObject().get(linkParam).isJsonNull()) { + parsed.getAsJsonObject().addProperty(linkParam, -1L); + } + } + } + final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand + .from(parsed.toString(), parsed, this.fromJsonHelper, DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME, null, + null, null, null, null, null, null, null, null, null, null, null, null); + return this.toApiJsonSerializer.serializeResult(this.fixedDepositAccountPreviewService.preview(command)); + } + private BigDecimal getActivationCharge(Long accountId) { BigDecimal activationCharge = BigDecimal.ZERO; Collection savingCharges = this.savingsAccountChargeReadPlatformService diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/FdSpecialProductTermsValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/FdSpecialProductTermsValidator.java new file mode 100644 index 000000000..947285920 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/FdSpecialProductTermsValidator.java @@ -0,0 +1,119 @@ +/** + * 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.savings.data; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; +import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; +import org.apache.fineract.portfolio.savings.domain.FixedDepositProduct; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +/** + * FD Special is a non-cumulative product: its interest is paid out to the linked savings account every month, so the + * principal never grows and nothing compounds on it - in effect simple interest on the principal. Its product terms are + * therefore fixed, and the product screen locks them: + *
    + *
  • Interest Calculation Type ({@code interestCalculateSelector}) = Simple
  • + *
  • Interest Compounding Period = Monthly
  • + *
  • Interest Posting Period = Monthly
  • + *
+ * Checked here, on the server, against the product as it will be saved, so a request from Postman or any other client + * that sends different values is refused with a validation error rather than saved. Used on create, on update (which + * the maker-checker approval also goes through) and when a maker submits an edit for approval. + */ +@Component +@RequiredArgsConstructor +public class FdSpecialProductTermsValidator { + + private static final String RESOURCE = "fixeddepositproduct"; + private static final String SIMPLE = "Simple"; + + private final JdbcTemplate jdbcTemplate; + + /** The product as assembled or updated, just before it is saved. */ + public void validate(final FixedDepositProduct product) { + validate(product.getFdType(), product.getInterestCalculateSelector(), valueOf(product.interestCompoundingPeriodType()), + valueOf(product.interestPostingPeriodType())); + } + + /** + * A maker's edit, before it waits for approval: each field as the edit leaves it - the submitted value where the + * request sends one, otherwise the product's current value. + */ + public void validateEdit(final FixedDepositProduct product, final JsonCommand command) { + final String fdType = command.parameterExists("fdType") ? command.stringValueOfParameterNamed("fdType") : product.getFdType(); + final Integer selector = command.parameterExists("interestCalculateSelector") + ? command.integerValueOfParameterNamed("interestCalculateSelector") + : product.getInterestCalculateSelector(); + final Integer compounding = command.parameterExists("interestCompoundingPeriodType") + ? command.integerValueOfParameterNamed("interestCompoundingPeriodType") + : valueOf(product.interestCompoundingPeriodType()); + final Integer posting = command.parameterExists("interestPostingPeriodType") + ? command.integerValueOfParameterNamed("interestPostingPeriodType") + : valueOf(product.interestPostingPeriodType()); + validate(fdType, selector, compounding, posting); + } + + private void validate(final String fdType, final Integer interestCalculateSelector, final Integer compoundingPeriodType, + final Integer postingPeriodType) { + if (!"SPECIAL".equalsIgnoreCase(fdType)) { + return; + } + final List errors = new ArrayList<>(); + + final String calculationType = interestCalculateSelector == null ? null + : this.jdbcTemplate.queryForList("SELECT code_value FROM m_code_value WHERE id = ?", String.class, interestCalculateSelector) + .stream().findFirst().orElse(null); + if (calculationType == null || !SIMPLE.equalsIgnoreCase(calculationType.trim())) { + errors.add(ApiParameterError.parameterError("validation.msg." + RESOURCE + ".special.interestCalculateSelector.must.be.simple", + "FD Special pays its interest out every month, so nothing compounds: Interest Calculation Type must be Simple" + + (calculationType == null ? "." : ", not " + calculationType + "."), + "interestCalculateSelector", interestCalculateSelector)); + } + if (compoundingPeriodType == null || compoundingPeriodType != SavingsCompoundingInterestPeriodType.MONTHLY.getValue()) { + errors.add(ApiParameterError.parameterError( + "validation.msg." + RESOURCE + ".special.interestCompoundingPeriodType.must.be.monthly", + "FD Special pays its interest out every month: Interest Compounding Period must be Monthly.", + "interestCompoundingPeriodType", compoundingPeriodType)); + } + if (postingPeriodType == null || postingPeriodType != SavingsPostingInterestPeriodType.MONTHLY.getValue()) { + errors.add(ApiParameterError.parameterError( + "validation.msg." + RESOURCE + ".special.interestPostingPeriodType.must.be.monthly", + "FD Special pays its interest out every month: Interest Posting Period must be Monthly.", "interestPostingPeriodType", + postingPeriodType)); + } + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors); + } + } + + private static Integer valueOf(final SavingsCompoundingInterestPeriodType type) { + return type == null ? null : type.getValue(); + } + + private static Integer valueOf(final SavingsPostingInterestPeriodType type) { + return type == null ? null : type.getValue(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountPreviewService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountPreviewService.java new file mode 100644 index 000000000..df7de9842 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountPreviewService.java @@ -0,0 +1,148 @@ +/** + * 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.savings.service; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.domain.DepositAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +/** + * Fixed deposit quote for the account-opening screen - every FD type - worked out by the account engine itself rather + * than by the screen or a separate calculator. + *

+ * The would-be account is built exactly as Submit builds it ({@code submitFDApplication}): the same assembler (product + * defaults, the product's interest rate chart, the CBS base-rate card and senior-citizen rate), the same maturity + * calculation, the same CBS fields. Nothing is saved: it all runs in a transaction that is always rolled back. So the + * rate, maturity date and maturity amount quoted are exactly what Submit would store - including the product's posting + * periods (e.g. Quarterly posting closes each period at the calendar quarter end, from the financial-year start month), + * which a stand-alone formula that counts quarters from the deposit date gets wrong. + *

+ * The interest periods come from {@link FixedDepositAccount#projectInterestSchedule}, the method the account's Monthly + * Interest Payout Schedule uses once it is open. For FD Special they are the monthly payouts; for a cumulative FD they + * are the periods interest is credited to the deposit and compounds. + *

+ * The schedule assumes the deposit starts on the submitted date; once opened, the account's own schedule runs from its + * actual activation date. + */ +@Service +@RequiredArgsConstructor +public class FixedDepositAccountPreviewService { + + private final PlatformSecurityContext context; + private final DepositAccountAssembler depositAccountAssembler; + private final SavingsAccountAssembler savingsAccountAssembler; + private final ConfigurationDomainService configurationDomainService; + private final PlatformTransactionManager transactionManager; + + public Map preview(final JsonCommand command) { + final TransactionTemplate template = new TransactionTemplate(this.transactionManager); + return template.execute(status -> { + // Never keep anything the assembler touched: this is a quote, not an application. + status.setRollbackOnly(); + return buildPreview(command); + }); + } + + private Map buildPreview(final JsonCommand command) { + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + // Same steps, in the same order, as DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl#submitFDApplication. + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(command, + this.context.authenticatedUser(), DepositAccountType.FIXED_DEPOSIT); + final boolean special = "SPECIAL".equalsIgnoreCase(account.fdType()); + account.updateMaturityDateAndAmountBeforeAccountActivation(MathContext.DECIMAL64, false, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account); + + // Same MathContext and clipping as DepositAccountWritePlatformServiceJpaRepositoryImpl#retrieveProjectedInterestSchedule. + final RoundingMode rounding = MoneyHelper.getRoundingMode(); + final List postingPeriods = account.projectInterestSchedule(new MathContext(15, rounding), + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + final LocalDate maturityDate = account.maturityDate(); + final LocalDate lastCalculableDate = maturityDate != null ? maturityDate.minusDays(1) : null; + + final List> periods = new ArrayList<>(); + BigDecimal totalInterest = BigDecimal.ZERO; + int number = 1; + for (final PostingPeriod period : postingPeriods) { + final LocalDate fromDate = period.getPeriodInterval().startDate(); + LocalDate toDate = period.getPeriodInterval().endDate(); + if (lastCalculableDate != null && toDate.isAfter(lastCalculableDate)) { + toDate = lastCalculableDate; + } + final BigDecimal interest = period.getInterestEarned().getAmount(); + totalInterest = totalInterest.add(interest); + final Map row = new LinkedHashMap<>(); + row.put("period", number++); + row.put("fromDate", fromDate.toString()); + row.put("toDate", toDate.toString()); + row.put("days", ChronoUnit.DAYS.between(fromDate, toDate) + 1); + row.put("interestAmount", interest); + periods.add(row); + } + + final BigDecimal depositAmount = account.getDepositAmount(); + final BigDecimal rate = account.getNominalAnnualInterestRate(); + final Map result = new LinkedHashMap<>(); + result.put("fdType", account.fdType()); + result.put("depositAmount", depositAmount); + result.put("annualInterestRate", rate); + result.put("seniorCitizen", account.isSeniorCitizen()); + result.put("interestPostingPeriodType", account.getInterestPostingPeriodType()); + result.put("interestCalculationDaysInYearType", account.getInterestCalculationDaysInYearType()); + result.put("startDate", periods.isEmpty() ? null : periods.get(0).get("fromDate")); + result.put("maturityDate", maturityDate == null ? null : maturityDate.toString()); + result.put("numberOfPayouts", special ? periods.size() : 0); + // FD Special: one full month at the applied rate; a period pays for the days it actually covers (see periods). + result.put("fullMonthPayout", !special || rate == null ? null + : depositAmount.multiply(rate, MoneyHelper.getMathContext()).divide(BigDecimal.valueOf(1200), MoneyHelper.getMathContext()) + .setScale(2, rounding)); + // The account's own maturity amount, as Submit stores it: for FD Special the principal alone (the interest is paid + // out month by month), otherwise principal plus the compounded interest. + final BigDecimal maturityAmount = account.maturityAmount(); + final BigDecimal interest = special ? totalInterest + : (maturityAmount == null ? null : maturityAmount.subtract(depositAmount)); + result.put("totalInterest", interest); + result.put("maturityAmount", maturityAmount); + result.put("totalValueAtMaturity", interest == null ? null : depositAmount.add(interest)); + result.put("periods", periods); + return result; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformServiceJpaRepositoryImpl.java index 053998bf9..c73789f6d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformServiceJpaRepositoryImpl.java @@ -89,6 +89,7 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; private final FromJsonHelper fromApiJsonHelper; private final CommandSourceRepository commandSourceRepository; + private final org.apache.fineract.portfolio.savings.data.FdSpecialProductTermsValidator fdSpecialProductTermsValidator; @Transactional @Override @@ -98,6 +99,8 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements this.fromApiJsonDataValidator.validateForFixedDepositCreate(command.json()); final FixedDepositProduct product = this.depositProductAssembler.assembleFixedDepositProduct(command); + // FD Special: Simple interest, monthly compounding and posting - refused here whatever client sent it. + this.fdSpecialProductTermsValidator.validate(product); this.fixedDepositProductRepository.saveAndFlush(product); @@ -136,6 +139,8 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements product.setHelpers(this.chartAssembler); final Map changes = product.update(command); + // The product as this update leaves it (also the path an approved maker-checker edit takes). + this.fdSpecialProductTermsValidator.validate(product); if (changes.containsKey(chargesParamName)) { final Set savingsProductCharges = this.depositProductAssembler.assembleListOfSavingsProductCharges(command, @@ -197,6 +202,8 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements } this.fromApiJsonDataValidator.validateForFixedDepositUpdate(command.json()); + // Refused before it waits for a checker, not only when the checker approves it. + this.fdSpecialProductTermsValidator.validateEdit(product, command); final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_FD, productId, maker); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index 5b012228b..b1f5f99a4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -366,11 +366,12 @@ public class SavingsConfiguration { ProductPendingEditChargeRepository pendingEditChargeRepository, ProductPendingEditGlMappingRepository pendingEditGlMappingRepository, FromJsonHelper fromApiJsonHelper, - CommandSourceRepository commandSourceRepository) { + CommandSourceRepository commandSourceRepository, + org.apache.fineract.portfolio.savings.data.FdSpecialProductTermsValidator fdSpecialProductTermsValidator) { return new FixedDepositProductWritePlatformServiceJpaRepositoryImpl(context, fixedDepositProductRepository, fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler, productPendingEditRepository, pendingEditSubmittedFieldRepository, pendingEditChargeRepository, - pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository); + pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository, fdSpecialProductTermsValidator); } @Bean