Skip to content

feat(reference-data): legal-document + company-info modules + country.displayOrder#3734

Open
Blume1977 wants to merge 2 commits into
developfrom
feat/legal-document-company-info
Open

feat(reference-data): legal-document + company-info modules + country.displayOrder#3734
Blume1977 wants to merge 2 commits into
developfrom
feat/legal-document-company-info

Conversation

@Blume1977
Copy link
Copy Markdown
Collaborator

@Blume1977 Blume1977 commented May 21, 2026

Summary

Wave 4 of the realunit-app API-as-Decision-Authority plan (DFXswiss/realunit-app:docs/api-authority-plan.md). Three additions that retire the last reference-data items the app used to ship hardcoded.

What changes

LegalDocument module

New module src/shared/models/legal-document/ (entity + repository + service + controller + DTO + mapper). GET /v1/legal-document accepts ?type= and ?language= filters and returns the enabled documents the backend owns.

  • Entity fields: type (enum), language (ISO 639-1, lowercase, nullable for language-agnostic), version, url, enabled.
  • A partial unique index enforces (type, language) uniqueness only across enabled rows so historical versions can stay in the table.

CompanyInfo module

New module src/shared/models/company-info/ with the same shape. GET /v1/company-info/:brand returns one record per brand (RealUnit, DFX, …) so a single backend can serve multiple white-labeled apps from one endpoint.

Country.displayOrder

New column on the existing country table, default 999, mapped to CountryDto.displayOrder. CountryService.getAllCountry now sorts by displayOrder ascending with name as the tiebreaker — the realunit-app's country picker can drop its priorityCountries = ['CH','DE','IT','FR'] constant and consume the backend-tagged order.

Migration

migration/1779370800171-AddLegalDocumentAndCompanyInfo.js (TypeORM schema migration, follows the AddSupportNode pattern):

  1. Creates legal_document and company_info tables.
  2. Adds the displayOrder column to country with default 999.
  3. Seeds the priority order CH=1, DE=2, IT=3, FR=4 to match the realunit-app's previous hardcoded list.
  4. Seeds the legal-document URLs and the RealUnit company-info row from the app's hardcoded data (legal_documents_config.dart:69-122, settings_contact_page.dart:82-134).

down reverses every step.

Tests

  • legal-document.service.spec.ts — covers the four query branches: no-filter, type-only filter, type+language filter, empty result.
  • company-info.service.spec.ts — happy path + 404 on unknown brand.
  • country.service.spec.ts — verifies the getAllCountry sort order including the displayOrder-then-name tiebreaker, and that the cached repository array is not mutated.

Backwards compatibility

All additions are additive:

  • Old clients that don't query the new endpoints see no change.
  • The displayOrder column has a sensible default; old clients ignore the new CountryDto.displayOrder field.

Local verification

  • npm run type-check — clean
  • npm run lint — clean
  • npm test946 / 946 passing (+8 new tests)

Closes (audit, P2)

  • V14 — country priority (country_field.dart:65-79)
  • V17 — legal-document URLs (legal_documents_config.dart:69-122)
  • V18 — company contact info (settings_contact_page.dart:82-134)

Manual test plan (DEV)

  • GET /v1/legal-document?type=RegistrationAgreement&language=de returns a single enabled document with the seeded realunit.de/agreement-de.pdf URL.
  • GET /v1/legal-document?type=DfxTerms returns the language-agnostic DFX terms entry.
  • GET /v1/company-info/RealUnit returns the RealUnit Schweiz AG seed.
  • GET /v1/country returns CH/DE/IT/FR in that order at the top, then everything else alphabetically.

Companion app PR

  • DFXswiss/realunit-app#499 — consumes /v1/legal-document, /v1/company-info, and the new Country.displayOrder field; drops the hardcoded RealUnit company info, priorityCountries, and legacy WebDocumentConfig hardcodes. Closes V14, V17, V18 of the audit. Must merge after this PR is deployed so the app can rely on the new endpoints.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

⚠️ Unverified Commits (2)

