diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java index 5f5e99096..daf7c1383 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java @@ -162,6 +162,15 @@ public class ClientApiConstants { public static final String ageDaysParamName = "ageDays"; public static final String nationalityIdParamName = "nationalityId"; public static final String isMinorParamName = "isMinor"; + /** + * Below this age a client is treated as a minor: guardian mapping required and a restricted KYC/OVD + * document set. Must stay in step with MINOR_AGE_THRESHOLD in the web app's client-age util -- the + * KYC template endpoint is authoritative for the document list, so a mismatch would serve an adult + * list (with mandatory PAN) for a client the UI classifies as a minor. + */ + public static final int MINOR_AGE_THRESHOLD = 10; + /** At or above this age a client qualifies for senior-citizen deposit rates. */ + public static final int SENIOR_CITIZEN_AGE_THRESHOLD = 60; public static final String guardianClientIdParamName = "guardianClientId"; public static final String guardianClientDisplayNameParamName = "guardianClientDisplayName"; public static final String guardianRelationParamName = "guardianRelation"; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java index f3e7eba4f..240914b13 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java @@ -589,6 +589,10 @@ public final class DepositsApiConstants { fixedDepositResponseData.add(SavingsApiConstants.jointHoldersParamName); fixedDepositResponseData.add(SavingsApiConstants.nominationFacilityParamName); fixedDepositResponseData.add(SavingsApiConstants.autoDebitFacilityParamName); + // FD Special's engine-computed monthly interest schedule (see FixedDepositAccountsApiResource#retrieveOne) + fixedDepositResponseData.add("projectedInterestSchedule"); + fixedDepositResponseData.add("totalProjectedInterest"); + fixedDepositResponseData.add("totalValueAtMaturity"); return fixedDepositResponseData; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientKycApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientKycApiResource.java index ebc4e49a3..9391a8aac 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientKycApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientKycApiResource.java @@ -160,10 +160,13 @@ public class ClientKycApiResource { template.put("clientTypeName", clientTypeName); template.put("legalFormId", legalFormId); - // Minor detection mirrors the frontend's own rule (age < 18) so the document list - // served here stays consistent with what the UI computes from the same date of birth. + // Minor detection mirrors the frontend's own rule so the document list served here stays + // consistent with what the UI computes from the same date of birth. Both sides read the + // threshold from a single named constant -- when they disagree, this endpoint wins and would + // silently serve an adult document list (mandatory PAN) to a minor. final LocalDate dateOfBirth = client.dateOfBirth(); - final boolean isMinor = dateOfBirth != null && Period.between(dateOfBirth, LocalDate.now()).getYears() < 18; + final boolean isMinor = dateOfBirth != null + && Period.between(dateOfBirth, LocalDate.now()).getYears() < ClientApiConstants.MINOR_AGE_THRESHOLD; template.put("isMinor", isMinor); template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId, isMinor)); 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 8d6e0c241..d0b4aa3f5 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 @@ -81,6 +81,7 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService; import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService; +import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService; import org.apache.fineract.portfolio.savings.service.FixedDepositAccountInterestCalculationService; import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; @@ -107,6 +108,7 @@ public class FixedDepositAccountsApiResource { private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService; private final FixedDepositAccountInterestCalculationService fixedDepositAccountInterestCalculationService; private final SqlValidator sqlValidator; + private final DepositAccountWritePlatformService depositAccountWritePlatformService; @GET @Path("template") @@ -245,6 +247,28 @@ public class FixedDepositAccountsApiResource { final FixedDepositAccountData accountTemplate = populateTemplateAndAssociations(accountId, account, staffInSelectedOfficeOnly, chargeStatus, uriInfo, mandatoryResponseParameters); accountTemplate.setActivationCharge(getActivationCharge(accountId)); + // FD Special customers/staff need to see exactly how much interest is projected each month across + // the whole term, not just a flat rate/12 approximation -- computed on demand from the real + // interest engine (FixedDepositAccount#projectInterestSchedule), side-effect-free, so this GET can + // safely recompute it on every request. SINGLE/DOUBLE FDs never get this field. + if ("SPECIAL".equalsIgnoreCase(account.getFdType())) { + final java.util.List> schedule = this.depositAccountWritePlatformService + .retrieveProjectedInterestSchedule(accountId, DepositAccountType.FIXED_DEPOSIT); + accountTemplate.setProjectedInterestSchedule(schedule); + // Computed once here -- the single source of truth for "what does the customer end up with + // overall" -- rather than the frontend re-summing the schedule array itself, the same way + // maturityAmount itself is always a backend-computed field, never a client-side derivation. + BigDecimal totalInterest = BigDecimal.ZERO; + for (final java.util.Map period : schedule) { + final Object amount = period.get("interestAmount"); + if (amount instanceof BigDecimal) { + totalInterest = totalInterest.add((BigDecimal) amount); + } + } + final BigDecimal principal = accountTemplate.getDepositAmount(); + final BigDecimal totalValue = principal != null ? principal.add(totalInterest) : totalInterest; + accountTemplate.setProjectedInterestTotals(totalInterest, totalValue); + } final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), mandatoryResponseParameters); return this.toApiJsonSerializer.serialize(settings, accountTemplate, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java index 7fa657aa5..a0deb4eab 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java @@ -32,6 +32,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.expecte import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isCalendarInheritedParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isMandatoryDepositParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isSeniorCitizenParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.linkedAccountParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.seniorCitizenInterestRateParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.mandatoryRecommendedDepositAmountParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maturityInstructionIdParamName; @@ -68,6 +69,7 @@ import java.util.Locale; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; @@ -248,6 +250,14 @@ public class DepositAccountAssembler { } } + // FD Special (fdType=SPECIAL) is a non-cumulative, monthly-interest-payout product: interest must + // post every month and leave the account for its linked savings account (see + // assembleAccountTermAndPreClosure below and the maturity-instruction override there), regardless + // of whatever compounding/posting period the product or the request otherwise specifies. SINGLE and + // DOUBLE FD types are untouched by this flag. + final boolean isSpecialFixedDeposit = depositAccountType.isFixedDeposit() && product instanceof FixedDepositProduct + && "SPECIAL".equalsIgnoreCase(((FixedDepositProduct) product).getFdType()); + SavingsCompoundingInterestPeriodType interestCompoundingPeriodType = null; final Integer interestPeriodTypeValue = command.integerValueOfParameterNamed(interestCompoundingPeriodTypeParamName); if (interestPeriodTypeValue != null) { @@ -255,6 +265,13 @@ public class DepositAccountAssembler { } else { interestCompoundingPeriodType = product.interestCompoundingPeriodType(); } + if (isSpecialFixedDeposit) { + // Keep compounding no coarser than the forced Monthly posting below -- compounded interest gets + // reset to zero every period anyway once transferInterestToLinkedAccount is true (see + // CompoundInterestHelper), so this is only about keeping the interest engine's own + // posting/compounding-period expectations internally consistent, not a behaviour choice. + interestCompoundingPeriodType = SavingsCompoundingInterestPeriodType.MONTHLY; + } SavingsPostingInterestPeriodType interestPostingPeriodType = null; final Integer interestPostingPeriodTypeValue = command.integerValueOfParameterNamed(interestPostingPeriodTypeParamName); @@ -263,6 +280,9 @@ public class DepositAccountAssembler { } else { interestPostingPeriodType = product.interestPostingPeriodType(); } + if (isSpecialFixedDeposit) { + interestPostingPeriodType = SavingsPostingInterestPeriodType.MONTHLY; + } SavingsInterestCalculationType interestCalculationType = null; final Integer interestCalculationTypeValue = command.integerValueOfParameterNamed(interestCalculationTypeParamName); @@ -366,7 +386,7 @@ public class DepositAccountAssembler { if (depositAccountType.isFixedDeposit()) { final DepositProductTermAndPreClosure prodTermAndPreClosure = ((FixedDepositProduct) product).depositProductTermAndPreClosure(); final DepositAccountTermAndPreClosure accountTermAndPreClosure = this.assembleAccountTermAndPreClosure(command, - prodTermAndPreClosure); + prodTermAndPreClosure, isSpecialFixedDeposit); FixedDepositAccount fdAccount = FixedDepositAccount.createNewApplicationForSubmittal(client, group, product, fieldOfficer, accountNo, externalIdFactory.create(externalId), accountType, submittedOnDate, submittedBy, interestRate, @@ -380,7 +400,7 @@ public class DepositAccountAssembler { final DepositProductTermAndPreClosure prodTermAndPreClosure = ((RecurringDepositProduct) product) .depositProductTermAndPreClosure(); final DepositAccountTermAndPreClosure accountTermAndPreClosure = this.assembleAccountTermAndPreClosure(command, - prodTermAndPreClosure); + prodTermAndPreClosure, false); final DepositProductRecurringDetail prodRecurringDetail = ((RecurringDepositProduct) product).depositRecurringDetail(); final DepositAccountRecurringDetail accountRecurringDetail = this.assembleAccountRecurringDetail(command, @@ -420,7 +440,7 @@ public class DepositAccountAssembler { } public DepositAccountTermAndPreClosure assembleAccountTermAndPreClosure(final JsonCommand command, - final DepositProductTermAndPreClosure productTermAndPreclosure) { + final DepositProductTermAndPreClosure productTermAndPreclosure, final boolean forceMonthlyPayoutToLinkedSavings) { final DepositPreClosureDetail productPreClosure = (productTermAndPreclosure == null) ? null : productTermAndPreclosure.depositPreClosureDetail(); final DepositTermDetail productTerm = (productTermAndPreclosure == null) ? null : productTermAndPreclosure.depositTermDetail(); @@ -435,17 +455,34 @@ public class DepositAccountAssembler { final SavingsPeriodFrequencyType depositPeriodFrequency = SavingsPeriodFrequencyType.fromInt(depositPeriodFrequencyId); final SavingsAccount account = null; final LocalDate expectedFirstDepositOnDate = command.localDateValueOfParameterNamed(expectedFirstDepositOnDateParamName); - final Boolean trasferInterest = command.booleanPrimitiveValueOfParameterNamed(transferInterestToSavingsParamName); + Boolean trasferInterest = command.booleanPrimitiveValueOfParameterNamed(transferInterestToSavingsParamName); // calculate maturity amount final BigDecimal maturityAmount = null;// calculated and updated in // account final LocalDate maturityDate = null;// calculated and updated in account final Integer accountOnClosureTypeId = command.integerValueOfParameterNamed(maturityInstructionIdParamName); - final DepositAccountOnClosureType accountOnClosureType = accountOnClosureTypeId != null + DepositAccountOnClosureType accountOnClosureType = accountOnClosureTypeId != null ? DepositAccountOnClosureType.fromInt(accountOnClosureTypeId) : null; - final Long transferToSavingsId = command.longValueOfParameterNamed(transferToSavingsIdParamName); + Long transferToSavingsId = command.longValueOfParameterNamed(transferToSavingsIdParamName); + + if (forceMonthlyPayoutToLinkedSavings) { + final Long linkedSavingsAccountId = command.longValueOfParameterNamed(linkedAccountParamName); + if (linkedSavingsAccountId == null) { + throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.special.requires.linked.savings.account", + "FD Special (Monthly Interest Payout) requires a linked savings account to be selected."); + } + // Non-cumulative payout FD: every month's interest leaves for the linked savings account + // (TransferInterestToSavingsTasklet) instead of compounding, and maturity re-uses the existing + // "Transfer to Savings" maturity-instruction path (see + // DepositAccountWritePlatformServiceJpaRepositoryImpl#updateMaturityDetails) to move the + // principal out and close the account automatically, with no staff action needed. + trasferInterest = true; + transferToSavingsId = linkedSavingsAccountId; + accountOnClosureType = DepositAccountOnClosureType.TRANSFER_TO_SAVINGS; + } + return DepositAccountTermAndPreClosure.createNew(updatedProductPreClosure, updatedProductTerm, account, depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, expectedFirstDepositOnDate, accountOnClosureType, trasferInterest, transferToSavingsId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index dd60c181e..2ce19027e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -107,6 +107,7 @@ import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrap import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; +import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException; import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException; @@ -561,6 +562,72 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo .build(); } + /** + * FD Special (monthly interest payout) shows customers/staff exactly how much interest is projected to + * post each month across the account's whole term, rather than a flat rate/12 approximation -- computed + * with the real PostingPeriod engine (same one the actual maturity calculation uses) via + * FixedDepositAccount#projectInterestSchedule, which is deliberately side-effect-free. readOnly=true + * overrides this class's default @Transactional so nothing from that computation is ever flushed. + */ + @Transactional(readOnly = true) + @Override + public List> retrieveProjectedInterestSchedule(final Long savingsId, final DepositAccountType depositAccountType) { + final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); + if (!(account instanceof FixedDepositAccount)) { + return java.util.Collections.emptyList(); + } + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + + final FixedDepositAccount fdAccount = (FixedDepositAccount) account; + final List postingPeriods = fdAccount.projectInterestSchedule(mc, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + + // determineInterestPostingPeriods always rounds the LAST period out to the full natural boundary + // (e.g. calendar month-end for Monthly posting) even when the account's own maturity date falls + // partway through it -- the INTEREST AMOUNT for that period is still correctly computed only up to + // the real cutoff (interestCalculationUpto = maturityDate - 1, the same day postMaturityInterest + // clamps its own posting date to), but the period's displayed end-date is not, and would otherwise + // show a "To Date" past the account's actual maturity date. Clip it here so the schedule shown + // never claims interest for days beyond maturity, matching the money that is actually calculated. + final LocalDate lastCalculableDate = fdAccount.maturityDate() != null ? fdAccount.maturityDate().minusDays(1) : null; + + // A period is "paid" once a real (non-reversed) interest-posting transaction actually landed for it. + // Matched EXACTLY against that period's own end date (Fineract can date the posting transaction + // either on the period's own last day, or on the first day of the following period, depending on + // the isSavingsInterestPostingAtCurrentPeriodEnd configuration) -- not a date RANGE, which would + // wrongly also match the very next period (whose own start date is that same "day after"). + final List postedInterestDates = new ArrayList<>(); + for (final SavingsAccountTransaction transaction : fdAccount.getTransactions()) { + if (transaction.isInterestPostingAndNotReversed()) { + postedInterestDates.add(transaction.getTransactionDate()); + } + } + + final List> schedule = new ArrayList<>(); + 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 LocalDate toDateFinal = toDate; + final boolean paid = postedInterestDates.stream() + .anyMatch(postedDate -> postedDate.isEqual(toDateFinal) || postedDate.isEqual(toDateFinal.plusDays(1))); + + final Map row = new HashMap<>(); + row.put("fromDate", fromDate); + row.put("toDate", toDateFinal); + row.put("interestAmount", period.getInterestEarned().getAmount()); + row.put("paid", paid); + schedule.add(row); + } + return schedule; + } + @Transactional @Override public CommandProcessingResult postInterest(final Long savingsId, final DepositAccountType depositAccountType) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index 7081fcdb7..7b0fcd06d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -80,6 +80,7 @@ import org.apache.fineract.portfolio.group.exception.GroupNotFoundException; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; import org.apache.fineract.portfolio.savings.DepositsApiConstants; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.data.DepositAccountDataValidator; @@ -429,11 +430,18 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl final Long savingsAccountId = command.longValueOfParameterNamed(DepositsApiConstants.linkedAccountParamName); AccountAssociations accountAssociations = this.accountAssociationsRepository.findBySavingsIdAndType(accountId, AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue()); + // Tracks whichever savings account ends up linked once this whole block finishes, across every + // branch below (kept as-is, removed, or replaced) -- used afterwards to re-enforce FD Special's + // "must always have a linked account" requirement regardless of what this request touched. + Long resolvedLinkedAccountId = accountAssociations != null && accountAssociations.linkedSavingsAccount() != null + ? accountAssociations.linkedSavingsAccount().getId() + : null; if (savingsAccountId == null) { if (accountAssociations != null) { if (this.fromJsonHelper.parameterExists(DepositsApiConstants.linkedAccountParamName, command.parsedJson())) { this.accountAssociationsRepository.delete(accountAssociations); changes.put(DepositsApiConstants.linkedAccountParamName, null); + resolvedLinkedAccountId = null; if (isLinkedAccRequired) { this.depositAccountDataValidator.throwLinkedAccountRequiredError(); } @@ -465,6 +473,23 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl changes.put(DepositsApiConstants.linkedAccountParamName, savingsAccountId); this.accountAssociationsRepository.save(accountAssociations); } + resolvedLinkedAccountId = savingsAccountId; + } + + // FD Special is a non-cumulative, monthly-interest-payout product: DepositAccountTermAndPreClosure#update + // above has no fdType awareness and will happily unset transferInterestToLinkedAccount, + // onAccountClosureType and transferToSavingsAccountId on this same request -- including by mere + // omission of maturityInstructionId/transferToSavingsId from the payload -- silently turning a SPECIAL + // FD into a plain cumulative one while it still displays as "FD Special". Re-force the three fields + // here, now that the linked-account resolution above has settled what account (if any) is linked. + if ("SPECIAL".equalsIgnoreCase(account.fdType())) { + if (resolvedLinkedAccountId == null) { + this.depositAccountDataValidator.throwLinkedAccountRequiredError(); + } + account.forceSpecialFdMonthlyPayoutSettings(resolvedLinkedAccountId); + changes.put(transferInterestToSavingsParamName, true); + changes.put(DepositsApiConstants.maturityInstructionIdParamName, DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue()); + this.savingAccountRepository.save(account); } // Replace nominees if provided diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java index c9fd58529..edb6c2d02 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java @@ -182,6 +182,31 @@ public final class FixedDepositAccountData extends DepositAccountData { this.renewalHistory = renewalHistory; } + /** + * FD Special only: the real, engine-computed interest schedule across the account's whole term -- one entry + * per posting period (fromDate, toDate, interestAmount) -- so staff/customers can see exactly how much + * interest is projected to post each month instead of a flat rate/12 approximation. Null for every other + * FD type (see FixedDepositAccountsApiResource#retrieveOne). + */ + private java.util.List> projectedInterestSchedule; + private BigDecimal totalProjectedInterest; + private BigDecimal totalValueAtMaturity; + + public void setProjectedInterestSchedule(final java.util.List> projectedInterestSchedule) { + this.projectedInterestSchedule = projectedInterestSchedule; + } + + /** + * Server-computed sum of projectedInterestSchedule, and principal + that sum -- the same "what does the + * customer end up with overall" total the account view shows, computed once here instead of the + * frontend re-summing the schedule array itself, so there is a single source of truth (matching how + * maturityAmount itself already works). + */ + public void setProjectedInterestTotals(final BigDecimal totalProjectedInterest, final BigDecimal totalValueAtMaturity) { + this.totalProjectedInterest = totalProjectedInterest; + this.totalValueAtMaturity = totalValueAtMaturity; + } + // CBS account-level fields (stored per-account in m_savings_account) private String accountStatus; private String modeOfOperation; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java index 9f15e9102..0490f3d09 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java @@ -340,6 +340,19 @@ public class DepositAccountTermAndPreClosure extends AbstractPersistableCustom> retrieveProjectedInterestSchedule(Long savingsId, DepositAccountType depositAccountType); + CommandProcessingResult postInterest(Long savingsId, DepositAccountType depositAccountType); CommandProcessingResult undoFDTransaction(Long savingsId, Long transactionId, boolean allowAccountTransferModification);