Repository navigation
Finish the deposit module's adapter migration, and three defects it exposed - #816
Conversation
…d the five reads it corrected
Seven screens still reached `src/app/api` directly — the two account lists, the two
transaction lists, both transaction forms and the recurring-deposit account form — which
is the last of the deposit module's ADR 0006 debt. Each is now stated once on
`DEPOSIT_ACCOUNT_API`, and every statement below was measured against a running Fineract
rather than inferred.
Five of them were wrong in a way the user could see:
- the recurring-deposit list rendered `recurringDepositAmount`, which that response does
not carry, so its Amount column was empty on every row. The field is
`mandatoryRecommendedDepositAmount`.
- both transaction lists interpolated the row date, which arrives as `[year, month, day]`,
so a date read "2026,10,18"; the fixed-deposit list also showed
`transactionType.code`, so a type read "savingsAccountTransactionType.payCharge".
- the on-hold list's "reason" column rendered `loanClientName`, not a field of that
response at all. What it sends is `savingsClientName`, `savingsAccountNo` and `loanId`,
and the loan is what a hold is about — these rows are created by loan guarantees.
- the recurring-deposit transaction form asked for its template without `?command=`, which
that endpoint answers `400 error.msg.query.parameter.value.unsupported` to, so its
payment-channel list was empty on every load. It also bound the raw option, which the
generated model types `Array<number>` and the platform sends as objects.
- the recurring-deposit account form read the product as `savingsProductId` on an edit; a
deposit account answers `depositProductId`, so an edit opened with no product selected.
Both account lists also gated Approve on `status.value === 'Submitted and pending
approval'` — the platform's own English label, which moves with its locale. They gate on
the status flag beside it now.
`template()` takes an optional `clientId`: `?clientId=0` answers `404` "Client not found
with valuer 0", so the fixed-deposit account form's `clientId ?? 0` listed no products
until a client was chosen.
The transactions are modelled rather than passed through, which is what lets the two
lists and the account view share one date and one label; `IsoDatePipe` renders an ISO
date in a template that has no formatter of its own, reading the parts out rather than
letting `new Date('2026-10-18')` land a day early west of Greenwich.
Twelve suppressions leave `eslint-suppressions.json`.
… not the browser `chartFromProduct` and `chartForRequest` dated the chart they build with `new Date()`. Fineract judges a `fromDate` by its own reckoning — the business date when one governs — so a browser running ahead of it has its chart refused, which is apache#758's class of defect and the last place in the products module still seeding a date from the client clock. The date is a required parameter rather than an injected service: this module is pure, has no injector, and a required argument is what stops the next caller forgetting it. Both deposit product forms pass `PlatformDateService.today()`. `slabsFromProduct` no longer takes one at all — it returns the slabs, and only the envelope carries a date — which the extracted slab mapper makes possible.
…ey sit in `NgModel` resolves its `ControlContainer` with `@Host()`, which stops at the component boundary, so a control declared inside `app-product-accounting-section` or `app-gl-account-select` could not reach the `ngForm` it visually sits in. Angular says so with NG01354, once per control, on every one of the six product forms that embed the section. The console noise was the smaller half. Because the controls never registered, the `required` on a mandatory GL slot never reached the parent form either: Save stayed enabled with the slots empty, and the platform refused the product — or took it unaccounted, which is what apache#288 is about. Resolved through a factory rather than `useExisting: NgForm`, so each component still renders outside a form: `skipSelf` looks past its own injector and `optional` answers `null` where there is no form above it, which is what `NgModel` expects of an absent container. A disabled control — a slot whose account class the tenant has none of — is excluded from validity by Angular, so an empty one cannot deadlock the form. The new spec renders the section inside a form and asserts both halves: every slot is registered, and the form is invalid until the mandatory ones are filled. Both assertions fail without the fix, and the NG01354 lines leave the test output with it.
E2E — mocked backend🎭 E2E Tests✅ All green — 427 passed · 0 failed · 1 skipped, across 40 spec files in 12m 41s. By spec file
All 428 tests — click to expand
Slowest 10 — what the shard counts should be tuned against
📼 Download the HTML report, videos and traces — see the Generated by run 39220204412 from |
E2E — real Fineract🎭 E2E Tests✅ All green — 171 passed · 0 failed · 0 skipped, across 50 spec files in 12m 35s. By spec file
All 171 tests — click to expand
Slowest 10 — what the shard counts should be tuned against
📼 Download the HTML report, videos and traces — see the Generated by run 39220204412 from |
🔄 What this change talks toFrom the 18 application TypeScript file(s) changed in this pull request. sequenceDiagram
autonumber
participant FineractDepositAccountApi as FineractDepositAccountApi (Service)
participant RecurringDepositAccountService as RecurringDepositAccountService (API client)
participant Fineract as Fineract (REST API)
participant FixedDepositAccountService as FixedDepositAccountService (API client)
participant FixedDepositAccountTransactionsService as FixedDepositAccountTransactionsService (API client)
participant RecurringDepositAccountTransactionsServi as RecurringDepositAccountTransactionsService (API client)
participant RecurringDepositProductService as RecurringDepositProductService (API client)
participant FixedDepositProductService as FixedDepositProductService (API client)
participant SavingsChargesService as SavingsChargesService (API client)
participant DepositAccountOnHoldFundTransactionsServ as DepositAccountOnHoldFundTransactionsService (API client)
participant DepositAccountViewComponent as DepositAccountViewComponent (Component)
participant DEPOSIT_ACCOUNT_API as DEPOSIT_ACCOUNT_API (Injected)
FineractDepositAccountApi->>RecurringDepositAccountService: getRecurringdepositaccounts()
RecurringDepositAccountService->>Fineract: getRecurringdepositaccounts()
Fineract-->>RecurringDepositAccountService: response
FineractDepositAccountApi->>FixedDepositAccountService: getFixeddepositaccounts()
FixedDepositAccountService->>Fineract: getFixeddepositaccounts()
Fineract-->>FixedDepositAccountService: response
FineractDepositAccountApi->>FixedDepositAccountTransactionsService: getFixeddepositaccountsFixedDepositAccountIdTransactions()
FixedDepositAccountTransactionsService->>Fineract: getFixeddepositaccountsFixedDepositAccountIdTransactions()
Fineract-->>FixedDepositAccountTransactionsService: response
FineractDepositAccountApi->>RecurringDepositAccountTransactionsServi: getRecurringdepositaccountsRecurringDepositAccountIdTransactionsTemplate()
RecurringDepositAccountTransactionsServi->>Fineract: getRecurringdepositaccountsRecurringDepositAccountIdTransactionsTemplate()
Fineract-->>RecurringDepositAccountTransactionsServi: response
FineractDepositAccountApi->>FixedDepositAccountTransactionsService: getFixeddepositaccountsFixedDepositAccountIdTransactionsTemplate()
FixedDepositAccountTransactionsService->>Fineract: getFixeddepositaccountsFixedDepositAccountIdTransactionsTemplate()
Fineract-->>FixedDepositAccountTransactionsService: response
FineractDepositAccountApi->>RecurringDepositAccountTransactionsServi: postRecurringdepositaccountsRecurringDepositAccountIdTransactions()
RecurringDepositAccountTransactionsServi->>Fineract: postRecurringdepositaccountsRecurringDepositAccountIdTransactions()
Fineract-->>RecurringDepositAccountTransactionsServi: response
FineractDepositAccountApi->>FixedDepositAccountTransactionsService: postFixeddepositaccountsFixedDepositAccountIdTransactions()
FixedDepositAccountTransactionsService->>Fineract: postFixeddepositaccountsFixedDepositAccountIdTransactions()
Fineract-->>FixedDepositAccountTransactionsService: response
FineractDepositAccountApi->>RecurringDepositAccountService: getRecurringdepositaccountsTemplate()
RecurringDepositAccountService->>Fineract: getRecurringdepositaccountsTemplate()
Fineract-->>RecurringDepositAccountService: response
FineractDepositAccountApi->>FixedDepositAccountService: getFixeddepositaccountsTemplate()
FixedDepositAccountService->>Fineract: getFixeddepositaccountsTemplate()
Fineract-->>FixedDepositAccountService: response
FineractDepositAccountApi->>RecurringDepositProductService: getRecurringdepositproductsProductId()
RecurringDepositProductService->>Fineract: getRecurringdepositproductsProductId()
Fineract-->>RecurringDepositProductService: response
FineractDepositAccountApi->>FixedDepositProductService: getFixeddepositproductsProductId()
FixedDepositProductService->>Fineract: getFixeddepositproductsProductId()
Fineract-->>FixedDepositProductService: response
FineractDepositAccountApi->>RecurringDepositAccountService: getRecurringdepositaccountsAccountIdTemplate()
RecurringDepositAccountService->>Fineract: getRecurringdepositaccountsAccountIdTemplate()
Fineract-->>RecurringDepositAccountService: response
FineractDepositAccountApi->>FixedDepositAccountService: getFixeddepositaccountsAccountIdTemplate()
FixedDepositAccountService->>Fineract: getFixeddepositaccountsAccountIdTemplate()
Fineract-->>FixedDepositAccountService: response
FineractDepositAccountApi->>RecurringDepositAccountService: postRecurringdepositaccounts()
RecurringDepositAccountService->>Fineract: postRecurringdepositaccounts()
Fineract-->>RecurringDepositAccountService: response
FineractDepositAccountApi->>FixedDepositAccountService: postFixeddepositaccounts()
FixedDepositAccountService->>Fineract: postFixeddepositaccounts()
Fineract-->>FixedDepositAccountService: response
Truncated for readability: 17 further participant(s) and 22 further call(s) not shown.
Generated by run 39220204414 from |
`main` has been red on Translation Check since apache#819 merged, and neither apache#819 nor the change it landed beside was wrong on its own — both were green. apache#816 renamed `ON_HOLD_TRANSACTIONS.REASON` to `LOAN`, because the column shows the loan the funds are held against and never held a reason. apache#819 was authored before that and translated the key as it then stood. Git merged the two cleanly: they touch different keys in different files, so there was nothing to conflict. The result is a catalogue that disagrees with `en.json` in both directions at once: fr.json defines ON_HOLD_TRANSACTIONS.REASON, which en.json does not — remove it. fr.json covers 2746 keys, down from 2747. So `Motif` becomes `Prêt` — a retranslation rather than a rename, since the column means something else now. The same merge left the sixteen keys from apache#817's password and access work untranslated, for the same reason: that branch predated `fr.json` existing, so there was no French file for it to add them to. They are translated here, which takes `fr` to 2763/2763, and the coverage baseline is refreshed so the ratchet holds the gain rather than the old floor. `app.language.fr` is added to `hi.json` too. apache#819 registered French in the language pickers and gave `en` and `fr` a label for it, so a Hindi user was offered the new language under a raw translation key.
Unit Tests and two of the three real-Fineract E2E shards have been failing on `main`, and
not because of anything that merged. They fail on the clock. Every run started before
17:45 UTC is green; both runs started after 18:26 UTC are red, with the same four jobs:
19:07Z apache#817 merge 4 failures
18:26Z apache#819 merge 4 failures
15:02Z all green
14:23Z apache#816 merge all green
12:46Z all green
`Asia/Kolkata` is UTC+5:30, so its date turns over at 18:30 UTC. `AppConfig.tenantTimezone`
defaults to it, which is the zone the dialog resolves today in — deliberately, because the
platform validates `activate` against a date it stamped in the tenant's zone, not the
browser's. Two cases here asserted `component.actionDate()` against
`toIsoDate(new Date())`, the browser's today. For five and a half hours each evening those
are different days, the component answers the tenant's and the assertion wants the
browser's, and the job goes red:
AssertionError: expected '2026-10-20' to be '2026-10-21'
The spec's own header claimed these cases "hold whatever time it is". They now do, by
pinning the platform's clock through `recordServerTime` instead of reading the real one.
It is pinned inside the window that was breaking them — 20:00Z on the 15th, already the
16th in `Asia/Kolkata` — so the cases assert the tenant's date against a browser that
would say the 15th, which is the disagreement they exist for. Verified green at
`Asia/Kolkata`, `UTC`, `America/New_York`, `Etc/GMT-10`, `Etc/GMT+12` and
`Pacific/Kiritimati`, and failing 4 of 5 when the component is reverted to the browser
clock — before this it caught that regression only between 18:30 and midnight UTC.
The relative dates move with it: `offsetFromToday` stepped the browser's clock with local
getters, so the floor cases drifted on the same boundary. They are ISO arithmetic from the
pinned date now, anchored at midday UTC so no offset can shift them.
The two E2E shards are the same disagreement — shard 1 holds `group-membership.spec.ts`
and shard 3 `tenant-date-defaults-backend.spec.ts`, both written for this window — but
they are not addressed here. Reproducing them needs a live platform inside the window, and
a guess at an E2E spec is not worth making.
Three pieces of migration debt this module's own comments named, and the defects that came out of doing the work. Everything below was measured against a running Fineract, not inferred.
1. The last seven deposit screens leave
src/app/api(ADR 0006)The two account lists, the two transaction lists, both transaction forms and the recurring-deposit account form now go through
DEPOSIT_ACCOUNT_API. Twelve entries leaveeslint-suppressions.json.Doing it found five reads a user could see were wrong:
recurringDepositAmountmandatoryRecommendedDepositAmount— so the Amount column was empty on every rowdateas-is, andtransactionType.code[year, month, day]andtransactionType.value— so a row read2026,10,18andsavingsAccountTransactionType.payChargeloanClientNamesavingsClientName,savingsAccountNoandloanId— so the column was blank on every row?command=400 error.msg.query.parameter.value.unsupportedwithout one, so the payment-channel list was empty on every loadsavingsProductIddepositProductId, so an edit opened with no product selectedAlso:
status.value === 'Submitted and pending approval', the platform's own English label. They use the status flag beside it now.template()takes an optionalclientId, because?clientId=0answers404Client not found with valuer 0 — the fixed-deposit account form sentclientId ?? 0and listed no products until a client was chosen.paymentTypeOptionsis typedArray<number>by the document and sent as objects; the recurring form bound the raw element, which renders[object Object].IsoDatePipeis new, for a template with no formatter of its own; it reads the parts out rather than lettingnew Date('2026-10-18')land a day early west of Greenwich.Relates to #653 and #295.
2. A deposit product's rate chart was dated from the browser
chartFromProductandchartForRequestbuilt theirfromDatefromnew Date(). Fineract judges a date by its own reckoning — the business date when one governs — so a browser ahead of it has the chart refused. That is #758's class of defect, and the last place in the products module still seeding a date from the client clock. The date is now a required parameter (this module is pure and has no injector), and both product forms passPlatformDateService.today().3. NG01354: the accounting section's controls never joined the form
NgModelresolves itsControlContainerwith@Host(), which stops at the component boundary, so no control insideapp-product-accounting-sectionorapp-gl-account-selectcould reach thengFormit sits in. Angular logs NG01354 once per control on all six product forms that embed the section.The console noise was the smaller half:
requiredon a mandatory GL slot never reached the parent form either, so Save stayed enabled with the slots empty and the platform refused the product — or took it unaccounted, which is #288. Fixed withviewProvidersresolved through a factory rather thanuseExisting: NgForm, so each component still renders outside a form.Checks
deposit-account-servicing,deposit-lifecycle-backend,loan-product-accountingandworking-capital-product-accounting-backendpass.deposit-product-configurationhas one failure on a long-reused instance — the product is created (verified through the API) but the spec's list filter types into the global header search and the new product is off page one with 103 products present. That is Backend e2e specs assume a freshly seeded instance and fail when one is reused #664, reproducible onmain.$500.00, the fixed-deposit transactions list10/19/2026 · $ 10.00 · Deposit, the on-hold list10/19/2026 · 50.00 · hold · —, and both transaction forms listMoney Transferand the other two channels by name.lint:pruneapplied,i18n:check,format:check,api:surface,check:template-text,check:a11y-names,check:ui-primitives,check:icons,check:responsive,check:internal-endpointsall clean.Not in this PR
The SMS campaign create form still cannot post:
runReportIdandparamValueare mandatory and the form asks for neither. Measured, that screen's own template answers403 error.msg.mobile.service.provider.not.availableon a stock instance, so neither the report picker nor the shape ofparamValuecan be established without a configured message gateway — #790. Left where it is rather than guessed at.