Repository navigation
fix(dates): finish the platform-date migration, fix a mutated signal, and sync the Fineract spec - #815
Conversation
`addPeriod()` called `periods().push(...)`, which mutates the array the signal already holds. The signal's value never changes, so nothing is notified: the new row reaches the screen only because this component runs with default change detection and the `@for` is re-evaluated on the next pass for unrelated reasons. `removePeriod()` spliced the same array. That made a row's arrival a matter of timing, and the e2e that adds two periods and counts the date pickers found one of them whenever the machine was busy — deterministically, in the mocked shard, on `main` as much as on a branch. It would stop arriving at all under `OnPush` or in a zoneless build. The spec was racing too: it read the picker ids immediately after adding the rows, so it asserted against whatever had rendered by then. It now waits for the count it expects, which is what makes the unique-id assertion mean anything. Both fixed: the shard this failed in is green, and so is the spec on its own.
…the browser apache#758 named one shared source of "tenant today" and left the migration at the seven create forms the issue reproduced. This finishes it: the other forty-odd seeds now read the same source, so no screen in the application dates a record by the browser's clock. They divide into three kinds, all refusable by the platform and all previously `new Date()`: - **commands** — the savings account view's three stamped dates, the client, group and centre action dialogs, the loan officer and disbursement dialogs, the working-capital actions - **transactions and charges** — the loan, savings and both deposit transaction forms, and the three charge due-date defaults - **entity and reference forms** — office, staff, journal entry, frequent postings, accounting closure, office transaction, reschedule, standing instruction, collection sheet, interest rate chart, floating rate Read synchronously through `today()` rather than subscribed to: by the time any of these renders the header has already resolved the date for the session, and where it has not — a cold deep link — `today()` answers the browser's date exactly as the old initialiser did, so nothing regresses. The seven create forms keep their resolve subscription, which is what makes the cold case correct for the screens the issue was filed about. The report filters (point-in-time search, portfolio summary, work queues) moved with them: a range defaulting past the tenant's today answers an empty report, which reads as no data rather than as a date problem. `deposit-chart.ts` is deliberately left: it is a pure function module with no injector, and its `fromDate` is a chart envelope rather than a user-facing default. No test changed. The suite's 2487 cases pass untouched, because the synchronous read falls back to the same value the initialisers used.
…ypes caught
The spec baked into `apache/fineract:latest` has moved since 12 October:
`sha256:7b23f0ae…` → `sha256:f9251770…`, spec `d752ac48…` → `368bbd85…`. Same 602 paths and
967 operations, so nothing was removed and no call site loses its endpoint; what changed is
precision. Sixteen schemas arrive — credit bureau, email, SMS campaign and two datatable request
bodies — twenty-one operations stop taking `object` or `string` and start taking them, and
`CommandWrapper` is gone.
That precision caught two screens posting bodies the platform has always refused. Both measured
against a running Fineract, both quoted here because neither could be guessed from the code:
**Email Messages could never send a message.** It posted
`JSON.stringify({ to, subject, body })` — a JSON *string* where an object belongs, carrying
three field names the endpoint does not accept:
The JSON syntax provided in the body of the request is invalid:
Expected BEGIN_OBJECT but was STRING at line 1 column 2
The request is `{ clientId, emailSubject, emailMessage, locale }`: the recipient is a client,
and the platform reads the address off their record ("The group, client or staff provided has no
email address" when they have none). So the free-text To field becomes the client search the
rest of the application already uses, and the send now succeeds — `resourceId: 2` against a
client with an address, where the old payload got a 400.
**An SMS campaign could not be activated.** `activate()` passed the string `'activate'` as the
request *body* and sent no `command` at all. The command belongs in the query and the body
carries the date, which is what `SmsCampaignHandlerDto` now says: `?command=activate` with
`activationDate`, and `?command=close` for the counterpart.
Left alone deliberately: creating an SMS campaign is refused for a third reason —
`validation.msg.sms.runReportId.cannot.be.blank` and `validation.msg.sms.paramValue.cannot.be.blank`
— because the form collects neither. The payload is now typed as the DTO, which is what makes
that gap visible, and filling it means asking for a report and its parameters: a change to the
form, not to this sync.
One test was leaning on two models agreeing: a share account's `accountNo` is now `string` in
the single-account response, which is what Fineract sends, while the page item still declares
`number`. The test now casts to the type the method it calls actually takes.
🔄 What this change talks toFrom the 44 application TypeScript file(s) changed in this pull request. sequenceDiagram
autonumber
participant AccountingClosureFormComponent as AccountingClosureFormComponent (Component)
participant PlatformDateService as PlatformDateService (Injected)
participant OFFICE_API as OFFICE_API (Injected)
participant ACCOUNTING_CLOSURE_API as ACCOUNTING_CLOSURE_API (Injected)
participant Router as Router (Injected)
participant FrequentPostingsComponent as FrequentPostingsComponent (Component)
participant AccountingRulesService as AccountingRulesService (API client)
participant Fineract as Fineract (REST API)
participant CurrencyService as CurrencyService (API client)
participant PaymentTypeService as PaymentTypeService (API client)
participant NotificationService as NotificationService (Injected)
participant I18N as I18N (Injected)
AccountingClosureFormComponent->>PlatformDateService: today()
AccountingClosureFormComponent->>OFFICE_API: list()
AccountingClosureFormComponent->>ACCOUNTING_CLOSURE_API: create()
AccountingClosureFormComponent->>Router: navigate()
FrequentPostingsComponent->>PlatformDateService: today()
FrequentPostingsComponent->>AccountingRulesService: getAccountingrules()
AccountingRulesService->>Fineract: getAccountingrules()
Fineract-->>AccountingRulesService: response
FrequentPostingsComponent->>CurrencyService: getCurrencies()
CurrencyService->>Fineract: getCurrencies()
Fineract-->>CurrencyService: response
FrequentPostingsComponent->>PaymentTypeService: getPaymenttypes()
PaymentTypeService->>Fineract: getPaymenttypes()
Fineract-->>PaymentTypeService: response
FrequentPostingsComponent->>NotificationService: error()
FrequentPostingsComponent->>I18N: translate()
FrequentPostingsComponent->>NotificationService: success()
FrequentPostingsComponent->>Router: navigate()
Truncated for readability: 91 further participant(s) not shown.
Generated by run 39147816054 from |
E2E — mocked backend🎭 E2E Tests✅ All green — 427 passed · 0 failed · 1 skipped, across 40 spec files in 9m 4s. 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 39147815980 from |
E2E — real Fineract🎭 E2E Tests✅ All green — 171 passed · 0 failed · 0 skipped, across 50 spec files in 10m 51s. 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 39147815980 from |
… users be withdrawn Two gaps on the Security screens, both measured against a running Fineract. ## The password is checked before it is sent (apache#753) Create User validated its two password fields for presence and nothing else: with different values in them Save stayed enabled, and the only feedback was a 400 in a toast — the same toast that printed the password in clear text until the previous commit. The rules are the tenant's own. `GET /passwordpreferences` answers the active policy and `/passwordpreferences/template` all three — `simple`, `secure` and `strong`, each with the description the platform words itself — so the description is shown as the field's helper text rather than restated here. What that endpoint does *not* answer is the regex it enforces: that appears only in the `args` of a rejected password, which carry the submitted password with them, so `core/security/password-policy.ts` mirrors the rules by `key`. Mirroring carries one obligation, and the fallback honours it: an unrecognised key gets the loosest rules, so a policy this code has not heard of cannot refuse a password the platform would have accepted. The form now names the rules a password still fails — "repeat no character twice in a row" for a password refused on its `ss` alone, which the platform's own message never says — reports a mismatch under the confirmation field, and holds Save until both are right. The profile screen's change-password form shares the same rules through the same adapter operation, so the two cannot drift. The password pair also moved out of the form model into signals. `[(ngModel)]="user().password"` mutates the object a signal holds without notifying it, so nothing derived from it would ever recompute — the trap that emptied the floating-rate editor in apache#815. The fields are hidden entirely when the platform is to mail a generated password, which it refuses to do if the request carries one. ## Access can be withdrawn (apache#755) Roles and Users offered create and edit only, so a departed officer's access could be cut only by editing their roles down to nothing, and a role created by mistake stayed in every picker for good. - a role can be disabled, re-enabled and deleted. `GET /roles` has always carried `disabled` — the stock `Self Service User` role answers `true` — and the screen now shows it as a status and acts on it. `GET /users/template` already filters disabled roles out of the options a new user can be given, so switching one off is enough to stop it being assigned again. - a user can be deleted, which is the only withdrawal the platform offers for an account. Never on the signed-in one: the platform allows that, and an administrator who deletes the account they are signed in with locks themselves out. Every action is gated on its permission (`DISABLE_ROLE`, `ENABLE_ROLE`, `DELETE_ROLE`, `DELETE_USER`) and confirmed first, destructively for the two deletes. Deleting a role the platform still refuses — `DELETE /roles/{id}` answers 403 `error.msg.role.associated.with.users.deleted` while any user holds it — reaches the user as "Role with identifier 34 associated with users", because `errorInterceptor` reports a domain-rule 403 in the platform's own words. Verified in the browser against a running instance, along with the status going ACTIVE → DISABLED → ACTIVE and both deletes.
Three things, each measured against a running
apache/fineract:latest.A mutated signal, and the shard failure it was causing
floating-rate-formadded a rate period withperiods().push(...), which mutates the array thesignal already holds. The signal's value never changes, so nothing is notified — the row reaches
the screen only because this component runs with default change detection and the
@forisre-evaluated on the next pass for unrelated reasons.
removePeriod()spliced the same array.That made a row's arrival a matter of timing.
date-picker-revisit.spec.tsadds two periods andcounts the date pickers; it found one of them, deterministically, in mocked shard 1/4 — on
mainas much as on a branch, which I confirmed with a clean worktree. It would stop arriving atall under
OnPushor without zones.The spec was racing too: it read the picker ids immediately after adding the rows. It now waits
for the count it expects, which is what makes the unique-id assertion mean anything. The shard is
green.
The rest of #758
#758 asked for one shared source of "tenant today" and the first pass covered the seven create
forms the issue reproduced. The other forty-odd seeds now read the same source, so no screen
dates a record by the browser's clock: the savings account view's three stamped dates, the client,
group, centre, officer and disbursement dialogs, the working-capital actions, the loan, savings
and both deposit transaction forms, the three charge due-date defaults, and the office, staff,
journal entry, closure, reschedule, standing instruction, collection sheet and rate-chart forms.
Report filters moved with them — a range defaulting past the tenant's today answers an empty
report, which reads as no data rather than as a date problem.
Read synchronously: by the time any of these renders, the header has resolved the date for the
session, and where it has not,
today()answers the browser's date exactly as the oldinitialiser did. No test changed — all 2487 pass untouched.
deposit-chart.tsis deliberately left: a pure function module with no injector, whosefromDateis a request envelope rather than a user-facing default.
The spec sync, and the two screens it caught
The spec in
apache/fineract:latesthas moved since 12 October (7b23f0ae…→f9251770…). Same602 paths and 967 operations — nothing removed, no call site loses its endpoint — but sixteen new
schemas, twenty-one operations that stop taking
objectorstring, and no moreCommandWrapper.That precision caught two screens posting bodies the platform has always refused:
Email Messages could never send a message. It posted
JSON.stringify({ to, subject, body })— a JSON string where an object belongs, with three field names the endpoint does not accept:
The request is
{ clientId, emailSubject, emailMessage, locale }— the recipient is a client,and the platform reads the address off their record. The free-text To field becomes the client
search the rest of the app already uses, and the send now succeeds (
resourceId: 2) where theold payload got a 400.
An SMS campaign could not be activated.
activate()passed the string'activate'as therequest body and sent no
command. The command belongs in the query, the body carries thedate:
?command=activatewithactivationDate, and?command=closefor the counterpart.Creating an SMS campaign is still refused, for a third reason the new DTO makes visible —
runReportIdandparamValueare mandatory and the form collects neither. The payload is typednow, which is what names the gap; filling it means asking for a report and its parameters, which
is a change to the form rather than to this sync.
public/api/fineract.provenance.jsonis updated the way the weekly workflow writes it, andverify-api-clientexits 0 against the committed client.Verification
main), and the picker spec green aloneservicing set
lint,lint:prune,prettier --check,check-api-surface,verify-api-client, translationcheck: all clean