adamsaghy commented on PR #6106:
URL: https://github.com/apache/fineract/pull/6106#issuecomment-5398108951

   ## Summary
   
   The core idea is sound. `ProductToGLAccountMapping.charge` becomes a plain 
`chargeId` column, the JPQL is
   rewritten accordingly, and charge lookups move behind an 
`AccountingChargeReadService` port implemented in
   `fineract-charge`. The batched `findChargesByIds` also removes the old 
per-mapping eager `@ManyToOne` fetch,
   which is a genuine improvement.
   
   However, the write-side validation that the removed 
`ChargeRepositoryWrapper` was implicitly providing has not
   been fully re-homed, and several read paths change their response shape. 
Details below.
   
   ---
   
   ## Findings
   
   ### 1. Charge-existence validation is lost for working-capital loan products
   
   `fineract-accounting/.../service/ProductToGLAccountMappingHelper.java:611`
   
   Removing `chargeRepositoryWrapper.findOneWithNotFoundDetection(chargeId)` 
from `saveChargeToFundSourceMapping`
   drops validation for **every** caller that does not go through
   `fineract-provider`'s `ProductToGLAccountMappingWritePlatformServiceImpl` — 
where the new `validateChargesExist()`
   guard lives.
   
   The working-capital loan product path does not go through that service:
   
   ```
   WorkingCapitalProductAccountingMappingServiceImpl.createAccountMapping
     -> WorkingCapitalLoanProductToGLAccountMappingHelper.saveAdvancedMappings  
 (fineract-working-capital-loan)
       -> ProductToGLAccountMappingHelper.saveChargesToGLAccountMappings
         -> saveChargeToFundSourceMapping
   ```
   
   **Scenario:** `POST /workingcapitalloanproducts` with
   `accountingRule = ACCRUAL_WITH_DEFERRED_REVENUE_AMORTIZATION` and
   `feeToIncomeAccountMappings: [{ "chargeId": 999999, "incomeAccountId": 
<valid> }]`.
   
   - **Before:** 404 `ChargeNotFoundException`.
   - **After:** `saveAndFlush` hits the DB FK `FK_acc_product_mapping_m_charge`
     (`0001_initial_schema.xml`, changeSet 693) → raw constraint violation / 
HTTP 500.
   
   The same gap applies to 
`WorkingCapitalLoanProductToGLAccountMappingHelper.updateAdvancedMappings`.
   
   **Suggested fix:** keep the validation inside 
`saveChargeToFundSourceMapping` (via the new
   `AccountingChargeReadService`, so no charge-domain dependency returns), 
rather than duplicating it in one
   caller. That preserves the guarantee for all current and future callers.
   
   ---
   
   ### 2. Nested `charge` object grows from 3 fields to the full `ChargeData`
   
   
`fineract-accounting/.../service/ProductToGLAccountMappingReadPlatformServiceImpl.java:273`
   (and the equivalent code in 
`WorkingCapitalLoanProductAdvancedAccountingReadHelper`)
   
   Previously each entry was built inline as a stub:
   
   ```java
   ChargeData.builder().id(...).name(...).penalty(...).build()
   ```
   
   Now `AccountingChargeReadServiceImpl` returns `Charge.toData()`, which 
populates `amount`, `currency`,
   `chargeTimeType`, `chargeAppliesTo`, `chargeCalculationType`, 
`chargePaymentMode`, `active`, `minCap`/`maxCap`,
   `incomeOrLiabilityAccount`, `taxGroup`, and more.
   
   **Scenario:** `GET /loanproducts/{id}` on a product with fee mappings. Any 
client or integration test asserting
   the shape of `feeToIncomeAccountMappings[].charge` breaks, and the payload 
grows noticeably for products with
   many charge mappings.
   
   **Suggested fix:** project only `{id, name, penalty}` in the port (e.g. a 
dedicated lightweight DTO or a
   JPQL constructor expression)
   
   ---
   
   ### 3. Fee/penalty classification switches basis, with no data migration
   
   `fineract-accounting/.../domain/ProductToGLAccountMappingRepository.java:53`
   
   `findAllFeeMappings` / `findAllPenaltyMappings` (filtered on 
`mapping.charge.penalty`) are replaced by
   `findAllFeeToIncomeAccountMappings` / 
`findAllPenaltyToIncomeAccountMappings` (filtered on
   `financialAccountType` 4/5).
   
   **Scenario:** a charge with `penalty = true` was configured through 
