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]