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:
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),
+ ];
+};