`feeToIncomeAccountMappings`.
   `saveChargeToFundSourceMapping` never validated fee-vs-penalty — the deleted 
`// TODO Vishwas: Need to validate
   if given charge is fee or Penalty` said exactly that — so the row is stored 
with `financial_account_type = 4`
   while `m_charge.is_penalty = true`.
   
   - **Before:** `GET /loanproducts/{id}` returned it under 
`penaltyToIncomeAccountMappings`.
   - **After:** it is returned under `feeToIncomeAccountMappings`.
   
   A client doing GET → PUT round-trips the mappings and silently moves them 
between buckets.
   
   The new behaviour is arguably the *more* consistent one — it matches what 
`updateChargeToIncomeAccountMappings`
   already used on the write side. But it is an unannounced behaviour change on 
existing data and should at
   minimum be called out in the PR description.
   
   ---
   
   ### 4. Empty charge mappings now serialize as `[]` instead of being omitted
   
   
`fineract-accounting/.../service/WorkingCapitalLoanProductAdvancedAccountingReadHelper.java:110`
   (also `ProductToGLAccountMappingReadPlatformServiceImpl.java:289`)
   
   `return result.isEmpty() ? null : result;` became `return result;`. Gson 
omits `null` fields, so the key
   previously did not appear in the JSON at all; it now appears as `[]`.
   
   Worse, the sibling methods in the *same class* — 
`fetchPaymentTypeToFundSourceMappings` (line 63) and
   `fetchReasonMappings` (line 122) — still return `null` on empty. A single 
response now mixes both conventions.
   
   **Suggested fix:** revert to `null` for consistency
   
   ---
   
   ### 5. `validateChargesExist` runs even when the mappings are discarded
   
   
`fineract-provider/.../service/ProductToGLAccountMappingWritePlatformServiceImpl.java:66`
   
   The call sits at the top of `createLoanProductToGLAccountMapping`, before 
`accountingRuleType` is decoded.
   
   **Scenario:** `POST /loanproducts` with `accountingRule = 1` (NONE) and a 
leftover
   `feeToIncomeAccountMappings` block containing a since-deleted `chargeId` — 
e.g. a UI that keeps the
   advanced-accounting section populated when the user switches the rule to 
*None*. The `switch` hits
   `case NONE: break;` and never persists or reads those arrays, so the request 
previously succeeded. It now
   throws `ChargeNotFoundException` (404).
   
   **Suggested fix:** move the call into the branches that actually persist the 
mappings, or guard it on
   `accountingRuleType != NONE`.
   
   ---
   
   ## Design notes
   
   **Dependency direction.** The PR removes `accounting → charge` but adds 
`charge → accounting` in
   `fineract-charge/dependencies.gradle`, purely so 
`AccountingChargeReadServiceImpl` can implement an interface
   owned by the accounting module. `ChargeData` already lives in 
`fineract-core` — putting the read contract there
   too would decouple both modules without introducing a new edge in the 
opposite direction. As it stands, the
   coupling is inverted rather than removed, and the new boundary test guards 
only one of the two directions.
   
   **`AccountingCrossFeatureBoundaryTest`.** A few things worth tightening:
   
   - It only sees `fineract-accounting`'s own test classpath, so it can only 
catch a re-added Gradle dependency.
     Worth stating that limitation in a class comment so the guarantee isn't 
over-read.
   - `private static ApplicationModules modules` is lazily initialized without 
synchronization — not safe under
     parallel JUnit execution.
   - `owningArtifact` falls back to `"(unknown-source)"` when 
`JavaClass.getSource()` is empty, and that value is
     not in `FOUNDATION_ARTIFACTS` — so it counts as a violation. False-failure 
risk.
   - `printAccountingCrossFeatureDependencyReport` is permanently disabled 
behind an undocumented system property
     (`-Daccounting.boundary.report=true`). Either document it or drop it.
   - `com.tngtech.archunit` is imported directly but only arrives transitively 
via `spring-modulith-core` —
     declare it explicitly as a test dependency.
   
   **Build.** The `test { maxHeapSize = '2g'; jvmArgs += 
['-XX:MaxMetaspaceSize=1g'] }` block in
   `fineract-accounting/build.gradle` is a whole-module cost incurred by one 
test. Please add a comment explaining
   it is for the Spring Modulith full-classpath scan.
   
   **Nit.** The blank line after the license header in 
`fineract-accounting/dependencies.gradle` was removed for no
   apparent reason — restore it to match every other module.


-- 
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