Whitelist and configure CICs (CIBIL, Experian, Equifax, CRIF) with encrypted
credentials, UI-driven request/response API mapping, consent register,
enquiries with report reuse, simulator mode, and a MENU_CREDIT_BUREAU menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New FixedDepositFundingValidator, used on every path money can reach an FD:
- an FD is funded once, with exactly its principal: it must be active and not matured,
hold no deposit yet, and the amount must equal its deposit amount;
- when funded from a Savings/Current account (clerk Bank/Fund Transfer), that account
must be a Savings or Current account, active, not debit-frozen (sub-status BLOCK /
BLOCK_DEBIT), out of lock-in, belong to the FD's client or be held jointly with them,
and have enough available balance after holds, lien, clearing holds and minimum
balance (plus overdraft for current accounts).
Applied in:
- FD deposit command (counter / FD screen / approved cashier deposit requests);
- cashier deposit request submit, so the counter sees the reason at once;
- clerk Disbursement / Collection / Fund Transfer / Bank Transfer, at submit and again
at approval before the request is locked; one request funds FDs of one customer, an
FD appears once, and an FD can never be a debit leg. After posting, each funded FD's
maturity date and amount are recomputed (new
DepositAccountWritePlatformService.recalculateFixedDepositMaturity), since the direct
book credit does not do it;
- clerk withdrawal request submit refuses an FD: money leaves an FD only through its
maturity or premature closure.
Previously a direct book debit in Collection / Bank Transfer could take money out of an
FD, bypassing the FD's own withdrawal refusal, and an FD could be credited any number of
times for any amount.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A client may now open any number of Fixed Deposit and Recurring Deposit accounts on
the same product, even while an earlier one is still active. The one-active-account-
per-product check is removed from submitFDApplication and submitRDApplication only;
the same rule on plain Savings and Current accounts is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cash no longer leaves the vault on one person's say-so. The allocation to a cashier is raised as a request and
posted only when a different manager or head cashier approves it.
- POST /cashtransfer/requests raises the request after every vault check (total and note by note); the same
checks run again at approval, since the vault can change in between.
- Approve posts the transfer exactly as the direct one did and records it against the maker; reject needs a
reason; the maker cannot approve their own request.
- An identical request from the same maker is refused while the first is still pending.
- The direct POST /cashtransfer is closed so the approval cannot be bypassed.
- Migration 0603 adds m_cash_transfer_request; m_cash_transfer and every position query are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A cash deposit posted to the wrong savings account is corrected without ever undoing it behind anyone's back:
the voucher is unpassed (admin or clerk), a manager approves, the cashier enters the correct account, and a
manager approves again. Only the final approval moves money: the deposit is reversed on the wrong account, the
same cash is deposited into the right one under a new voucher, and the cashier's denominations follow it.
- VoucherCorrectionApiResource: eligible list, unpass, approve/reject unpass, account lookup, correction,
approve/reject correction, close request, voided transactions.
- The wrong account's balance is checked when the cashier enters the correction and again at approval.
- A cashier whose customer has already used the money closes the request and deposits separately.
- Roles are enforced on the server: unpass for admin/clerk, correction and close for a cashier, approvals for
a manager or head cashier other than the maker.
- Migration 0601 adds m_voucher_correction; 0602 drops the status columns an earlier draft put on m_voucher,
since the correction status is read from m_voucher_correction alone.
- Voucher registers (register, My Vouchers, day book, voucher detail) report each voucher's correction status
and leave a revoked voucher out of their totals, so its cash is not counted twice.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A GL withdrawal slip (e.g. Sundry Creditors paid out in cash) now follows the same
maker-checker path as a savings withdrawal:
- the request carries gl_account_id, contra_gl_id, office_id and currency_code
(new nullable columns, changeset 0600) instead of a savings account;
- at submit it is validated: the paying GL must be a mapped cash vault GL and the
slip's own GL must not be a cash GL;
- at manager approval it posts Dr the GL / Cr the cash vault GL as a journal entry;
- the cashier pays it against its token, and the denomination and voucher lines are
recorded under the journal entry's id. KYC debit checks are skipped for GL requests.
Co-Authored-By: Claude Sonnet 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>
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>
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>
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>
super("error.msg.party.not.collected.by.agent","Party "+partyCode+" is not collected by agent "+agentCode,
partyCode,agentCode);
}
}
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.