The following commits are not signed/verified:

  • 80bd574 feat(reference-data): legal-document + company-info modules + country.displayOrder (Blume1977)
  • 0b8f822 style: prettier (Blume1977)
How to sign commits
# SSH signing (recommended)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

# Re-sign last commit
git commit --amend -S --no-edit
git push --force-with-lease

ℹ️ New TODOs/FIXMEs (1)

+    symbol3: 'XXX',

Blume1977 added a commit to Blume1977/realunit-app that referenced this pull request May 21, 2026
…o, country.displayOrder

Wave 4.4 of the API-as-Decision-Authority audit, companion to API PR
DFXswiss/api#3734. Retires three hardcoded reference-data sites by
consuming the new backend endpoints.

Services + DTOs
---------------

- `DfxLegalDocumentService` — calls `GET /v1/legal-document` with
  optional `?type=` and `?language=` filters, caches per query key.
- `DfxCompanyInfoService` — calls `GET /v1/company-info/:brand`,
  caches per brand (case-insensitive).
- DTO mirrors: `DfxLegalDocumentDto`, `DfxCompanyInfoDto` (with nested
  `DfxCompanyInfoAddressDto`).
- `DfxCountryDto` + `Country` domain model gain `displayOrder` (default
  999); pre-PR backends keep their alphabetic order.

DI
--

- Both services registered as lazy singletons in `lib/setup/di.dart`.

Consumers
---------

- `country_field.dart` — drops the hardcoded
  `priorityCountries = ['CH','DE','IT','FR']` and trusts the order the
  backend ships (sorted by `displayOrder` then `name` server-side).
- `settings_contact_page.dart` — every "phone / email / website /
  imprint" tile is rebuilt from `SettingsContactState.companyInfo`. If
  a field is null, the tile is omitted. The imprint block reads
  `info.name` + the assembled `address` lines instead of the hardcoded
  `'RealUnit Schweiz AG'` / `'Schochenmühlestrasse 6\n6340 Baar,
  Schweiz'` block.
- `SettingsContactCubit` now takes a second argument
  (`DfxCompanyInfoService`), loads both `/v2/user` and
  `/v1/company-info/RealUnit` in parallel, and exposes
  `companyInfo: DfxCompanyInfoDto?` on the Success state.

Tests
-----

- `dfx_legal_document_service_test.dart` — covers happy path, filter
  query parameters, cache reuse, different-filter cache misses,
  language-null pass-through, and the 500 error path.
- `dfx_company_info_service_test.dart` — happy path with full payload,
  case-insensitive cache hit, payload without an address block, and the
  404 error path.
- `settings_contact_cubit_test.dart` — extends every fixture with a
  mock `DfxCompanyInfoService`, adds explicit cases for the company-info
  failure path and the multi-call setup.

Verification
------------

- `flutter analyze` — clean
- `flutter test` — **1426 / 1426 passing**

Closes V14 (country priority), V17 (registration-agreement URLs), V18
(company contact info) on the realunit-app audit.
Blume1977 added 2 commits May 21, 2026 17:04
….displayOrder

Wave 4 of the realunit-app API-as-Decision-Authority plan
(`DFXswiss/realunit-app:docs/api-authority-plan.md`). Three additions
that retire the last three reference-data items the app used to ship
hardcoded.

LegalDocument module
--------------------

New module `src/shared/models/legal-document/` (entity + repository +
service + controller + DTO + mapper). `GET /v1/legal-document` accepts
`?type=` and `?language=` filters and returns the enabled documents the
backend currently owns. The entity carries `type` (enum), `language`
(ISO 639-1, lowercase, nullable for language-agnostic), `version`,
`url`, and `enabled`. A partial unique index enforces
`(type, language)` uniqueness only across enabled rows so historical
versions can stay in the table.

CompanyInfo module
------------------

New module `src/shared/models/company-info/` with the same shape.
`GET /v1/company-info/:brand` returns one record per brand
(`RealUnit`, `DFX`, …) so a single backend can serve multiple
white-labeled apps from one endpoint.

