Skip to content

feat(realunit): model wallet-scoped registration as aktionariat_registration entity#3786

Open
davidleomay wants to merge 1 commit into
developfrom
feat/realunit-registration-entity-pr
Open

feat(realunit): model wallet-scoped registration as aktionariat_registration entity#3786
davidleomay wants to merge 1 commit into
developfrom
feat/realunit-registration-entity-pr

Conversation

@davidleomay
Copy link
Copy Markdown
Member

@davidleomay davidleomay commented May 28, 2026

Summary

  • New aktionariat_registration entity (named after the infrastructure layer, not the issuer) with FK to user (wallet) and user_data (account)
  • Replaces JSON blob lookups in kyc_step.result with indexed DB queries using ILike for case-insensitive wallet matching
  • Dual-write: KycStep rows still created for audit trail, new entity is source of truth for lookups
  • Unique index on userId prevents duplicate registrations per wallet
  • Backfill migration converts existing kyc_step rows (latest per wallet via DISTINCT ON)
  • Controller endpoints drop kycSteps: true eager loading
  • Fix migration/generate.sh to use PSQL datasource

Addresses review feedback

  • Renamed from realunit_registrationaktionariat_registration per TaprootFreak's review
  • findRegistration now uses ILike for DB-level case-insensitive primary lookup, falls back to loading all only for merge scenarios
  • Transaction wrapping for dual-write deferred: cross-service EntityManager propagation is out of scope; KycStep-first ordering is safe (orphaned KycStep = audit record, not data loss)

Test plan

  • npm run build — clean
  • npm run lint — clean
  • npm run format:check — clean
  • npx jest --testPathPattern=realunit — 35/35 passing
  • CI green
  • Run backfill migration on DEV, verify registrations populated
  • Test GET /realunit/wallet/status returns same shape
  • Test GET /realunit/register/status returns same boolean

Closes #3785

@davidleomay davidleomay requested a review from TaprootFreak as a code owner May 28, 2026 10:54
@TaprootFreak
Copy link
Copy Markdown
Collaborator

Solide Migration — Tabelle, Backfill, Dual-Write und das Eager-Load-Cleanup auf den 4 Controller-Endpoints sind alle sauber gelöst.

Eine Naming-Frage, die mir beim Lesen wichtig erscheint, bevor das Schema gegossen ist:

RealUnit (Brand) vs Aktionariat (Infrastruktur)

Was die neue Tabelle modelliert (signature, registrationDate, forwardRegistration → /directinvestment/..., Brokerbot-Adresse als externer FK-Endpoint) ist konzeptionell eine Aktionariat-Aktionärsregistrierung, nicht eine RealUnit-Registrierung:

Schicht Bezeichnung Was es ist
Produkt / Brand RealUnit (Schweiz AG, REALU-Token, AGB) Eine konkrete Emittentin, die ihre Aktien tokenisiert hat
Infrastruktur / Service Aktionariat (Aktienbuch, EIP-712-Struktur, REST-API, Brokerbot-Smart-Contract) Aktionariat AG — generischer Share-Tokenization-Service in CH

