diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java
index 2fa7358c0..cc489048f 100644
--- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java
+++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java
@@ -264,6 +264,13 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
savingsProductId, CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
createOrmergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(),
savingsProductId, CashAccountsForSavings.ROUNDING_ADJUSTMENT.getValue(), changes);
+ // Interest Payable is editable on cash too, and optional. Interest posting routes
+ // through it on either basis, so a product can name one to carry interest declared
+ // but not yet credited; a product that names none still posts straight to Savings
+ // Control. Created-or-merged because a product configured before this row existed
+ // has no mapping to merge into.
+ createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.INTEREST_PAYABLE.getValue(),
+ savingsProductId, AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), changes);
break;
case ACCRUAL_PERIODIC:
// asset
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
index ee5b62605..ba41bb64c 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
@@ -73,6 +73,7 @@ public enum JobName {
CTR_REPORT_GENERATION("AML Monthly CTR Report Generation"), //
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
CLIENT_BEHAVIOUR_WINDOW("Client Behaviour Window Computation"), //
+ SAVE_RBI_FORM_A_RETURN("Save RBI Form A Return"), //
; //
private final String name;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java
index 35af580f8..ef33e9074 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java
@@ -329,6 +329,18 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
break;
case CASH_BASED:
saveSavingsBaseAccountMapping(savingProductId, accountType, command, element);
+ // Interest Payable is saved on cash too, and optionally: interest posting routes
+ // through it on either basis (see AccountingProcessorHelper
+ // .createInterestPostingJournalEntriesForSavings), so that interest declared but not
+ // yet credited to the member reads as the bank's own liability rather than appearing
+ // only at the moment it is paid. Mapped only when the screen supplies it - a product
+ // naming none still posts straight to Savings Control, as it always did.
+ if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.INTEREST_PAYABLE.getValue(),
+ element)) {
+ this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element,
+ SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), savingProductId,
+ AccrualAccountsForSavings.INTEREST_PAYABLE.getValue());
+ }
break;
case ACCRUAL_PERIODIC:
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java
index cbfc2ee8e..31ac82030 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java
@@ -200,7 +200,7 @@ public class BranchDayRegisterApiResource {
final BigDecimal countedIn = zero(jdbcTemplate.queryForObject("""
SELECT COALESCE(SUM(std.total_amount), 0) FROM m_savings_txn_denomination std
WHERE std.txn_date = ? AND std.txn_type = 'DEPOSIT'
- AND """ + CountedCashLedger.REAL_TXN_ID + """
+ AND """ + CountedCashLedger.COUNTED_ROW + """
AND (std.officer_id IS NULL
OR NOT EXISTS (SELECT 1 FROM m_appuser au JOIN m_cashiers c ON c.staff_id = au.staff_id
WHERE au.id = std.officer_id)
@@ -211,7 +211,7 @@ public class BranchDayRegisterApiResource {
final BigDecimal countedOut = zero(jdbcTemplate.queryForObject("""
SELECT COALESCE(SUM(std.total_amount), 0) FROM m_savings_txn_denomination std
WHERE std.txn_date = ? AND std.txn_type = 'WITHDRAWAL'
- AND """ + CountedCashLedger.REAL_TXN_ID + """
+ AND """ + CountedCashLedger.COUNTED_ROW + """
AND (std.officer_id IS NULL
OR NOT EXISTS (SELECT 1 FROM m_appuser au JOIN m_cashiers c ON c.staff_id = au.staff_id
WHERE au.id = std.officer_id)
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashierdeposit/api/CashierDepositRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashierdeposit/api/CashierDepositRequestApiResource.java
index 592a66d32..5b71779ea 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashierdeposit/api/CashierDepositRequestApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashierdeposit/api/CashierDepositRequestApiResource.java
@@ -192,7 +192,7 @@ public class CashierDepositRequestApiResource {
// reject() will delete these records so the vault position is reversed.
if (denominationData != null && !denominationData.isBlank()) {
savePendingDenomination(denominationData, newId, savingsAccountId, accountNo, clientId,
- currentUser.getId(), currentUser.getUsername(), paymentTypeName);
+ currentUser.getId(), currentUser.getUsername(), paymentTypeName, txnDate);
}
return gson.toJson(Map.of("resourceId", newId, "status", "PENDING", "voucherNo", voucherNo));
@@ -364,6 +364,17 @@ public class CashierDepositRequestApiResource {
updatedLog, Timestamp.from(Instant.now()), id
);
+ // Rejection deleted this request's notes (the cash went back to the customer). Resubmitting
+ // means the cashier took the cash again, so the notes are recorded again -- without this an
+ // approved resubmission credited the account while the drawer never received its notes.
+ // Cleared first so a resubmit can never leave the old breakdown alongside the new one.
+ jdbcTemplate.update("DELETE FROM m_savings_txn_denomination WHERE txn_id = ?", id + PENDING_DEPOSIT_TXN_ID_OFFSET);
+ if (denominationData != null && !denominationData.isBlank()) {
+ savePendingDenomination(denominationData, id, toLong(record.get("savings_account_id")),
+ (String) record.get("account_no"), toLong(record.get("client_id")),
+ currentUser.getId(), currentUser.getUsername(), paymentTypeName, txnDate);
+ }
+
// Keep the central voucher register in sync with the edited amount
voucherService.updateAmount(VoucherService.REF_CASHIER_DEPOSIT_REQUEST, id, amount);
@@ -1004,9 +1015,14 @@ public class CashierDepositRequestApiResource {
*/
// Called at submit time — uses request id as temporary txn_id.
// approve() updates txn_id to real Fineract id; reject() deletes these records.
+ private static Long toLong(final Object value) {
+ return value instanceof Number n ? n.longValue() : null;
+ }
+
private void savePendingDenomination(final String denominationData,
final Long requestId, final Long savingsAccountId, final String accountNo,
- final Long clientId, final Long cashierId, final String cashierUsername, final String paymentTypeName) {
+ final Long clientId, final Long cashierId, final String cashierUsername, final String paymentTypeName,
+ final String txnDate) {
final JsonObject denomMap;
try {
denomMap = gson.fromJson(denominationData, JsonObject.class);
@@ -1037,10 +1053,14 @@ public class CashierDepositRequestApiResource {
INSERT INTO m_savings_txn_denomination
(txn_id, savings_account_id, account_no, client_id, denomination_id,
denomination_value, count, total_amount, txn_date, txn_type, officer_id, officer_username, payment_type)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_DATE, 'DEPOSIT', ?, ?, ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, COALESCE(CAST(? AS DATE), CURRENT_DATE), 'DEPOSIT', ?, ?, ?)
""",
requestId + PENDING_DEPOSIT_TXN_ID_OFFSET, savingsAccountId, accountNo, clientId, denomId,
value, count, BigDecimal.valueOf((long) value * count),
+ // Dated on the request's own date, which is what the branch day's book figure reads.
+ // CURRENT_DATE is the database clock, so a deposit keyed on one date could land in the
+ // drawer on another and leave both days' cash wrong.
+ txnDate != null && !txnDate.isBlank() ? Date.valueOf(txnDate) : null,
cashierId, cashierUsername, paymentTypeName
);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java
index 8f3cb2437..19a757709 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java
@@ -90,7 +90,7 @@ public class CashReconciliationApiResource {
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
+ "WHERE std.txn_type = 'DEPOSIT' "
+ "AND std.txn_date <= CAST(? AS DATE) "
- + "AND " + CountedCashLedger.REAL_TXN_ID
+ + "AND " + CountedCashLedger.COUNTED_ROW
+ "AND std.officer_id IN ("
+ " SELECT au.id FROM m_appuser au "
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
@@ -120,7 +120,7 @@ public class CashReconciliationApiResource {
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
+ "WHERE std.txn_type = 'WITHDRAWAL' "
+ "AND std.txn_date <= CAST(? AS DATE) "
- + "AND " + CountedCashLedger.REAL_TXN_ID
+ + "AND " + CountedCashLedger.COUNTED_ROW
+ "AND std.officer_id IN ("
+ " SELECT au.id FROM m_appuser au "
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java
index edde51cf2..761ca117f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java
@@ -60,34 +60,11 @@ public class CashReportApiResource {
*
*
*
- * Which ids are real and which are stamped timestamps is {@link CountedCashLedger}'s rule, shared
- * with the day register, the reconciliation screen and the withdrawal stock check, so a drawer
- * reads the same on all four.
+ * The rule itself, including the mis-keyed-breakdown test, is {@link CountedCashLedger#COUNTED_ROW},
+ * shared with the day register, the reconciliation screen and the withdrawal stock check, so a
+ * drawer reads the same on all four.
*/
- private static final String COUNTER_NOT_REVERSED = "AND NOT EXISTS ("
- + " SELECT 1 FROM m_savings_account_transaction rt "
- + " WHERE rt.id = std.txn_id AND rt.is_reversed = true) "
- + "AND " + CountedCashLedger.REAL_TXN_ID
- // A breakdown has to add up to the transaction it belongs to. Where it does not, the
- // notes were mis-keyed and the transaction amount is the fact -- ₹1,60,000 of notes
- // recorded against a ₹918 deposit is not ₹1,60,000 of cash in anybody's drawer. Seven
- // such transactions in this database were carrying ₹1.68 lakh that never existed.
- //
- // The tolerance is sub-rupee, not exact: Indian cash has no paisa coins in circulation,
- // so a paisa-exact ledger amount (e.g. 12998.56) is legitimately handed over as the
- // nearest whole rupee (12999) -- a 44-paisa gap is real rounding, not a mis-keyed
- // breakdown, and treating it as one silently dropped an entire genuine withdrawal (and
- // everything counted after it) out of the cashier's position.
- //
- // Compared per transaction inside a correlated subquery rather than as a join, so a
- // breakdown with no transaction behind it -- a Demand Draft keyed by its request id, or a
- // manual GL entry keyed by its decoded reference -- is untouched by this test and left to
- // CountedCashLedger's rule above.
- + "AND NOT EXISTS ("
- + " SELECT 1 FROM m_savings_account_transaction mt "
- + " WHERE mt.id = std.txn_id"
- + " AND ABS(mt.amount - (SELECT SUM(sd.count * sd.denomination_value)"
- + " FROM m_savings_txn_denomination sd WHERE sd.txn_id = std.txn_id)) >= 1) ";
+ private static final String COUNTER_NOT_REVERSED = "AND " + CountedCashLedger.COUNTED_ROW;
/**
* Day book — every voucher raised on one date, in one list, newest first.
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java
index 57f9cb070..d579ec92e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java
@@ -722,10 +722,13 @@ public class ClerkWithdrawalRequestApiResource {
INSERT INTO m_savings_txn_denomination
(txn_id, savings_account_id, account_no, client_id, denomination_id,
denomination_value, count, total_amount, txn_date, txn_type, officer_id, officer_username, payment_type)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_DATE, 'WITHDRAWAL', ?, ?, ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, COALESCE(CAST(? AS DATE), CURRENT_DATE), 'WITHDRAWAL', ?, ?, ?)
""",
txnId, savingsAccountId, accountNo, clientId, denomId,
value, count, BigDecimal.valueOf((long) value * count),
+ // The request's own date, which the branch day's paid-out figure reads -- not the
+ // database clock, which put the notes on a different day from the withdrawal.
+ record.get("txn_date"),
cashier.getId(), cashier.getUsername(), paymentTypeName
);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/CountedCashLedger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/CountedCashLedger.java
index 3fa7bcd11..d07b43810 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/CountedCashLedger.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/CountedCashLedger.java
@@ -87,4 +87,53 @@ public final class CountedCashLedger {
+ " OR CAST(to_timestamp(std.txn_id / 1000.0) AS DATE) NOT BETWEEN"
+ " COALESCE(std.txn_date, CAST(std.created_at AS DATE)) - " + SAME_DAY_TOLERANCE_DAYS
+ " AND COALESCE(std.txn_date, CAST(std.created_at AS DATE)) + " + SAME_DAY_TOLERANCE_DAYS + ") ";
+
+ /**
+ * {@link #REAL_TXN_ID} plus the two tests a real id can still fail, as one predicate over the alias
+ * {@code std}. Every drawer figure reads this, not {@code REAL_TXN_ID} alone.
+ *
+ *
+ * Only the cashier position report used to apply the two extra tests, so it and the reconciliation
+ * screen disagreed: a reversed ₹200 deposit left the settlement screen expecting three ₹100 notes
+ * while the settlement check, reading the position, allowed one — and refused the hand-over.
+ *
+ *
+ *
a breakdown whose savings transaction is reversed -- the money went back, so the notes did too.
+ * The table has no reversal marker of its own, so it is read off the transaction;
+ *
a breakdown that does not add up to its transaction -- ₹1,60,000 of notes against a ₹918
+ * deposit is mis-keyed, not cash. Sub-rupee tolerance, because a paisa-exact amount is handed
+ * over as the nearest whole rupee.
+ *
+ *
+ *
+ * The transaction is found by id and account number together. Savings and current
+ * transactions keep separate id sequences, and a current-account deposit writes its own
+ * transaction id into {@code txn_id}. Matched on id alone, a ₹210 current-account deposit was
+ * judged against an unrelated savings transaction that happened to share its id, failed the
+ * amount test, and vanished from the cashier's drawer. A row with no account number (older
+ * writers) is still matched on id alone, as before.
+ *
+ *
+ * All tests are correlated {@code NOT EXISTS} rather than joins, so a breakdown with no account
+ * transaction behind it (a Demand Draft, a manual GL entry) is untouched by them.
+ */
+ public static final String COUNTED_ROW = " (NOT EXISTS ("
+ + " SELECT 1 FROM m_savings_account_transaction rt"
+ + " JOIN m_savings_account rsa ON rsa.id = rt.savings_account_id"
+ + " WHERE rt.id = std.txn_id"
+ + " AND (std.account_no IS NULL OR rsa.account_no = std.account_no)"
+ + " AND (rt.is_reversed = true OR " + breakdownDiffersFrom("rt.amount") + "))"
+ + " AND NOT EXISTS ("
+ + " SELECT 1 FROM m_current_account_transaction rct"
+ + " JOIN m_current_account rca ON rca.id = rct.current_account_id"
+ + " WHERE rct.id = std.txn_id AND rca.account_no = std.account_no"
+ + " AND (rct.is_reversed = true OR " + breakdownDiffersFrom("rct.amount") + "))"
+ + " AND" + REAL_TXN_ID + ") ";
+
+ /** Sub-rupee tolerance: a paisa-exact amount is handed over as the nearest whole rupee. */
+ private static String breakdownDiffersFrom(final String amountColumn) {
+ return "ABS(" + amountColumn + " - (SELECT SUM(sd.count * sd.denomination_value)"
+ + " FROM m_savings_txn_denomination sd WHERE sd.txn_id = std.txn_id"
+ + " AND sd.account_no IS NOT DISTINCT FROM std.account_no)) >= 1";
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java
index 3d22b4fdf..02a6498af 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java
@@ -286,7 +286,7 @@ public class DenominationBreakdownWriter {
+ " SELECT std.denomination_id, CAST(SUM(std.count) AS BIGINT)"
+ " FROM m_savings_txn_denomination std"
+ " WHERE std.txn_type = 'DEPOSIT'"
- + " AND " + CountedCashLedger.REAL_TXN_ID
+ + " AND " + CountedCashLedger.COUNTED_ROW
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
+ " GROUP BY std.denomination_id"
@@ -310,7 +310,7 @@ public class DenominationBreakdownWriter {
+ " SELECT std.denomination_id, CAST(-SUM(std.count) AS BIGINT)"
+ " FROM m_savings_txn_denomination std"
+ " WHERE std.txn_type = 'WITHDRAWAL'"
- + " AND " + CountedCashLedger.REAL_TXN_ID
+ + " AND " + CountedCashLedger.COUNTED_ROW
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
+ " GROUP BY std.denomination_id"
@@ -407,7 +407,7 @@ public class DenominationBreakdownWriter {
+ " SELECT std.denomination_id, CAST(SUM(std.count) AS BIGINT)"
+ " FROM m_savings_txn_denomination std"
+ " WHERE std.txn_type = 'DEPOSIT'"
- + " AND " + CountedCashLedger.REAL_TXN_ID
+ + " AND " + CountedCashLedger.COUNTED_ROW
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
+ " GROUP BY std.denomination_id"
@@ -431,7 +431,7 @@ public class DenominationBreakdownWriter {
+ " SELECT std.denomination_id, CAST(-SUM(std.count) AS BIGINT)"
+ " FROM m_savings_txn_denomination std"
+ " WHERE std.txn_type = 'WITHDRAWAL'"
- + " AND " + CountedCashLedger.REAL_TXN_ID
+ + " AND " + CountedCashLedger.COUNTED_ROW
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
+ " GROUP BY std.denomination_id"
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/rbireserve/api/RbiFormAFilingApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/rbireserve/api/RbiFormAFilingApiResource.java
new file mode 100644
index 000000000..fdb524c9a
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/rbireserve/api/RbiFormAFilingApiResource.java
@@ -0,0 +1,305 @@
+/**
+ * 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.rbireserve.api;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * Saved Form A / Form 9 returns: keeping a generated return so it can be read again later exactly as
+ * it was generated.
+ *
+ * A saved return is served from its stored snapshot, never recomputed. Recomputing would quietly
+ * produce a different statement from the one that was reviewed or filed - a backdated journal entry,
+ * a changed GL mapping or a corrected rate all move figures that were already reported - and a
+ * report whose figures change after it was filed is worse than no report at all.
+ *
+ * Deliberately not maker-checker: saving a snapshot changes no balance and posts no entry. The
+ * filing lifecycle (draft, checked, approved, submitted) is what carries the approval, and that is
+ * recorded on the row's status rather than in a separate command queue.
+ */
+@Path("/v1/rbiformafiling")
+@Component
+@RequiredArgsConstructor
+@Produces({ MediaType.APPLICATION_JSON })
+public class RbiFormAFilingApiResource {
+
+ private static final String RESOURCE_NAME_FOR_PERMISSION = "RBIFORMAFILING";
+
+ /**
+ * The filing lifecycle RBI's own timetable implies: a return is drafted, checked and approved
+ * inside the bank, sent provisionally within a week of the fortnight ending and finally within
+ * twenty days, and revised if a figure turns out wrong after that.
+ */
+ private static final List STATUSES = List.of("DRAFT", "GENERATED", "CHECKED", "APPROVED",
+ "PROVISIONAL_SUBMITTED", "FINAL_SUBMITTED", "REVISED");
+
+ private final JdbcTemplate jdbcTemplate;
+ private final PlatformSecurityContext context;
+ private final FromJsonHelper fromJsonHelper;
+ private final Gson gson = new Gson();
+
+ /** Saved returns, newest first. The stored snapshot is left out - the list does not need it. */
+ @GET
+ public String retrieveAll(@QueryParam("formCode") final String formCode) {
+ this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
+
+ final StringBuilder sql = new StringBuilder("""
+ SELECT f.id, f.form_code, f.reporting_date, f.fortnight_end, f.period_start, f.period_end,
+ f.status, f.ndtl_amount, f.required_amount, f.maintained_amount, f.average_maintained,
+ f.rate_percentage, f.remarks, f.submitted_on, f.created_on, u.username AS created_by_name
+ FROM m_rbi_form_a_filing f
+ LEFT JOIN m_appuser u ON u.id = f.created_by
+ """);
+ final List