Skip to content

feat: add investor eligibility explanation mapper (Closes #52) - #157

Open
panditdhamdhere wants to merge 1 commit into
Raegis-RWA:mainfrom
panditdhamdhere:feat/investor-eligibility-explanation
Open

feat: add investor eligibility explanation mapper (Closes #52)#157
panditdhamdhere wants to merge 1 commit into
Raegis-RWA:mainfrom
panditdhamdhere:feat/investor-eligibility-explanation

Conversation

@panditdhamdhere

Copy link
Copy Markdown
Contributor

Description

ComplianceModule.checkWhitelist() returns a bare boolean, so a dashboard showing
"not eligible" cannot tell the user why: never approved, KYC revoked, or the
compliance query simply failed. Each of those needs a different UI response, and a
false today collapses them into one.

This PR adds a mapper that turns observable compliance signals into a UI-friendly
explanation carrying a stable reason code, a safe message, and a suggested next
action.

Why this approach: the mapper is a pure function (following the
buildNetworkFailureDiagnostic and buildAdminActionReceipt patterns) with a thin
InvestorModule wrapper for the live path, rather than a new client module. That
keeps it usable both with and without RPC — dashboards that already loaded a
portfolio or role result can explain eligibility without a second round trip.

Two decisions worth reviewer attention:

  • A bare whitelist false maps to blocked, not revoked. checkWhitelist
    cannot distinguish "never approved" from "approved then revoked", so inferring a
    revoke from false would be inventing information. revoked is only emitted when
    a caller supplies isKycRevoked: true from a source that actually knows — an admin
    whitelist-remove receipt, a decoded whitelist_remove event, or an off-chain KYC
    system.
  • No legal overclaiming is structurally enforced, not just documented. Every
    result carries a fixed disclaimer field and verified: false, so the
    non-guarantee language travels with the object and cannot be dropped when
    serialising for UI or support tooling.

Files added

  • src/types/eligibility.ts — status union, reason codes, next actions, input/result types
  • src/investor/eligibility.tsbuildInvestorEligibilityExplanation, explainWhitelistResult, normalizeInvestorEligibilityStatus, ELIGIBILITY_DISCLAIMER
  • src/errors/eligibility.tsEligibilityExplanationError
  • docs/investor-eligibility.md
  • tests/investor-eligibility.test.ts

Files modified

  • src/investor/portfolio.ts — adds explainEligibility (live compliance check) and explainEligibilityFromSignals (pure, no RPC)
  • src/index.ts — public exports for helpers, types, and error
  • docs/api-reference.mdInvestorModule methods, standalone helpers, and an open note on the boolean limitation
  • docs/investor-portfolio.md — cross-link
  • README.md — quickstart section
  • CONTRIBUTING.md — doc-update rule and review checklist item

Closes: #52


Evidence Checklist

1. Issue Reference

  • This PR references a tracked issue (Closes #52).
  • The linked issue's acceptance criteria are copied into Section 6 below.

2. Implementation Summary

  • A clear description of what changed is provided above.
  • A brief explanation of why this approach was chosen is included.
  • All files added, modified, or removed are listed or summarised.

3. Tests

  • New or updated unit tests cover every added or changed public method.
  • Tests use predictable mocks rather than live RPC.
  • If tests are not applicable, a justification is provided below.

tests/investor-eligibility.test.ts covers all five states, alias normalisation,
unrecognised future statuses resolving to unknown (never approved), signal
priority when inputs conflict (revoke wins over a contradictory approved boolean),
frozen results, and invalid-timestamp rejection.

The live InvestorModule path is tested by spying on
client.compliance.checkWhitelist — approved, blocked, a rejected compliance query,
and an invalid address that must not reach compliance at all. One test asserts a
thrown RPC error containing a token in its message never appears in the serialised
explanation.

4. Commands Run

npm run check
Command output
> @aegis/sdk@0.1.0 check
> npm run build && npm test -- --runInBand && npm run test:compat

> @aegis/sdk@0.1.0 build
> tsc

> @aegis/sdk@0.1.0 test
> jest --runInBand

PASS tests/investor-eligibility.test.ts
PASS tests/client-factory.test.ts
PASS tests/events-decoder.test.ts
PASS tests/admin-receipts.test.ts
PASS tests/role.test.ts
PASS tests/network-failures.test.ts
PASS tests/investor.test.ts
PASS tests/config.test.ts
PASS tests/client.test.ts
PASS tests/events-module.test.ts
PASS tests/mock-client.test.ts
PASS tests/mock-client-examples.test.ts

Test Suites: 12 passed, 12 total
Tests:       145 passed, 145 total
Snapshots:   0 total
Time:        2.734 s, estimated 3 s
Ran all test suites.

> @aegis/sdk@0.1.0 test:compat
> node scripts/check-compat.mjs

Browser compatibility: bundle resolved without Node-only imports.
Node compatibility: public SDK entrypoint and signer initialized.

5. CI Status

  • All GitHub Actions checks pass on this PR.

(Tick once CI finishes.)

6. Acceptance Criteria Coverage

  • Eligibility reason codes are implemented. InvestorEligibilityReasonCode:
    WHITELISTED, NOT_WHITELISTED, KYC_REVOKED, COMPLIANCE_QUERY_FAILED,
    INVALID_ADDRESS, INSUFFICIENT_DATA, UNRECOGNIZED_STATUS.
  • Approved, blocked, revoked, unknown, and unavailable states are represented.
    InvestorEligibilityStatus union, one mapping per state, each with its own
    reason code and suggested next action.
  • Mapper produces safe messages. Messages are fixed constants — raw RPC
    errors, URLs, and credentials are never interpolated. A test asserts a token
    present in a thrown error does not survive into the result.
  • Tests cover major states. All five states plus alias handling, unrecognised
    statuses, conflicting-signal priority, and the live compliance paths.
  • Dashboard usage is documented. docs/investor-eligibility.md covers the
    status model, blocked vs revoked, a reason-code/next-action table, pure and
    live usage, and UI guidance.
  • No legal guarantee is implied. Every result carries ELIGIBILITY_DISCLAIMER
    and verified: false. Docs state explicitly this is not legal, financial, or
    regulatory advice and does not guarantee a transaction will succeed. A test
    asserts the disclaimer is present on every state.

Reviewer Notes

Protocol limitation, documented rather than worked around: the live
explainEligibility path cannot emit revoked on its own, because checkWhitelist
only returns a boolean. Emitting revoked requires a caller-supplied signal via
explainEligibilityFromSignals. This is called out as an open note in
docs/api-reference.md. If the contract later exposes a richer compliance query
(e.g. distinguishing revoke from never-approved), the mapper can consume it without
changing its public shape.

npm run lint and npm run format cannot run on this branch or on main. Both
scripts invoke eslint and prettier, but neither package is in devDependencies,
so npm run verify exits at eslint: command not found. Pre-existing and unrelated
to this change; CI runs npm run check, which passes.

Naming: InvestorEligibilityStatus is intentionally separate from the existing
PortfolioStatus (active / empty / blocked / unavailable). Portfolio status
describes holdings; eligibility status describes whitelist standing. Both use
blocked, but they answer different questions and are documented as such.

Relationship to the existing TransferEligibility: that type stays untouched —
it is per-asset and factors in balance. This mapper is address-level and
compliance-only. No behaviour change to getPortfolio.

Explain why an investor is approved, blocked, revoked, unknown, or unavailable
instead of surfacing a bare whitelist boolean, so dashboards can show a reason
code and a next-step CTA.

A bare whitelist false maps to blocked rather than revoked, because
checkWhitelist cannot distinguish "never approved" from "approved then revoked".
Revoked requires an explicit signal from an admin receipt, whitelist_remove
event, or off-chain KYC system. Messages are fixed safe strings and every result
carries a non-guarantee disclaimer with verified false, so no legal or
regulatory determination is implied.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant