Aman-Mittal opened a new pull request, #802:
URL: https://github.com/apache/fineract-backoffice-ui/pull/802

   Closes #773, closes #774, closes #775, closes #776.
   
   ## What and why
   
   The last four open Working Capital issues, after #770–#772 and #777 landed 
in #800. Each is a
   gap between what the module's API exposes and what this UI could reach.
   
   ### Transactions could not be opened, undone or adjusted (#773)
   
   The transactions tab was a read-only table: a mistaken repayment could not 
be reversed from this
   application at all, and nothing showed how a transaction was allocated 
between principal, fees
   and penalties.
   
   A row now opens a detail panel carrying the allocation, the external id and 
the reversal state,
   with the three corrections the module supports. **Which correction is 
offered depends on the
   transaction type**, because the platform refuses the rest:
   `WorkingCapitalLoanWritePlatformServiceImpl` accepts undo for 
`WAIVE_CHARGES`, `REPAYMENT`,
   `GOODWILL_CREDIT`, `CHARGE_ADJUSTMENT` and `PAYOUT_REFUND`, and adjust for 
the same list without
   the waiver — a waiver has no amount to re-state. Measured as well as read: 
undo of a disbursement
   is refused by type, undo of a repayment and of a charge adjustment succeed, 
undo of an
   already-reversed transaction is refused, and adjust reverses the original 
and books a
   replacement. A discount fee carries no boolean flag on its type enum — there 
is one per type and
   none for this one — so it is recognised by `loanTransactionType.discountFee` 
and takes
   `discountFeeAdjustment` rather than `adjust`.
   
   Reversed rows are struck through **and** carry a text marker, because 
strike-through alone reads
   to a screen reader as money that moved.
   
   With maker-checker on, Fineract answers 200 with a `commandId` and no 
`resourceId`: the reversal
   is queued and nothing has happened yet. The panel says so rather than 
re-reading and showing the
   transaction unchanged with no explanation.
   
   ### Charges could not be opened or adjusted, and the discount fee could not 
