diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkClosureRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkClosureRequestApiResource.java index 4d3d6f201..c8f2e453d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkClosureRequestApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkClosureRequestApiResource.java @@ -37,7 +37,6 @@ import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.math.BigDecimal; -import java.math.MathContext; import java.math.RoundingMode; import java.sql.Date; import java.sql.Timestamp; @@ -53,13 +52,12 @@ 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.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.voucher.service.VoucherLine; import org.apache.fineract.portfolio.voucher.service.VoucherService; import org.apache.fineract.useradministration.domain.AppUser; @@ -67,10 +65,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionTemplate; /** * Maker-checker Cash-only savings account closure. Mirrors {@link ClerkWithdrawalRequestApiResource}'s @@ -99,10 +94,9 @@ public class ClerkClosureRequestApiResource { private final FromJsonHelper fromJsonHelper; private final PlatformSecurityContext context; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; - // Used only by preview(), to compute the closing figure the way close() will actually compute it. + // Used only by preview(), to post interest for real the same way close() eventually will. private final SavingsAccountAssembler savingsAccountAssembler; - private final ConfigurationDomainService configurationDomainService; - private final PlatformTransactionManager transactionManager; + private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; private final VoucherService voucherService; private final Gson gson = new GsonBuilder().serializeNulls().create(); @@ -175,47 +169,45 @@ public class ClerkClosureRequestApiResource { return gson.toJson(Map.of("resourceId", newId, "status", "PENDING")); } - // ── GET /v1/clerk/closure-requests/preview/{savingsAccountId} ────────────── + // ── POST /v1/clerk/closure-requests/preview/{savingsAccountId} ───────────── /** - * "What would this member actually receive" preview, so a clerk sees the real closing figure - * before submitting. + * Posts interest to today for real -- the same domain call close() makes + * ({@code postInterest(account, true, businessDate, false)}) -- and reports back the resulting + * balance, so the clerk sees the exact figure the account will close with, already credited. * - * This must reproduce what close() will pay out, to the paisa, and reading the stored - * {@code total_interest_earned_derived} does not: that column is only as fresh as whatever last - * touched the account, so it reads days stale. Nor does the CALCULATEINTEREST command, which is - * close but not identical -- it uses {@code MathContext(15)} and passes - * {@code postInterestOnDate = null}, whereas close() calls - * {@code postInterest(account, true, closedDate, false)}, which uses {@code MathContext(10)} and - * feeds {@code closedDate} in as an extra posting-period boundary - * (SavingsAccount#calculateInterestUsing adds it to postedAsOnDates). Splitting the final period - * changes how many per-period roundings happen, so the two can disagree by paise. + * This is a genuine posting, not an estimate. It has to be: a dry run reproducing close()'s own + * MathContext(10) and postInterestOnDate was tried first, but calling + * {@link org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService + * #postInterest(SavingsAccount, boolean, LocalDate, boolean)} directly -- the exact method + * close() itself calls -- makes the account's own real balance the source of truth instead, and + * removes an entire class of "did the simulation really match reality" risk. * - * So this runs the real thing as a dry run: assemble the account, invoke the same DOMAIN method - * close() ends up in with the same arguments, read the resulting balance -- which is exactly the - * figure close() withdraws (it takes {@code account.getSummary().getAccountBalance()} right - * after posting interest) -- and then roll the whole thing back. + * Called directly on the write-platform service, not through the ordinary POSTINTEREST command + * pipeline: that permission ({@code POSTINTEREST_SAVINGSACCOUNT}) ships maker-checker-enabled in + * base Fineract, and a Clerk is not a checker for it. Routed through + * {@code commandsSourceWritePlatformService} it would silently roll back and land in Fineract's + * own generic Checker Inbox -- a second, unrelated approval queue on top of this one. This + * feature already has its own maker-checker (submit here, approve in Closure Requests), so the + * interest posting rides inside that instead: real the moment the clerk ticks the box, same as + * "Post Interest As On" always was, with the actual approval gate being who signs off on the + * closure itself. * - * Why this is safe to run per keystroke: - *