Country.displayOrder
--------------------

New column on the existing `country` table, default 999, mapped to
`CountryDto.displayOrder`. `CountryService.getAllCountry` now sorts
by `displayOrder` ascending with `name` as the tiebreaker — the
realunit-app's country picker can drop its
`priorityCountries = ['CH','DE','IT','FR']` constant and consume the
backend-tagged order.

Migration
---------

`migration/1779370800171-AddLegalDocumentAndCompanyInfo.js` (TypeORM
schema migration, follows the pattern of `AddSupportNode`):
1. Creates the `legal_document` and `company_info` tables.
2. Adds the `displayOrder` column to `country` with default 999.
3. Seeds the priority order CH=1, DE=2, IT=3, FR=4 to match the
   realunit-app's previous hardcoded list.
4. Seeds the legal-document URLs and the RealUnit company-info row
   from the app's hardcoded data
   (`legal_documents_config.dart:69-122`,
   `settings_contact_page.dart:82-134`).
`down` reverses every step.

Tests
-----

- `legal-document.service.spec.ts` — covers the four query branches:
  no-filter, type-only filter, type+language filter, empty result.
- `company-info.service.spec.ts` — happy path + 404 on unknown brand.
- `country.service.spec.ts` — verifies the `getAllCountry` sort
  order including the `displayOrder`-then-name tiebreaker, and that
  the cached repository array is not mutated.

Backwards compatibility
-----------------------

All additions are additive. Old clients that don't query the new
endpoints see no change; the `displayOrder` column has a sensible
default and old clients ignore the new `CountryDto.displayOrder` field.

Local verification
------------------

- `npm run type-check` — clean
- `npm run lint` — clean
- `npm test` — **946 / 946 passing** (+8 new tests for the three
  service spec files; 938 baseline preserved)

Closes V14 (country priority), V17 (legal documents), V18 (company
contact info) on the realunit-app audit.
@Blume1977 Blume1977 force-pushed the feat/legal-document-company-info branch from f8d2832 to 0b8f822 Compare May 21, 2026 15:05
@Blume1977 Blume1977 marked this pull request as ready for review May 21, 2026 15:18
TaprootFreak pushed a commit to Blume1977/realunit-app that referenced this pull request May 21, 2026
…o, country.displayOrder

Wave 4.4 of the API-as-Decision-Authority audit, companion to API PR
DFXswiss/api#3734. Retires three hardcoded reference-data sites by
consuming the new backend endpoints.

Services + DTOs
---------------

- `DfxLegalDocumentService` — calls `GET /v1/legal-document` with
  optional `?type=` and `?language=` filters, caches per query key.
- `DfxCompanyInfoService` — calls `GET /v1/company-info/:brand`,
  caches per brand (case-insensitive).
- DTO mirrors: `DfxLegalDocumentDto`, `DfxCompanyInfoDto` (with nested
  `DfxCompanyInfoAddressDto`).
- `DfxCountryDto` + `Country` domain model gain `displayOrder` (default
  999); pre-PR backends keep their alphabetic order.

DI
--

- Both services registered as lazy singletons in `lib/setup/di.dart`.

Consumers
---------

- `country_field.dart` — drops the hardcoded
  `priorityCountries = ['CH','DE','IT','FR']` and trusts the order the
  backend ships (sorted by `displayOrder` then `name` server-side).
- `settings_contact_page.dart` — every "phone / email / website /
  imprint" tile is rebuilt from `SettingsContactState.companyInfo`. If
  a field is null, the tile is omitted. The imprint block reads
  `info.name` + the assembled `address` lines instead of the hardcoded
  `'RealUnit Schweiz AG'` / `'Schochenmühlestrasse 6\n6340 Baar,
  Schweiz'` block.
- `SettingsContactCubit` now takes a second argument
  (`DfxCompanyInfoService`), loads both `/v2/user` and
  `/v1/company-info/RealUnit` in parallel, and exposes
  `companyInfo: DfxCompanyInfoDto?` on the Success state.

Tests
-----

- `dfx_legal_document_service_test.dart` — covers happy path, filter
  query parameters, cache reuse, different-filter cache misses,
  language-null pass-through, and the 500 error path.
- `dfx_company_info_service_test.dart` — happy path with full payload,
  case-insensitive cache hit, payload without an address block, and the
  404 error path.
- `settings_contact_cubit_test.dart` — extends every fixture with a
  mock `DfxCompanyInfoService`, adds explicit cases for the company-info
  failure path and the multi-call setup.

Verification
------------

- `flutter analyze` — clean
- `flutter test` — **1426 / 1426 passing**

Closes V14 (country priority), V17 (registration-agreement URLs), V18
(company contact info) on the realunit-app audit.
TaprootFreak pushed a commit to Blume1977/realunit-app that referenced this pull request May 21, 2026
…o, country.displayOrder

Wave 4.4 of the API-as-Decision-Authority audit, companion to API PR
DFXswiss/api#3734. Retires three hardcoded reference-data sites by
consuming the new backend endpoints.

Services + DTOs
---------------

- `DfxLegalDocumentService` — calls `GET /v1/legal-document` with
  optional `?type=` and `?language=` filters, caches per query key.
- `DfxCompanyInfoService` — calls `GET /v1/company-info/:brand`,
  caches per brand (case-insensitive).
- DTO mirrors: `DfxLegalDocumentDto`, `DfxCompanyInfoDto` (with nested
  `DfxCompanyInfoAddressDto`).
- `DfxCountryDto` + `Country` domain model gain `displayOrder` (default
  999); pre-PR backends keep their alphabetic order.

DI
--

- Both services registered as lazy singletons in `lib/setup/di.dart`.

Consumers
---------

- `country_field.dart` — drops the hardcoded
  `priorityCountries = ['CH','DE','IT','FR']` and trusts the order the
  backend ships (sorted by `displayOrder` then `name` server-side).
- `settings_contact_page.dart` — every "phone / email / website /
  imprint" tile is rebuilt from `SettingsContactState.companyInfo`. If
  a field is null, the tile is omitted. The imprint block reads
  `info.name` + the assembled `address` lines instead of the hardcoded
  `'RealUnit Schweiz AG'` / `'Schochenmühlestrasse 6\n6340 Baar,
  Schweiz'` block.
- `SettingsContactCubit` now takes a second argument
  (`DfxCompanyInfoService`), loads both `/v2/user` and
  `/v1/company-info/RealUnit` in parallel, and exposes
  `companyInfo: DfxCompanyInfoDto?` on the Success state.

Tests
-----

- `dfx_legal_document_service_test.dart` — covers happy path, filter
  query parameters, cache reuse, different-filter cache misses,
  language-null pass-through, and the 500 error path.
- `dfx_company_info_service_test.dart` — happy path with full payload,
  case-insensitive cache hit, payload without an address block, and the
  404 error path.
- `settings_contact_cubit_test.dart` — extends every fixture with a
  mock `DfxCompanyInfoService`, adds explicit cases for the company-info
  failure path and the multi-call setup.

Verification
------------

- `flutter analyze` — clean
- `flutter test` — **1426 / 1426 passing**

Closes V14 (country priority), V17 (registration-agreement URLs), V18
(company contact info) on the realunit-app audit.
TaprootFreak added a commit to DFXswiss/realunit-app that referenced this pull request May 21, 2026
…o, country.displayOrder (#499)

## Summary

Wave 4.4 of the API-as-Decision-Authority audit
([`docs/api-authority-plan.md`](docs/api-authority-plan.md)). Companion
to API PR
[DFXswiss/api#3734](DFXswiss/api#3734). Retires
the last hardcoded reference-data sites by consuming the three new
backend endpoints.

## Changes

### Services + DTOs

- `DfxLegalDocumentService` — calls `GET /v1/legal-document` with
optional `?type=` and `?language=` filters, caches per query key
(`${type ?? '*'}:${language ?? '*'}`).
- `DfxCompanyInfoService` — calls `GET /v1/company-info/:brand`, caches
per brand (case-insensitive).
- DTO mirrors: `DfxLegalDocumentDto`, `DfxCompanyInfoDto` (with nested
`DfxCompanyInfoAddressDto`).
- `DfxCountryDto` + `Country` domain model gain `displayOrder` (default
999); pre-PR backends keep alphabetic order.

### Consumers

- **`country_field.dart`** — drops the hardcoded `priorityCountries =
['CH','DE','IT','FR']` and trusts the order the backend ships (sorted by
`displayOrder` then `name` server-side).
- **`settings_contact_page.dart`** — every "phone / email / website /
imprint" tile is rebuilt from `SettingsContactState.companyInfo`. If a
field is null, the tile is omitted. The imprint block reads `info.name`
+ the assembled `address` lines instead of the hardcoded `'RealUnit
Schweiz AG'` / `'Schochenmühlestrasse 6\n6340 Baar, Schweiz'` block.
- **`SettingsContactCubit`** now takes a second argument
(`DfxCompanyInfoService`), loads both `/v2/user` and
`/v1/company-info/RealUnit` **in parallel**, and exposes `companyInfo:
DfxCompanyInfoDto?` on the Success state. A company-info failure no
longer kills the support tile — the cubit degrades gracefully and
surfaces the user data anyway.
- **`legal_document_page.dart`** — the hardcoded
`_registrationAgreementPdfUrls` map is **gone entirely** (no last-resort
fallback). The page renders a 3-state UI:
  - **loading** (spinner + localized `pdfLoading` label),
- **available** (the inline PDF viewer with the URL returned by
`/v1/legal-document`),
- **unavailable** (tap-to-retry surface with localized `pdfNotAvailable`
label) when the API returns no document for any of the candidate
languages.
The language picker is deterministic: requested → `en` → `de` → first
available, no random order.

### i18n

- New keys `pdfLoading`, `pdfNotAvailable` in both `strings_en.arb` and
`strings_de.arb`.

## Tests

- `dfx_legal_document_service_test.dart` — happy path, filter query
parameters, cache reuse, different-filter cache misses, language-null
pass-through, 500 error path.
- `dfx_company_info_service_test.dart` — happy path with full payload,
case-insensitive cache hit, payload without an address block, 404 error
path.
- `settings_contact_cubit_test.dart` — extends every fixture with a mock
`DfxCompanyInfoService`, adds explicit cases for the company-info
failure path and the multi-call setup.
- `legal_document_page_test.dart` / cubit tests — cover the 3-state UI
(loading / available / unavailable + retry) and the deterministic
language fallback chain.

## Verification

- [x] `flutter analyze` — clean
- [x] `flutter test` — **1450 / 1450 passing**

## Closes (audit, P2)

- **V14** — country priority (`country_field.dart:65-79`)
- **V17** — registration-agreement URLs
(`legal_documents_config.dart:69-122` — hardcoded map fully removed; the
static WebDocumentConfig links to realunit.de/.ch downloads pages remain
as accepted boundary cases below)
- **V18** — company contact info (`settings_contact_page.dart:82-134`)

## Dependency

Requires API PR DFXswiss/api#3734 to be merged + deployed first. The
DTOs default to safe values (`displayOrder = 999`, missing fields
tolerated), so against an older API the country picker still renders
alphabetically and contact tiles surface no info — the app degrades
gracefully rather than crashing. The PDF page falls through to its
"unavailable" state instead of showing a stale hardcoded URL.

## Boundary cases left as documented exceptions

- The five WebDocumentConfig URLs in `legal_documents_config.dart`
(eu_securities prospectus pages, articles of association, investment
regulations) point to anchors on realunit.de/.ch download pages, not
file URLs. They are RealUnit-brand static content managed outside the
API by RealUnit's marketing/legal team; lifting them into a generic
`LegalDocument` table would creep schema. Accept as P3.
- `assetBaseName: 'privacy_policy'` loads a local bundled asset, not a
remote URL — local asset, not an API concern.

---------

Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
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