Bring in the latest team changes (FD interest posting approval, recurring
deposit rules, tenant setup, cash exchange PAN, and others).
final-changelog-tenant.xml conflicted because both sides appended
includes: keep the team's includes first, then the investment
0593-0597 includes. File names differ, so the changesets do not clash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settlement-date accounting (RBI UCB Investment Directions, confirmed by
Jitendra Sir): a saved purchase is PENDING_SETTLEMENT with nothing in GL.
On or after its settlement date, POST /investment/purchases/{id}/settle
posts the journal entry dated the settlement date at Head Office - Dr
investment GL (book value), Dr 500606 broken-period interest, Dr charge
GLs, Cr the paying bank - through the journal entry command, which also
raises the JV voucher. The deal becomes ACTIVE and keeps the GL
transaction id and settlement day. Settle goes through maker-checker; a
settled deal can no longer be dropped.
Broken-period interest is now counted up to the settlement date instead
of the deal date, and the interest schedule starts from it.
Migration 0597 adds the settlement columns and the SETTLE permission.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rejects journal entry reverse, savings/current undo and reverse, fixed and recurring deposit
undo, and loan transaction undo (adjust with zero amount) with a clear error, so posted entries
cannot be reversed by calling the API directly.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Cash exchange: customer name/mobile/PAN (PAN mandatory above Rs 50,000), denomination validation
against m_denomination_master, cashier required; Liquibase 0599 adds the three columns.
- Inter-cashier: sender must be the logged-in cashier, cash-allocated validation, new
cashier-summary endpoint.
- Reconciliation restricted to the logged-in cashier's own records.
- New cashreport/branch-position endpoint (vault + cashier drawers, denomination-wise).
- Customer search returns the KYC PAN.
- Reverse endpoints removed from cash transfer, settlement, exchange and inter-cashier.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Defining opening balances was rejected by the debit/credit balance
check that ordinary journal entries go through. For opening balances
that check does not hold: every leg is posted against the opening
balance contra account, so the books always balance, and the submitted
legs legitimately differ by the opening balance of the locked,
denomination-managed cash account, which is already posted and can
never be part of the request.
- validateBusinessRulesForJournalEntries takes a requireBalanced flag.
Ordinary journal entries keep the full check (requireBalanced = true).
- defineOpeningBalance passes false: it still needs at least one leg
and an account and amount on every leg, but no longer requires debits
and credits to be present on both sides or to add up.
Verified with :fineract-provider:compileJava.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings Sonali's four new commits (7-12 Oct):
- Loans A/C extension fields (collateral, eligibility, repayment, rate
source, prepayment)
- Maker-checker for Delinquency Ranges and Buckets
- Loan interest rate register product master and GL account seeding
- Her merge of dev-latest (7 Oct)
Conflict resolved:
- changelog-tenant.xml: kept both. 0594_loan_rate_register_product_
master, 0595_seed_loan_rate_register_gl_accounts,
0596_enable_delinquency_maker_checker and 0597_add_loan_product_
loans_ext_fields go with the earlier advance loan changelogs, before
the DDS block that stays last. No file name is shared with
dev-latest's 0594-0597 files.
0581_enable_loan_rate_register_maker_checker.xml: taken as on
feature/advanceLoanProduct. It edits the existing 0581a/0581b
changesets (CBSLOANBASEINTERESTRATE -> LOANBASEINTERESTRATE) and adds
0581c to rename rows already seeded; all three carry
<validCheckSum>ANY</validCheckSum>, so databases that ran the old
version still validate.
CommandSourceService: Sonali's LOANBASEINTERESTRATE, DELINQUENCY_BUCKET
and DELINQUENCY_RANGE checks sit alongside the DDS isDdsEntity check.
Nothing from dev-latest, DDS or Sonali is removed; no file deleted; no
DB password change. Verified with :fineract-provider:compileJava.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stores the Loans A/C (EMI) wizard's fields that have no home in
Fineract's core loan-product schema on the same per-product satellite
used for Advances: collateral/guarantor rules, borrower eligibility,
disbursement, repayment mode, rate source / floating reset, and
part-prepayment/foreclosure. Columns are loan_-prefixed so they never
collide with the Advances columns on the shared table.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Range/bucket create, update and delete now route through the standard
command-source approval pipeline, since a bucket drives the SMA/NPA
classification of every loan product linked to it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the Advance Loan Product backend on top of the DDS merge of
dev-latest:
- Advance loan product ext entity, API and Liquibase
(LoanProductAdvanceExtService; loan product API accepts and returns
"advanceExtensions")
- Fees-Penalty-Interest-Principal repayment transaction processor,
enabled by fineract.loan.transactionprocessor.
fees-penalties-interest-principal.enabled (default true)
- Loan interest rate register backend
Conflict resolved:
- changelog-tenant.xml: kept both. The four advance loan changelogs
(0571_create_loan_product_advance_ext, 0572_add_loan_product_advance_
ext_fields, 0580_create_loan_rate_register_tables,
0581_enable_loan_rate_register_maker_checker) go just before the DDS
block, which its comment says stays last. Their numbers overlap
dev-latest files but no file name is shared.
Not taken from feature/advanceLoanProduct:
- Commit b72657703 changed the default DB password in
application.properties to a UAT database password. That merged
without a conflict; the dev-latest password lines are kept so local
setups keep working and the UAT password does not go into the
repository. The new transaction processor property from the same
file is kept.
Nothing from dev-latest or DDS is removed: no file deleted; only
LoanProductsApiResource (one supported parameter added),
LoanProductConfiguration (one constructor argument added) and
application.properties change existing lines. CommandWrapperBuilder,
changed on both sides, has no duplicate methods.
Verified with :fineract-provider:compileJava.
Branch renamed from merge/dds-into-dev-latest, since it now carries
both DDS and Advance Loan Product.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RBI UCB Investment Directions 2025, para 152: broken-period interest paid
to the seller on G-Sec / approved securities is not part of cost but an
expense (confirmed by Jitendra Sir; GL 500606). The interest schedule
therefore no longer carries it as a receivable to be cleared by the first
coupon: the first coupon is income in full. Tests updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Market rate of each script for a day (open / high / low / close), as the
bank's existing CBS keeps it, so Purchase Entry can take its rate from
here instead of the dealer typing it (Jitendra Sir).
- m_investment_daily_rate, one row per script per day (migration 0596),
with READ/CREATE/DELETE permissions granted to Investment menu roles.
- /v1/investment/daily-rates: list (optionally per script), save, drop,
and /latest for the newest rate of a script on or before a date.
- Close rate is mandatory; high may not be below low, and open / close
must sit inside low-high, with readable messages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the DDS (Daily Deposit Scheme) backend from feature/dds onto the
current dev-latest:
- Agent settlement (Agent Tran), Close Party account, reports
(Supplementary-DDS, Master Data Export, Close Register), Party-Savings
link, savings product category field
- Collection entries held before posting, per-agent GL accounts,
maturity auto close
- Vouchers, maker-checker approvals, payout & close, Sundry GL, closure
tab and fixes
- POST_DDS_INTEREST job
No content conflicts. No dev-latest file is removed; only three
dev-latest lines are extended with a DDS condition:
- CommandSourceService: DDS entities join ROLE in the maker-checker
entity check (isDdsEntity).
- PortfolioCommandSourceWritePlatformServiceImpl: a DDS entry can never
be approved by the user who made it, even a checker super user. The
rule for every other entity is unchanged.
- SavingsProductDataValidator: productCategory accepted as a savings
product parameter.
Files changed on both branches and auto-merged: CommandWrapperBuilder,
CommandSourceService, JobName, SavingsApiConstants, SavingsProductData,
SavingsProduct, FixedDepositProductData, RecurringDepositProductData,
DepositProductReadPlatformServiceImpl. Checked for duplicate methods,
fields and enum names.
Liquibase: 55 DDS changelogs (0470-0525) included as one block at the
end of changelog-tenant.xml. Their numbers overlap dev-latest's files
but no file name is shared, and Liquibase tracks changesets by file
path, so nothing clashes. They keep their names so databases that
already ran them do not run them again. No permission code, job name
or configuration name is inserted by both branches.
Verified with :fineract-provider:compileJava. The full compileJava
fails only in fineract-client (generated CKYC, committee meeting and
election APIs), which feature/dds does not touch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the FD interest posting work into dev-latest:
- FD interest posting approval with pooled GL postings, posting period
shown per line, maturity postings surfaced, double-posting guard
- FD maturity approval report added and then reverted (maturity now
runs directly through the scheduler, no approval step)
- Pending user approval encodes the password and emails it
- New-tenant user personalisation, with the 'system' username kept so
scheduler jobs and COB can still find the system user
Conflicts resolved:
- SavingsAccountReadPlatformServiceImpl#retrieveAllSavingsDataForInterestPosting:
kept both exclusions. Recurring deposits (deposit_type_enum 300) stay
out of the generic interest posting job (dev-latest), and fixed
deposits are also left out when FD interest posting approval is
enabled (feature branch), so FD interest is not paid twice.
Comments from both sides merged.
- final-changelog-tenant.xml: kept dev-latest's 0592-0598 TDS and
recurring deposit changelogs and added
0593_fd_interest_posting_approval.xml after them. The file keeps its
name although its number clashes with 0593_tds_...: Liquibase tracks
changesets by file path, so renaming it would re-run it on databases
that already applied it. Changeset ids in the two files differ.
Verified with :fineract-provider:compileJava.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Commit 3d3ce92e9 personalised the three users seeded into a brand-new
tenant database and renamed user id 2 from 'system' to
'<tenant>_system' (for example 'default_system'). Fineract looks this
user up by the fixed name AppUserConstants.SYSTEM_USER_NAME ('system')
through AppUserRepositoryWrapper.fetchSystemUser(), so on every fresh
tenant:
- every scheduler job failed once a minute with
"UserNotFoundException: User with name 'system' does not exist"
(seen with the Send Asynchronous Events job);
- COB initialisation, savings escheat, current account closure and the
stuck-job listener would fail in the same way.
Existing tenants were not affected because the provisioner only runs
right after the very first migration of a new tenant.
Fix
- TenantInitialUserProvisioner now sets user id 2's username to
AppUserConstants.SYSTEM_USER_NAME and only replaces its password.
- Admin and interop users are still personalised as
'<tenant>_admin' and '<tenant>_interop'.
- Javadoc and the startup log line now say the system user keeps its
name and why.
Resulting users on a new tenant
- <tenant>_admin (password: initial-user-password or built-in default)
- system (same password, name unchanged)
- <tenant>_interop (same password)
Tenants already created with '<tenant>_system' need a one-off fix:
UPDATE m_appuser SET username = 'system' WHERE id = 2;
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brokerage, stamp duty and the new other charges are expenses of the deal
(Jitendra Sir): they are stored on the purchase and added to the
settlement amount, but no longer to the book value, for securities and
mutual funds alike. Settlement = book value + accrued interest + charges.
Adds m_investment_purchase.other_charges (migration 0595) and the
otherCharges request/response field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring in the latest team changes (TDS, shares, cashier, RBI reserve).
final-changelog-tenant.xml conflicted because both sides appended after
0592: keep the team's 0592/0593 TDS includes first, then the investment
0593/0594 includes. File names differ, so the changesets do not clash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add GET /v1/investment/purchases/{id}/interest-schedule. For a holding it
lists every half-yearly coupon and every 31 Mar / 30 Sep closing provision
from purchase to maturity, worked out on the face value still held:
provision = interest earned since the last coupon (30/360), and the coupon
clears what is already booked (purchased accrued interest or the earlier
provision). Lines up to the as-on date are marked due. Calculation only;
nothing is posted.
The day-count and interest arithmetic move into InvestmentInterestCalculator,
shared with Purchase Entry, and are covered by unit tests using Jitendra
Sir's worked example. Duplicate-code errors on the masters now keep the
original exception as the cause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fixed deposit's maturity is settled directly by the nightly
updateMaturityDetails job again, unconditionally, for every deposit
type and every closure instruction — the approval gate this introduced
turned out to risk more than it protected and is being removed before
it ever shipped enabled by default.
Removes the fdmaturity package entire (report service, orchestrator,
API resource, noon-generation job), its migration and global
configuration, the day-close integration in BranchDayRegisterApiResource,
and FixedDepositAccount#previewMaturityInterestPeriods, which existed
only for this report's preview and has no other caller.
The periodic FD interest posting approval report (0593) is untouched —
a matured deposit's final interest, paid directly by the nightly job,
already surfaces there under "Posted outside approval", same as it did
before this report existed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds TenantInitialUserProvisioner: right after the very first Liquibase
migration of a tenant that was never on Flyway, renames the three
seeded users (ids 1-3, otherwise shipping as mifos/system/interopUser
with the public default password) to <tenantIdentifier>_admin/_system/
_interop, with a password from fineract.tenant.initial-user-password
or a built-in default. An existing (Flyway-era) tenant already has its
real users and is left untouched. Wired through TenantDatabaseUpgradeService
and its two test configurations.
Also fixes the two pooled interest-posting services' system-user
lookup, which read username = 'system' — now wrong since that username
is personalised per tenant — to find the seeded system user by its
fixed id (2) instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the FD maturity approval workflow: a deposit with no closure
instruction of its own (Maturity Instructions left "Not selected")
waits — status, final interest and whatever renewal is later chosen
from the FD Dashboard all run together in one real maturity run, only
once a second user approves the report, as the branch closes its day.
A deposit that already named Transfer to Savings or a Reinvest option
settles itself the moment it matures, unchanged — a decision the
customer already made in writing needs no second approval. The
nightly job is narrowed to skip only the undecided ones;
FixedDepositAccount#previewMaturityInterestPeriods reads the same
maturity-interest calculation the real posting uses, without writing
anything down, so the report's figure is never a guess.
Posting an approved report calls the existing, unchanged
DepositAccountWritePlatformService#updateMaturityDetails per deposit,
each in its own transaction — one with a bad closure target does not
hold up the others, and a retry only touches what did not post last
time.
Adds a reconciliation view (outside-approval) listing deposits that
matured via their own closure instruction, same reasoning as the
interest report's: found by what has no report line, not by naming
the instruction, so a future direct path is caught here too.
Day-close integration mirrors the interest report exactly: blocks on
a pending maturity report, posts approved ones alongside approved
interest reports, independently so one failing does not stop the
other.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
finalizeCreateUser() (run when a checker approves a CREATE_USER request queued
in m_user_pending_request) saved AppUser.fromJson()'s result straight to
m_appuser via appUserRepository.saveAndFlush() with no encoding step for the
password AppUser.fromJson() auto-generates when the maker ticks "send password
to email" -- AppUser's constructor stores whatever string it is given
verbatim, so that random password was landing in m_appuser.password as plain
text. Only the maker-chosen-password case (request.getPasswordHash() != null)
was ever hashed, because that happens earlier, at submitCreateUser() time.
It also never called the email service at all, in either case. The live
create-user flow (JpaUserDomainService.create()) captures the unencoded
password, encodes it, saves, then emails the welcome message --
finalizeCreateUser() duplicates everything up to the save but stops there, so
a user created through this maker-checker flow never received their
credentials regardless of what sendPasswordToEmail said in the submitted
payload.
Fix: capture the auto-generated plaintext right after AppUser.fromJson(),
encode it with this class's own PasswordEncoder bean (already used the same
way for the maker-chosen-password branch, consistent within the file) before
saving, and call PlatformEmailService.sendToUserAccount() afterwards when
sendPasswordToEmail was true and no maker-chosen password overrides it. A
maker-chosen password is unrecoverable by approval time (submitCreateUser()
redacts it to "[PROTECTED]" immediately), so that case still cannot be
emailed, and isn't -- there is nothing wrong to fix there.
Found by manually submitting a CREATE_USER request locally and reading both
AppUser.fromJson() and the approval path end to end; no existing approved user
was re-created to verify in place, since doing so would need resetting a real
password.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Treasury Investment module, first slice, following the bank's existing CBS
Investment screens:
- Bank / Party Master (m_investment_party): auto party code PTY-nnn, address,
bank type, GL A/c, RBI code, exposure limit, LEI, counterparty bank details.
- Script Master (m_investment_script): auto script code by bond type
(G00001, B00001, ...), SLR type (GOI / STATE_GOVT / TREASURY / NON_SLR),
GL A/c, ISIN, interest rate, issue / maturity dates, issuer details.
- Purchase Entry (m_investment_purchase): one deal = one holding. FY deal
number (e.g. 202627000001), auto cert no, settlement date from T+n.
Units, book value, premium/discount, accrued interest (30/360 up to the
deal date), settlement amount and YTM are computed by the backend.
APIs: /v1/investment/parties, /v1/investment/scripts, /v1/investment/purchases,
all through the command pipeline. Permissions READ/CREATE/UPDATE/DELETE plus
_CHECKER rows are seeded with maker-checker off and granted to roles that
hold MENU_INVESTMENT. Mapped GLs must be enabled ASSET detail accounts.
No GL entry is posted by a purchase yet.
Migrations: 0593_investment_masters, 0594_investment_purchase.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds interest_period_from to the report line so each posting shows the
span it covers (e.g. "01 Oct - 31 Dec, Quarterly"), not just the date
it ends on, and a postingPeriodLabel on the report itself.
Adds a reconciliation endpoint (outside-approval) that lists fixed
deposit interest posted without this report's approval — chiefly a
deposit that matured before its own posting cycle completed, whose
last period the maturity job settles directly. Read-only, so a branch
can see every rupee of deposit interest it has paid, approved or not.
Guards posting itself against the same situation: before crediting a
line, checks whether its deposit already carries an interest posting
for that exact date (raised by the maturity job in the window between
this report being generated and being posted). If so, the line is
linked to what actually paid it instead of being paid again, and its
amount is reconciled off the report's approved total rather than
failing the whole posting.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a maker-checker approval report in front of fixed deposit interest
posting, mirroring the existing savings interest posting report: a
report is generated per branch and product (SINGLE/DOUBLE/SPECIAL), a
second user approves it, and it posts when the branch closes its day.
Day close is blocked until deposit interest due that day is approved.
Ledger entries are pooled per the report's interestPostingDate rather
than raised per deposit: Dr Interest Expense and the Payable/Paid legs
are each raised once for the group's total, and only the final leg
crediting each deposit's own GL is per account — the same pattern the
savings interest posting report already uses, instead of repeating the
full three-stage chain for every deposit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ClerkShareWithdrawalRequestApiResource: extracted validate(Long)'s checklist
logic into buildShareWithdrawalChecks(Map<?, ?>) and added a new
POST /preview-validate endpoint that runs it against a draft submission (same
fields submit() accepts, same Sundry-downgrade rule applied first) instead of
an already-persisted request. Lets the clerk see the exact checklist a manager
will see on approval before the request is even submitted, with zero risk of
the two checklists drifting apart, since both now call the same method. The
one check validate() runs that a draft cannot -- "Maker and Checker are
different users" -- is reported as a fixed reminder instead of silently
dropped, since the clerk previewing this is necessarily the maker and no
checker has been chosen yet.
DocumentReadPlatformServiceImpl.retrieveDocumentContent(): a document/signature
whose m_document row survives but whose underlying file is missing from the
content store (the usual cause is a database copied between environments
without its file store) was throwing an uncaught ContentStoreException,
surfacing as a raw 500 on every fetch. Now caught and converted to the same
DocumentNotFoundException (404) ImageReadPlatformServiceImpl already uses for
the identical situation on client photos, so the frontend's existing
graceful-fallback handling for a missing attachment applies here too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
redemptionValueOf() read from "m_share_account_transaction" with a
"share_account_id" column. The table is m_share_account_transactions and the
column is account_id, so every share withdrawal approval failed with a
BadSqlGrammarException on the Share Sundry balance guard, before any posting was
attempted. The lookup now also filters on is_active, matching
ShareAdvanceCollectionApiResource.shareValueOf(), so a reversed purchase cannot
price a redemption.
validate() branched on "GL" versus everything else, but approve() handles a
third settlement mode: submit() downgrades a request to NONE when the clerk
names the product's own Share Sundry GL as the destination, and that mode posts
Dr Share Capital / Cr Share Sundry and stops. Validating it as a savings
settlement reported a missing fund source mapping that no posting in this mode
ever reads, so a correct request showed a failed check. NONE is now reported as
not applicable for both the linked-savings and the fund-source checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
git-versioning's rev{} fallback previously derived the SNAPSHOT version from
the nearest reachable git tag (major.(minor+1).0-SNAPSHOT), so the built jar's
name drifted as tags moved. Pins it to a fixed 1.15.0-SNAPSHOT instead so the
jar name stays predictable across builds on dev-latest/feature branches.
release/*.*.* and maintenance/*.* branches still version off their own tags,
unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reverts commits 2afd4aaf8 and 11a0d502b (both by anshul, same title).
Restores the CHECKER_SUPER_USER self-approval bypass in CommandSourceService
and the six *EditWritePlatformServiceJpaRepositoryImpl classes, and restores
the original isSameMakerCheckerEnabled()/isCheckerSuperUser()-gated same-user
check in PortfolioCommandSourceWritePlatformServiceImpl. Removes the
0588_persist_manually_enabled_maker_checker_permissions.xml migration added
alongside the restriction.
The unconditional same-user block these commits introduced was blocking
legitimate single-admin operation/testing (e.g. creating and then activating
a client as the same CHECKER_SUPER_USER), with no way to resolve it from
inside the app.
11a0d502b's only change (0012_seed_default_platform_api_client.xml comment
wording) is a no-op here: that exact text was independently reworded again by
82336cbe9 after 11a0d502b landed, so there is nothing left of 11a0d502b's
change to revert in that file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same pattern as 0588_persist_manually_enabled_maker_checker_permissions:
converts a manually-applied Configure Maker Checker Tasks change into a
repeatable migration, this time for the Teller/Cashier module. Found by
diffing can_maker_checker across every permission code common to
fineract_lincpay and a reference environment -- ALLOCATECASHIER_TELLER,
ALLOCATECASHTOCASHIER_TELLER, SETTLECASHFROMCASHIER_TELLER, and
UPDATECASHIERALLOCATION_TELLER were true there but still seeded false here,
confirming they were toggled on manually and never captured for any other
environment (including UAT). The _CHECKER permissions already exist and are
already granted to Clerk/Teller/Branch Manager, so approval works as soon
as this migration runs -- no role-grant changes needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Details of changes:
1. Premature Closure 1-Day Shortfall Fix (FixedDepositAccount & RecurringDepositAccount):
- In calculatePreMatureInterest, explicitly reset transaction end-of-day balances to preMatureDate before invoking calculateInterestPayable.
- Ensure end-of-day balances are restored to current business local date in a finally block.
- Resolves the issue where premature closure missed 1 day of interest (e.g., -17.80 clawback) due to transaction end-of-day balances defaulting to the previous day when premature date matched current business date.
2. Interest Transfer Exclusions (SavingsAccountInterestPostingServiceImpl):
- Exclude interest-transfer withdrawal transactions from non-interest posting transactions and end-of-day cumulative balance updates for fixed deposits transferring interest to linked accounts.
3. Interest Payable GL Load on Edit (AccountingConstants & ProductToGLAccountMappingReadPlatformServiceImpl):
- Added INTEREST_PAYABLE(17) enum constant to CashAccountsForSavings in AccountingConstants.
- Mapped CashAccountsForSavings.INTEREST_PAYABLE to SavingProductAccountingDataParams.INTEREST_PAYABLE in ProductToGLAccountMappingReadPlatformServiceImpl.
- Fixes the bug where Liability -> Interest Payable Account GL was not loading in the product edit form (e.g., /products/fixed-deposit-products/:id/edit) even after being saved.
A pending user edit's stored payload keeps a "[PROTECTED]" placeholder for
the password rather than the real value. The actual new password is applied
separately via request.getPasswordHash(), so the placeholder must not reach
AppUser.update() or it tries to re-encode the literal placeholder string.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A premature FD closure that reprices interest downward (e.g. a pre-closure
penalty) posts the difference as a negative interest-posting transaction --
a clawback of interest already recognised in earlier periods. Both the cash-
and accrual-based accounting processors only posted journal entries when the
transaction amount was greater than zero, so a clawback updated the FD
subledger but silently posted nothing to the General Ledger, leaving the two
out of sync.
Treats a negative amount as a mirror-image reversal: positive magnitude,
debit/credit legs swapped -- the same mechanism already used for isReversal.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a Closure Reason to premature FD closure (Normal, Premature Extension,
Conversion to Another TD, Deceased Settlement, Nominee/Legal-Heir), persisted
as an RBI audit trail on m_deposit_account_term_and_preclosure. RBI-mandated
reasons auto-waive the pre-closure penalty regardless of the maker's own
checkbox; the maker can also waive it explicitly for any other reason.
Interest rate for a premature closure is always the deposit's own contracted
rate -- the same one its regular scheduled postings use throughout its life
-- rather than a slab re-lookup against the actual period held. An earlier
"actual-period vs contracted" toggle was built, tested, then deliberately
removed per explicit request: simpler, and avoids disagreeing with interest
the account was already accruing all along.
Also fixes a real bug found while testing this: a renewed FD term's premature-
closure interest was including the previous term's closing transaction a
second time (it's dated the same day the new term starts), understating the
first day's balance by the whole prior term's payout. Scoped the interest
calculation to transactions strictly after the current term's start date.
Known open issue: a renewed/auto-renewed account can still show a materially
wrong (sometimes negative) premature-closure interest figure in some cases
(seen on account 10, a QUARTERLY-posting auto-renewed FD) -- root cause not
yet confirmed; needs further investigation with debug instrumentation before
this is considered fully correct for every renewal shape.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause: ProductPendingEditFieldMapper.populateCoreFields() captured
otherGlMappings with stringValueOfParameterNamed(), which only handles a
JSON string primitive. The maker's edit actually submits otherGlMappings
as a nested {assets,liabilities,expenses,income} object -- the same shape
SavingsProduct#update() already reads correctly via jsonFragment() -- so
the pending-edit row was staged with this field blank every time,
regardless of what the maker changed.
Two consequences, both now fixed:
- The checker's diff dialog showed "No changes to review" even when a GL
account mapping (e.g. Savings/Current/FD/RD Interest Payable or Paid)
was genuinely changed, since the submitted value never made it into
proposedChanges.
- Approving the edit replayed a payload with no otherGlMappings at all,
so the GL account change was silently dropped rather than applied --
the edit showed as approved, but the product's accounting mapping
never actually moved.
Fixed the write side (jsonFragment instead of stringValueOfParameterNamed)
and the read-side replay (writeCoreFieldsToJson now re-adds the stored
value as a parsed JSON element via the new putJsonFragment, not a quoted
string, which would have double-encoded it on replay). Shared by Savings,
Current, FD and RD products, since they all go through this one mapper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same mistake as an earlier migration this session: a literal "--" inside
an XML comment is illegal per the XML spec and crashes the entire backend
startup (Liquibase's XML parser rejects the whole changelog, not just this
file). Reworded the prose to avoid it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the previous DefaultApiClientBootstrapper (ApplicationRunner
minting a random key on first startup) with a plain Liquibase seed on the
tenant-store database: one fixed, well-known api_key_hash row, inserted
once via migration 0012, same on every deployment and every tenant --
exactly like this fork already seeds default codes/permissions, rather
than generating a secret at runtime.
The trade-off is explicit in the migration's own comment: a fixed key
checked into source is a bootstrap convenience, not a secret, and any
deployment beyond local/dev must rotate it (insert a new hash, deactivate
this row) before going live -- rotation is a direct m_api_client write,
same as the seed itself, since POST /v1/apiclients stays disabled.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A fresh deployment's m_api_client table starts empty, and reaching
#/platform-admin/menu-access requires a valid X-Api-Key -- with no key,
every tenant 401s identically (this is by design, not tenant-specific).
Add DefaultApiClientBootstrapper: an ApplicationRunner that mints exactly
one random key on first startup (guarded by "table still empty", so later
restarts are a no-op) and logs the raw value once to the server's own
application log -- never to the database, never over HTTP.
Disable ApiClientAdminApiResource's POST /v1/apiclients entirely: it only
required ALL_FUNCTIONS, a per-tenant role grant, so any bank's own super
user (or a misconfigured role, as "Clerk" was found to carry it in two
tenants this session) could mint themselves a platform-wide operator key
and bypass the exact ceiling TenantMenuAccessApiResource exists to enforce.
New keys past the first are now a direct m_api_client insert only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
feature/bugfix independently used 0585/0586 for unrelated migrations
(loan COB catch-up, system module checker permissions), colliding with
this session's own 0585 file. Renumbered to the first free slot after
theirs so both sets of changesets coexist without ambiguity; content and
semantics are unchanged, only the file name and changeset ids moved.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add migration 0585 to add the OD-scheme columns to the correctly-named
pending-edit shadow table (m_saving_product_temp, singular), fixing
migration 2033's typo that made it a permanent no-op and broke editing
any Current product with OD fields set.
- Fix PortfolioProductType.fromInt() to switch on each constant's declared
value instead of declaration position, which silently mis-mapped ids 3-5
and NPE'd on id 6 (CURRENT), crashing Journal Entries for Current-account
GL rows.
- Stop generating a second Bank Transfer voucher at approve time; reuse the
one already created at raise/submit time instead, eliminating duplicate
vouchers for the same transfer under two different users' names.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reinvest Principal Only was still on the legacy close-and-reopen path (open
a new FD with just the principal, close the old one) while Reinvest
Principal + Interest and Partial Reinvest had already moved to renewInPlace
-- the same customer choosing a different split ended up with two different
outcomes: same account number, or a brand new one. Unified all three onto
renewInPlace: Principal Only is now exactly Partial Reinvest with the split
fixed at the closing principal, so it renews on the same account with only
the leftover interest actually leaving (to the chosen/linked Savings
account), in both handleFDAccountPayment (Payment dialog / FD Dashboard
Renew) and handleFDAccountMaturityClosure (the nightly job's path for an
account with an already-stored instruction).
Also fixes a related bug found while testing this: recordInPlaceRenewal
re-archived the closing term's full rate chart on every renewal of the same
term, even when it had already been archived by an earlier renewal --
archiveChartSlabs now only runs the first time a term's closing row is
created.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
m_current_account and m_savings_account each keep their own identity
sequence, so the same numeric id can be a real row in both. Every place
that links a GL journal entry back to its account for display -- so the
Journal Entries screen shows an Account No instead of a blank "-" --
was tagging entity_type_enum=2 (PortfolioProductType.SAVING)
unconditionally, even when the account was actually a current account.
Whenever a current account's id collided with an unrelated savings
account's id, the entry resolved to the wrong one entirely: a customer's
savings deposit could display someone else's current account number, or
vice versa.
Fixed at the write side, in every place found doing this:
- ClerkDisbursementRequestApiResource (Bank Transfer, Collection,
Disbursement) -- GlEntry now carries an isCurrentAccount flag, decided
once where the account was already resolved, instead of being
re-guessed.
- ChequeBookIssuanceChargeService, ClearingJournalService,
SponsorDDIssueRequestWritePlatformServiceImpl -- same pattern, each
already knew savings vs current at the point it linked the entry; that
answer is now threaded through instead of discarded.
Also fixed on the read side:
- JournalEntryReadPlatformServiceImpl: entity_type_enum=6 now resolves
unambiguously to the current account; entity_type_enum=2 still falls
back to savings_transaction_id (a reliable signal only a genuine
savings transaction sets) before guessing, for older rows written
before this fix.
- JournalEntryReferralServiceImpl (AML referral resolution): the same
ambiguity could attribute a suspicious-activity alert to the wrong
client entirely -- a same-numbered stranger's account -- when a
current account's entity_id collided with a savings account's.
Resolved with the same entity_type=6 / savings_transaction_id
precedence as the read-side fix.
Not touched: JournalEntryAggregationJobReader (year-end aggregation
batch job) has the same ambiguity for legacy entity_type_enum=2 rows,
but only ever joins m_savings_account -- it has no current-account
handling at all, standard-Fineract in origin. Extending it is a real
feature addition, not a targeted fix, and touches a scheduled financial
batch job this session cannot rebuild or run to verify; flagged for
separate, deliberate work instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gap-period interest is earned at the linked account's rate, not the FD's --
folding it onto the FD's own balance meant "Re-Invest Maturity Amount"
silently carried savings interest into the new FD term as if it were FD
interest, and "Re-Invest Principal Only" paid it out through the FD's own
closure math instead of it already being where it belongs. Now posted
directly onto the linked account as its own INTEREST_POSTING transaction;
the FD's balance is left untouched, so every closure type renews/transfers
its real maturity amount. Renewal Preview updated to match (projectedBalance
is now just the FD's own balance) and now also returns the linked account's
own account number, so staff can see exactly where the gap interest is
about to land before confirming.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GET /fixeddepositaccounts/{id}/renewal-preview -- shown in the FD
Dashboard's Payment dialog before staff confirm a late renewal, so they see
the actual numbers (linked account's rate, gap days, gap-interest amount,
projected new balance) instead of confirming blind. Shares its calculation
with the posting path (creditGapInterest) via a new computeGapInterestAmount
helper, so the previewed number is exactly what gets credited.
Also fixes fdAccount.getLinkedAccountId() returning null for an account
confirmed to have linked_account_id set in the database -- every caller
that needs the FD's Deposit Account Link now reads it fresh via SQL
(resolveLinkedAccountId) instead of that stale JPA entity accessor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
createInterestPostingJournalEntriesForSavings() documents a 3-stage
posting (Dr Interest On Savings -> Cr Payable, Dr Payable -> Cr Paid, Dr
Paid -> Cr Savings Control), but the first stage was never actually coded
-- the interest expense account was never even fetched. For a product with
no distinct Interest Payable/Paid accounts configured (every product in
this system, checked directly), the remaining two stages then collapsed
into a same-account no-op that correctly refuses to post, leaving interest
silently added to the account's balance with zero GL entries. Affects every
deposit type equally (Savings, FD, RD), not just FD -- confirmed by
checking every product's accounting mappings. Added the missing stage.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The FD product's exception mapper fell back to a bare "Unknown data
integrity issue with savings product." for any DB error it didn't
recognize, with no detail -- unlike its sibling class for regular Savings
products, which at least appends the underlying error. Added a specific,
actionable message for the numeric-overflow case (an amount field entered
too large for its column), and made the generic fallback include the real
detail too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The maturity-dashboard query only ever selected ACTIVE(300) accounts. That
was fine while nothing ever really reached MATURED(800) in this dev setup
(test dates were only SQL-backdated, never run through the real nightly
job), but the first time the job actually flipped an account's real status,
it silently vanished from the ladder instead of showing under "Matured" --
exactly the state the ladder exists to surface. Now selects ACTIVE or
MATURED.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
countActiveByClientAndProduct only excluded status 600 (CLOSED), so a
client whose prior application for the same product was Withdrawn (400)
or Rejected (500) -- both terminal, per CurrentAccount.withdraw()/
reject() -- was still blocked from applying again, same as if that
account were genuinely still open. Only 100/200/300 (submitted,
approved, active) should collide with a fresh application.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
entity_type_enum=2 (PortfolioProductType.SAVING) is written for both
savings and current-account journal entries -- current products
deliberately share SAVING's GL type space -- so it can't distinguish
them, and m_savings_account/m_current_account each have their own id
sequence, meaning an entity_id can be a real row in both. The account-
number column's COALESCE(ca.account_no, sa.account_no) always preferred
the current account's number whenever both matched, so a savings
deposit's own journal entry displayed an unrelated current account's
number instead of the account it actually posted against.
savings_transaction_id is the one reliable signal available on this
query: only a genuine savings transaction sets it. Checking it first
resolves the correct account whenever it's present, and falls back to
the previous behavior only when it's absent (current-account entries,
which don't set it).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
m_current_account and m_savings_account each keep their own identity
sequence, so the same numeric id can be a real row in both -- an id-only
lookup used to decide "is this a current account?" once sent a real
deposit into an unrelated client's current account instead of the
savings account a request actually named, across every entry point that
did this: cashier deposit approval/reprocess, clerk withdrawal approval,
and clerk disbursement collection/disbursement/bank-transfer. All of
those now also verify the account_no before trusting an id match.
Separately, GL posting for current-account transactions has been
silently failing since acc_product_mapping had two rows for the same
(product_id, product_type, financial_account_type) slot -- current
products share PortfolioProductType.SAVING's id space with real savings
products, and a NULL payment_type doesn't collide under that column's
own unique constraint, so duplicates went unnoticed. The single-result
repository lookup that read this mapping threw on the duplicate, which
was swallowed by a try/catch, so account balances kept updating while no
journal entry was ever posted. Both current-account GL lookups now take
the most recently created mapping instead of failing outright.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A SINGLE/DOUBLE FD opened with an explicit maturity instruction still auto-processes on its own
via the nightly job, exactly as before -- the grace period never delays that. An FD opened with
NO instruction at all (the new default -- see the frontend's account-creation changes) is now
never touched by the job; it just sits matured until staff renew it manually from the FD
Dashboard, backdated to the original maturity date within the product's grace window, or from
today with gap-period interest (at the linked account's rate) credited first once it's passed.
New endpoints:
- POST /fixeddepositaccounts/{id}/renew -- the Dashboard's own quick Reinvest choice
(Principal+Interest / Principal Only), for the two-option case.
- POST /fixeddepositaccounts/{id}/credit-gap-interest -- a pre-step the frontend's Payment
dialog now calls before submitting the existing generic `payment` command, so the Dashboard's
Renew action offers every closure type Payment does (Transfer to Savings, both Reinvest
options, Partial Reinvest), not just two, while still crediting gap-period interest first.
Also fixes a real, pre-existing bug found while building this: REINVEST_PRINCIPAL_ONLY never
actually paid its leftover interest anywhere -- handleFDAccountMaturityClosure withdrew only the
principal into the new deposit and never referenced its own toSavingsId parameter, silently
stranding the interest on an account it also never closed. Now transfers the remainder to the
target savings account and actually closes the old account.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New per-FD-product "Renewal Grace Period (Days)" term field: null/0 (every existing product)
keeps today's instant, same-night auto-renewal; a positive value is later read by the nightly
maturity job and the manual Renew action to decide whether/how a matured FD waits for staff to
act instead of renewing itself.
Wired into the live product path end-to-end -- DepositsApiConstants param, DepositTermDetail
column + diffing, DepositProductAssembler (create and update), DepositProductReadPlatformServiceImpl,
FixedDepositProductData -- migration 0572 adds the column.
Also wired into the maker-checker pending-edit mirror (m_saving_product_temp /
FixedDepositEditFields / ProductPendingEditFieldMapper), a second, independent storage path a
governed product edit uses -- without this the field silently dropped out of a checker's
"Proposed changes" review even though it was submitted. Migration 0573 adds its column.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New GET /v1/fixeddepositaccounts/maturity-dashboard: a narrow, purpose-built query for the FD
Dashboard's maturity ladder (account/client/product identity, maturity date, deposit/maturity
amounts) instead of routing through the general FixedDepositAccountsApiResource#retrieveAll,
whose mapper joins six extra tables for audit/currency/tax fields this screen never shows.
Adds the index migration (0571) the dashboard's WHERE clause needs -- neither
m_savings_account.deposit_type_enum/status_enum nor
m_deposit_account_term_and_preclosure.maturity_date had one before, so the endpoint would
otherwise run a full table scan on top of being wide.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A clerk raising a disbursement request with a Dr leg against a savings account had no way to
know the withdrawal would be refused (inactive account, still locked-in, or insufficient
balance against its own minimum) until a manager tried to approve it and the whole request
bounced back. Checks the same conditions SavingsAccount.withdraw() enforces at entry time
instead, against the same columns, so the clerk sees the real reason immediately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SYMPTOM
An FD account whose entire term fits inside one interest-posting
period (e.g. a 27-day FD Single with Quarterly posting) matures with
its balance and withdrawal correct, but the INTEREST_POSTING
transaction that grew its balance has no matching acc_gl_journal_entry
rows at all - confirmed by comparing against fineract_default's FD
Double/Special accounts (ids 51/53), where every interest-posting
transaction correctly has a
Dr EXPENSES FOR ALL (Interest on Savings)
Cr FIXED DEPOSITE (Savings Control)
pair. Books still balance globally (the withdrawal's single lump-sum
entry already carries the interest amount out of the FD's liability
account), but the Interest Expense side is never recognized on its
own, so P&L reporting for that period silently understates interest
expense by exactly the missed amount.
ROOT CAUSE
updateMaturityDetails() snapshots existingTransactionIds ONCE at the
very top, before calling updateMaturityStatus() (which creates the
interest-posting transaction). The old comment here said this was
deliberate - "interest postings ... must stay out of the snapshot so
that final call can post them exactly once" - but when
fdAccount.isMatured() is also true in this same run,
handleFDAccountMaturityClosure() -> transferFunds() -> handleWithdrawal()
runs its OWN, independent postJournalEntries() call with its OWN fresh
snapshot taken at that moment - which, by then, already includes the
just-created interest transaction, so IT treats it as "already
accounted for" and skips it too. Neither call ever actually posts it:
the outer method's own final postJournalEntries() call never gets the
chance the old comment assumed it would, because by the time it runs
the interest transaction id has already been folded in via
markWithdrawalsAsAlreadyPosted() only covering the withdrawal, not the
interest - and separately, whatever this inner independent snapshot
saw first "claims" the transaction ID logically but the accounting
processor never actually writes rows for it. Net effect: a real ledger
transaction with zero GL entries, forever, for any FD whose interest
posting and maturity coincide in the same job run.
Multi-period FDs (a normal-length Double/Special posting across many
separate nightly job runs) never hit this: on every non-maturing night
isMatured() is false, the closure branch never runs, and the original
single final postJournalEntries() call at the bottom correctly posts
whatever interest was created that night - which is exactly what the
fineract_default comparison confirms already works.
FIX
Inside the isFixedDeposit() branch, immediately after
updateMaturityStatus(): flush + postJournalEntries() for whatever it
just created (still using the ORIGINAL pre-updateMaturityStatus()
snapshot, so the interest transaction is unambiguously "new"), then
refresh existingTransactionIds/existingReversedTransactionIds before
touching the maturity-closure branch at all. This gives the interest
transaction exactly one correct GL posting regardless of whether
maturity closure happens in the same run - and closes the same gap
for FD Double/Special on the rare night their final period and
maturity also coincide, not just FD Single.
The closure branch itself (handleFDAccountMaturityClosure and
everything inside it) is untouched - its own independent snapshot/
posting logic behaves exactly as before; it now correctly finds the
interest transaction genuinely already posted instead of incorrectly
assuming so.
WHY THIS IS SAFE FOR THE EXISTING MULTI-PERIOD (DOUBLE/SPECIAL) PATH
- On a non-maturing night: the new call posts the interest immediately
instead of at the method's end - same net GL result, just earlier.
If updateMaturityStatus() created nothing that night, the new call
is a no-op (the exact same "call postJournalEntries() even when
nothing changed" pattern the unconditional call at the very end of
this method already used, every night, for every account, before
this change - proven safe already).
- The RecurringDepositAccount (else-if) branch is completely untouched.
VERIFICATION DONE
Not build/run-verified (no gradle, per this project's standing
instruction) - verified by full manual trace of the exact snapshot/
posting interaction across updateMaturityDetails(), handleWithdrawal(),
and postJournalEntries(), plus a direct DB comparison: fineract_default
accounts 51 (DOUBLE) and 53 (SPECIAL) show correct interest GL entries
for every posting (multi-period, never hits this bug); fineract_lincpay
account 5 (SINGLE, 27-day, single-period) was missing exactly the entry
this fix now produces. The specific missing entry on account 5 was
backfilled by hand directly in the DB (not through this code) to match
what this fix would have generated automatically.
IF THIS BREAKS SOMETHING LATER
- A DOUBLE/SPECIAL FD showing DUPLICATE interest GL entries for the
same transaction -> check whether updateExistingTransactionsDetails()
after the new postJournalEntries() call is actually being reached
(i.e. no exception thrown by that call swallowing the refresh).
- An FD's interest-posting transaction still missing its GL entry ->
query acc_gl_journal_entry WHERE savings_transaction_id = <the
INTEREST_POSTING txn id> and confirm 0 rows; if so, this fix (or its
ordering relative to a future change here) has regressed.
- Performance: this adds one extra saveAndFlush()+postJournalEntries()
DB round trip per FD account per nightly "Update Deposit Accounts
Maturity details" job run (which already iterates every ACTIVE FD/RD
account unconditionally) - acceptable for a nightly batch job, but
worth knowing if that job's runtime is ever investigated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SYMPTOM
A cashier could credit an FD/RD account with cash and the money moved
in instantly, with no Manager approval step - unlike a Savings/Current
deposit from the same screen, which correctly creates a PENDING
m_cashier_deposit_request. Discovered by tracing why a real FD
funding deposit never showed up in Branch Ops -> Approvals: it had
posted directly, no request record ever existed for it.
ROOT CAUSE (two layers, both fixed here / in the paired frontend commit)
1. Frontend gate (transaction-entry.component.ts confirmSubmit(), see
frontend commit): the maker-checker branch for a cashier deposit
only matched row.acTyp in ('SD','CA','CB','GL') - 'FD' and 'RD' were
never in that list, so a deposit into either fell through every
other branch to the generic, unguarded direct POST at the bottom of
confirmSubmit().
2. Even with the frontend gated correctly, approve() here only knew
how to tell Current apart from "everything else -> plain savings
deposit command" (isCurrentAccountById()). FD/RD accounts share the
m_savings_account table with plain Savings (distinguished only by
deposit_type_enum 200/300 vs 100), so an FD/RD deposit request would
have been posted through the generic savingsAccountDeposit command,
which does not accept a term-deposit account id.
FIX
- resolveDepositAccountCategory(accountId): looks up the real category
fresh from the DB (m_current_account / m_savings_account.
deposit_type_enum) instead of trusting the request's own account_type
string - same defensive pattern this class already used for Current.
- New executeFixedDepositDeposit() / executeRecurringDepositDeposit():
post through Fineract's actual FD/RD deposit commands
(CommandWrapperBuilder#fixedDepositAccountDeposit /
#recurringAccountDeposit, hitting /fixeddepositaccounts/{id}/
transactions and /recurringdepositaccounts/{id}/transactions) instead
of the generic savings command.
- approve() now branches on all four categories (Current / Fixed
Deposit / Recurring Deposit / Savings) instead of two.
RELATED DATA CLEANUP DONE THIS SESSION (not in this commit)
The one FD deposit made before this fix existed left an orphaned
m_savings_txn_denomination row (physically "counted" cash with no
approved request backing it), which inflated the Branch Day Register's
"Vault + Cashiers" total above the approved-requests-only "book" total
by exactly that amount every day going forward. That artifact row was
deleted directly (not backfilled with a fabricated approval - the
Claude Code safety layer correctly refused that approach as audit-log
tampering). No code in this repo does that cleanup automatically.
IF THIS BREAKS SOMETHING LATER
- A cashier FD/RD deposit posting instantly again -> check the
frontend gate list in confirmSubmit() (paired commit) still includes
'FD'/'RD'.
- approve() throwing on an FD/RD deposit request -> check
resolveDepositAccountCategory() is returning FIXED_DEPOSIT/
RECURRING_DEPOSIT (query m_savings_account.deposit_type_enum for the
account id in question: 200 = FD, 300 = RD).
- Branch Day Register showing a mysterious Vault-vs-book Excess again
-> check for an m_savings_txn_denomination row with no matching
m_cashier_deposit_request; that combination should be impossible now
that this fix is in place, so a new one appearing means this fix (or
its frontend half) regressed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SYMPTOM
Creating an FD/RD account with "Deposit Account Link" enabled and a
Savings/Current account correctly picked in the UI still failed with:
"A share account must be chosen when Share Linking is enabled."
even though no share account field exists anywhere on the FD/RD form.
ROOT CAUSE
SavingsAccountAssembler.applyCbsFields() has one boolean flag,
shareLinkingEnabled, serving two unrelated features that got
collided together (traceable to commit dc0539d53, which repurposed
the flag for co-op Share Account linking on plain Savings products):
- On a plain Savings product: "Share Linking" = link a co-op
m_share_account (shareAccountId).
- On an FD/RD product: the SAME flag is literally labeled "Deposit
Account Link" on the product screen
(fixed-deposit-product-details-step.component.html) and is meant
to require the pre-existing, native linkedAccountId field (already
wired end-to-end: SavingsAccountAssembler sets it a few lines below
this check, the FD "Maturity Instructions: Transfer to Savings"
flow already depends on it, DepositAccountReadPlatformServiceImpl
already reads it back for display).
Before dc0539d53, 7 real historical FD accounts (ids 51-57 in
fineract_default) prove linkedAccountId was the correct, working
mechanism for this. The block added by that commit made shareAccountId
mandatory unconditionally, for every product, breaking FD/RD.
FIX
SavingsAccountAssembler.applyCbsFields(): when shareLinkingEnabled is
true, branch on account.depositAccountType():
- FIXED_DEPOSIT / RECURRING_DEPOSIT: require linkedAccountId (the
field the FD/RD form already sends) instead of shareAccountId.
- everything else (plain Savings): unchanged, still requires and
validates shareAccountId against m_share_account.
SCOPE
Single method, single file. No new column, no new flag, no frontend
change - the frontend's Deposit Account Link picker was already
correct; only this one validation block was reading the wrong field
for FD/RD.
IF THIS BREAKS SOMETHING LATER
- "A share account must be chosen..." reappearing for FD/RD -> check
account.depositAccountType() is resolving to FIXED_DEPOSIT/
RECURRING_DEPOSIT at this point (it's a simple override per account
subclass, see FixedDepositAccount#depositAccountType()).
- A plain Savings product's genuine co-op Share Linking breaking ->
that path (the `else` branch here) is untouched; if it breaks, the
regression is elsewhere.
- A much larger version of this fix (a whole new
depositAccountLinkRequired flag decoupled from shareLinkingEnabled,
~18 files) was attempted earlier in the same session and fully
reverted at the user's request after repeated Gradle build-cache
issues made it impossible to verify was even running; this is the
intentionally minimal replacement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SYMPTOM
Opening the FD account create screen for a "SINGLE" (Days < 1 Year)
product, entering a Deposit Tenure under 30 days (e.g. 27), and
clicking "Find Rate" returned a 400:
"The parameter `tenureInMonths` must be greater than 0."
ROOT CAUSE
The Rate Calculator preview always converted the entered tenure to
months before calling GET /fixeddepositaccounts/calculate-fd-interest
(fixed-deposits.service.ts -> fixed-deposit-account-interest-rate-
chart-step.component.ts, calculateRate()):
tenureInMonths = Math.floor(enteredDays / 30)
For any tenure under 30 days this floors to 0, and
DepositAccountDataValidator.validateForInterestCalc() requires
tenureInMonths > 0 unconditionally, so the request was rejected
before it ever reached the calculation service.
FIX
Added `tenureInDays` as an alternative to `tenureInMonths` for this
one calculator endpoint only (DepositsApiConstants.
FIXED_DEPOSIT_ACCOUNT_INTEREST_CALCULATION_PARAMETERS) - not a
general-purpose FD field, not touched anywhere else:
- DepositAccountDataValidator: tenureInMonths is required unless
tenureInDays is a positive value (either satisfies the check).
- FixedDepositAccountsApiResource#calculateFixedDepositInterest:
accepts the optional tenureInDays query param.
- FixedDepositAccountInterestCalculationServiceImpl#calculateInterest:
when tenureInDays > 0, the exact-calendar maturityDate is computed
via submittedOnDate.plusDays(tenureInDays) instead of
plusMonths(tenureInMonths) (which would silently compute a 0-day
maturity window for a sub-month tenure).
- Frontend (separate commit) now sends tenureInDays instead of a
truncated-to-zero tenureInMonths whenever the entered tenure is
under 30 days.
SCOPE / WHAT THIS DOES NOT TOUCH
Only the interest-preview calculator endpoint above. Real FD/RD
account submission still uses depositPeriod + depositPeriodFrequencyId
(Days=0) exactly as before; this commit does not change how an actual
account's maturity date/amount gets computed at activation.
IF THIS BREAKS SOMETHING LATER
- "tenureInMonths must be greater than 0" reappearing for a SHORT FD
tenure -> check the frontend is actually sending tenureInDays (network
tab on the Find Rate call); the validator's OR-logic is in
DepositAccountDataValidator.validateForInterestCalc().
- A wrong preview maturity amount for a sub-month FD Single -> check
FixedDepositAccountInterestCalculationServiceImpl#calculateInterest's
branch choosing plusDays vs plusMonths for maturityDate.
- This bug and fix were previously implemented once, then fully
reverted mid-session at the user's request (unrelated Deposit Account
Link regression got tangled up with it), then reapplied cleanly here
as a standalone, narrowly-scoped fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collections used to reach a customer the moment they were typed, on the reasoning that the money is
theirs from the moment the agent takes it. That holds until the agent does not arrive - and then the
books say a customer was credited with notes the bank never held, and there was nothing to count the
agent's bag against when they did.
An agent now records their own round. Those entries sit in m_dds_collection_entry and move nothing:
no balance, no ledger, no agent cash. A clerk counts the cash against what they add up to and
settles the day, and what that writes is an ordinary collection - posted through the counter's own
code rather than a copy of it, so the two cannot drift apart.
Each agent may also hold their own ledger account now, one agent to an account. Both sides of the
entry, their direction and the amount are unchanged; only which account plays the agent's part
differs, and only for agents that name one. What is left standing on an agent's own account is the
cash they are holding and have not brought in - a question nobody could answer while every agent
shared a single account.
A scheme can close its own accounts on maturity, for accounts whose customer named where the
proceeds go. The run is generated and reviewed before anything moves, and an account that named no
destination is simply not on the list and is paid out by hand as before.
Also: the payment mode is a fixed set and stored one way; an agent's birth date must be in the past
and of age; a maturity credit account is checked whenever it is written rather than only when the
Adv Code happened to arrive beside it; and two report columns stop printing money ungrouped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only vouchers tagged with the "Share Collection" entry type reach a share
account. The adoption path already checked this, but the listing did not, so an
already-adopted "General" voucher kept reappearing at Apply Collection and Apply
Additional Shares time. SHARE_COLLECTION_TAGGED_ONLY now filters the listing
query itself, against both the cashier-deposit and clerk-disbursement request
tables.
/unattributed takes an optional clientId and answers with both lists in one
response -- that member's own unapplied receipts under "own", every other
available receipt under "rows", with the member's own excluded from the latter.
The two endpoints read the same table under the same filters, so asking twice
only ever fetched the same rows twice, and keeping the query in two places is
what let the entry-type filter land on one and miss the other. Both now share
unappliedFor().
The entry fee is never posted a second time. A receipt already sitting in an
INCOME GL was recognised as income by the cashier's own entry when it was taken,
so applying the collection now skips both legs for it and counts it only towards
verifying the fee was collected.
Adds GET /sharereports/product-summary: authorised, issued, available and unpaid
shares per product, with the distinctive range and account count.
Adds a series-overlap pre-check to sponsor DD inventory registration, which
previously surfaced as a bare duplicate-key error.
Adds migration 0568 for the columns the clerk share-withdrawal settlement path
reads (settlement_mode, tok_no, cash_settlement_txn_id, cashier_id, disbursed_by,
disbursed_at, disbursement_denomination_data), each changeset guarded so it is a
no-op where the column already exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Remove the client-mismatch validation in the Additional Shares tranche
submit path (ShareAccountDataSerializer) for parity with the same decision
already made on the first-application Apply Collection flow -- a voucher a
counter was shown and allowed to pick should not then be rejected for the
same reason at the very last step
- Rewrite m_share_advance_collection.client_id to the settling account's own
client at the moment a voucher is actually applied (both
ShareAdvanceCollectionApiResource.apply() and
AdditionalShareTrancheFundingService.settle()) -- a voucher can now be found
and applied from any member's screen, so whoever last searched it is not
necessarily who its money became share capital for, and the stale value was
misnaming the receipt in its own detail popup after settlement
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add ShareReportsApiResource (final balance, account statement, issued/status/
nominal/share-wise/guarantor/member-interest reports, enquiry, payment
certificate, share detail, dividend statement, folio statement, distinctive
numbers, area-wise, mailing list) and ShareTransferApiResource, applying the
paid-up-capital predicate everywhere so an unsettled application never
counts as issued capital
- Add share transfer migration (0567)
- Enforce the original application's collection before a further tranche can
be applied (ShareAccountDataSerializer), matching the same gate on screen
- Fix postGlOffsetEntry silently discarding the real GL transaction id on the
clerk-collection fallback posting path, leaving gl_transaction_id blank and
the receipt's GL-detail popup empty
- Add gl_code to the by-voucher and adoption responses so the GL Account
picker on screen follows whichever receipt was found or adopted
- Require an exact "Share Collection" entry-type tag to adopt a voucher --
"General" and untagged rows no longer qualify
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Voucher numbers now embed the year (PREFIX/DDMMYYYY/NNNN) so the same day
and month a year apart can never produce the same voucher_no text, which
every lookup/dedup across the cash registers matches on literally.
/shareadvancecollection/unattributed now lists every unapplied Share
Collection receipt across every share account screen, not just ownerless
GL deposits -- filtered to rows whose current Entry Type is still Share
Collection even after adoption, with the linked member named for
transparency. Applying a receipt to a share account no longer blocks on a
client mismatch, per product decision.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A cashier Receipt or clerk Collection into a GL account never named who or what it was for,
so a manager applying a share request could adopt a voucher a cashier had actually collected
for a Demand Draft, and vice versa. Adds a GLEntryType code (General/Share Collection/DD Issue,
extensible via Manage Codes) that the counter sets at save time; Share Advance Collection and
Sponsor DD issuance both check it before adopting a voucher, and also cross-check each other's
own consumption tables directly so the guard holds even for untagged legacy rows.
Also:
- Share-Collection-tagged vouchers auto-surface on Apply Share / Additional Share screens
instead of requiring a manual GL-account search, but only when a matching client_id already
exists on the row or the request is scoped to a specific member's screen -- a clientless GL
receipt is never auto-attributed to whoever's screen happened to be open.
- gl_transaction_id column on m_clerk_disbursement_request captures the real GL posting a
Collection approval makes, previously generated and discarded.
- New GET .../gl-entries endpoint exposes a receipt's actual Dr/Cr legs for detail popups.
- Additional-share tranches restricted to a single funding voucher for now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ledger transaction ids:
- New LedgerTransactionId generator (PREFIX-YYMMDD-NNNN, e.g. DIVRND-260916-0001) backed by a
dedicated m_ledger_txn_sequence counter (migration 0563) that belongs to no business table, so
no table's sequence reset can make an id repeat. Migrated 18 hand-rolled id sites (dividend,
branch day, closure, year-end, share collection/advance) off ad hoc nanoTime()/PK-concatenation
strings.
- ShareAccount.getShareAccountTransaction() matched a just-saved transaction by date+shares+price+
type and took the first hit -- two identical transactions on the same day resolved to the wrong
one and posted under the older transaction's id. Now matches by id where one exists.
Share accounting -- Cash at Vault fully retired from the share account/product lifecycle:
- New ShareAccountingSuppression lets a register that posts its own accounting tell the platform
not to post its native version (mirrors VoucherService.withOwnVoucher). Wired into share
withdrawal, additional-shares apply/approve/reject and account close, so every governed flow now
posts one balanced entry against the real counterparty instead of the platform's fixed Shares
Reference placeholder, reversed and reposted.
- Modify Application and Reject (the account's first application, pre-approval) no longer post via
the native bridge at all -- no money has moved at that stage, so there was nothing to post.
- The native "close" command no longer force-redeems a share account's remaining holding in one
unguarded step; closing with a balance outstanding is now refused, so redemption can only happen
through the governed Share Withdrawal flow the frontend already restricts it to.
- "Share Reference (Cash Vault or Suspense)" is optional on the product, not mandatory: validator,
GL mapping write/merge (including removing a stale mapping when the field is cleared on edit),
and AccountingProcessorHelper's resolver (now throws a named error instead of NPEing when a
product carries no mapping) all updated together. Removed from the product UI entirely (create,
edit, preview, view).
- AdditionalShareTrancheFundingService: submit posts nothing; settle() posts the whole entry itself,
debiting whichever GL a voucher receipt actually landed in (read back from its own posting) or the
member's savings account, crediting Share Capital. A voucher-funded tranche whose receipts total
more than it costs now credits the difference to the member's linked savings account -- every
named receipt is visited, not just enough to cover the tranche, since one left untouched here was
still marked APPLIED and its money left untraceable.
- ShareAccountDataSerializer: redeeming every share down to zero is exempt from the product's
minimum-shares check (the check protects an account staying open with a reduced holding, not one
being closed via a full exit); an over-payment on a voucher-funded tranche is refused at submit
when no savings account is linked to receive it, rather than accepted and left unable to settle;
six failWithCode(code, message) calls in the tranche validator were silently discarding their own
message for the generic fallback text (failWithCode's second argument is a template arg, not a
displayed string) -- switched to failWithCodeAndMessage.
Removed: ShareCollectionApiResource, a direct-cash counter-collection endpoint that read
m_share_account.cashier_collected, a column that has never existed in this schema -- no call site
in this codebase's frontend reached it, and no entry it could have produced ever has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Share withdrawal (clerk):
- Add GL and NONE settlement modes alongside the existing SAVINGS mode. A clerk
can now credit any GL account on the CR row of a Fund Transfer instead of the
member's linked savings account; picking the product's own Sundry account
self-cancels, so it downgrades to NONE and the request stands on leg 1 alone.
- Post Dr Sundry / Cr chosen GL for GL mode; raise a standalone Dr Share /
Cr Sundry voucher for NONE mode, read back from the ledger.
Sponsor DD:
- Let a clerk fund a Demand Draft by debiting a GL account directly
(payment_method = GL) under the existing maker-checker flow, reusing the
shared postGlOffsetEntry() so only the debit-side GL resolution differs.
Cash reports and vouchers:
- Fix the Closing Cash Report total row and opening-balance carry-forward.
- Read the real till GL back from the underlying transaction when raising
withdrawal, closure and loan vouchers, instead of labelling every leg
"Cash in Hand".
Ledger transaction-id hardening:
A derived transaction_id text (e.g. "SH" + share transaction id) is not
guaranteed unique if the underlying numeric id is ever reused, which a manual
sequence reset can cause. Queries that read those rows back now disambiguate,
and flag flips can no longer touch an unrelated transaction:
- Read-back sites pick the most recently posted matching rows (ORDER BY id DESC
+ LIMIT) or, where the read happens long after posting, the candidate group
whose created_date sits closest to the owning request's actioned_at.
- Every UPDATE ... SET reversed = true/false WHERE transaction_id = ? is now
guarded by the opposite reversed state, so a collision can only flip rows
that are currently live.
- Covers clerk disbursement, sponsor DD, cheque book, clearing, share tranche
funding and share withdrawal.
Migrations 0551-0554: settlement GL account and voucher_no columns for the
clerk share withdrawal, dividend claim and sponsor DD issue request tables.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Add CountedCashLedger: one shared rule for which m_savings_txn_denomination
rows are real counted cash. Replaces a magnitude-only guard (which had
itself been silently excluding legitimate manual-GL-entry cash from every
cash screen) with a date-plausibility test -- a stamped garbage id decodes
back to the day it was written, a real GL reference to an arbitrary date --
so a hex reference that happens to land in the old guard's excluded band is
no longer misclassified as timestamp garbage
- Apply it consistently in the day register close preview, the cashier
position/day-book reports, the cash reconciliation screen, and the
withdrawal stock check, so all four agree on the same drawer
- GL Entries report: join m_voucher_line to show both legs of each voucher
(debit and credit account) instead of only the cash leg its own
denomination row happens to carry
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add balancesheet API, GL account transaction guard, and disabled-feature filter
- Recognize an approved Clerk Disbursement Request (COLLECTION direction) as a
second valid source when resolving a Share Advance Collection voucher, not
just a Cashier Transaction Entry deposit
- Fix a NullPointerException in ShareAccount.getShareAccountTransaction() when
an existing charge transaction has no total-shares/purchase-price recorded
- Null out DormancyClock's "last live" date before an account is ever
activated, instead of falling through to the client's own KYC re-verification
date via Postgres's null-skipping GREATEST()
- Stop seeding "Cash at Tellers" for new tenants (0502); drop the unused
Facility & Service Charges Income accounts (0548); group header-less GL
accounts under real-world header accounts (0549); uppercase GL account
names and rename Share Sundry to match the bank's own chart (0550)
- Fix 0527's savings-daily-interest changeset to skip objects 0517 already
creates on a fresh tenant, instead of failing tenant startup
- Update the local Postgres dev password default to match this environment
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audited role usage/permissions across all tenants and cleaned up several
inconsistencies:
- Removed 'Self Service User' (zero users, no wired-up purpose in the app).
- Merged 'As Manager' into 'Branch Manager' (union of both permission sets,
users reassigned, As Manager dropped) since the two covered overlapping
ground with no clear split.
- Revoked Clerk's ALL_FUNCTIONS grant: it unconditionally bypassed every
individual permission check (HasPermissionDirective), so unticking any
menu/permission for Clerk in Roles & Permissions had no effect. Clerk's
own explicit permission rows are untouched and remain the effective set.
- Renamed 'CASHIER' to 'Cashier' for display consistency with every other
role name; the role-name check this touches (CashierRoleHelper) is
case-insensitive, so behavior is unchanged.
('Principal Officer' was audited too but kept - it's the statutory
PMLA/RBI compliance seat from 0435, just not yet assigned to anyone.)
Each change was applied directly to all three tenant DBs and captured as
a migration (0543, 0546, 0547), dry-run verified as a no-op against the
now-updated DBs so it applies cleanly here and correctly for any new tenant.
Adds a per-tenant ceiling on which of the 27 top-nav sections a bank can ever
show, closing a real gap the "Clerk" role's ALL_FUNCTIONS misconfiguration
exposed this session: HasPermissionDirective already hid a menu a user's own
role lacked, but any role carrying ALL_FUNCTIONS bypassed that check
unconditionally, so a bank's own super user (or a role accidentally granted
ALL_FUNCTIONS) could always see everything regardless of intent.
New table m_tenant_menu_access (0541), one row per menu code, seeded enabled
for every existing tenant so nothing already deployed changes behaviour.
New TenantMenuAccessApiResource:
- GET /v1/tenant-menu-access — dual-mode (session or X-Api-Key), same posture
as ClientAllAccountsApiResource. A tenant's own users read their own list
to render their nav; the platform operator reads any tenant's list with
the key, to know current state before toggling it.
- GET /v1/tenant-menu-access/tenants — API key only, the master tenant list
for the operator's own tenant picker.
- PUT /v1/tenant-menu-access — API key only, no session fallback at all, even
for a super user. This is the actual security boundary: a bank's own admin
has no path to lift its own ceiling, because there is nothing inside their
own tenant that can satisfy this endpoint's auth requirement.
0542 splits two nav items that had been sharing a permission code with their
neighbour — Loan Origination rode on MENU_ADVANCES, Investment rode on
MENU_RBI — into their own MENU_LOAN_ORIGINATION / MENU_INVESTMENT codes, so
all 27 top-nav items are now independently controllable. Every role that
already held the shared code is granted the new one in the same migration,
so this changes nothing about who sees what today.
SecurityConfig: permitAll for the two API-key-only paths, same posture as
the existing clientaccounts carve-out — the handler itself is what actually
authenticates the caller, not Spring Security's outer gate.
BranchDayRegisterApiResource:
- Make openDay/closeDay transactional and add an atomic closed_at guard so a
double-close or a mid-write failure cannot leave the register half-updated.
- Fix bookBreakdown()'s slip/closure withdrawal queries: m_clerk_withdrawal_request
and m_clerk_closure_request store cashier_id as an appuser id in some registers
and an m_cashiers id in others; try both via a dual EXISTS instead of assuming
one, which was silently dropping real withdrawals from expected cash and
producing a false "Short" on close.
- Office-scope vault-in/settled-out and close-preview queries so a multi-office
tenant's close no longer mixes another branch's cash into this one's.
- closeDay requires closingCash and uses DateUtils.getBusinessLocalDate() for
late-close detection instead of the server's own LocalDate.now().
- saveDenominationDetail() now deletes any prior snapshot for the same
register/leg/teller before inserting, so a repeated save no longer sums
duplicate denomination rows into the vault position.
- resolveOffice() enforces office-hierarchy authorization via
PlatformSecurityContext.validateAccessRights().
CashReconciliationApiResource / CashReportApiResource / DenominationBreakdownWriter:
- Add a MAX_PLAUSIBLE_TXN_ID filter (consistent with the existing cash report
guard) so a fabricated/oversized transaction id can no longer distort a
cashier's expected or counted position.
- Tolerate sub-rupee (paisa) rounding differences instead of requiring an exact
amount match — Indian cash has no paisa coins, so exact-equality was
rejecting correctly-counted cash.
- CashReconciliationApiResource now trusts its own server-computed expected
count rather than the client-submitted one when recording a reconciliation.
VaultPositionService:
- Only fall back to the branch-total OPENING leg when the office has exactly
one teller; a multi-teller branch was getting one teller's vault entry
applied to all of them.
CashSettlementApiResource / CashInterCashierApiResource / CashExchangeApiResource:
- Make settlement creation transactional; add a partial unique index
(0540) blocking two active settlements for the same cashier/day, closing a
double-submit race the application-level count check alone could not.
- Validate a cash transfer against the sending cashier's actual denomination
holdings and require both cashiers to be in the same office.
- Default cash exchange date to the business date instead of null, and add the
missing CREATE/REVERSE permission checks (0539 seeds them).
CashierDepositRequestApiResource:
- Namespace pending-deposit denomination rows with a +500,000,000 offset so
they cannot collide with real transaction ids during approve/reject.
ClerkDisbursementRequestApiResource (Disbursement/Collection/Fund Transfer/Bank
Transfer register):
- executeSavingsWithdrawal() was missing the withOwnVoucher() suppression its
sibling methods already had, so every Collection-mode approval raised a
second, orphaned voucher for the same money on top of the request's own —
double-counting the figure in Received and Payments on My Vouchers.
- Voucher numbering no longer hard-codes CR/DR by money direction. It now
checks whether the payment type the clerk actually selected is cash
(m_payment_type.is_cash_payment) and numbers into the Journal series when it
is not, since a Transfer/Cheque/DD instrument never crosses the counter
regardless of which GL it posts against. Bank Transfer vouchers (always
account-to-account, never cash) are now unconditionally Journal too.
- The voucher's TYPE column stopped being hard-coded to "DISBURSEMENT" for
every request this register raises; it now stores the clerk's own request
type (Disbursement/Collection/Fund Transfer/Bank Transfer) so My Vouchers
shows what was actually submitted.
VoucherVerificationApiResource (/my-work):
- Classify a voucher's side (Received/Payment/Journal) by whether either of
its lines actually touches the till (Cash in Hand or a cash-at-vault/teller
GL), the same test the register listing already uses, instead of trusting
the voucher number's own CR/DR prefix at face value. A Collection or
Disbursement posted against a plain GL (no cash involved) was being counted
as real cash received/paid; it now reads as Journal, matching what actually
happened at the counter. Existing CR/DR requests with no lines yet are left
alone, since there is nothing to test the till against.
Adds the missing DELETE /offices/{officeId} endpoint -- the UI's "Remove"
action called it already, but no backend route existed, so every attempt
failed with a generic error regardless of how empty the office was. Wired
the standard command-handler chain (CommandWrapperBuilder.deleteOffice ->
DeleteOfficeCommandHandler -> OfficeWritePlatformService.deleteOffice),
which pre-checks sub-offices/staff/clients/groups/users before deleting,
and catches any remaining FK violation on delete itself. The new
DELETE_OFFICE permission (0537) is seeded maker-checker-enabled (0538),
since this app runs with the global maker-checker switch on and office
deletion is irreversible -- 0538 is a separate changeset rather than
0537 itself because 0537 had already executed on local tenants by the
time that requirement surfaced, and editing an already-run changeset's
content breaks Liquibase's checksum validation on every tenant that ran it.
Also bundles a few DB/startup fixes found while getting this feature
running across local and a remote environment:
- application.properties: restores the tenant DB password default that a
teammate's own local password had overwritten in a previous push, and
adds server.error.include-message=always so a servlet-level rejection
(e.g. TenantAwareBasicAuthenticationFilter on a missing/invalid
Fineract-Platform-TenantId header) carries a real message instead of a
bare {timestamp,status,error,path}.
- changelog-tenant.xml / final-changelog-tenant.xml: moves the
0517/0518 table-creation includes ahead of 0525-0527/0533, which
read/write those tables but previously ran first per
db.changelog-master.xml's include order -- caused "relation ... does
not exist" on any tenant reaching that point for the first time.
- 0536_sync_job_identity_sequence.xml: job.id is GENERATED BY DEFAULT AS
IDENTITY; explicit-id job-seed inserts never advanced the backing
sequence, so a later identity-generated insert could collide with an
earlier explicit id ("duplicate key value violates ... job_pkey").
- ClientAllAccountsApiResource: NotFoundException(String) only sets the
exception's own message for logs, it never reaches the response body,
so a "client not found" 404 always came back empty; now built as a
proper JSON error body. Also collapsed the message/defaultUserMessage/
errors[0].defaultUserMessage triple down to one message field -- this
endpoint never raises more than one error at a time, so the repetition
added nothing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GET /v1/clientaccounts/{identifier} -- a single client's full deposit + share
picture (Savings, Fixed Deposit, Recurring Deposit, Current, Share), looked
up by client number or mobile number rather than the internal client id, so
a teller with only the slip in front of them (or a third-party caller with
only a mobile number) needs no separate client-search step first. Meant to
answer "is there a problem here" (e.g. a share purchase that would exceed a
product's issuable shares) BEFORE a manager approves something, not after.
Deliberately its own lightweight resource rather than a reuse of the
existing GET /clients/{clientId}/accounts (AccountSummaryCollectionData):
that endpoint's row mappers each carry six LEFT JOINs to m_appuser for a
full submitted/approved/rejected/activated/closed audit trail per account,
which this caller doesn't need. One UNION ALL query (SB/FD/RD from
m_savings_account split by deposit_type_enum, CD from its own
m_current_account table, Share from m_share_account), each leg filtering on
an indexed client_id -- fewer joins and one round trip is what actually
makes this fast, not a different query engine.
Identifier resolution tries m_client.account_no first (what a teller has),
then mobile_no (what a third-party caller more often has) -- both unique
indexed columns, so neither path costs more than a single index lookup.
Reachable two ways: an ordinary logged-in Fineract user exactly as with any
other endpoint, or a third-party caller via the new X-Api-Key mechanism
(see the ApiClientCredentialService commit) -- one key now valid across
every tenant, so the response echoes back tenantId/tenantName and the
identifier that was searched, and a not-found error names the bank actually
searched. Without that, "no client found" from the wrong bank (a stale or
mistyped Fineract-Platform-TenantId header) was indistinguishable from a
genuinely unknown client.
Every date field is serialized as a plain ISO string, never a raw
java.time.LocalDate: this Gson instance has no java.time adapter registered,
and reflectively touching LocalDate's private fields is blocked outright by
the JVM module system (java.base does not open java.time to an unnamed
module) -- passing one through untouched turns the whole response into a
500.
Endpoints that need to be reachable by a third-party integration without a
Fineract username/password used to have no supported way to do that other
than the heavyweight IMPS/NPCI pattern (RSA-signed JWT envelopes, built for
one specific bank-switch protocol). This adds a general-purpose one: mint an
X-Api-Key credential, hand it to the partner, done.
New table m_api_client lives in the tenant-STORE database (fineract_tenants),
not any one bank's own tenant database. That is deliberate: a key identifies
a CALLER, independent of which bank's data a given request asks for, so one
minted key keeps working against every tenant that exists now and every one
provisioned later -- a per-tenant copy would need a manual re-insert on every
new bank and a row per tenant per key, which is exactly what storing it once,
centrally, avoids. Fineract-Platform-TenantId keeps doing what it always
did: routes the rest of a request to the right bank's database. This table
only decides who is allowed to make the request at all.
ApiClientCredentialService.authenticate() is what an endpoint calls in place
of PlatformSecurityContext#authenticatedUser() when it sees an X-Api-Key
header. Security properties:
- Only a SHA-256 hash of the key is ever stored -- same reason Fineract
never stores a user's plaintext password. The raw key is returned exactly
once, at creation (POST /v1/apiclients), and cannot be retrieved again.
- Every failure path (key doesn't exist, is deactivated, caller's IP isn't
on its allowlist) raises the identical 401 message, so a probing caller
can't use the response to tell a wrong key apart from a valid-but-blocked
one.
- allowedIps is a plain comma-separated exact-match list, adequate for a
small set of known partner integrations, not CIDR ranges.
ApiClientAdminApiResource (create/list/deactivate/reactivate a key) is
itself gated by an ordinary Fineract login restricted to a super user
(ALL_FUNCTIONS) -- issuing a credential that skips login is not something to
hand out casually.
SecurityConfig gets one new permitAll() matcher, narrowly scoped to
GET /api/*/clientaccounts/** -- the same posture this fork already uses for
the IMPS and NPCI receivers: the outer Spring Security gate stays open for
one specific path, and the resource itself is what actually authenticates
the caller. A normal logged-in SPA user hitting that same path is
unaffected -- Basic Auth still runs for any request that carries an
Authorization header, permitAll only stops an anonymous one from being
rejected before the handler gets a chance to check for a key instead.
Cashier reports now reconcile against the drawer:
- Received/Payment exclude what cannot be cash rather than admitting
only proven cash, so registers that pay out without recording a
denomination breakdown are no longer silently dropped
- Closing report scoped to the cashier, with opening/closing balance,
reversed counter transactions excluded, and cash DD receipts counted
- Day book split into Cashier and Clerk sections, filed by whose cash
moved rather than who raised the request
Fixed one deposit or withdrawal raising two vouchers: the cashier
deposit and clerk withdrawal registers did not suppress the inner
savings command's voucher, doubling the day's cash on both reports.
Sponsor DD:
- Amendment request flow (beneficiary, payable-at branch and city)
under maker-checker; face value and funding stay unamendable
- Cash DD denominations were collected but never saved, and the amount
check demanded a figure notes could not add to; both fixed and the
breakdown is now required
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ticking "Close this account" used to only preview the payout: assemble the
account, run the same postInterest domain call close() will, read the
resulting balance, then roll the whole thing back (GET /preview/{id}).
Nothing was ever credited until the manager approved, and the account's own
Interest Posted / Interest Earned figures stayed stale in the meantime.
Changed to actually post the interest at that moment (POST
/post-interest/{id} and again in submit()), via
SavingsAccountWritePlatformService#postInterest(SavingsAccount, boolean,
LocalDate, boolean) -- the exact service method close() itself calls, not a
new calculation. Not routed through the POSTINTEREST_SAVINGSACCOUNT command:
that command ships maker-checker-enabled in base Fineract, and
CommandSourceService#processCommand runs the handler first, then throws
RollbackTransactionNotApprovedException from inside the @Transactional
method -- which would silently roll back the very posting just made.
Depends on the sibling SavingsAccount fix (double-posting across runs) --
without it, ticking the checkbox on one business day and having the manager
approve on a later one re-slices posting periods and credits the overlapping
days twice.
Also:
- Locks the closure date at submission (stored on the request row) and
replays that same date at approve() instead of re-reading today's business
date, so close()'s own postInterest(account, true, closedOnDate, false)
slices identical periods, matches the amounts already posted at submission,
and adds nothing -- the figure quoted to the member at submission is what
gets paid out however long the request sits in the manager's queue.
- Populates closing_amount/rounding_amount/cashier_disbursed_amount at
submission time instead of leaving them null until approval, since the
interest posting above already makes the real payout figure known then --
the manager's review screen no longer shows a blank amount for a PENDING
request.
SavingsAccount#postInterest silently double-credited interest whenever a
manual posting ("Post Interest As On", or close() closing an account) ran on
two different dates against the same account.
Root cause: postInterestOnDate=D makes SavingsHelper#determineInterestPostingPeriods
close a period at D-1, so the resulting transaction is dated D-1, not D. The next
run's getManualPostingDates() feeds that D-1 date back in as a boundary, which
closes a period at D-2 instead. The old posting's date (D-1) then matches no
period end in the new run, so findInterestPostingTransactionFor() never finds
it, it is never reversed or corrected, and a new posting is created alongside
it covering the very same days. total_interest_posted_derived drifts above
what was actually earned by exactly the overlapping amount.
Reproduced live: posting "as on" 02 Sep then 03 Sep on the same account left
a stray 0.35 posting dated 01 Sep plus a new 0.97 posting for 01-02 Sep --
1.32 posted against 0.97 actually earned.
Fix: before applying this run's periods, reverse any manual posting whose
date matches no period-posting-date this run computed AND whose days are
covered by a period this run WILL post (posting date <= interestPostingUpToDate).
The second condition is deliberate -- it is what stops this from reversing a
posting whose days are simply not being re-posted yet (e.g. the recalculation
Fineract runs right after a withdrawal, where the remaining period matures in
the future and posts nothing for those days). Automatic (non-manual) postings
are untouched; they sit on natural period ends, which always recur correctly.
Affects every caller of the base postInterest/calculateInterestUsing pair,
including FixedDepositAccount and RecurringDepositAccount (both delegate to
super.postInterest for their day-to-day interest recalculation, though not
for postMaturityInterest, which is a separate, untouched method) -- so this
also closes the same double-count window for FD/RD accounts, not just plain
savings.
Ticking "Close this account" previewed the payout with a dry run — assemble
the account, call the same postInterest domain method close() will, roll the
whole transaction back. The clerk asked for the actual thing: post the
interest now, the same as "Post Interest As On" always did and the same basis
FD uses at maturity, so the figure shown is the account's real balance rather
than a simulation of one.
Switched to calling SavingsAccountWritePlatformService#postInterest(SavingsAccount,
boolean, LocalDate, boolean) directly -- the exact method close() itself calls --
instead of going through the ordinary POSTINTEREST command pipeline. That matters:
POSTINTEREST_SAVINGSACCOUNT ships maker-checker-enabled in base Fineract, and a
Clerk is not a checker for it. Routed through commandsSourceWritePlatformService,
CommandSourceService#processCommand runs the handler first, then evaluates
maker-checker and throws RollbackTransactionNotApprovedException -- which, thrown
from inside that @Transactional method, rolls back everything the handler just
did and drops the request into Fineract's own generic Checker Inbox. This feature
already has its own maker-checker (submit here, approve in Closure Requests); the
posting now rides inside that instead of opening a second, unrelated approval
queue for the same click.
Left uncommitted-to on purpose: unticking the box or a later rejection leaves the
posting in place, since it is interest the member had genuinely earned either way.
Re-ticking re-runs postInterest safely -- the domain layer finds today's posting
already there and no-ops rather than posting twice. If manager approval lands on
a later business date, close() tops up the extra days on top; it never re-posts
what already matches the date it is closing on.
Endpoint changed GET -> POST to match: it now mutates. The dry-run machinery
(SavingsAccountAssembler kept, but PlatformTransactionManager/TransactionTemplate/
ConfigurationDomainService/MathContext all dropped) is gone with it -- the real
service method already handles all of that internally, the same way it does for
every other caller of postInterest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Day Close reported SHORT by exactly the amount a closure had paid out. The
"Cash paid out (approved withdrawals)" line only summed
m_clerk_withdrawal_request, so a closure -- which pays cash over the same
counter but lives in its own register -- was missing from the total. Expected
cash stayed high by what had actually been handed over, and a closure that
balanced perfectly read as a shortage.
Verified against live data: opening 145,304 + deposits 0 - slip withdrawals 0
- closure 12,999 = 132,305, which is exactly the counted branch total. Variance
goes from -12,999 to 0.
Sums cashier_disbursed_amount rather than closing_amount: the member's ledger
gives up the paisa-exact figure, but what physically leaves the drawer is that
figure rounded to whole rupees, and this total has to agree with a physical
count. Restricted to CLOSED rows dated on disbursed_at, since a closure takes
nothing out of the drawer until the cashier hands it over.
Note: dividend cash claims (m_share_dividend_claim_request) are also absent
from this total and will misreport the same way. Left alone deliberately --
that register splits a claim across loan/savings/cash with its own cash_status
column that nothing in the Java sets, so the right predicate cannot be
established from the code as it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in maker-checker for GL accounts, charges, product creation (loan,
savings, current, share, FD), admin users/roles and base interest rates, plus
the UserPendingRequest domain and AppUserPreference.
Two resolutions:
changelog-tenant.xml — both branches appended <include> lines at the end of the
file, so the conflict was append-vs-append with no semantic overlap. Kept both
sets in order: dev's 0470-0499 followed by maker-checker's eight. Liquibase runs
in <include> order, not filename order, so the reused 0466-0476 numbers change
nothing (dev already carries duplicate numbers in that range).
application.properties — the branch carried another developer's local database
password, which would have stopped this backend connecting at startup. Restored
ours; the file is now byte-identical to its pre-merge state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closing a savings account paid real cash out of a teller's drawer in one
click, with no maker-checker, no cash-position tracking, and no way to hand
over a paisa balance (no paisa coins circulate, and the denomination writer
demands an exact match). This adds a Clerk -> Manager -> Cashier flow for
Cash closures, modelled on the existing Clerk Withdrawal Request.
New ClerkClosureRequestApiResource:
- submit: Cash only, token required, guards ACTIVE status and open requests
- approve: runs Fineract's own close command (pro-rata interest and the real
GL withdrawal post here, atomically), reads the posted transaction back,
rounds the payout to whole rupees and posts the difference to a new
Round Off Account. A nil-balance closure goes straight to CLOSED rather
than parking forever in a cashier queue with no cash to hand over.
- disburse: cashier records the denomination breakdown against the rounded
figure and the account closes
- preview: dry-runs the very same interest posting close() performs, inside
a REQUIRES_NEW transaction that is rolled back, so the figure shown to the
clerk matches the real payout to the paisa. Reading the stored accrual
instead gave a days-stale number, and CALCULATEINTEREST is not equivalent:
it uses MathContext(15) and no postInterestOnDate, where close() uses
MathContext(10) and feeds the closing date in as a posting-period
boundary, which changes how many per-period roundings happen.
Vouchers: close() reaches handleWithdrawal directly rather than through the
withdrawal command handler, so the handler's own raiseSavingsVoucher never
ran and a closure paid cash out with nothing in the voucher register. One is
now raised at disbursement -- when the cash actually crosses the counter, in
the cashier's name -- with three lines that match the posted GL exactly:
Dr Savings Control + Dr Round Off = Cr Cash.
Cash direction: the rounding adjustment debited Cash where cash leaving the
drawer is a credit, leaving the cash GL disagreeing with what was physically
handed over. Both legs corrected.
Dates: closure_date, closedOnDate, the preview and the rounding entry now all
use the CBS business date, so preview and approval cannot land on different
days across a day roll.
Day Book: closures were absent entirely (it reads the request registers, not
m_voucher). Added as a fourth register, restricted to CLOSED rows and dated
on disbursed_at, since a closure only becomes a cash-book event at hand-over.
Its terminal status CLOSED was also missing from the totals gate, so a
completed closure moved real money out without appearing in the DR total.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Surfaces shareAccountId/shareAccountNo on the savings account read API so the
UI can display and auto-select an already-linked share account; adds an
optional search query param to the clerk cash and share withdrawal request
list endpoints for the Approval Center's new search boxes.
- Savings account creation validates and stores a required "shareAccountId" once Share Linking is
enabled, checked against the client's own active share accounts (raw SQL - fineract-savings has
no dependency on the shareaccounts domain). On save, mirrors the link back onto the share
account's own savings_account_id, the same way that field already gets set from the share side.
- New GET /clients/{id}/activeshareaccounts - a client's active share accounts, for the savings
creation screen's picker (kept separate from the shared Savings/FD/RD/Current template DTO).
- ShareAccountDataSerializer.validateAndCreate no longer requires savingsAccountId on create - the
DB column and domain relationship were already nullable, only this validation blocked it.
fix(branchday): stop the denomination carry-forward preview from doubling yesterday's closing count
GET /branchday/denominations classified every detail row that wasn't literally leg='OPENING' as
"closing" - which, once CLOSING_VAULT rows existed alongside CLOSING (single-teller offices record
the same breakdown under both), silently doubled every count shown as "yesterday's closing" when
opening the next business day. Filters to leg IN ('OPENING','CLOSING') explicitly instead of an
else-catches-everything branch.
feat(accounting): seed missing Income/Expense/Transfers-Suspense/Escheat-Liability accounts
The Basoda-derived chart of accounts (0523) carried none of these - a balance sheet has no P&L
heads, and Transfers Suspense/Escheat Liability aren't balance-sheet line items either - so a
Savings Product's accounting mapping had nowhere to point those required fields. Same guard as
0523: only seeds a Basoda-derived tenant that hasn't already gotten it, never touches a tenant with
its own chart of accounts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ClerkShareWithdrawalRequestApiResource: reverse native redeem GL legs and reroute through Sundry
(Dr Share Reference / Cr Sundry, then Dr Sundry / Cr Savings Control), reusing the native
transaction_id so both legs display as balanced; wholesale-reverse and reroute the linked savings
credit the same way.
- ShareAccountWritePlatformServiceJpaRepositoryImpl: undoApproveShareAccount no longer re-triggers
the native "isApplied()" phantom GL posting Fineract's accounting bridge makes on approval.
- VoucherService/ClerkWithdrawalRequestApiResource: attribute a voucher to the cashier who actually
disbursed cash, not the submitting clerk, once disbursal completes.
- CashReportApiResource: scope the GL-entries deposit/withdrawal joins by txn_type to stop a
hex/decimal txn_id collision from borrowing an unrelated transaction's voucher number; resolve
GL account names (not just client names) in the "A/C NAME" column.
fix(branchday,cashreport): stop the vault position from double-counting a cashier's unsettled cash
BranchDayRegisterApiResource's day-close folded every cashier's still-held cash into the branch's
combined closing figure, which then carried forward as next day's vault opening. When that cash was
later settled back to the vault, CashReportApiResource added it a second time - once already baked
into the carried-forward opening, once as the settlement itself - manufacturing a phantom "Excess"
equal to whatever was unsettled.
Adds a vault-only OPENING_VAULT/CLOSING_VAULT leg (new VaultPositionService, shared by both
resources) alongside the existing branch-total OPENING/CLOSING legs, so the vault's own anchor never
includes cash that still physically sits with a cashier.
feat(accounting): seed a missing opening-balance contra account and a real bank's chart of accounts
- Financial Activity 300 (opening-balance transfer contra) was never mapped in any tenant's chart of
accounts, so any opening-balance flow failed outright; backfilled it and added a migration to seed
it for every tenant going forward.
- Seeds a brand-new, otherwise-empty tenant with a real cooperative bank's chart of accounts
(transcribed from its balance sheet), guarded to never touch a tenant that already has its own.
- Removes 0451's hardcoded demo branch offices, which were seeding into every new tenant regardless
of institution.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add ShareAdditionRequestsApiResource (/v1/shareadditionrequests): lists every pending
additional-share tranche across all accounts, with a per-tranche detail view carrying the
route-specific proof of funding a list row can't show (which voucher(s) fund a cash tranche, or
which savings account a savings tranche already debited). Approval/rejection still go through
Fineract's own approveadditionalshares/rejectadditionalshares commands unchanged -- this is a read
side only, so the existing settle()/unwind() hooks fire from exactly one code path.
- Remove ShareSavingsDebitRequestApiResource and its m_share_savings_debit_request table (migration
0520): confirmed zero rows on both tenants since it was built, and superseded by Additional Shares'
own savings-funding route, which values a tranche on its own rather than by summing the account's
entire holding history.
- Update comments in ShareAccountWritePlatformServiceJpaRepositoryImpl, ShareAccountDataSerializer,
AdditionalShareTrancheFundingService and AdditionalSharesFundingApiResource that referenced the
removed class/endpoint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ShareAdvanceCollectionApiResource: drop the hardcoded/auto-resolved Share Suspense GL lookup for
by-voucher search; GL account and voucher are now both required, manually-selected inputs. apply()
now accepts a separately-verified entry-fee receipt (chargeCollectionId) so a fee collected into a
different GL than the share value is never swept into the Sundry leg or double-posted to income.
- ClerkShareWithdrawalRequestApiResource: hard-block approval when collection_applied is false, so
shares nobody has actually paid for can't be redeemed off an Active-but-unsettled account.
- ProductPendingEditFieldMapper / ShareProductDataSerializer: fix maker-checker edits on share
products silently dropping charges (chargesSelected vs charges field-name mismatch) and NPEing on
approval when the replay JSON carries no currencyCode.
- SavingsAccount: initialize the clearing-hold field to zero so new savings submissions don't violate
the NOT NULL constraint on total_savings_amount_on_hold_clearing.
- CashReportApiResource: fix voucher number resolution for GL-type ledger entries.
- application.properties: revert local tenant DB password default.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closing a savings account used to only ever validate that interest had
already been posted on the closing date (opt-in, off by default) -- with
nothing posted beforehand the partial period's interest was simply never
paid. It now always calculates and posts it before the account closes,
reusing the same machinery the manual "post interest as of date" action
already relied on.
Savings accounts now track two separate hold totals instead of one: a
manual/staff-placed Cash Hold (the existing lien mechanism, unchanged for
every caller that doesn't ask for the new category) and a Clearing Hold for
money still moving through the clearing process -- a Cheque/DD cashier
deposit now automatically places one for the full amount at approval time,
and CTS inward-clearing holds are now bucketed here too, since both are
genuinely about the clearing pipeline rather than a lien someone chose to
place. A new self-contained endpoint releases due Clearing Holds (wire it
into an external scheduler); the existing manual release action now credits
back whichever bucket the original hold actually debited. Approving a
Cheque/DD deposit dispatches this automatically as the approving manager,
so the checker roles that call deposit-approval needed the underlying
HOLDAMOUNT/RELEASEAMOUNT permissions granted -- without it every such
approval would now fail outright; a stray column-name bug in the payment-
type lookup that surfaced this in testing is fixed too, guarded going
forward by a try/catch so a problem in this best-effort step can never
sink the deposit approval it rides along with.
A clerk withdrawal's own terminal status is WITHDRAWN, not APPROVED --
deposits and disbursements use APPROVED, withdrawal is the one register
that doesn't. Three places were still filtering on APPROVED alone and
silently excluding every completed withdrawal: the Day Book's CR/DR
totals, and the branch day register's own "cash the branch should be
holding" formula (which overstated the expected figure by exactly what
had been paid out, manufacturing a phantom cash-short every time a
withdrawal happened that day).
The GL Entries report showed a bare internal transaction id under
"Voucher No" instead of the real formatted voucher stamped on the
underlying deposit/withdrawal request; it now resolves and shows that
instead. Denomination rows saved at deposit/withdrawal submission never
recorded which payment type was used, always leaving "Payment Type" blank
regardless of what the cashier actually collected -- both insert paths
now carry it through, and existing rows are backfilled.
FacilityChargeSeederService no longer hardcodes an income GL account onto
the facility charges (ATM, UPI, Cheque Book, NEFT, ...) it seeds. Fineract's
accounting engine uses a charge's own GL account ahead of its savings
product's own "Income From Fees" mapping whenever one is set, so seeding
all sixteen onto the same shared account (picked essentially by accident)
silently overrode every product's own accounting configuration with it.
Leaving it unset lets each product's own mapping decide where this income
actually posts, which is the point of that mapping existing at all.
Institution/Branch Profile's READ permission was scoped to manager/admin
roles only, since it started as an org-setup-only screen -- now that the
Passbook and Statement screens read it for their letterhead (real bank
name, license number, branch IFSC/MICR) instead of a hardcoded name, and
both are reachable by a cashier/clerk/teller, the read-only permission is
extended to those roles too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Opened-by/closed-by times on the day register were rendering blank: the
JDBC Timestamp's own toString() separates date and time with a space, not
the ISO-8601 'T' every frontend consumer parses on. str() now formats a
Timestamp explicitly instead of falling through to the generic toString().
Opening a business day with a note/coin count or lump sum that did not
match the branch's own closing figure from the day before was previously
accepted with no record of why -- every downstream figure (vault position,
EOD summary, the next day's own carry-forward) would then be built on an
unexplained short or excess baseline with nothing to explain it later. The
open endpoint now requires an openingDifferenceReason whenever the caller
supplies a figure that differs from the prior close, enforced server-side
so it can't be bypassed by calling the API directly; a day with no prior
close, or one left to the automatic carry-forward untouched, is unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes the gap where a vault could hand out more cash (in total or in a
specific note/coin) than it actually held: CashTransferApiResource now
validates against the branch day's opening denomination breakdown plus
today's settlements/transfers, and blocks entirely when no business day is
open. BranchDayRegisterApiResource gains a real denomination breakdown per
open/close leg (migration 0510), carried forward automatically day to day,
and its book-cash formula no longer double-counts vault-to-cashier transfers
as new cash in. CashReportApiResource adds a vault-position report (the
teller/vault analogue of cashier-position) and fixes an off-by-one date
comparison that excluded same-day transfers from a vault's anchor position.
Institution/Branch Master: new InstitutionProfileApiResource and
BranchProfileApiResource (migrations 0511-0513) give the bank a real
"Bank Master" profile (identity, regulatory details, address, SWIFT/BIC) and
each office its own branch-level fields (code, IFSC/MICR, working hours,
geo-location) as side tables on m_office rather than touching the stock
Fineract office entity. ClearingGlConfigApiResource exposes the previously
config-only-via-SQL m_clearing_gl_config mapping (CTS/NACH/RTGS suspense
accounts) as a proper admin API.
ClerkDisbursementRequestApiResource now accepts a FUND_TRANSFER request type
alongside DISBURSEMENT/COLLECTION/BANK_TRANSFER, and migration 0514 grants
the teller-facing roles READ_CLIENTNOMINEE, which they were missing entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the admin read/write API for m_facility_charge_mapping (list, template,
update) so facilities can be pointed at a real charge instead of relying on
the seeder's implicit auto-link, and makes enable-on-account paths (activation
and edit-approval, both savings and current accounts) throw instead of
silently skipping the charge when a facility isn't mapped or is inactive.
Migrations 0497-0509 back this up: reset all facility mappings to
unmapped/inactive by default, backfill lost roles/permissions/charges, enforce
INR as the only organisation currency, backfill the full real-world chart of
accounts (44 core + 60 priority GL heads) with standardized gl_codes, and
retire the unused system-defined payment types (Share Redemption Transfer and
friends) in favor of Cash/Transfer only, matching the teller dashboard's
simplified payment-type flow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Migration-hygiene fixes found while auditing whether a fresh database would
end up with the same schema/permissions/roles as this dev database.
1. Remove fake test-data seed migrations
0449_seed_aml_test_alerts.xml and 0459_seed_aml_alerts_awaiting_review.xml
unconditionally inserted 3 fictional clients (CLIENT_RAJESH/SONIA/VIKRAM)
and 5 fake AML alerts against them on every environment, including
production if this migration path ever ran there. 0459's own comment
confirms it was written purely to exercise the AML review UI. Deleted
both files and their <include> entries; cleaned the same fake rows out
of this dev database (3 clients, 5 alerts, plus the now-orphaned
databasechangelog bookkeeping rows for the two deleted files).
2. Backfill 3 permissions lost to a migration-number collision
Auditing all 2712 changesets applied to this dev database against the
current codebase found 18 changeset filenames with no matching file
anywhere in git; changelog-tenant.xml itself already documents the root
cause (multiple branches independently reusing the same numeric prefix
for unrelated migrations, e.g. 0456-0462 twice). Cross-checking each
affected table/permission against actual Java usage (@CommandType
handlers, domain/API classes, and both permission-insert styles used in
this codebase -- Liquibase <insert> columns and raw <sql> literals)
showed 15 of the 18 are genuinely dead (reconciliation-batch/settlement-
cycle prototype tables, fixed asset/inventory tracking) with zero
current code referencing them, so nothing to backfill there. The
remaining 3 -- MENU_RECONCILIATION, READ_RECONCILIATION,
SIDEBAR_RECONCILIATION -- gate a live module (CashReconciliation /
ClearingReconciliation have real API resources and domain classes) but
were never seeded by any migration; without them a fresh database can
build and expose the reconciliation screens with no way to grant a role
access to them. Added 0491_add_reconciliation_menu_permissions.xml.
3. Merge two pairs of redundant roles
- "Bank Cashier" (4 fund-transfer permissions, 0 of which overlapped
"CASHIER", 0 users assigned) folded into CASHIER.
- "AML Officer" (2 permissions, both already present on "Principal
Officer", 0 users assigned) folded into Principal Officer.
Both migrations copy any missing permissions and move any assigned
users onto the surviving role before deleting the redundant one, so
they're safe to run against an environment that isn't empty the way
this dev database is. Applied the same merge directly to this dev
database's m_role / m_role_permission / m_appuser_role rows.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three related pieces of backend support for this session's FD Special
(monthly interest payout) work, plus one more instance of the same
transaction-ordering bug fixed elsewhere this session.
1. FD Special account creation/modification auto-configuration
FD Special (fdType=SPECIAL) is a non-cumulative product: interest must
post monthly and leave for a linked savings account, and maturity must
auto-transfer the principal out -- regardless of whatever compounding/
posting period or maturity instruction the request otherwise carries.
Previously none of this was enforced, so a SPECIAL account could be
created with e.g. quarterly compounding, or a later "modify application"
request could silently unset transferInterestToLinkedAccount / the
maturity instruction / the linked account by mere omission of those
fields from the payload, while the account kept displaying as "FD
Special".
- DepositAccountAssembler: on create, forces interestCompoundingPeriodType
and interestPostingPeriodType to MONTHLY, transferInterestToSavings to
true, transferToSavingsId to the (now required) linked savings account,
and accountOnClosureType to TRANSFER_TO_SAVINGS, whenever fdType is
SPECIAL. Throws a clear domain error if no linked account was selected.
- DepositAccountTermAndPreClosure: new forceMonthlyPayoutToLinkedSavings(),
re-applying those same three standing-instruction fields.
- DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl: calls
the equivalent re-enforcement on every modify-application request for
a SPECIAL account, after the linked-account resolution in that same
method has settled what account (if any) ends up linked.
2. FD Special projected interest schedule (engine + API)
Staff/customers previously only saw a flat rate/12 approximation of
monthly interest. Added a real, engine-computed month-by-month schedule:
- DepositAccountWritePlatformService(Impl): new
retrieveProjectedInterestSchedule(), running the same PostingPeriod
engine the real maturity calculation uses (FixedDepositAccount#
projectInterestSchedule), side-effect-free (@Transactional(readOnly)),
clipping the last period's displayed end-date to maturityDate-1 (the
engine otherwise rounds it out to the next calendar boundary even when
maturity falls partway through), and flagging each period "paid" by
matching real posted-interest transaction dates.
- FixedDepositAccountsApiResource: computes and attaches the schedule,
plus server-computed totalProjectedInterest/totalValueAtMaturity, to
the FD template response for SPECIAL accounts only.
- FixedDepositAccountData / DepositsApiConstants: carrier fields and
response-parameter whitelist entries for the three new fields.
3. Minor-age threshold: backend now matches the frontend's shared constant
Companion to this session's frontend minor-age-threshold change
(18 -> 10, see client-age.util.ts). ClientKycApiResource's minor
detection (which gates the required-KYC-document list, including
whether PAN is mandatory) was still hardcoded to age < 18. Added
ClientApiConstants.MINOR_AGE_THRESHOLD (10) and SENIOR_CITIZEN_AGE_
THRESHOLD (60) as the backend's own named constants, and switched the
KYC endpoint to use MINOR_AGE_THRESHOLD, so the two sides can't silently
disagree on who counts as a minor.
4. SavingsAccountTransactionComparator: same-date tie-break fixes another
instance of this session's core reversal bug
For a Special FD's interest-transfer sweep, the withdrawal shares the
exact transaction_date of the interest posting it pays out. When that
posting is reversed and recreated, the new posting transaction gets a
higher id than the already-existing withdrawal; the comparator's id-order
tiebreaker then sorted the withdrawal before the new posting, which
understated the running balance shown between them (e.g. showing
Rs.99,901.39 instead of Rs.100,000.00 immediately after the pair). Added
an explicit tiebreaker so a same-date, same-createdDate interest posting
always sorts before a withdrawal/debit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FD Special (monthly interest payout) accounts post interest each month and
immediately sweep it to a linked savings account. Two independent interest
recalculation paths were treating that sweep withdrawal as ordinary account
activity and silently reversing + reposting already-settled periods at a
slightly different (rounding-drifted) amount — after the original, correct
amount had already been transferred out. This left the FD short by the
difference every time it happened.
Root causes (two separate engines, same underlying bug pattern):
1. SavingsAccountDomainServiceJpa#handleWithdrawal re-triggers a full
postInterest()/calculateInterestUsing() after every withdrawal, including
the sweep withdrawal itself. At that exact moment the withdrawal's own
m_account_transfer_transaction linking row does not exist yet, so the
balance-exclusion logic can't recognise it as a transfer leg — it gets
treated as a real balance-reducing transaction and corrupts that period's
(and any later period's) interest calculation.
2. SavingsAccountInterestPostingServiceImpl (the "Post Interest For Savings"
batch job) has its own, separate reverse-and-repost correction loop with
the same lack of a "was this period already settled" guard.
Fixes, both scoped strictly to FD Special (transfer_interest_to_linked_account
= true) so plain savings and recurring deposit behaviour is provably
unchanged (the new hooks all default to the prior behaviour unless
overridden):
- SavingsAccount: new isInterestPostingCorrectionAllowed(...) extension
point guarding the postInterest() correction loop; new
isPostWithdrawalInterestRecalculationRequired(...) hook checked by
handleWithdrawal before it re-recalculates; new
refreshBalancesAndSummaryAfterTransfer(...) — the running-balance/summary
bookkeeping postInterest() always performs, without re-running the
posting/correction loop, so skipping the full recalculation doesn't leave
the withdrawal's own running balance and the account summary stale.
- FixedDepositAccount: overrides both hooks using the existing
interestAlreadyPaidOut(...) date-matched sweep-withdrawal check (already
used by postMaturityInterest) so an already-swept period is never
recalculated again, from either the withdrawal path or a later job run.
- SavingsAccountInterestPostingServiceImpl: mirrors the same guard
(isInterestAlreadySweptOut) in its own correction loop for the batch job.
Unrelated correctness fixes carried in the same files, applicable to any
account type (not FD-Special-specific), found while investigating the above:
- SavingsAccountWritePlatformServiceJpaRepositoryImpl: interest-payable GL
entries were reading a financial_account_type that never exists in
acc_product_mapping, always resolving to account id 0. Now reads the
correct getGlAccountIdForInterestOnSavings().
- DepositAccountInterestRateChart#getApplicableInterestRate: missing break
in the slab-matching loop meant HashSet's non-deterministic iteration
order could pick an arbitrary matching slab instead of the intended one.
- SavingsAccountReadPlatformServiceImpl / SavingsAccountData: thread the
real transfer_interest_to_linked_account flag into the batch interest
engine's per-account data so it can correctly disable interest
compounding for FD Special (previously hard-coded false, causing the
batch job to compound interest that should reset to zero each period).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WHAT THIS CHANGES
==================
Three related FD (Fixed Deposit) features, built incrementally in one session:
1. IN-PLACE RENEWAL -- when an FD matures with "Re-Invest Principal and
Interest", the SAME m_savings_account row is now reused across every
renewal (account number never changes) instead of closing the account
and opening a new one. FixedDepositAccount.renewInPlace() does this by
setting start_interest_calculation_date as a pivot: interest for terms
before the pivot is never recalculated, only the new term's window is
computed from there. A new one-to-many audit table m_fd_renewal_history
records one row per term (renewal_no 0,1,2...) with that term's own
principal, rate, dates, interest earned, maturity, and status. A
companion table m_fd_renewal_chart_slab_history archives each closing
term's full rate-by-tenure chart, captured just before renewInPlace
overwrites it.
2. SENIOR-CITIZEN RATE -- the CBS Base Interest Rate master
(m_cbs_base_interest_rate_slab_history) was the only place that ever
stored a senior-citizen premium. It is now also persisted on
m_savings_account_interest_rate_slab (new senior_citizen_rate column,
nullable, added only to this account-level table -- NOT to the shared
InterestRateChartSlabFields embeddable, so the product-level table
m_interest_rate_slab is untouched). Applied both at account creation
(DepositAccountAssembler now prices a new FD off the active CBS base
version when one exists for that FD type, falling back to the
product's own chart otherwise) and at renewal (renewInPlace refreshes
the chart from the same base version).
3. PAYMENT ACTION -- a new manual command, POST
/fixeddepositaccounts/{id}?command=payment, available whether the FD
is ACTIVE or MATURED, unlike Close (Matured-only) and Premature Close
(Active-only, no reinvest). Offers: Transfer to Savings, Re-Invest
Principal and Interest (via renewInPlace), Re-Invest Principal Only
(legacy close-and-reopen, now also transfers the leftover interest to
a chosen savings account instead of stranding it), and a new closure
type PARTIAL_REINVEST (500) -- part of the matured balance renews in
place on the same account, the remainder transfers to a savings
account picked at request time. Withdraw Deposit is deliberately
excluded from this command. New permission PAYMENT_FIXEDDEPOSITACCOUNT
plus its CHECKER twin were added in migration 0478.
KEY FILES
=========
- DepositAccountDomainServiceJpa.java: handleFDAccountPayment (new),
renewInPlace call sites, recordInPlaceRenewal, senior-citizen rate
matching with unit conversion across days/weeks and months/years
families.
- FixedDepositAccount.java: renewInPlace, validateRenewalDate,
isLockedOnDate, fdAccountNumber/renewalCycleNo chain tracking.
- SavingsAccount.java (SHARED with Savings and RD): added
findLastTransactionBefore -- the interest-posting opening balance for a
pivot-dated account now seeds from the last transaction strictly before
the pivot, not the one on it. This fixes a bug where a renewed term
could accrue zero interest, or double-subtract a same-day withdrawal,
depending on transaction list order. Also widened
calculateDateAccountIsLockedUntil to protected so renewInPlace can
refresh lock-in on each renewed term.
- DepositAccountTransactionDataValidator.java: new validatePayment.
validateClosing now explicitly rejects PARTIAL_REINVEST (500) so it can
never reach the Close or Premature-Close handlers, which have no branch
for it and would otherwise pay the whole balance out in cash. This
applies to both FD and RD, since they share validateClosing.
- DepositAccountReadPlatformServiceImpl.java: retrieveOneWithClosureTemplate
(Close and Premature Close, excludes PARTIAL_REINVEST) split from the
new retrieveOneWithPaymentTemplate (Payment, includes it), sharing one
private method with a flag. The renewal-history fallback query, for
chains that predate m_fd_renewal_history, was rewritten to emit the
exact same column shape as the primary query.
- New: FixedDepositRenewalHistory plus its repository,
FixedDepositRenewalChartSlabHistory plus its repository,
PaymentFixedDepositAccountCommandHandler.
- Migrations 0471 through 0478 under tenant/parts, each registered once
in changelog-tenant.xml.
RD AND SAVINGS IMPACT -- READ THIS IF SOMETHING BREAKS THERE
============================================================
RD and plain Savings share several of the touched files. Verified during
this work:
- handleRDAccountClosure and RecurringDepositAccount.java were not
modified.
- PARTIAL_REINVEST (500) is now rejected by validateClosing for both FD
and RD close and prematureClose commands, and excluded from their
template option lists. This was a real regression caught mid-session:
the enum value briefly leaked into RD's Close dropdown as a working
looking option that actually paid the whole RD balance out in cash.
It was found and fixed before this commit.
- The SavingsAccount.calculateInterestUsing opening-balance change only
affects accounts where start_interest_calculation_date differs from
activatedon_date. On the dev DB this was exactly two accounts, both
FD test accounts created this session. Before deploying, run:
SELECT deposit_type_enum, COUNT(*) FROM m_savings_account
WHERE start_interest_calculation_date IS NOT NULL
AND start_interest_calculation_date <> activatedon_date
GROUP BY deposit_type_enum;
Any non-FD rows (100 or 300) there mean a Savings or RD account will
get a different, corrected interest amount than before. This is
expected to be rare or zero, but confirm before deploy.
- DepositAccountAssembler's creation-time base-rate pricing is guarded
by depositAccountType.isFixedDeposit, so RD account creation is
unaffected.
- is_active on InterestRateChart (migration 0474) is read only by
FixedDepositProduct.activeChart, which only FD's renewInPlace and
reInvest call. RD's applicableChart path is untouched. Migration 0474
also retires (is_active = false) any chart whose end_date is already
past. On the dev DB this retired zero rows; check on the target DB
before deploy, since there is currently no API to reactivate a
retired chart.
KNOWN CAVEATS -- NOT YET DONE, FLAGGING RATHER THAN HIDING
============================================================
- Migrations 0471 through 0477, the DDL ones, have never actually
executed via Liquibase. They were mirrored by hand directly into the
dev DB (ADD COLUMN IF NOT EXISTS, CREATE TABLE IF NOT EXISTS) so
testing could proceed without a full rebuild each time. Liquibase
will mark them MARK_RAN via preConditions on the dev DB's next real
run, but their first genuine execution will be on whatever
environment does not already have these columns and tables, such as
a fresh QA or production database. Test a migration-from-scratch run
before relying on this in production.
- This code has not been compiled. Every new or changed method
signature, import, and call site was manually cross-checked with
grep against real declarations, but that is not a substitute for
gradle actually compiling it. Run a full build before deploying and
treat any compiler error as expected until confirmed clean.
- DepositAccountAssembler pricing every new FD off the active CBS base
rate master, instead of the product's own chart, whenever one exists
is a real behaviour change affecting all new FD accounts of a type
that has a base-rate version, not just senior-citizen ones. Flagging
this for explicit product sign-off, not assuming it in this commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reinvestment on maturity was creating the new deposit with a zero balance, because it
sourced the amount from activateWithBalance() -- a guard that deliberately returns zero to
stop a manually opened FD from auto-crediting itself. It now carries over what reInvest()
actually put on the new account. Both legs of the renewal are also posted as an internal
transfer, so the funds move through the transfer-suspense GL instead of appearing to leave
and re-enter the cash vault. Reinvested accounts no longer fall back to a 0% rate when no
chart slab covers the tenure, and a senior-citizen depositor keeps their premium rate on the
renewed term.
Adds a reinvestment trail: the new account records which account it came from, and the
account view resolves both directions of the link so either side can be traced.
Also fixes product edit submission, which failed outright for any product carrying
payment-channel GL mappings: the pending-edit mapper read "glAccountId" while the payload
carries "fundSourceAccountId" (and "incomeAccountId" for fee/penalty arrays), leaving
gl_account_id null and tripping the satellite table's NOT NULL constraint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds validation so Transfer-to-Savings can only target a real savings account,
with a proper user-facing message instead of the generic validation error.
Fixes three duplicate journal-entry bugs uncovered while testing FD/RD
maturity: the dormant-accounts job re-posting already-closed accounts
(escheat/setSubStatusInactive/setSubStatusDormant), and the maturity-closure
flow itself re-deriving accounting for a withdrawal that the account-transfer
service had already posted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every mobile-number lookup was calling out to a local mock HTTP service
on port 8090 that had to be started separately, so the check failed
with a connection error whenever that process wasn't running.
Ports the mock's full verdict table (suspected, disconnected,
suspected+disconnected, clean) into MobileLookupServiceImpl itself, so
the demo/test numbers resolve in-process with no HTTP call. Adds
fineract.mobile-lookup.enabled (default off) to skip the real DIP/MNRL
call entirely for local work; every number then resolves CLEAN and is
stamped LOOKUP_DISABLED_BY_CONFIG in the log so it's never mistaken
for a genuine pass. Must be turned back on before UAT/production.
/v1/currentaccounts/{id}/documents had no handler at all -- the
account's own /v1/currentaccounts resource matches the request path
first and has no documents method, so it never fell through to the
generic /v1/{entityType}/{entityId}/documents resource that every
other entity type relies on. Current accounts got a 404 on every
Documents-tab request.
Delegates straight to the existing DocumentApiResource so current
accounts get identical behaviour to clients, loans, etc.
Charges collected when a maker-checker Nominee/Digital-Feature edit is
approved deducted the account balance and appeared in the passbook,
but never posted a journal entry -- collectCharges() saved the charge
transaction directly and skipped the accounting bridge entirely, so
the GL never balanced against the subledger.
Also fixes an unrelated NPE this exposed: collectCharges() used
saveAll() instead of saveAllAndFlush(), so the new charge transaction
had no id yet when the journal-entry code tried to read it.
Activation for Savings, RD, FD and Current accounts no longer books an
opening deposit or enforces a minimum opening balance -- accounts go
active at zero balance and a clerk deposits funds afterward as a
separate transaction, matching how these branches actually operate.
Fixed Deposit and Recurring Deposit have their own override of
activateWithBalance() that a prior fix to the shared SavingsAccount
base class missed, so they kept auto-depositing after Savings was
already fixed. Removing that auto-deposit exposed two follow-on bugs
in the FD/RD activation flow, now also fixed:
- the opening deposit's journal entries were posted twice (once by the
deposit helper, once by the activation command's own posting call)
- FD/RD maturity amount was computed from the account's real (now
empty) transaction history at activation and never recalculated once
a deposit actually landed, so it showed no projected interest until
the account was reloaded from a lucky code path
2026-08-19 10:34:54 +05:30
1005 changed files with 113936 additions and 14238 deletions
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.