RealUnit ist einer von potenziell mehreren Issuern, die Aktionariat als Infrastruktur nutzen. Sobald ein zweiter Issuer dazukommt, hat die Schicht-Benennung wieder Drift wie bei /wallet/status und /brokerbot/buyPrice — beides hat PR #3782 und #3783 letzte Woche aus genau diesem Grund aufgeräumt:

  • /v1/realunit/wallet/status/v1/realunit/registration (Resource ist die Registrierung, nicht „Status")
  • /v1/realunit/brokerbot/buyPrice/v1/realunit/quote/buyPrice (Endpoint ist ein Preis-Quote, kein On-Chain-Brokerbot-Call)

Die hier vorgeschlagene Tabelle macht den gleichen Sprung eine Ebene tiefer: was wirklich modelliert wird, ist die Aktionariat-Aktionärsregistrierung.

Vorschlag

Konsistent mit dem /quote/* und /registration Pattern: Tabelle und Entity nach der Infrastruktur benennen, RealUnit als Issuer-Wert. Konkret:

aktionariat_registration
  id PK
  user             FK → user.id            (UNIQUE — bleibt wie in deinem PR)
  userData         FK → user_data.id
  kycStep          FK → kyc_step.id        (audit, bleibt)
  issuer           enum                    (heute: nur 'RealUnit'; YAGNI-Wert für Zukunft)
  walletAddress, signature, registrationDate, status
  externalRef, comment, result

Konsequenz auf die Endpoint-/Service-Schicht (kann auch in eigener Folge-PR, Tabellen-Naming alleine ist schon der grosse Schritt):

  • RealUnitService.getRegistrationInfo() bleibt — RealUnit ist hier Issuer-Scope (passt zum Controller /v1/realunit/*)
  • Aber Repository / Entity: AktionariatRegistration, AktionariatRegistrationRepository
  • KycStepName bleibt RealUnitRegistration (Issuer-Scope, wie es heute ist)

Der issuer-Wert ist YAGNI heute, aber das Feld kostet nichts und macht den Skalierungspfad klar. Per CONTRIBUTING-Regel 3 müsste man es weglassen, bis ein zweiter Issuer real wird — dann ist die Frage, ob die Migration auf aktionariat_registration jetzt billig ist oder nach Merge mit Daten drin teuer. Mein Vote: jetzt, solange die Tabelle noch frisch ist.

Andere Findings (kleiner)

  1. Race-Window beim Dual-Write (completeRegistration ~Z.637, completeRegistrationForWalletAddress ~Z.711): createCustomKycStep und registrationRepo.save sind zwei separate DB-Calls. Wenn der zweite fehlschlägt, verwaister KycStep ohne korrespondierende Registration. Sollte in EntityManager.transaction(...) gewrappt sein.

  2. findRegistration lädt alle Registrations des Accounts und filtert in-memory (Z.622). Mit dem neuen FK+Index wäre where: { userData: { id }, walletAddress: ILike(...) } möglich — die PR verspricht „indexed DB queries", der App-Filter relativiert das.

  3. Backfill ist destruktiv für historische DuplikateDISTINCT ON … ORDER BY id DESC behält nur die jüngste Registrierung pro Wallet. Bei Wallets mit fehlgeschlagenem Retry + erfolgreichem späteren Submit fehlt der Audit-Trail in der neuen Tabelle (im kyc_step bleibt's, ist also kein Daten-Verlust, aber das neue Repo zeigt nur den jüngsten).

  4. getResult<T>() parst JSON bei jedem Call, obwohl walletAddress, signature, registrationDate jetzt explizite Spalten sind. result ist redundant geworden — langfristig könnte das raus.

Wenn die Naming-Frage dich überzeugt, biete ich gerne an, einen kleinen Folge-Commit zu pushen, der realunit_registrationaktionariat_registration umbenennt (Entity, Repo, Migration-Filename, Tests) — keine Schema-Drift, weil PR noch nicht gemerged. Sag Bescheid wie du es haben willst.

#3785)

Promote RealUnit registration from a JSON blob in kyc_step.result to a
proper aktionariat_registration table with FK to user (wallet) and
user_data (account). Named after the infrastructure layer (Aktionariat)
not the issuer (RealUnit), consistent with the /quote/* endpoint rename.

- New entity, repository, and migration with backfill from existing kyc_steps
- Dual-write: KycStep rows still created for audit trail
- Indexed DB lookups with ILike for case-insensitive wallet matching
- Unique index on userId (one registration per wallet)
- Remove kycSteps eager loading from controller endpoints
- Fix migration generate script to use PSQL datasource

Closes #3785
@davidleomay davidleomay force-pushed the feat/realunit-registration-entity-pr branch from cb981ab to a13f363 Compare May 28, 2026 12:11
@davidleomay davidleomay changed the title feat(realunit): model wallet-scoped registration as first-class entity feat(realunit): model wallet-scoped registration as aktionariat_registration entity May 28, 2026
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.

RealUnit: model wallet-scoped registration as a first-class entity (FK), not a JSON blob in kyc_step.result

2 participants