Repository navigation
feat(products): map share capital to the ledger, and route channels and charges - #342
Merged
Aman-Mittal merged 1 commit intoAug 14, 2026
Conversation
…nd charges
Finishes the product accounting epic. Two remaining families of mapping had
nowhere to be configured.
**Share products.** The form pinned `accountingRule: 1` on both create and
load, so every share product this application made was unaccounted, and opening
an accounted one and saving it reset it. Share capital is the members' stake in
the institution — the balance-sheet line that cannot be reconstructed
afterwards.
Enumerated from the platform, cash accounting needs four slots, not the three
the equity story suggests:
POST /products/share {"accountingRule": 2, …no mapping ids…}
→ shareReferenceId, shareSuspenseId, incomeFromFeeAccountId, shareEquityId
`shareEquityId` is the only equity slot in the application. Three things about
share products differ from every other family and are handled rather than
assumed:
* The template carries no `accountingRuleOptions`, so the rule selector's
options are supplied by the form rather than the platform.
* Only NONE and CASH are offered. The validator accepts 1, 2 and 3 — a 4 is
refused with "must be between 1 and 3" — but a product created under rule 3
asks for no mappings and reads back with `accountingMappings: {}`. That is a
product labelled ACCRUAL PERIODIC which posts nothing, which is worse than
one labelled NONE because it looks configured.
* The response uses the same keys as the request, `Id` suffix and all. Every
other family drops the suffix on the way out.
Fineract omits an option list entirely when the tenant holds no accounts of
that class, so a chart with no equity account returns no `equityAccountOptions`
at all rather than an empty array. The section already says so out loud.
**Advanced mappings** on loan and savings products: payment channel to fund
source, fee to income, penalty to income. Without them every collection lands
in one fund source and every fee in one income account, so a till cannot be
reconciled at close of day and a processing fee cannot be told from an
insurance fee after the fact.
The platform validates almost nothing here. Verified accepted, silently:
* a fee mapping naming a charge the product does not carry;
* a fee mapping naming a charge whose `penalty` flag is the other way round;
* two rows naming the same payment type.
None of those ever fire. Silent acceptance is worse than rejection because the
product looks configured, so the options are narrowed to what can work rather
than left to the server. Charges come from the product's own list — this form
has no charge picker, so on create the tables correctly report there is nothing
to map yet.
Request and response disagree about every key here too: written as
`{paymentTypeId, fundSourceAccountId}` and `{chargeId, incomeAccountId}`, read
back as `{paymentType: {id}, fundSourceAccount: {id}}` and `{charge: {id},
incomeAccount: {id}}`. A form feeding the response back unchanged would show
empty selects on a configured product and blank it on the next save.
Empty tables send no key. Both spellings are in fact accepted — a loan product
posts identically with `[]` and with the key absent — but omitting matches how
the base slots are built.
Closes apache#293. Closes apache#294.
The rows are rendered from computed view models rather than from methods the
template calls: a method returning a fresh array hands back a new identity on
every change-detection pass, which the dev-mode exhaustive checkNoChanges pass
reports as NG0100 and the e2e fixtures fail the run on.
Verified by two real-backend suites — a share product mapped to equity and
round-tripped through edit, and a loan product carrying a payment-channel
override — plus unit coverage for the gating and the two payload shapes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Finishes the product accounting epic #288. Two remaining families of mapping had nowhere to be configured.
Closes #293. Closes #294.
Share products
share-product-formpinnedaccountingRule: 1on both create and load, so every share product this application made was unaccounted, and opening an accounted one and saving it reset it. Share capital is the members' stake in the institution — the balance-sheet line that cannot be reconstructed afterwards.Enumerated from the platform, cash accounting needs four slots, not the three the equity story suggests:
shareEquityIdis the only equity slot in the application. Three things about share products differ from every other family, and are handled rather than assumed:accountingRuleOptions. It is the one product template that omits them, so the rule selector's options are supplied by the form.NoneandCashare offered. The validator accepts 1, 2 and 3 — a 4 is refused withmust be between 1 and 3— but a product created under rule 3 asks for no mappings and reads back withaccountingMappings: {}. That is a product labelled ACCRUAL PERIODIC which posts nothing, which is worse than one labelled NONE, because it looks configured.Idsuffix and all. Every other family drops the suffix on the way out (fundSourceAccountIdin,fundSourceAccountout).Also worth knowing: Fineract omits an option list entirely when the tenant holds no accounts of that class — a chart with no equity account returns no
equityAccountOptionsat all rather than an empty array. That is what the section's empty-state note is for, and why the e2e creates the equity account through the chart of accounts first.Advanced mappings on loan and savings products
Payment channel → fund source, fee → income, penalty → income. Without them every collection lands in one fund source and every fee in one income account, so a till cannot be reconciled at close of day and a processing fee cannot be told apart from an insurance fee after the fact.
The platform validates almost nothing here. Verified accepted, silently, against a live instance:
penaltyflag is the other way roundSilent acceptance is worse than rejection: the product looks configured and is not. So the options are narrowed to what can actually work — charges come from the product's own list rather than the tenant's catalogue, the two tables read opposite sides of the
penaltyflag, and a payment type already routed is not offered again.This corrects the assumption in #294 that unattached charges "produce mappings the platform will reject". It does not reject them.
Request and response disagree about every key, as with the base slots: written
{paymentTypeId, fundSourceAccountId}and{chargeId, incomeAccountId}, read back{paymentType: {id}, fundSourceAccount: {id}}and{charge: {id}, incomeAccount: {id}}. A form feeding the response straight back would show empty selects on a configured product and blank it on the next save.Empty tables send no key. Both spellings are in fact accepted — a loan product posts identically with
[]and with the key absent, contrary to what #294 anticipated — but omitting matches how the base slots are built.One scope note: these forms have no charge picker, so a product created here carries no charges and the two charge tables correctly report there is nothing to map yet. They come into their own on edit. A charges step on the product forms is a separate gap.
Change detection
The rows render from computed view models rather than from methods the template calls. A method returning a fresh array hands back a new identity on every change-detection pass, which the dev-mode exhaustive
checkNoChangespass reports as NG0100 and the e2e fixtures fail the run on.Unrelated but visible in the e2e logs:
GlAccountSelectComponentemits an NG0100 forng-untouchedon the product edit pages, from Angular's ownNgControlStatushost binding. Pre-existing, currently a warning rather than a failure, and not addressed here.Verification
npm run lint,format:check,check:icons,i18n:check,buildall clean.Checklist
src/app/api/.| appTranslate), per ADR-0003.en.json; existing ones left alone.