rymghosn commented on code in PR #6235:
URL: https://github.com/apache/fineract/pull/6235#discussion_r3735865006


##########
fineract-provider/src/main/resources/db/changelog/tenant/parts/0245_delete_trailing_space_standinginstruction_permissions.xml:
##########
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements. See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership. The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied. See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<!--
+    Liquibase changeSet to safely delete duplicate m_permission rows
+    for Standing Instruction permissions WITHOUT trailing spaces,
+    keeping the legacy trailing-space versions.
+
+    Deletes ONLY:
+      - 'CREATE_STANDINGINSTRUCTION'
+      - 'UPDATE_STANDINGINSTRUCTION'
+      - 'DELETE_STANDINGINSTRUCTION'
+
+    Safety rules:
+    - Delete ONLY if a trailing-space version already exists.
+    - Do NOT modify or insert any permissions.
+    - Do NOT touch m_role_permission.
+    - Idempotent and safe to re-run.
+    - PostgreSQL only.
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog";
+                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+                   
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd";>
+
+    <changeSet id="0245_delete_non_spaced_standinginstruction_permissions"

Review Comment:
   Great question — the trailing space is definitely wrong, but it's actually 
the *original* value, not the new one.
   
   `0002_initial_data.xml` (the base seed data every tenant has run since 
Fineract's very first release) inserts `'CREATE_STANDINGINSTRUCTION '`, 
`'UPDATE_STANDINGINSTRUCTION '` and `'DELETE_STANDINGINSTRUCTION '` **with** 
the trailing space (ids 450-452). The non-spaced duplicate was introduced much 
later, by accident, in `0194_fix_missing_permission.xml` (changeSets 20-22): 
that changeset's precondition does an exact-match check (`code = 
'CREATE_STANDINGINSTRUCTION'`), which — because the existing row already has 
the trailing space — never matches, so it always reports "0 rows found" and 
inserts a second, non-spaced row on every tenant that has run through the 
changelog.
   
   Since `m_role_permission` references a permission by its numeric id, not by 
its code, any tenant that's been running since before changeset 0194 could 
already have granted Create/Update/Delete Standing Instruction to a custom role 
against the *original* (spaced) id. This changeset explicitly promises not to 
touch `m_role_permission`, so the only row we can safely delete without needing 
a role-permission migration is the newer accidental duplicate — which is what 
it does.
   
   It's also not just cosmetic: `PermissionRepository.findOneByCode()` already 
TRIM/LOWER-cases both sides of the comparison specifically because of this bug 
(see the comment on that method) — so with both rows present, any code path 
resolving this permission "by code" today throws a `NonUniqueResultException`, 
not just an ugly space.
   
   Properly fixing the underlying ~decade-old trailing space (e.g. `UPDATE 
m_permission SET code = TRIM(code)` on the surviving row) is a good idea, but 
it's a separate, larger change — it'd need a sweep to confirm nothing else 
compares against the exact `'... '` string, and probably deserves its own 
ticket rather than riding along with this SI disbursement fix. Happy to file 
that as a follow-up if useful.
   



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java:
##########
@@ -299,8 +299,21 @@ public Page<StandingInstructionData> retrieveAll(final 
StandingInstructionDTO st
                 sqlBuilder.append(" fromsavacc.id=? ");
                 paramObj.add(standingInstructionDTO.fromAccount());
             } else if (PortfolioAccountType.LOAN.equals(accountType)) {
-                sqlBuilder.append(" fromloanacc.id=? ");
-                paramObj.add(standingInstructionDTO.fromAccount());
+                // For LOAN_REPAYMENT transfers, loan is stored in 
to_loan_account_id (FROM=SAVINGS, TO=LOAN)

Review Comment:
   Totally fair to double check — the naming here is genuinely confusing, but 
it's not a mix-up.
   
   `standingInstructionDTO.fromAccount()` / `fromAccountType()` at this point 
in the method are the API's generic search filter ("give me standing 
instructions involving this account"), not literally "the source side of the 
money movement." Which physical column a loan account actually lives in depends 
on the transfer type, not on how the caller happened to name the query 
parameter.
   
   For the standing instructions this PR is about — the ones auto-created at 
loan disbursement — 
`LoanWritePlatformServiceJpaRepositoryImpl.createStandingInstruction()` builds 
the transfer via:
   
   ```java
   AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, 
linkedSavingsAccount, toOffice, toClient, loan, transferType);
   ```
   
   That factory always stores the savings account as `fromSavingsAccount` and 
the loan as `toLoanAccount` (`from_loan_account_id` is left `null`). So for 
`LOAN_REPAYMENT` standing instructions, the loan is *always* on the 
`to_loan_account_id` side in the database — never `from_loan_account_id`.
   
   The old code compared the caller's loan-id filter against `fromloanacc.id` 
(i.e. `from_loan_account_id`), which is always null for these rows, so it could 
never find the auto-created standing instruction for a given loan. That's the 
actual "not found" symptom from the bug report. The fix just routes the 
comparison to whichever column the loan is genuinely stored in, based on the 
same `transferType` the write path used to create it.
   



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java:
##########
@@ -299,8 +299,21 @@ public Page<StandingInstructionData> retrieveAll(final 
StandingInstructionDTO st
                 sqlBuilder.append(" fromsavacc.id=? ");
                 paramObj.add(standingInstructionDTO.fromAccount());
             } else if (PortfolioAccountType.LOAN.equals(accountType)) {
-                sqlBuilder.append(" fromloanacc.id=? ");
-                paramObj.add(standingInstructionDTO.fromAccount());
+                // For LOAN_REPAYMENT transfers, loan is stored in 
to_loan_account_id (FROM=SAVINGS, TO=LOAN)
+                // For other transfer types, loan is stored in 
from_loan_account_id
+                // Defensive fallback: if transferType is null, check both 
columns to handle UI inconsistencies
+                Integer transferTypeValue = 
standingInstructionDTO.transferType();
+                if (transferTypeValue != null && 
transferTypeValue.equals(AccountTransferType.LOAN_REPAYMENT.getValue())) {
+                    sqlBuilder.append(" toloanacc.id=? ");

Review Comment:
   Because `fromAccount()` here is the loan id the *caller* is searching for 
(e.g. "show me the standing instruction for loan #42"), not literally the 
from-side of the money movement.
   
   As shown in `AccountTransferDetails.savingsToLoanTransfer(...)` (used by 
`LoanWritePlatformServiceJpaRepositoryImpl.createStandingInstruction()`), for 
`LOAN_REPAYMENT` transfers the loan is persisted as `toLoanAccount` / 
`to_loan_account_id` — `from_loan_account_id` is left null for these rows. So 
to find the standing instruction for loan #42 we have to match `toloanacc.id = 
42`, even though the caller supplies that same loan id via the `fromAccount` 
parameter.
   
   I agree the parameter name is misleading — it really means "the account of 
interest" rather than "the from side" — but renaming it would touch the public 
search API's query params, which is a bigger, more invasive change than this 
bugfix needs.
   



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java:
##########
@@ -299,8 +299,21 @@ public Page<StandingInstructionData> retrieveAll(final 
StandingInstructionDTO st
                 sqlBuilder.append(" fromsavacc.id=? ");
                 paramObj.add(standingInstructionDTO.fromAccount());
             } else if (PortfolioAccountType.LOAN.equals(accountType)) {
-                sqlBuilder.append(" fromloanacc.id=? ");
-                paramObj.add(standingInstructionDTO.fromAccount());
+                // For LOAN_REPAYMENT transfers, loan is stored in 
to_loan_account_id (FROM=SAVINGS, TO=LOAN)
+                // For other transfer types, loan is stored in 
from_loan_account_id
+                // Defensive fallback: if transferType is null, check both 
columns to handle UI inconsistencies
+                Integer transferTypeValue = 
standingInstructionDTO.transferType();
+                if (transferTypeValue != null && 
transferTypeValue.equals(AccountTransferType.LOAN_REPAYMENT.getValue())) {
+                    sqlBuilder.append(" toloanacc.id=? ");
+                    paramObj.add(standingInstructionDTO.fromAccount());
+                } else if (transferTypeValue == null) {
+                    sqlBuilder.append(" (toloanacc.id=? OR fromloanacc.id=?) 
");

Review Comment:
   That branch only runs when `transferType` is `null`, i.e. the caller didn't 
filter by transfer type at all — they just asked for standing instructions 
touching a given loan account, regardless of type.
   
   Since which column (`to_loan_account_id` vs `from_loan_account_id`) holds 
the loan id depends on the transfer type (see the LOAN_REPAYMENT case above, 
where it's always `to_loan_account_id`), and we don't know the transfer type in 
this branch, we can't know in advance which single column to check — so we 
check both with `OR`, using the same caller-supplied loan id on each side. If a 
transfer type *is* given, we're back in one of the other two branches where we 
already know which single column applies.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to