be posted (#774)
   
   Charges could be added and waived. A charge entered wrongly could only be 
waived in full, and the
   amounts paid, waived and outstanding were invisible. A charge row now opens 
a panel with all of
   them plus **Adjust**, which takes an amount and nothing else — an empty body 
answers
   `validation.msg.workingCapitalLoanChargeAdjustment.amount.cannot.be.blank`, 
and the platform
   dates the adjustment itself. It is prefilled with what is outstanding, which 
is both the usual
   figure and the platform's cap.
   
   An adjustment books a **Charge Adjustment transaction**, so the panel, the 
list, the transactions
   tab and the loan header are all re-read after it. A waiver does too, and 
only the charge list was
   being refreshed before — fixed here because the panel made it visible.
   
   `discountFee` joins the action route, offered on an active loan that has no 
discount yet:
   Fineract refuses a second one, and a loan priced with a discount at 
disbursement reads back
   carrying it. Changing an agreed discount stays the `PUT /discount` 
sub-resource behind "Apply
   discount" — a different operation with a different code. Its amount is 
entered rather than
   offered, because its template carries the currency and the classification 
options and no amount.
   
   ### A product could be saved with a posting rule and no GL accounts (#775)
   
   The form offered `accountingRule` and nothing else, so an accrual product 
had no accounts mapped
   and nothing it booked could post. Worse, **the rule itself could not be 
saved**: the select bound
   each option's `code`, the dotted `wcAccountingRuleType.none` spelling, and 
the platform accepts
   only the name —
   
       POST /working-capital-loan-products {"accountingRule": 
"wcAccountingRuleType.none"}
       → 400 …accountingRule.is.not.one.of.expected.enumerations
   
   so any product saved after touching that select was refused. The form now 
uses the shared
   accounting section, which binds `id`.
   
   Nineteen slots, twelve mandatory, enumerated from the platform the way the 
other five families
   were — post the rule with no mapping ids and read the 400. 
`AccountingMappingField` gains
   `required`, defaulting to true, because every slot of those five families 
*is* mandatory under the
   rules that name it; the Working Capital product is the first with optional 
ones (the charge-off,
   fraud and goodwill slots).
   
   Two differences from the term-loan list look like omissions and are not. 
There is **no interest
   slot** — this product earns a discount fee, and 
`incomeFromDiscountFeeAccountId` is the mandatory
   slot in its place — and there is a **deferred income liability**, because 
the fee is recognised
   over the loan's life. The module also has only two rules, `NONE` and 
`ACC_DEF_REV_AM`, so the
   shared model and section are generalised from a numeric rule to `number | 
string`; the other five
   families call them unchanged.
   
   Charge-off and write-off **reason mappings** are new UI, and route a loss to 
an expense account by
   *why* it happened. The trap they absorb: the request keys disagree
   (`chargeOffReasonCodeValueId` against `writeOffReasonCodeValueId`) while 
both read back under
   `reasonCodeValue`. **Payment allocation** reuses the existing editor with 
its future-installment
   rule and credit-allocation half turned off — this family has neither, and 
the parameter is
   refused.
   
   The issue asks for a clear message where a user holds 
`UPDATE_WORKINGCAPITALLOANPRODUCT` but not
   `READ_GLACCOUNT`, on the premise that the picker loads `/glaccounts`. It 
does not: the options come
   from the product template, and a user holding only the two product codes 
gets all four option
   lists back — measured. There is no call to be refused. The real empty case 
is a tenant with no
   accounts of a class, which the section already states per group.
   
   ### Custom datatables had nowhere to live (#776)
   
   `EntityTables` declares `WC_LOAN` and `WC_LOAN_PRODUCT`, so an institution 
can attach datatables
   to both, and this UI could neither show nor enter that data. Loans get a 
**Custom fields** tab;
   the product form gets a custom fields section on edit, since a row is keyed 
by the product.
   
   The shared `EntityDatatablesComponent` gains three things, which reach 
beyond Working Capital —
   clients, groups, centres, offices, loans and deposit accounts render the 
same component:
   
   - it **lists only the tables the user can read**. Removed rather than 
disabled, which is right
     here: a table they cannot read has no rows to show. It also stops the 
component fetching rows
     that can only answer 403.
   - **Add, Edit and Delete are gated** on the code generated for that table, 
disabled with the
     missing code named.
   - **a row can be edited and deleted**, which needs the one-row and many-row 
cases told apart. The
     listing carries no `multiRow` flag, so the shape is the discriminator: a 
multi-row table's
     primary key is its own `id` and the entity key rides alongside, while a 
single-row table's
     primary key *is* the entity key — which decides whether a row is addressed 
by id or by entity.
     Both were measured by registering one of each.
   
   Create-time checks are **honoured, not reported**. With a datatable 
registered as a CREATE check,
   `POST /working-capital-loans` answers 403 "Please fill in the data table [x] 
before creating this
   working capital loan", and the row cannot be added afterwards because the 
create request carries
   the rows inline. The application form reads the checks that apply, collects 
each table's fields
   through the existing entry dialog in a new collect-only mode, and sends them 
as
   `datatables: [{registeredTableName, data}]`. Submit is held back while one 
is outstanding, rather
   than letting the user fill a long form and collect a 403.
   
   ## Permissions
   
   Each new control carries its own code, and the pairing was measured by the 
probe #691 and #777
   established — a role holding exactly one code, the command sent with an 
empty body:
   
   | Command                        | Code                                      
 |
   | ------------------------------ | 
------------------------------------------ |
   | `undo` (transaction)           | `UNDO_WORKINGCAPITALLOANTRANSACTION`      
 |
   | `adjust` (transaction)         | `ADJUST_WORKINGCAPITALLOANTRANSACTION`    
 |
   | `discountFee`                  | `DISCOUNTFEE_WORKINGCAPITALLOAN`          
 |
   | `discountFeeAdjustment`        | 
`DISCOUNTFEEADJUSTMENT_WORKINGCAPITALLOAN` |
   | `adjustment` (charge)          | `ADJUSTMENT_WORKINGCAPITALLOANCHARGE`     
 |
   | datatable rows                 | `{READ,CREATE,UPDATE,DELETE}_<table>`     
 |
   
   `UPDATE_WORKINGCAPITALLOAN` authorises none of the 22 commands the diagonal 
now covers.
   
   Two things that fell out of the probe and are worth recording. 
`ADJUST_WORKINGCAPITALLOANTRANSACTION`
   was **missing from `scripts/permission-codes.json`**, so the snapshot is 
refreshed from the live
   platform: 707 codes against the recorded 720, which had drifted in both 
directions. And the probe
   itself had to get stricter — it treated any 403 as "not authorised", and the 
platform also answers
   403 for a domain rule violation, which made `WAIVE_WORKINGCAPITALLOANCHARGE` 
look unauthorised on
   a charge an earlier spec had waived. It now reads the globalisation code.
   
   ## Why the detail views are panels rather than routes
   
   #773 and #774 propose detail routes. A new routed component cannot be 
written today without
   naming Ionic and the generated client directly, which ADR 0005 and ADR 0006 
forbid outside
   `src/app/ui` and the API adapters — and the form primitives those ADRs plan 
do not exist yet, so
   there is nothing to build an amount or a date field from. 
`eslint-suppressions.json` may only
   shrink, so the detail lives in the already-suppressed loan screen that owns 
the tab. The loan
   route is read-gated, which is what the issues ask the detail routes to be, 
and each action still
   carries its own code.
   
   ## Verification
   
   - `npm test` — **2215 unit tests pass across 299 files**, up from 2156. New 
coverage: which
     corrections each transaction type is offered, the undo and adjust 
payloads, the maker-checker
     answer, the charge adjustment payload and refresh, the discount-fee gate 
and command, the
     accounting payload per rule and its prefill (including the reason tables' 
key renaming), the
     per-table datatable permissions, the one-row/many-row discrimination, and 
the create-check
     collection.
   - `npx playwright test --project=backend 
e2e/working-capital-*-backend.spec.ts` — **14 pass in
     46s** against `apache/fineract:latest`, across four specs. Two are new:
     `working-capital-servicing-backend.spec.ts` (undo, the disbursement's 
refusal by type, adjust
     reversing and replacing, a charge adjustment appearing as a transaction, a 
datatable row added,
     edited and deleted, and a loan created that a CREATE check would otherwise 
refuse) and
     `working-capital-product-accounting-backend.spec.ts` (the twelve accounts 
configured through the
     form and read back, and a configured product re-saved without losing 
anything).
   - `npx playwright test --project=mocked 
e2e/working-capital-loan-actions.spec.ts` — 17 pass,
     unchanged.
   - `npx playwright test --project=mocked e2e/rbac-feature-flag.spec.ts 
e2e/rbac-route-protection.spec.ts`
     — 27 pass, exercising the changed permission paths across the rest of the 
application.
   - All fifteen repo checks, `eslint` (no new suppressions), `prettier 
--check` and `typecheck:e2e`
     pass. Hindi stays at 100% coverage.
   
   Two defects in the existing e2e seeding came out of this and are fixed: the 
product short name was
   built from the leading characters of a base-36 timestamp, which every run in 
the same minute
   shares, so two runs a minute apart collided; and a teardown that threw 
inside a `finally` was
   reporting a timeout in the test body as a failed DELETE.
   
   One note on the output: configuring the GL selects raises the 
interval-sampling `NG0100` on
   `ng-untouched` that issue #572 documents, which the fixture classifies as a 
known artifact and does
   not enforce. It comes from `NgControlStatus` on a programmatically set 
`ngModel`.
   
   ## Screenshots
   
   Not applicable — no new screens. The loan screen gains a Custom fields tab 
and two detail panels
   inside tabs it already had; the product form gains accounting, 
reason-mapping and allocation
   sections.
   
   ## Checklist
   
   - [x] I did not hand-edit generated files under `src/app/api/`; 
`api-surface.json` was regenerated
         with `check-api-surface.mjs --write`.
   - [x] New component or service code uses the adapter boundary — no new file 
names Ionic or the
         generated client, which is why the detail views are panels; see the 
section above.
   - [x] User-facing strings use translation keys (39 new keys, `en` and `hi`).
   - [x] I added or updated tests appropriate to this change.
   - [x] UI workflow changes include suitable e2e coverage, including 
real-backend testing where
         relevant — two new backend specs, and the existing RBAC probe extended.
   - [x] Commits are signed.
   - [x] I followed the AI-assisted contributions guidance.
   - [x] These are the UI side of endpoints the platform already exposes, 
raised as issues #773–#776
         before this change.
   
   ## AI assistance
   
   - Tool / model: Claude Opus 5 (1M context)
   - Harness / workflow: Claude Code


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