Skip to content

feat(products): map share capital to the ledger, and route channels and charges - #342

Merged
Aman-Mittal merged 1 commit into
apache:mainfrom
Aman-Mittal:feat/share-and-advanced-accounting
Aug 14, 2026
Merged

Aman-Mittal merged 1 commit into
apache:mainfrom
Aman-Mittal:feat/share-and-advanced-accounting

Conversation

@Aman-Mittal

Copy link
Copy Markdown
Member

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-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 (ASSET), shareSuspenseId (LIABILITY),
  incomeFromFeeAccountId (INCOME), shareEquityId (EQUITY)

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. It is the one product template that omits them, so the rule selector's options are supplied by the form.
  • 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 (fundSourceAccountId in, fundSourceAccount out).

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 equityAccountOptions at 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:

Accepted Fires
a fee mapping naming a charge the product does not carry never
a fee mapping naming a charge whose penalty flag is the other way round never
two rows naming the same payment type not reliably

Silent 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 penalty flag, 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 checkNoChanges pass reports as NG0100 and the e2e fixtures fail the run on.

Unrelated but visible in the e2e logs: GlAccountSelectComponent emits an NG0100 for ng-untouched on the product edit pages, from Angular's own NgControlStatus host binding. Pre-existing, currently a warning rather than a failure, and not addressed here.

Verification

  • Two real-backend suites: a share product mapped to equity and round-tripped through edit (new), and the loan product suite extended with a payment-channel override.
  • Unit: 925 pass (16 new) — the share slots and rule set, both payload directions, and the option-narrowing rules.
  • Mocked e2e: 243 pass. Four client-CRUD tests failed only under two local workers and pass when that file runs alone; they are untouched by this change.
  • npm run lint, format:check, check:icons, i18n:check, build all clean.

Checklist

  • I did not hand-edit generated files under src/app/api/.
  • New component uses the adapter boundary (| appTranslate), per ADR-0003.
  • New strings added to en.json; existing ones left alone.
  • Commits are signed.

…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.
@Aman-Mittal
Aman-Mittal merged commit fc524e1 into apache:main Aug 14, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant