taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit f1de7ab5590d86e19e99bf2369c55486d74ee28a
parent 9a3feb091ec646415aa9a41c5d2eb6dc25705e21
Author: Florian Dold <dold@taler.net>
Date:   Mon, 31 Aug 2026 22:13:42 +0200

web-util: add VQF foundation and trust declarations

Diffstat:
Apackages/web-util/src/forms/gana/VQF_902_12.stories.tsx | 45+++++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/forms/gana/VQF_902_12.ts | 319+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/forms/gana/VQF_902_12_13.test.ts | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/forms/gana/VQF_902_12_13_common.ts | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/forms/gana/VQF_902_13.stories.tsx | 43+++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/forms/gana/VQF_902_13.ts | 353+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/forms/gana/index.stories.ts | 2++
Mpackages/web-util/src/forms/index.stories.ts | 2++
Mpackages/web-util/src/forms/index.ts | 133+++++++++++++++++++++++++++++++++++++++++++------------------------------------
9 files changed, 1097 insertions(+), 61 deletions(-)

diff --git a/packages/web-util/src/forms/gana/VQF_902_12.stories.tsx b/packages/web-util/src/forms/gana/VQF_902_12.stories.tsx @@ -0,0 +1,45 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. +*/ + +import { i18n, setupI18n, TalerFormAttributes } from "@gnu-taler/taler-util"; +import * as tests from "../../tests/hook.js"; +import { DefaultForm as TestedComponent } from "../forms-ui.js"; +import { VQF_902_12 } from "./VQF_902_12.js"; + +export default { title: "vqf_902_12" }; + +setupI18n("en", {}); + +export const EmptyForm = tests.createExample(TestedComponent, { + initial: {}, + design: VQF_902_12(i18n), +}); + +export const PrefilledForm = tests.createExample(TestedComponent, { + initial: { + [TalerFormAttributes.IDENTITY_CONTRACTING_PARTNER]: + "Example Foundation Council", + [TalerFormAttributes.LEGAL_STRUCTURE_NAME]: "Example Foundation", + [TalerFormAttributes.LEGAL_STRUCTURE_TYPE]: "DISCRETIONARY", + [TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY]: "REVOCABLE", + [TalerFormAttributes.ORIGINATOR_LIST]: [ + { + [TalerFormAttributes.NAME_OR_ENTITY]: "Alex Founder", + [TalerFormAttributes.DOMICILE_OR_REGISTERED_OFFICE]: + "Main Street 12, Zürich", + [TalerFormAttributes.DOMICILE_OR_REGISTERED_OFFICE_COUNTRY]: "CH", + [TalerFormAttributes.DATE_OF_BIRTH]: "1970-01-02", + [TalerFormAttributes.NATIONALITY]: "CH", + }, + ], + [TalerFormAttributes.ORIGINATOR_HAS_REVOCATION_RIGHT]: true, + [TalerFormAttributes.BENEFICIARY_GROUPS]: "Descendants of Alex Founder", + }, + design: VQF_902_12(i18n), +}); diff --git a/packages/web-util/src/forms/gana/VQF_902_12.ts b/packages/web-util/src/forms/gana/VQF_902_12.ts @@ -0,0 +1,319 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. +*/ + +import { TalerFormAttributes } from "@gnu-taler/taler-util"; +import { format } from "date-fns"; +import { InternationalizationAPI } from "../../context/translation.js"; +import { DoubleColumnFormDesign, FormMetadata } from "../forms-types.js"; +import { vqfIdentityList, yesNoChoices } from "./VQF_902_12_13_common.js"; +import { vqfLanguagePolicy } from "./vqf.js"; + +export const form_vqf_902_12 = ( + i18n: InternationalizationAPI, +): FormMetadata => ({ + label: i18n.fixed({ + en: "Foundations (as well as similar constructs) (S)", + de: "Stiftungen (sowie ähnliche Konstrukte) (S)", + }), + description: i18n.fixed({ + en: "Declaration for foundations and similar constructs.", + de: "Erklärung über Stiftungen und ähnliche Konstrukte.", + }), + id: "vqf_902_12", + version: 1, + config: VQF_902_12(i18n), +}); + +export function VQF_902_12( + i18n: InternationalizationAPI, +): DoubleColumnFormDesign { + const today = format(new Date(), "yyyy-MM-dd"); + return { + type: "double-column", + languagePolicy: vqfLanguagePolicy(i18n), + title: i18n.fixed({ + en: "Foundations (as well as similar constructs) (S)", + de: "Stiftungen (sowie ähnliche Konstrukte) (S)", + }), + sections: [ + { + title: i18n.fixed({ + en: "Contracting partner and foundation", + de: "Vertragspartner und Stiftung", + }), + fields: [ + { + id: TalerFormAttributes.IDENTITY_CONTRACTING_PARTNER, + label: i18n.fixed({ + en: "Contracting partner", + de: "Vertragspartner", + }), + type: "textArea", + required: true, + }, + { + type: "caption", + label: i18n.fixed({ + en: "The undersigned hereby declare(s) that as board member of the foundation, or of the highest supervisory body of an underlying company of a foundation, known as:", + de: "Der/die Unterzeichnete(n) erklärt/erklären hiermit, dass er/sie Mitglied des Stiftungsrates oder des obersten Aufsichtsorgans einer unterliegenden Gesellschaft einer Stiftung, mit dem Namen/der Firma:", + }), + }, + { + id: TalerFormAttributes.LEGAL_STRUCTURE_NAME, + label: i18n.fixed({ + en: "Name of the foundation", + de: "Name/Firma der Stiftung", + }), + type: "text", + required: true, + }, + { + type: "caption", + label: i18n.fixed({ + en: "and, in such capacity, provide(s) to the best of his/her/their knowledge the following:", + de: "ist/sind und in dieser Funktion nach seinem/ihrem besten Wissen die folgenden Informationen übermittelt/übermitteln:", + }), + }, + ], + }, + { + title: i18n.fixed({ + en: "Name and information pertaining to the foundation", + de: "Namen und Angaben zur Stiftung", + }), + fields: [ + { + id: TalerFormAttributes.LEGAL_STRUCTURE_TYPE, + label: i18n.fixed({ + en: "Type of foundation", + de: "Art der Stiftung", + }), + type: "choiceHorizontal", + required: true, + choices: [ + { + value: "DISCRETIONARY", + label: i18n.fixed({ + en: "Discretionary foundation", + de: "Diskretionäre Stiftung", + }), + }, + { + value: "NON_DISCRETIONARY", + label: i18n.fixed({ + en: "Non-discretionary foundation", + de: "Nicht-diskretionäre Stiftung", + }), + }, + ], + }, + { + id: TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY, + label: i18n.fixed({ en: "Revocability", de: "Widerrufbarkeit" }), + type: "choiceHorizontal", + required: true, + choices: [ + { + value: "REVOCABLE", + label: i18n.fixed({ + en: "Revocable foundation", + de: "Widerrufbare Stiftung", + }), + }, + { + value: "IRREVOCABLE", + label: i18n.fixed({ + en: "Irrevocable foundation", + de: "Nicht-widerrufbare Stiftung", + }), + }, + ], + }, + ], + }, + { + title: i18n.fixed({ + en: "Information pertaining to the founder", + de: "Angaben zum Errichter/Stifter", + }), + description: i18n.fixed({ + en: "Information pertaining to the ultimate economic, not fiduciary, founder (individual(s) or entity/entities).", + de: "Angaben zum tatsächlichen, nicht treuhänderischen Errichter/Stifter (Einzelperson/en oder Gesellschaft/en).", + }), + fields: [ + vqfIdentityList( + i18n, + TalerFormAttributes.ORIGINATOR_LIST, + i18n.fixed({ en: "Founder(s)", de: "Errichter/Stifter" }), + true, + true, + ), + { + id: TalerFormAttributes.ORIGINATOR_HAS_REVOCATION_RIGHT, + label: i18n.fixed({ + en: "Does the founder have the right to revoke the foundation?", + de: "Hat der Stifter das Recht, die Stiftung zu widerrufen?", + }), + type: "choiceHorizontal", + choices: yesNoChoices(i18n), + required: true, + hide(_value, root) { + return ( + root[TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY] !== + "REVOCABLE" + ); + }, + }, + ], + }, + { + title: i18n.fixed({ + en: "Founders of pre-existing foundations", + de: "Errichter/Stifter vorbestehender Stiftungen", + }), + description: i18n.fixed({ + en: "Complete this section if the foundation results from a restructuring (re-settlement) or merger of pre-existing foundations.", + de: "Auszufüllen, wenn die Stiftung aus einer Restrukturierung (Re-Settlement) oder Zusammenlegung (Merger) vorbestehender Stiftungen entstanden ist.", + }), + fields: [ + vqfIdentityList( + i18n, + TalerFormAttributes.PREDECESSOR_ORIGINATOR_LIST, + i18n.fixed({ + en: "Founder(s) of the pre-existing foundation(s)", + de: "Errichter/Stifter der vorbestehenden Stiftung(en)", + }), + true, + false, + ), + ], + }, + { + title: i18n.fixed({ + en: "Beneficiaries", + de: "Angaben zu Begünstigten", + }), + fields: [ + vqfIdentityList( + i18n, + TalerFormAttributes.BENEFICIARY_LIST, + i18n.fixed({ + en: "Beneficiaries determined at the time this form is signed", + de: "Im Zeitpunkt der Unterzeichnung bestimmte Begünstigte", + }), + false, + false, + ), + { + id: TalerFormAttributes.BENEFICIARIES_HAVE_DISTRIBUTION_CLAIM, + label: i18n.fixed({ + en: "Does the beneficiary have an actual right to claim distributions?", + de: "Hat der Begünstigte einen festen Anspruch auf Ausschüttungen?", + }), + type: "choiceHorizontal", + choices: yesNoChoices(i18n), + required: true, + hide(_value, root) { + return !root[TalerFormAttributes.BENEFICIARY_LIST]?.length; + }, + }, + { + id: TalerFormAttributes.BENEFICIARY_GROUPS, + label: i18n.fixed({ + en: "Known group(s) of beneficiaries", + de: "Bekannte Begünstigtengruppe(n)", + }), + help: i18n.fixed({ + en: "In addition to defined beneficiaries, or if no beneficiary has been defined (for example, descendants of the founder).", + de: "Zusätzlich zu bestimmten Begünstigten oder falls keine bestimmten Begünstigten festgelegt wurden (z.B. Nachkommen des Errichters/Stifters).", + }), + type: "textArea", + required: false, + validator(text, form) { + if ( + !text?.trim() && + !form[TalerFormAttributes.BENEFICIARY_LIST]?.length + ) { + return i18n.fixed({ + en: "Provide at least one beneficiary or group of beneficiaries", + de: "Bitte geben Sie mindestens einen Begünstigten oder eine Begünstigtengruppe an.", + }); + } + return undefined; + }, + }, + ], + }, + { + title: i18n.fixed({ en: "Further persons", de: "Weitere Personen" }), + description: i18n.fixed({ + en: "Persons entitled to determine or nominate representatives of the foundation when those representatives may dispose of assets or change asset distributions or the nomination of beneficiaries.", + de: "Personen, welche Vertreter der Stiftung bestimmen oder ernennen können, sofern diese Vertreter über Vermögenswerte verfügen oder die Vermögenszuteilung beziehungsweise Ernennung von Begünstigten ändern können.", + }), + fields: [ + vqfIdentityList( + i18n, + TalerFormAttributes.FURTHER_PERSON_LIST, + i18n.fixed({ en: "Further person(s)", de: "Weitere Person(en)" }), + false, + false, + ), + { + id: TalerFormAttributes.FURTHER_PERSON_HAS_REVOCATION_RIGHT, + label: i18n.fixed({ + en: "Is/are there (a) further person(s) with the right to revoke the foundation?", + de: "Hat/Haben diese weitere(n) Person(en) das Recht, die Stiftung zu widerrufen?", + }), + type: "choiceHorizontal", + choices: yesNoChoices(i18n), + required: true, + hide(_value, root) { + return ( + root[TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY] !== + "REVOCABLE" + ); + }, + }, + ], + }, + { + title: i18n.fixed({ en: "Signature(s)", de: "Unterschrift(en)" }), + description: i18n.fixed({ + en: "It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery).", + de: "Die vorsätzliche Angabe falscher Informationen in diesem Formular ist eine strafbare Handlung (Urkundenfälschung gemäss Artikel 251 des Schweizerischen Strafgesetzbuchs).", + }), + fields: [ + { + type: "caption", + label: i18n.fixed({ + en: "The contracting partner(s) undertake(s) to automatically inform of any changes to the information contained herein.", + de: "Der/die Vertragspartner verpflichtet/verpflichten sich, allfällige Änderungen an den hier gemachten Angaben unverzüglich mitzuteilen.", + }), + }, + { + id: TalerFormAttributes.SIGNATURE, + label: i18n.fixed({ en: "Signature(s)", de: "Unterschrift(en)" }), + type: "text", + required: true, + }, + { + id: TalerFormAttributes.SIGN_DATE, + label: i18n.fixed({ en: "Date", de: "Datum" }), + type: "isoDateText", + placeholder: "dd/MM/yyyy", + pattern: "dd/MM/yyyy", + defaultValue: today, + disabled: true, + required: true, + }, + ], + }, + ], + }; +} diff --git a/packages/web-util/src/forms/gana/VQF_902_12_13.test.ts b/packages/web-util/src/forms/gana/VQF_902_12_13.test.ts @@ -0,0 +1,125 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. +*/ + +import { i18n, setupI18n, TalerFormAttributes } from "@gnu-taler/taler-util"; +import assert from "node:assert/strict"; +import test from "node:test"; +import { preloadedForms } from "../index.js"; +import { UIFormElementConfig } from "../forms-types.js"; +import { VQF_902_12 } from "./VQF_902_12.js"; +import { VQF_902_13 } from "./VQF_902_13.js"; + +function findField( + fields: UIFormElementConfig[], + id: string, +): UIFormElementConfig | undefined { + for (const field of fields) { + if ("id" in field && field.id === id) return field; + if ("fields" in field) { + const nested = findField(field.fields, id); + if (nested) return nested; + } + } + return undefined; +} + +test("new VQF forms are preloaded with the prescribed language policy", () => { + setupI18n("en", {}); + const forms = preloadedForms(i18n); + for (const id of ["vqf_902_12", "vqf_902_13"]) { + const form = forms.find((candidate) => candidate.id === id); + assert.ok(form, `${id} must be preloaded`); + assert.equal(form.version, 1); + assert.notEqual(typeof form.config, "function"); + if (typeof form.config === "function") + throw Error("unexpected dynamic form"); + assert.deepEqual(form.config.languagePolicy?.officialLanguages, [ + "de", + "en", + ]); + assert.deepEqual(form.config.languagePolicy?.legallyBindingLanguages, [ + "de", + ]); + } +}); + +test("foundation and trust forms use their structure-specific attributes", () => { + setupI18n("en", {}); + const foundationFields = VQF_902_12(i18n).sections.flatMap( + (section) => section.fields, + ); + const trustFields = VQF_902_13(i18n).sections.flatMap( + (section) => section.fields, + ); + + assert.ok(findField(foundationFields, TalerFormAttributes.ORIGINATOR_LIST)); + assert.ok( + findField(foundationFields, TalerFormAttributes.FURTHER_PERSON_LIST), + ); + assert.equal( + findField(foundationFields, TalerFormAttributes.PROTECTOR_LIST), + undefined, + ); + assert.ok(findField(trustFields, TalerFormAttributes.ORIGINATOR_LIST)); + assert.ok(findField(trustFields, TalerFormAttributes.PROTECTOR_LIST)); + assert.ok(findField(trustFields, TalerFormAttributes.FURTHER_PERSON_LIST)); +}); + +test("revocation questions only apply to revocable structures", () => { + setupI18n("en", {}); + const fields = VQF_902_13(i18n).sections.flatMap((section) => section.fields); + const settlorQuestion = findField( + fields, + TalerFormAttributes.ORIGINATOR_HAS_REVOCATION_RIGHT, + ); + assert.ok( + settlorQuestion && "hide" in settlorQuestion && settlorQuestion.hide, + ); + assert.equal( + settlorQuestion.hide(undefined, { + [TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY]: "IRREVOCABLE", + }), + true, + ); + assert.equal( + settlorQuestion.hide(undefined, { + [TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY]: "REVOCABLE", + }), + false, + ); +}); + +test("prescribed German VQF wording does not depend on gettext catalogs", () => { + setupI18n("de", {}); + const forms = preloadedForms(i18n); + assert.equal( + forms.find((form) => form.id === "vqf_902_12")?.label, + "Stiftungen (sowie ähnliche Konstrukte) (S)", + ); + assert.equal( + forms.find((form) => form.id === "vqf_902_13")?.label, + "Erklärung für Trusts (T)", + ); + + const existingVqf = forms.find((form) => form.id === "vqf_902_4"); + assert.equal(existingVqf?.label, "Risikoprofil GwG"); + assert.ok(existingVqf && typeof existingVqf.config !== "function"); + assert.equal( + existingVqf.config.languagePolicy?.legallyBindingLanguageLabel, + "Deutsch", + ); + assert.equal( + forms.find((form) => form.id === "vqf_902_9_customer")?.label, + "Feststellung des wirtschaftlich Berechtigten (A) — Kunde", + ); + assert.equal( + forms.find((form) => form.id === "vqf_902_9_officer")?.label, + "Feststellung des wirtschaftlich Berechtigten (A) — GwG-Beauftragter", + ); +}); diff --git a/packages/web-util/src/forms/gana/VQF_902_12_13_common.ts b/packages/web-util/src/forms/gana/VQF_902_12_13_common.ts @@ -0,0 +1,136 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. +*/ + +import { TalerFormAttributes } from "@gnu-taler/taler-util"; +import { intervalToDuration, isFuture, isValid, parse } from "date-fns"; +import { InternationalizationAPI } from "../../context/translation.js"; +import { + countryNameList, + countryNationalityList, +} from "../../utils/select-ui-lists.js"; +import { UIFormElementConfig } from "../forms-types.js"; + +function optionalPastDateValidator( + i18n: InternationalizationAPI, + limitAge: boolean, +) { + return (text: string | undefined) => { + if (!text) return undefined; + const time = parse(text, "yyyy-MM-dd", new Date()); + if (!isValid(time)) { + return i18n.fixed({ en: "invalid format", de: "ungültiges Format" }); + } + if (isFuture(time)) { + return i18n.fixed({ + en: "it can't be in the future", + de: "darf nicht in der Zukunft liegen", + }); + } + if (limitAge) { + const { years } = intervalToDuration({ start: time, end: new Date() }); + if (years && years > 120) { + return i18n.fixed({ + en: "it can't be greater than 120 years", + de: "darf nicht größer als 120 Jahre sein", + }); + } + } + return undefined; + }; +} + +export function vqfStructureIdentityFields( + i18n: InternationalizationAPI, + includeDateOfDeath: boolean, +): UIFormElementConfig[] { + const fields: UIFormElementConfig[] = [ + { + id: TalerFormAttributes.NAME_OR_ENTITY, + label: i18n.fixed({ + en: "Last name(s), first name(s)/entity", + de: "Name(n), Vorname(n)/Firma", + }), + type: "text", + required: true, + }, + { + id: TalerFormAttributes.DOMICILE_OR_REGISTERED_OFFICE, + label: i18n.fixed({ + en: "Actual address of domicile/registered office", + de: "Effektive Wohnsitzadresse/Sitzadresse", + }), + type: "textArea", + required: true, + }, + { + id: TalerFormAttributes.DOMICILE_OR_REGISTERED_OFFICE_COUNTRY, + label: i18n.fixed({ en: "Country", de: "Land" }), + type: "selectOne", + choices: countryNameList(i18n), + preferredChoiceVals: ["CH"], + required: true, + }, + { + id: TalerFormAttributes.DATE_OF_BIRTH, + label: i18n.fixed({ en: "Date(s) of birth", de: "Geburtsdatum/-daten" }), + type: "isoDateText", + placeholder: "dd/MM/yyyy", + pattern: "dd/MM/yyyy", + required: false, + validator: optionalPastDateValidator(i18n, true), + }, + { + id: TalerFormAttributes.NATIONALITY, + label: i18n.fixed({ en: "Nationality", de: "Nationalität" }), + type: "selectOne", + choices: countryNationalityList(i18n), + preferredChoiceVals: ["CH"], + required: false, + }, + ]; + if (includeDateOfDeath) { + fields.push({ + id: TalerFormAttributes.DATE_OF_DEATH, + label: i18n.fixed({ + en: "Date of death (if deceased)", + de: "Todesfall (falls verstorben)", + }), + type: "isoDateText", + placeholder: "dd/MM/yyyy", + pattern: "dd/MM/yyyy", + required: false, + validator: optionalPastDateValidator(i18n, false), + }); + } + return fields; +} + +export function vqfIdentityList( + i18n: InternationalizationAPI, + id: string, + label: string, + includeDateOfDeath: boolean, + required: boolean, +): UIFormElementConfig { + return { + id, + label, + type: "array", + labelFieldId: TalerFormAttributes.NAME_OR_ENTITY, + fields: vqfStructureIdentityFields(i18n, includeDateOfDeath), + required, + }; +} + +export function yesNoChoices(i18n: InternationalizationAPI) { + return [ + { value: true, label: i18n.fixed({ en: "Yes", de: "Ja" }) }, + { value: false, label: i18n.fixed({ en: "No", de: "Nein" }) }, + ]; +} diff --git a/packages/web-util/src/forms/gana/VQF_902_13.stories.tsx b/packages/web-util/src/forms/gana/VQF_902_13.stories.tsx @@ -0,0 +1,43 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. +*/ + +import { i18n, setupI18n, TalerFormAttributes } from "@gnu-taler/taler-util"; +import * as tests from "../../tests/hook.js"; +import { DefaultForm as TestedComponent } from "../forms-ui.js"; +import { VQF_902_13 } from "./VQF_902_13.js"; + +export default { title: "vqf_902_13" }; + +setupI18n("en", {}); + +export const EmptyForm = tests.createExample(TestedComponent, { + initial: {}, + design: VQF_902_13(i18n), +}); + +export const PrefilledForm = tests.createExample(TestedComponent, { + initial: { + [TalerFormAttributes.IDENTITY_CONTRACTING_PARTNER]: "Example Trustee Ltd.", + [TalerFormAttributes.LEGAL_STRUCTURE_NAME]: "Example Trust", + [TalerFormAttributes.LEGAL_STRUCTURE_TYPE]: "DISCRETIONARY", + [TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY]: "IRREVOCABLE", + [TalerFormAttributes.ORIGINATOR_LIST]: [ + { + [TalerFormAttributes.NAME_OR_ENTITY]: "Sam Settlor", + [TalerFormAttributes.DOMICILE_OR_REGISTERED_OFFICE]: + "Market Square 4, Bern", + [TalerFormAttributes.DOMICILE_OR_REGISTERED_OFFICE_COUNTRY]: "CH", + [TalerFormAttributes.DATE_OF_BIRTH]: "1965-03-04", + [TalerFormAttributes.NATIONALITY]: "CH", + }, + ], + [TalerFormAttributes.BENEFICIARY_GROUPS]: "Descendants of Sam Settlor", + }, + design: VQF_902_13(i18n), +}); diff --git a/packages/web-util/src/forms/gana/VQF_902_13.ts b/packages/web-util/src/forms/gana/VQF_902_13.ts @@ -0,0 +1,353 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. +*/ + +import { TalerFormAttributes } from "@gnu-taler/taler-util"; +import { format } from "date-fns"; +import { InternationalizationAPI } from "../../context/translation.js"; +import { DoubleColumnFormDesign, FormMetadata } from "../forms-types.js"; +import { vqfIdentityList, yesNoChoices } from "./VQF_902_12_13_common.js"; +import { vqfLanguagePolicy } from "./vqf.js"; + +export const form_vqf_902_13 = ( + i18n: InternationalizationAPI, +): FormMetadata => ({ + label: i18n.fixed({ + en: "Declaration for trusts (T)", + de: "Erklärung für Trusts (T)", + }), + description: i18n.fixed({ + en: "Declaration for trusts.", + de: "Erklärung über Trusts.", + }), + id: "vqf_902_13", + version: 1, + config: VQF_902_13(i18n), +}); + +export function VQF_902_13( + i18n: InternationalizationAPI, +): DoubleColumnFormDesign { + const today = format(new Date(), "yyyy-MM-dd"); + return { + type: "double-column", + languagePolicy: vqfLanguagePolicy(i18n), + title: i18n.fixed({ + en: "Declaration for trusts (T)", + de: "Erklärung für Trusts (T)", + }), + sections: [ + { + title: i18n.fixed({ + en: "Contracting partner and trust", + de: "Vertragspartner und Trust", + }), + fields: [ + { + id: TalerFormAttributes.IDENTITY_CONTRACTING_PARTNER, + label: i18n.fixed({ + en: "Contracting partner", + de: "Vertragspartner", + }), + type: "textArea", + required: true, + }, + { + type: "caption", + label: i18n.fixed({ + en: "The undersigned hereby declare(s) that as trustee or a member of highest supervisory body of an underlying company of a trust known as:", + de: "Der/die Unterzeichnete(n) erklärt/erklären hiermit, dass er/sie Trustee oder Mitglied des obersten Aufsichtsorgans einer unterliegenden Gesellschaft eines Trusts usw. mit dem Namen/der Firma:", + }), + }, + { + id: TalerFormAttributes.LEGAL_STRUCTURE_NAME, + label: i18n.fixed({ + en: "Name of the trust", + de: "Name/Firma des Trusts", + }), + type: "text", + required: true, + }, + { + type: "caption", + label: i18n.fixed({ + en: "and, in such capacity, provide(s) to the best of his/her/their knowledge the following information:", + de: "ist/sind und in dieser Funktion nach seinem/ihrem besten Wissen die folgenden Informationen übermittelt/übermitteln:", + }), + }, + ], + }, + { + title: i18n.fixed({ + en: "Name and information pertaining to the trust", + de: "Namen und Angaben zum Trust", + }), + fields: [ + { + id: TalerFormAttributes.LEGAL_STRUCTURE_TYPE, + label: i18n.fixed({ en: "Type of trust", de: "Art des Trusts" }), + type: "choiceHorizontal", + required: true, + choices: [ + { + value: "DISCRETIONARY", + label: i18n.fixed({ + en: "Discretionary trust", + de: "Discretionary Trust", + }), + }, + { + value: "NON_DISCRETIONARY", + label: i18n.fixed({ + en: "Non-discretionary trust", + de: "Non-discretionary Trust", + }), + }, + ], + }, + { + id: TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY, + label: i18n.fixed({ en: "Revocability", de: "Widerrufbarkeit" }), + type: "choiceHorizontal", + required: true, + choices: [ + { + value: "REVOCABLE", + label: i18n.fixed({ + en: "Revocable trust", + de: "Revocable Trust", + }), + }, + { + value: "IRREVOCABLE", + label: i18n.fixed({ + en: "Irrevocable trust", + de: "Irrevocable Trust", + }), + }, + ], + }, + ], + }, + { + title: i18n.fixed({ + en: "Information pertaining to the settlor", + de: "Angaben zum Settlor", + }), + description: i18n.fixed({ + en: "Information pertaining to the ultimate economic, not fiduciary, settlor of the trust (individual(s) or entity/entities).", + de: "Angaben zum tatsächlichen, nicht treuhänderischen Settlor des Trusts (Einzelperson/en oder Gesellschaft/en).", + }), + fields: [ + vqfIdentityList( + i18n, + TalerFormAttributes.ORIGINATOR_LIST, + i18n.fixed({ en: "Settlor(s)", de: "Settlor" }), + true, + true, + ), + { + id: TalerFormAttributes.ORIGINATOR_HAS_REVOCATION_RIGHT, + label: i18n.fixed({ + en: "Does the settlor have the right to revoke the trust?", + de: "Hat der Settlor das Recht, den Trust zu widerrufen?", + }), + type: "choiceHorizontal", + choices: yesNoChoices(i18n), + required: true, + hide(_value, root) { + return ( + root[TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY] !== + "REVOCABLE" + ); + }, + }, + ], + }, + { + title: i18n.fixed({ + en: "Settlors of pre-existing trusts", + de: "Settlor(s) vorbestehender Trusts", + }), + description: i18n.fixed({ + en: "Complete this section if the trust results from a restructuring (re-settlement) or merger of pre-existing trusts.", + de: "Auszufüllen, wenn der Trust aus einer Restrukturierung (Re-Settlement) oder Zusammenlegung (Merger) vorbestehender Trusts entstanden ist.", + }), + fields: [ + vqfIdentityList( + i18n, + TalerFormAttributes.PREDECESSOR_ORIGINATOR_LIST, + i18n.fixed({ + en: "Settlor(s) of the pre-existing trust(s)", + de: "Settlor(s) des/der vorbestehenden Trusts", + }), + true, + false, + ), + ], + }, + { + title: i18n.fixed({ + en: "Beneficiaries", + de: "Angaben zu Begünstigten", + }), + fields: [ + vqfIdentityList( + i18n, + TalerFormAttributes.BENEFICIARY_LIST, + i18n.fixed({ + en: "Beneficiaries determined at the time this form is signed", + de: "Im Zeitpunkt der Unterzeichnung bestimmte Begünstigte", + }), + false, + false, + ), + { + id: TalerFormAttributes.BENEFICIARIES_HAVE_DISTRIBUTION_CLAIM, + label: i18n.fixed({ + en: "Does the beneficiary have an actual right to claim distributions?", + de: "Hat der Begünstigte einen festen Anspruch auf Ausschüttungen?", + }), + type: "choiceHorizontal", + choices: yesNoChoices(i18n), + required: true, + hide(_value, root) { + return !root[TalerFormAttributes.BENEFICIARY_LIST]?.length; + }, + }, + { + id: TalerFormAttributes.BENEFICIARY_GROUPS, + label: i18n.fixed({ + en: "Known group(s) of beneficiaries", + de: "Bekannte Begünstigtengruppe(n)", + }), + help: i18n.fixed({ + en: "In addition to defined beneficiaries, or if no beneficiary has been defined (for example, descendants of the settlor).", + de: "Zusätzlich zu bestimmten Begünstigten oder falls keine bestimmten Begünstigten festgelegt wurden (z.B. Nachkommen des Settlors).", + }), + type: "textArea", + required: false, + validator(text, form) { + if ( + !text?.trim() && + !form[TalerFormAttributes.BENEFICIARY_LIST]?.length + ) { + return i18n.fixed({ + en: "Provide at least one beneficiary or group of beneficiaries", + de: "Bitte geben Sie mindestens einen Begünstigten oder eine Begünstigtengruppe an.", + }); + } + return undefined; + }, + }, + ], + }, + { + title: i18n.fixed({ en: "Protectors", de: "Protektoren" }), + fields: [ + vqfIdentityList( + i18n, + TalerFormAttributes.PROTECTOR_LIST, + i18n.fixed({ en: "Protector(s)", de: "Protektor(en)" }), + false, + false, + ), + { + id: TalerFormAttributes.PROTECTOR_HAS_REVOCATION_RIGHT, + label: i18n.fixed({ + en: "Does the protector have the right to revoke the trust?", + de: "Hat der Protektor das Recht, den Trust zu widerrufen?", + }), + type: "choiceHorizontal", + choices: yesNoChoices(i18n), + required: true, + hide(_value, root) { + return ( + root[TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY] !== + "REVOCABLE" || + !root[TalerFormAttributes.PROTECTOR_LIST]?.length + ); + }, + }, + ], + }, + { + title: i18n.fixed({ en: "Further persons", de: "Weitere Personen" }), + description: i18n.fixed({ + en: "Further persons having the right to appoint the trustee of a trust.", + de: "Weitere Personen, welche das Recht haben, den Trustee eines Trusts zu bestimmen.", + }), + fields: [ + vqfIdentityList( + i18n, + TalerFormAttributes.FURTHER_PERSON_LIST, + i18n.fixed({ en: "Further person(s)", de: "Weitere Person(en)" }), + false, + false, + ), + { + id: TalerFormAttributes.FURTHER_PERSON_HAS_REVOCATION_RIGHT, + label: i18n.fixed({ + en: "Do any further persons have the right to revoke the trust?", + de: "Hat/Haben diese weitere(n) Person(en) das Recht, den Trust zu widerrufen?", + }), + type: "choiceHorizontal", + choices: yesNoChoices(i18n), + required: true, + hide(_value, root) { + return ( + root[TalerFormAttributes.LEGAL_STRUCTURE_REVOCABILITY] !== + "REVOCABLE" || + !root[TalerFormAttributes.FURTHER_PERSON_LIST]?.length + ); + }, + }, + ], + }, + { + title: i18n.fixed({ en: "Signature(s)", de: "Unterschrift(en)" }), + description: i18n.fixed({ + en: "It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery).", + de: "Die vorsätzliche Angabe falscher Informationen in diesem Formular ist eine strafbare Handlung (Urkundenfälschung gemäss Artikel 251 des Schweizerischen Strafgesetzbuchs).", + }), + fields: [ + { + type: "caption", + label: i18n.fixed({ + en: "The contracting partner(s) hereby declare(s) to be entitled to open a business relationship for the trust above or its underlying company.", + de: "Der/die Vertragspartner bestätigt/bestätigen, dass er/sie zur Aufnahme der Geschäftsbeziehung für den oben genannten Trust oder dessen unterliegender Gesellschaft berechtigt ist/sind.", + }), + }, + { + type: "caption", + label: i18n.fixed({ + en: "The contracting partner(s) hereby undertake(s) to automatically inform of any changes to the information contained herein.", + de: "Der/die Vertragspartner verpflichtet/verpflichten sich, allfällige Änderungen an den hier gemachten Angaben unverzüglich mitzuteilen.", + }), + }, + { + id: TalerFormAttributes.SIGNATURE, + label: i18n.fixed({ en: "Signature(s)", de: "Unterschrift(en)" }), + type: "text", + required: true, + }, + { + id: TalerFormAttributes.SIGN_DATE, + label: i18n.fixed({ en: "Date", de: "Datum" }), + type: "isoDateText", + placeholder: "dd/MM/yyyy", + pattern: "dd/MM/yyyy", + defaultValue: today, + disabled: true, + required: true, + }, + ], + }, + ], + }; +} diff --git a/packages/web-util/src/forms/gana/index.stories.ts b/packages/web-util/src/forms/gana/index.stories.ts @@ -22,6 +22,8 @@ export * as personal_info_stories from "./personal-info.stories.js"; export * as vqf_902_11_customer_stories from "./VQF_902_11_customer.stories.js"; export * as vqf_902_11_officer_stories from "./VQF_902_11_officer.stories.js"; +export * as vqf_902_12_stories from "./VQF_902_12.stories.js"; +export * as vqf_902_13_stories from "./VQF_902_13.stories.js"; export * as vqf_902_14_stories from "./VQF_902_14.stories.js"; export const vqf_902_1_customer_stories = { default: vqf9021CustomerMeta, diff --git a/packages/web-util/src/forms/index.stories.ts b/packages/web-util/src/forms/index.stories.ts @@ -52,5 +52,7 @@ export const vqfForms = { vqf9029Officer: ganaStories.vqf_902_9_officer_stories, vqf90211Customer: ganaStories.vqf_902_11_customer_stories, vqf90211Officer: ganaStories.vqf_902_11_officer_stories, + vqf90212: ganaStories.vqf_902_12_stories, + vqf90213: ganaStories.vqf_902_13_stories, vqf90214: ganaStories.vqf_902_14_stories, }; diff --git a/packages/web-util/src/forms/index.ts b/packages/web-util/src/forms/index.ts @@ -27,6 +27,8 @@ import { nameAndDob } from "./gana/nameAndBirthdate.js"; import { simplest } from "./gana/simplest.js"; import { form_vqf_902_11_customer } from "./gana/VQF_902_11_customer.js"; import { form_vqf_902_11_officer } from "./gana/VQF_902_11_officer.js"; +import { form_vqf_902_12 } from "./gana/VQF_902_12.js"; +import { form_vqf_902_13 } from "./gana/VQF_902_13.js"; import { form_vqf_902_14 } from "./gana/VQF_902_14.js"; import { form_vqf_902_1_customer } from "./gana/VQF_902_1_customer.js"; import { form_vqf_902_1_officer } from "./gana/VQF_902_1_officer.js"; @@ -34,6 +36,7 @@ import { form_vqf_902_4 } from "./gana/VQF_902_4.js"; import { form_vqf_902_5 } from "./gana/VQF_902_5.js"; import { form_vqf_902_9_customer } from "./gana/VQF_902_9_customer.js"; import { form_vqf_902_9_officer } from "./gana/VQF_902_9_officer.js"; +import { withVqfTranslations } from "./gana/vqf.js"; export * from "./Calendar.js"; export * from "./AcceptTosForm.js"; @@ -66,6 +69,8 @@ export * from "./gana/generic_note.js"; export * from "./gana/VQF_902_11_customer.js"; export * from "./gana/VQF_902_11_officer.js"; +export * from "./gana/VQF_902_12.js"; +export * from "./gana/VQF_902_13.js"; export * from "./gana/VQF_902_14.js"; export * from "./gana/VQF_902_1_customer.js"; export * from "./gana/VQF_902_1_officer.js"; @@ -73,71 +78,77 @@ export * from "./gana/VQF_902_4.js"; export * from "./gana/VQF_902_5.js"; export * from "./gana/VQF_902_9_customer.js"; export * from "./gana/VQF_902_9_officer.js"; +export * from "./gana/vqf.js"; export * from "./Group.js"; export * from "./HtmlIframe.js"; export const preloadedForms: ( i18n: InternationalizationAPI, -) => Array<FormMetadata> = (i18n) => [ - { - label: i18n.str`Simple comment`, - id: "__simple_comment", - version: 1, - config: simplest(i18n), - }, - { - label: i18n.str`Terms of service`, - id: "accept-tos", - version: 1, - config: (context: any) => acceptTos(i18n, context), - officerView: AcceptTosOfficerView, - contextForm: { - type: "single-column", - fields: [ - { - id: "tos_url", - type: "text", - label: i18n.str`Terms URL`, - required: true, - }, - { - id: "tos_version", - type: "text", - label: i18n.str`Terms version`, - required: true, - }, - { - id: "provider_name", - type: "text", - label: i18n.str`Provider name`, - }, - { - id: "link_only", - type: "toggle", - label: i18n.str`Only show the external link`, - }, - ], +) => Array<FormMetadata> = (i18n) => { + const vqfI18n = withVqfTranslations(i18n); + return [ + { + label: i18n.str`Simple comment`, + id: "__simple_comment", + version: 1, + config: simplest(i18n), }, - }, - { - label: i18n.str`Name and birthdate`, - id: "name_and_dob", - version: 1, - config: nameAndDob(i18n), - }, - form_challenger_email(i18n), - form_challenger_sms(i18n), - form_challenger_postal(i18n), - form_multi_upload(i18n), - form_vqf_902_1_customer(i18n), - form_vqf_902_1_officer(i18n), - form_vqf_902_4(i18n), - form_vqf_902_5(i18n), - form_vqf_902_9_customer(i18n), - form_vqf_902_9_officer(i18n), - form_vqf_902_11_customer(i18n), - form_vqf_902_11_officer(i18n), - form_vqf_902_14(i18n), - form_generic_note(i18n), -]; + { + label: i18n.str`Terms of service`, + id: "accept-tos", + version: 1, + config: (context: any) => acceptTos(i18n, context), + officerView: AcceptTosOfficerView, + contextForm: { + type: "single-column", + fields: [ + { + id: "tos_url", + type: "text", + label: i18n.str`Terms URL`, + required: true, + }, + { + id: "tos_version", + type: "text", + label: i18n.str`Terms version`, + required: true, + }, + { + id: "provider_name", + type: "text", + label: i18n.str`Provider name`, + }, + { + id: "link_only", + type: "toggle", + label: i18n.str`Only show the external link`, + }, + ], + }, + }, + { + label: i18n.str`Name and birthdate`, + id: "name_and_dob", + version: 1, + config: nameAndDob(i18n), + }, + form_challenger_email(i18n), + form_challenger_sms(i18n), + form_challenger_postal(i18n), + form_multi_upload(i18n), + form_vqf_902_1_customer(vqfI18n), + form_vqf_902_1_officer(vqfI18n), + form_vqf_902_4(vqfI18n), + form_vqf_902_5(vqfI18n), + form_vqf_902_9_customer(vqfI18n), + form_vqf_902_9_officer(vqfI18n), + form_vqf_902_11_customer(vqfI18n), + form_vqf_902_11_officer(vqfI18n), + form_vqf_902_12(vqfI18n), + form_vqf_902_13(vqfI18n), + form_vqf_902_14(vqfI18n), + form_generic_note(i18n), + ]; +};