exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 3080a6c63b6e9842af94a9f82eec4001bd2a55e2
parent 66c969bdb0ec6c02f97780e86bb9323bccd4269a
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed,  4 Mar 2026 22:37:02 +0100

package all typst resources

Diffstat:
Mconfigure.ac | 22++++++++++++++++++++++
Mcontrib/typst/Makefile.am | 51+++++++++++++++++++++++----------------------------
Dcontrib/typst/_cover_.typ | 326-------------------------------------------------------------------------------
Acontrib/typst/_cover_/Makefile.am | 10++++++++++
Acontrib/typst/_cover_/_cover_.typ | 266+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/_cover_/template/main.typ | 48++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/_cover_/typst.toml | 9+++++++++
Dcontrib/typst/accept-tos.typ | 87-------------------------------------------------------------------------------
Acontrib/typst/accept-tos/Makefile.am | 10++++++++++
Acontrib/typst/accept-tos/accept-tos.typ | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/accept-tos/template/main.typ | 7+++++++
Acontrib/typst/accept-tos/typst.toml | 15+++++++++++++++
Dcontrib/typst/challenger-postal.typ | 92-------------------------------------------------------------------------------
Acontrib/typst/challenger-postal/Makefile.am | 10++++++++++
Acontrib/typst/challenger-postal/challenger-postal.typ | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/challenger-postal/template/main.typ | 10++++++++++
Acontrib/typst/challenger-postal/typst.toml | 9+++++++++
Dcontrib/typst/challenger-sms.typ | 88-------------------------------------------------------------------------------
Acontrib/typst/challenger-sms/Makefile.am | 10++++++++++
Acontrib/typst/challenger-sms/challenger-sms.typ | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/challenger-sms/template/main.typ | 9+++++++++
Acontrib/typst/challenger-sms/typst.toml | 9+++++++++
Acontrib/typst/common/Makefile.am | 12++++++++++++
Acontrib/typst/common/lib.typ | 24++++++++++++++++++++++++
Rcontrib/typst/pointing_finger.svg -> contrib/typst/common/pointing_finger.svg | 0
Rcontrib/typst/taler-logo.svg -> contrib/typst/common/taler-logo.svg | 0
Acontrib/typst/common/typst.toml | 9+++++++++
Rcontrib/typst/vss_vqf_verein.png -> contrib/typst/common/vss_vqf_verein.png | 0
Dcontrib/typst/generic_note.typ | 79-------------------------------------------------------------------------------
Acontrib/typst/generic_note/Makefile.am | 10++++++++++
Acontrib/typst/generic_note/generic_note.typ | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/generic_note/template/main.typ | 7+++++++
Acontrib/typst/generic_note/typst.toml | 14++++++++++++++
Dcontrib/typst/generic_upload.typ | 88-------------------------------------------------------------------------------
Acontrib/typst/generic_upload/Makefile.am | 10++++++++++
Acontrib/typst/generic_upload/generic_upload.typ | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/generic_upload/template/main.typ | 8++++++++
Acontrib/typst/generic_upload/typst.toml | 9+++++++++
Dcontrib/typst/vqf_902_1.typ | 457-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_1/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_1/template/main.typ | 23+++++++++++++++++++++++
Acontrib/typst/vqf_902_1/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_1/vqf_902_1.typ | 422+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_11.typ | 248-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_11/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_11/template/main.typ | 19+++++++++++++++++++
Acontrib/typst/vqf_902_11/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_11/vqf_902_11.typ | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_11_customer.typ | 244-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_11_customer/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_11_customer/template/main.typ | 19+++++++++++++++++++
Acontrib/typst/vqf_902_11_customer/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_11_customer/vqf_902_11_customer.typ | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_11_officer.typ | 245-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_11_officer/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_11_officer/template/main.typ | 20++++++++++++++++++++
Acontrib/typst/vqf_902_11_officer/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_11_officer/vqf_902_11_officer.typ | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_12.typ | 403-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_12/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_12/template/main.typ | 24++++++++++++++++++++++++
Acontrib/typst/vqf_902_12/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_12/vqf_902_12.typ | 368+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_13.typ | 496-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_13/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_13/template/main.typ | 23+++++++++++++++++++++++
Acontrib/typst/vqf_902_13/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_13/vqf_902_13.typ | 462+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_14.typ | 242-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_14/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_14/template/main.typ | 15+++++++++++++++
Acontrib/typst/vqf_902_14/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_14/vqf_902_14.typ | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_15.typ | 321-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_15/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_15/template/main.typ | 21+++++++++++++++++++++
Acontrib/typst/vqf_902_15/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_15/vqf_902_15.typ | 289++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_1_customer.typ | 422-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_1_customer/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_1_customer/template/main.typ | 20++++++++++++++++++++
Acontrib/typst/vqf_902_1_customer/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_1_customer/vqf_902_1_customer.typ | 391+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_1_officer.typ | 233-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_1_officer/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_1_officer/template/main.typ | 11+++++++++++
Acontrib/typst/vqf_902_1_officer/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_1_officer/vqf_902_1_officer.typ | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_4.typ | 669-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_4/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_4/template/main.typ | 24++++++++++++++++++++++++
Acontrib/typst/vqf_902_4/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_4/vqf_902_4.typ | 633+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_5.typ | 249-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_5/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_5/template/main.typ | 16++++++++++++++++
Acontrib/typst/vqf_902_5/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_5/vqf_902_5.typ | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_9.typ | 211-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_9/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_9/template/main.typ | 19+++++++++++++++++++
Acontrib/typst/vqf_902_9/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_9/vqf_902_9.typ | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_9_customer.typ | 207-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_9_customer/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_9_customer/template/main.typ | 19+++++++++++++++++++
Acontrib/typst/vqf_902_9_customer/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_9_customer/vqf_902_9_customer.typ | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/typst/vqf_902_9_officer.typ | 208-------------------------------------------------------------------------------
Acontrib/typst/vqf_902_9_officer/Makefile.am | 10++++++++++
Acontrib/typst/vqf_902_9_officer/template/main.typ | 20++++++++++++++++++++
Acontrib/typst/vqf_902_9_officer/typst.toml | 9+++++++++
Acontrib/typst/vqf_902_9_officer/vqf_902_9_officer.typ | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdebian/control | 9+++++++++
Adebian/taler-exchange-typst.install | 4++++
Mdebian/taler-exchange.tmpfiles | 1+
116 files changed, 5883 insertions(+), 5643 deletions(-)

diff --git a/configure.ac b/configure.ac @@ -523,6 +523,28 @@ AM_CONDITIONAL([HAVE_TWISTER], [false]) AC_CONFIG_FILES([Makefile contrib/Makefile contrib/typst/Makefile + contrib/typst/_cover_/Makefile + contrib/typst/common/Makefile + contrib/typst/accept-tos/Makefile + contrib/typst/challenger-postal/Makefile + contrib/typst/challenger-sms/Makefile + contrib/typst/generic_note/Makefile + contrib/typst/generic_upload/Makefile + contrib/typst/vqf_902_1/Makefile + contrib/typst/vqf_902_1_customer/Makefile + contrib/typst/vqf_902_1_officer/Makefile + contrib/typst/vqf_902_4/Makefile + contrib/typst/vqf_902_5/Makefile + contrib/typst/vqf_902_9/Makefile + contrib/typst/vqf_902_9_customer/Makefile + contrib/typst/vqf_902_9_officer/Makefile + contrib/typst/vqf_902_11/Makefile + contrib/typst/vqf_902_11_customer/Makefile + contrib/typst/vqf_902_11_officer/Makefile + contrib/typst/vqf_902_12/Makefile + contrib/typst/vqf_902_13/Makefile + contrib/typst/vqf_902_14/Makefile + contrib/typst/vqf_902_15/Makefile doc/Makefile doc/doxygen/Makefile po/Makefile.in diff --git a/contrib/typst/Makefile.am b/contrib/typst/Makefile.am @@ -1,28 +1,23 @@ -SUBDIRS = . - -formdatadir = $(datadir)/taler-exchange/typst-forms/ -dist_formdata_DATA = \ - _cover_.typ \ - accept-tos.typ \ - challenger-postal.typ \ - challenger-sms.typ \ - generic_note.typ \ - generic_upload.typ \ - taler-logo.svg \ - pointing_finger.svg \ - vqf_902_1_customer.typ \ - vqf_902_1_officer.typ \ - vqf_902_4.typ \ - vqf_902_5.typ \ - vqf_902_9_customer.typ \ - vqf_902_9_officer.typ \ - vqf_902_11_customer.typ \ - vqf_902_11_officer.typ \ - vqf_902_12.typ \ - vqf_902_13.typ \ - vqf_902_14.typ \ - vqf_902_15.typ \ - vss_vqf_verein.png - -EXTRA_DIST = \ - $(dist_formdata_DATA) +SUBDIRS = . \ + _cover_ \ + common \ + accept-tos \ + challenger-postal \ + challenger-sms \ + generic_note \ + generic_upload \ + vqf_902_1 \ + vqf_902_1_customer \ + vqf_902_1_officer \ + vqf_902_4 \ + vqf_902_5 \ + vqf_902_9 \ + vqf_902_9_customer \ + vqf_902_9_officer \ + vqf_902_11 \ + vqf_902_11_customer \ + vqf_902_11_officer \ + vqf_902_12 \ + vqf_902_13 \ + vqf_902_14 \ + vqf_902_15 diff --git a/contrib/typst/_cover_.typ b/contrib/typst/_cover_.typ @@ -1,325 +0,0 @@ -// Cover page for AML files. -// Renders all account properties, current rules, etc. - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Helper function to get nice labels for standard properties - let get_property_label(key) = { - if key == "pep" { "Politically exposed person (PEP)" } - else if key == "sanctioned" { "Sanctioned account" } - else if key == "high_risk" { "High risk account" } - else if key == "business_domain" { "Business domain" } - else if key == "is_frozen" { "Account frozen" } - else if key == "was_reported" { "Reported to authorities" } - else { key } - } - - // Helper function to format timeframe - let format_timeframe(d_us) = { - if d_us == "forever" { - "forever" - } else { - let us = int(d_us) - let s = calc.quo(us, 1000000) - let m = calc.quo(s, 60) - let h = calc.quo(m, 60) - let d = calc.quo(h, 24) - - if calc.rem(us, 1000000) == 0 { - if calc.rem(s, 60) == 0 { - if calc.rem(m, 60) == 0 { - if calc.rem(h, 24) == 0 { - str(d) + " day" + if d != 1 { "s" } else { "" } - } else { - str(h) + " hour" + if h != 1 { "s" } else { "" } - } - } else { - str(m) + " minute" + if m != 1 { "s" } else { "" } - } - } else { - str(s) + " s" - } - } else { - str(us) + " μs" - } - } - } - - - // Helper function to format timestamp; ignores leap seconds (too variable) - let format_timestamp(ts) = { - if type(ts) == dictionary and "t_s" in ts { - let t_s = ts.t_s - if t_s == "never" { - "never" - } else { - // Convert Unix timestamp to human-readable format - let seconds = int(t_s) - let days_since_epoch = calc.quo(seconds, 86400) - let remaining_seconds = calc.rem(seconds, 86400) - let hours = calc.quo(remaining_seconds, 3600) - let minutes = calc.quo(calc.rem(remaining_seconds, 3600), 60) - let secs = calc.rem(remaining_seconds, 60) - - // Helper to check if year is leap year - let is_leap(y) = { - calc.rem(y, 4) == 0 and (calc.rem(y, 100) != 0 or calc.rem(y, 400) == 0) - } - - // Calculate year, month, day - let year = 1970 - let days_left = days_since_epoch - - // Find the year - let done = false - while not done { - let days_in_year = if is_leap(year) { 366 } else { 365 } - if days_left >= days_in_year { - days_left = days_left - days_in_year - year = year + 1 - } else { - done = true - } - } - - // Days in each month - let days_in_months = if is_leap(year) { - (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - } else { - (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - } - - // Find month and day - let month = 1 - for days_in_month in days_in_months { - if days_left >= days_in_month { - days_left = days_left - days_in_month - month = month + 1 - } else { - break - } - } - let day = days_left + 1 - - // Format with leading zeros - let m_str = if month < 10 { "0" + str(month) } else { str(month) } - let d_str = if day < 10 { "0" + str(day) } else { str(day) } - let h_str = if hours < 10 { "0" + str(hours) } else { str(hours) } - let min_str = if minutes < 10 { "0" + str(minutes) } else { str(minutes) } - let s_str = if secs < 10 { "0" + str(secs) } else { str(secs) } - - str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + ":" + s_str + " UTC" - } - } else { - str(ts) - } - } - - - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("taler-logo.svg", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [AMLA File No.], [#get("FILE_NUMBER")], - [Account open?], [#checkbox(get("is_active"))], - [Active investigation?], [#checkbox(get("to_investigate"))], - ) - ] - ) - - v(1em) - - // Section 1: Properties - text(size: 11pt, weight: "bold")[Properties:] - - v(0.5em) - - block(breakable: false)[ - #v(0.5em) - #let props = get("properties", default: (:)) - #let standard_props = ("pep", "sanctioned", "high_risk", "business_domain", "is_frozen", "was_reported") - #let all_keys = props.keys() - - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - ..for key in all_keys { - let value = props.at(key) - let label = get_property_label(key) - - // Render based on value type - if type(value) == bool { - ([#label], [#checkbox(value)]) - } else { - ([#label], [#value]) - } - } - ) - #v(0.5em) - ] - - // Section 2: Rules - let rules_data = get("rules", default: none) - - if rules_data != none { - text(size: 11pt, weight: "bold")[ - Rules - #if "expiration_time" in rules_data { - [ (expires: #format_timestamp(rules_data.expiration_time))] - } - : - ] - - v(0.5em) - - let rules = rules_data.at("rules", default: ()) - - if rules.len() > 0 { - block(breakable: true)[ - #table( - columns: (17%, 13%, 13%, 20%, 17%, 10%, 10%), - stroke: 0.5pt + black, - inset: 4pt, - align: (left, left, left, left, left, center, center), - table.header( - [*Operation*], - [*Threshold*], - [*Timeframe*], - [*Rule Name*], - [*Measures*], - [*Exposed*], - [*Verboten*] - ), - ..for rule in rules { - let op_type = rule.at("operation_type", default: "") - let threshold = rule.at("threshold", default: "") - let timeframe_raw = rule.at("timeframe", default: (:)) - let timeframe = if "d_us" in timeframe_raw { - format_timeframe(timeframe_raw.d_us) - } else { "" } - let rule_name = rule.at("rule_name", default: "") - let measures = rule.at("measures", default: ()) - let exposed = rule.at("exposed", default: false) - let is_verboten = if type(measures) == array { "verboten" in measures } else { "verboten" == measures } - let measures_text = if type(measures) == array { - measures.filter(m => m != "verboten").map(m => str(m)).join(", ") - } else if measures != "verboten" { - str(measures) - } else { - "" - } - - ( - [#op_type], - [#threshold], - [#timeframe], - [#rule_name], - [#measures_text], - [#checkbox(exposed)], - [#checkbox(is_verboten)] - ) - } - ) - ] - } else { - text(style: "italic")[No rules defined.] - } - } -} - -// Example usage: -#form(( - "FILE_NUMBER": "42", - "is_active": true, - "to_investigate": false, - "properties": ( - "pep": false, - "sanctioned": false, - "high_risk": true, - "business_domain": "Financial services", - "is_frozen": false, - "was_reported": false, - "custom_field": "Custom value" - ), - "rules": ( - "expiration_time": ("t_s": 1764967786), // Fri Dec 5 20:49:46 UTC 2025 - "rules": ( - ( - "operation_type": "WITHDRAW", - "rule_name": "large_withdrawal", - "threshold": "EUR:10000", - "timeframe": ("d_us": 86400000000), - "measures": ("kyc_review"), - "display_priority": 10, - "exposed": true - ), - ( - "operation_type": "DEPOSIT", - "rule_name": "suspicious_deposit", - "threshold": "EUR:50000", - "timeframe": ("d_us": 604800000000), - "measures": ("verboten"), - "display_priority": 20, - "exposed": false - ), - ( - "operation_type": "BALANCE", - "threshold": "EUR:5000", - "timeframe": ("d_us": 3600000000), - "measures": ("aml_check", "manager_approval"), - "display_priority": 5, - "exposed": true - ) - ) - ) -)) -\ No newline at end of file diff --git a/contrib/typst/_cover_/Makefile.am b/contrib/typst/_cover_/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/_cover_/0.0.0/ +typstpackage_DATA = \ + _cover_.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/_cover_/_cover_.typ b/contrib/typst/_cover_/_cover_.typ @@ -0,0 +1,266 @@ +// Cover page for AML files. +// Renders all account properties, current rules, etc. +#import "@taler-exchange/common:0.0.0": checkbox, talerlogo + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Helper function to get nice labels for standard properties + let get_property_label(key) = { + if key == "pep" { "Politically exposed person (PEP)" } + else if key == "sanctioned" { "Sanctioned account" } + else if key == "high_risk" { "High risk account" } + else if key == "business_domain" { "Business domain" } + else if key == "is_frozen" { "Account frozen" } + else if key == "was_reported" { "Reported to authorities" } + else { key } + } + + // Helper function to format timeframe + let format_timeframe(d_us) = { + if d_us == "forever" { + "forever" + } else { + let us = int(d_us) + let s = calc.quo(us, 1000000) + let m = calc.quo(s, 60) + let h = calc.quo(m, 60) + let d = calc.quo(h, 24) + + if calc.rem(us, 1000000) == 0 { + if calc.rem(s, 60) == 0 { + if calc.rem(m, 60) == 0 { + if calc.rem(h, 24) == 0 { + str(d) + " day" + if d != 1 { "s" } else { "" } + } else { + str(h) + " hour" + if h != 1 { "s" } else { "" } + } + } else { + str(m) + " minute" + if m != 1 { "s" } else { "" } + } + } else { + str(s) + " s" + } + } else { + str(us) + " μs" + } + } + } + + + // Helper function to format timestamp; ignores leap seconds (too variable) + let format_timestamp(ts) = { + if type(ts) == dictionary and "t_s" in ts { + let t_s = ts.t_s + if t_s == "never" { + "never" + } else { + // Convert Unix timestamp to human-readable format + let seconds = int(t_s) + let days_since_epoch = calc.quo(seconds, 86400) + let remaining_seconds = calc.rem(seconds, 86400) + let hours = calc.quo(remaining_seconds, 3600) + let minutes = calc.quo(calc.rem(remaining_seconds, 3600), 60) + let secs = calc.rem(remaining_seconds, 60) + + // Helper to check if year is leap year + let is_leap(y) = { + calc.rem(y, 4) == 0 and (calc.rem(y, 100) != 0 or calc.rem(y, 400) == 0) + } + + // Calculate year, month, day + let year = 1970 + let days_left = days_since_epoch + + // Find the year + let done = false + while not done { + let days_in_year = if is_leap(year) { 366 } else { 365 } + if days_left >= days_in_year { + days_left = days_left - days_in_year + year = year + 1 + } else { + done = true + } + } + + // Days in each month + let days_in_months = if is_leap(year) { + (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + } else { + (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + } + + // Find month and day + let month = 1 + for days_in_month in days_in_months { + if days_left >= days_in_month { + days_left = days_left - days_in_month + month = month + 1 + } else { + break + } + } + let day = days_left + 1 + + // Format with leading zeros + let m_str = if month < 10 { "0" + str(month) } else { str(month) } + let d_str = if day < 10 { "0" + str(day) } else { str(day) } + let h_str = if hours < 10 { "0" + str(hours) } else { str(hours) } + let min_str = if minutes < 10 { "0" + str(minutes) } else { str(minutes) } + let s_str = if secs < 10 { "0" + str(secs) } else { str(secs) } + + str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + ":" + s_str + " UTC" + } + } else { + str(ts) + } + } + + + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + talerlogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [AMLA File No.], [#get("FILE_NUMBER")], + [Account open?], [#checkbox(get("is_active"))], + [Active investigation?], [#checkbox(get("to_investigate"))], + ) + ] + ) + + v(1em) + + // Section 1: Properties + text(size: 11pt, weight: "bold")[Properties:] + + v(0.5em) + + block(breakable: false)[ + #v(0.5em) + #let props = get("properties", default: (:)) + #let standard_props = ("pep", "sanctioned", "high_risk", "business_domain", "is_frozen", "was_reported") + #let all_keys = props.keys() + + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + ..for key in all_keys { + let value = props.at(key) + let label = get_property_label(key) + + // Render based on value type + if type(value) == bool { + ([#label], [#checkbox(value)]) + } else { + ([#label], [#value]) + } + } + ) + #v(0.5em) + ] + + // Section 2: Rules + let rules_data = get("rules", default: none) + + if rules_data != none { + text(size: 11pt, weight: "bold")[ + Rules + #if "expiration_time" in rules_data { + [ (expires: #format_timestamp(rules_data.expiration_time))] + } + : + ] + + v(0.5em) + + let rules = rules_data.at("rules", default: ()) + + if rules.len() > 0 { + block(breakable: true)[ + #table( + columns: (17%, 13%, 13%, 20%, 17%, 10%, 10%), + stroke: 0.5pt + black, + inset: 4pt, + align: (left, left, left, left, left, center, center), + table.header( + [*Operation*], + [*Threshold*], + [*Timeframe*], + [*Rule Name*], + [*Measures*], + [*Exposed*], + [*Verboten*] + ), + ..for rule in rules { + let op_type = rule.at("operation_type", default: "") + let threshold = rule.at("threshold", default: "") + let timeframe_raw = rule.at("timeframe", default: (:)) + let timeframe = if "d_us" in timeframe_raw { + format_timeframe(timeframe_raw.d_us) + } else { "" } + let rule_name = rule.at("rule_name", default: "") + let measures = rule.at("measures", default: ()) + let exposed = rule.at("exposed", default: false) + let is_verboten = if type(measures) == array { "verboten" in measures } else { "verboten" == measures } + let measures_text = if type(measures) == array { + measures.filter(m => m != "verboten").map(m => str(m)).join(", ") + } else if measures != "verboten" { + str(measures) + } else { + "" + } + + ( + [#op_type], + [#threshold], + [#timeframe], + [#rule_name], + [#measures_text], + [#checkbox(exposed)], + [#checkbox(is_verboten)] + ) + } + ) + ] + } else { + text(style: "italic")[No rules defined.] + } + } +} diff --git a/contrib/typst/_cover_/template/main.typ b/contrib/typst/_cover_/template/main.typ @@ -0,0 +1,47 @@ +#import "@taler-exchange/_cover_:0.0.0": form + +#form(( + "FILE_NUMBER": "42", + "is_active": true, + "to_investigate": false, + "properties": ( + "pep": false, + "sanctioned": false, + "high_risk": true, + "business_domain": "Financial services", + "is_frozen": false, + "was_reported": false, + "custom_field": "Custom value" + ), + "rules": ( + "expiration_time": ("t_s": 1764967786), // Fri Dec 5 20:49:46 UTC 2025 + "rules": ( + ( + "operation_type": "WITHDRAW", + "rule_name": "large_withdrawal", + "threshold": "EUR:10000", + "timeframe": ("d_us": 86400000000), + "measures": ("kyc_review"), + "display_priority": 10, + "exposed": true + ), + ( + "operation_type": "DEPOSIT", + "rule_name": "suspicious_deposit", + "threshold": "EUR:50000", + "timeframe": ("d_us": 604800000000), + "measures": ("verboten"), + "display_priority": 20, + "exposed": false + ), + ( + "operation_type": "BALANCE", + "threshold": "EUR:5000", + "timeframe": ("d_us": 3600000000), + "measures": ("aml_check", "manager_approval"), + "display_priority": 5, + "exposed": true + ) + ) + ) +)) +\ No newline at end of file diff --git a/contrib/typst/_cover_/typst.toml b/contrib/typst/_cover_/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "_cover_" +version = "0.0.0" +entrypoint = "_cover_.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["AML", "KYC"] diff --git a/contrib/typst/accept-tos.typ b/contrib/typst/accept-tos.typ @@ -1,86 +0,0 @@ -// Form to render ToS acceptance form. - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("taler-logo.svg", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [AMLA File No.], - [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - // Section 1: Acceptance data - text(size: 11pt, weight: "bold")[Accepted terms of service:] - - v(0.5em) - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Version:], [#get("ACCEPTED_TERMS_OF_SERVICE")], - [Downloaded:], [#checkbox(get("DOWNLOADED_TERMS_OF_SERVICE"))], - ) - #v(0.5em) - ] -} - -// Example usage: -#form(( - "ACCEPTED_TERMS_OF_SERVICE": "v1", - "DOWNLOADED_TERMS_OF_SERVICE": true, -)) -\ No newline at end of file diff --git a/contrib/typst/accept-tos/Makefile.am b/contrib/typst/accept-tos/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/accept-tos/0.0.0/ +typstpackage_DATA = \ + accept-tos.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/accept-tos/accept-tos.typ b/contrib/typst/accept-tos/accept-tos.typ @@ -0,0 +1,68 @@ +// Form to render ToS acceptance form. +#import "@taler-exchange/common:0.0.0": talerlogo, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + talerlogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [AMLA File No.], + [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + // Section 1: Acceptance data + text(size: 11pt, weight: "bold")[Accepted terms of service:] + + v(0.5em) + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Version:], [#get("ACCEPTED_TERMS_OF_SERVICE")], + [Downloaded:], [#checkbox(get("DOWNLOADED_TERMS_OF_SERVICE"))], + ) + #v(0.5em) + ] +} diff --git a/contrib/typst/accept-tos/template/main.typ b/contrib/typst/accept-tos/template/main.typ @@ -0,0 +1,6 @@ +#import "@taler-exchange/accept-tos:0.0.0": form + +#form(( + "ACCEPTED_TERMS_OF_SERVICE": "v1", + "DOWNLOADED_TERMS_OF_SERVICE": true, +)) +\ No newline at end of file diff --git a/contrib/typst/accept-tos/typst.toml b/contrib/typst/accept-tos/typst.toml @@ -0,0 +1,15 @@ +[package] +name = "accept-tos" +version = "0.0.0" +entrypoint = "accept-tos.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Form for terms-of-service acceptance" +repository = "git://git.taler.net/exchange" +keywords = ["KYC"] + + + +[template] +path = "template" +entrypoint = "main.typ" diff --git a/contrib/typst/challenger-postal.typ b/contrib/typst/challenger-postal.typ @@ -1,91 +0,0 @@ -// Form to render postal addresses validated via Challenger. -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("taler-logo.svg", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [AMLA File No.], - [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - // Section 1: Validated address - text(size: 11pt, weight: "bold")[Validated address:] - - v(0.5em) - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Contact name:], [#get("CONTACT_NAME")], - [Address:], [#get("ADDRESS_LINES").split("\n").join(linebreak())], - [Country:], [#get("ADDRESS_COUNTRY")], - ) - #v(0.5em) - ] -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "CONTACT_NAME": "Bob Builder", - "ADDRESS_LINES" : "Parkallee 42\n12345 Bielefeld", - "ADDRESS_COUNTRY" : "Germany", -)) -\ No newline at end of file diff --git a/contrib/typst/challenger-postal/Makefile.am b/contrib/typst/challenger-postal/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/challenger-postal/0.0.0/ +typstpackage_DATA = \ + challenger-postal.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/challenger-postal/challenger-postal.typ b/contrib/typst/challenger-postal/challenger-postal.typ @@ -0,0 +1,70 @@ +// Form to render postal addresses validated via Challenger. +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": talerlogo + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + talerlogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [AMLA File No.], + [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + // Section 1: Validated address + text(size: 11pt, weight: "bold")[Validated address:] + + v(0.5em) + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Contact name:], [#get("CONTACT_NAME")], + [Address:], [#get("ADDRESS_LINES").split("\n").join(linebreak())], + [Country:], [#get("ADDRESS_COUNTRY")], + ) + #v(0.5em) + ] +} diff --git a/contrib/typst/challenger-postal/template/main.typ b/contrib/typst/challenger-postal/template/main.typ @@ -0,0 +1,9 @@ +#import "@taler-exchange/challenger-postal:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "CONTACT_NAME": "Bob Builder", + "ADDRESS_LINES" : "Parkallee 42\n12345 Bielefeld", + "ADDRESS_COUNTRY" : "Germany", +)) +\ No newline at end of file diff --git a/contrib/typst/challenger-postal/typst.toml b/contrib/typst/challenger-postal/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "challenger-postal" +version = "0.0.0" +entrypoint = "challenger-postal.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["KYC"] diff --git a/contrib/typst/challenger-sms.typ b/contrib/typst/challenger-sms.typ @@ -1,87 +0,0 @@ -// Form to render SMS (phone) addresses validated via Challenger. -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("taler-logo.svg", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [AMLA File No.], - [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - // Section 1: Validated phone number - text(size: 11pt, weight: "bold")[Validated phone number:] - - v(0.5em) - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Phone number:], [#get("CONTACT_PHONE")], - ) - #v(0.5em) - ] -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "CONTACT_PHONE": "+41123456789", -)) -\ No newline at end of file diff --git a/contrib/typst/challenger-sms/Makefile.am b/contrib/typst/challenger-sms/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/challenger-sms/0.0.0/ +typstpackage_DATA = \ + challenger-sms.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/challenger-sms/challenger-sms.typ b/contrib/typst/challenger-sms/challenger-sms.typ @@ -0,0 +1,68 @@ +// Form to render SMS (phone) addresses validated via Challenger. +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": talerlogo + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + talerlogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [AMLA File No.], + [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + // Section 1: Validated phone number + text(size: 11pt, weight: "bold")[Validated phone number:] + + v(0.5em) + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Phone number:], [#get("CONTACT_PHONE")], + ) + #v(0.5em) + ] +} diff --git a/contrib/typst/challenger-sms/template/main.typ b/contrib/typst/challenger-sms/template/main.typ @@ -0,0 +1,8 @@ +#import "@taler-exchange/challenger-sms:0.0.0": form + + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "CONTACT_PHONE": "+41123456789", +)) +\ No newline at end of file diff --git a/contrib/typst/challenger-sms/typst.toml b/contrib/typst/challenger-sms/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "challenger-sms" +version = "0.0.0" +entrypoint = "challenger-sms.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["KYC"] diff --git a/contrib/typst/common/Makefile.am b/contrib/typst/common/Makefile.am @@ -0,0 +1,12 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/common/0.0.0/ +typstpackage_DATA = \ + lib.typ \ + pointing_finger.svg \ + taler-logo.svg \ + typst.toml \ + vss_vqf_verein.png + +EXTRA_DIST = \ + $(typstpackage_DATA) diff --git a/contrib/typst/common/lib.typ b/contrib/typst/common/lib.typ @@ -0,0 +1,24 @@ +// Helper function for checkbox +#let checkbox(checked) = { + box( + width: 3mm, + height: 3mm, + stroke: 0.5pt + black, + inset: 0.3mm, + if checked == true or checked == "true" { + place(center + horizon, text(size: 8pt, sym.checkmark)) + } + ) +} + +#let talerlogo() = { + image("taler-logo.svg", width: 80%) +} + +#let vqflogo() = { + image("vss_vqf_verein.png", width: 80%) +} + +#let pointingfinger() = { + image("pointing_finger.svg", height: 2em) +} diff --git a/contrib/typst/pointing_finger.svg b/contrib/typst/common/pointing_finger.svg diff --git a/contrib/typst/taler-logo.svg b/contrib/typst/common/taler-logo.svg diff --git a/contrib/typst/common/typst.toml b/contrib/typst/common/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "common" +version = "0.0.0" +entrypoint = "lib.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vss_vqf_verein.png b/contrib/typst/common/vss_vqf_verein.png Binary files differ. diff --git a/contrib/typst/generic_note.typ b/contrib/typst/generic_note.typ @@ -1,78 +0,0 @@ -// Generic note form. - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("taler-logo.svg", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [AMLA File No.], - [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - // Section 1: Note - text(size: 11pt, weight: "bold")[Note:] - - v(0.5em) - - get("NOTE_TEXT") - - v(0.5em) -} - -// Example usage: -#form(( - "GENERIC_NOTE": "This is a great customer.", - "FILE_NUMBER": 42, -)) -\ No newline at end of file diff --git a/contrib/typst/generic_note/Makefile.am b/contrib/typst/generic_note/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/generic_note/0.0.0/ +typstpackage_DATA = \ + generic_note.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/generic_note/generic_note.typ b/contrib/typst/generic_note/generic_note.typ @@ -0,0 +1,61 @@ +// Generic note form. +#import "@taler-exchange/common:0.0.0": talerlogo, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + // image("taler-logo.svg", width: 80%), + talerlogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [AMLA File No.], + [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + // Section 1: Note + text(size: 11pt, weight: "bold")[Note:] + + v(0.5em) + + get("NOTE_TEXT") + + v(0.5em) +} diff --git a/contrib/typst/generic_note/template/main.typ b/contrib/typst/generic_note/template/main.typ @@ -0,0 +1,6 @@ +#import "@taler-exchange/generic_note:0.0.0": form + +#form(( + "GENERIC_NOTE": "This is a great customer.", + "FILE_NUMBER": 42, +)) +\ No newline at end of file diff --git a/contrib/typst/generic_note/typst.toml b/contrib/typst/generic_note/typst.toml @@ -0,0 +1,14 @@ +[package] +name = "generic_note" +version = "0.0.0" +entrypoint = "generic_note.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] + + +[template] +path = "template" +entrypoint = "main.typ" diff --git a/contrib/typst/generic_upload.typ b/contrib/typst/generic_upload.typ @@ -1,87 +0,0 @@ -// Generic note form. - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("taler-logo.svg", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [AMLA File No.], - [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - // Section 1: File upload with note - text(size: 11pt, weight: "bold")[File upload with note:] - - v(0.5em) - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Note:], [#get("NOTE_TEXT")], - [Filename:], [#get("FILE").at("FILENAME")], - ) - #v(0.5em) - ] -} - -// Example usage: -#form(( - "NOTE_TEXT": "This is a great customer.", - "FILE" : ( "FILENAME":"test.txt", "CONTENTS":"..."), - "FILE_NUMBER": 42, -)) -\ No newline at end of file diff --git a/contrib/typst/generic_upload/Makefile.am b/contrib/typst/generic_upload/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/generic_upload/0.0.0/ +typstpackage_DATA = \ + generic_upload.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/generic_upload/generic_upload.typ b/contrib/typst/generic_upload/generic_upload.typ @@ -0,0 +1,68 @@ +// Generic upload meta data form. +#import "@taler-exchange/common:0.0.0": talerlogo + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + talerlogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [AMLA File No.], + [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + // Section 1: File upload with note + text(size: 11pt, weight: "bold")[File upload with note:] + + v(0.5em) + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Note:], [#get("NOTE_TEXT")], + [Filename:], [#get("FILE").at("FILENAME")], + ) + #v(0.5em) + ] +} diff --git a/contrib/typst/generic_upload/template/main.typ b/contrib/typst/generic_upload/template/main.typ @@ -0,0 +1,7 @@ +#import "@taler-exchange/generic_upload:0.0.0": form + +#form(( + "NOTE_TEXT": "This is a great customer.", + "FILE" : ( "FILENAME":"test.txt", "CONTENTS":"..."), + "FILE_NUMBER": 42, +)) +\ No newline at end of file diff --git a/contrib/typst/generic_upload/typst.toml b/contrib/typst/generic_upload/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "generic_upload" +version = "0.0.0" +entrypoint = "generic_upload.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["AML", "KYC"] diff --git a/contrib/typst/vqf_902_1.typ b/contrib/typst/vqf_902_1.typ @@ -1,457 +0,0 @@ -// VQF 902.1 Identification Form Template -// Pass JSON data as content dictionary -// NOTE: This is the original form. We don't use -// it as this form was split in customer + officer parts. -// Preserved here in case some auditor insists on us -// combining the two! - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.1#linebreak() - Version of 1 September 2021 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function to get value or false - let getb(key, default: false) = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Identification Form]) - - v(-1em) - line(length:100%) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold. - ] - ) - - v(1em) - - text(weight: "bold")[This form was completed by:] - - v(0.3em) - - table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [Full name], [#get("FILED_BY_NAME")], - [Date], [#get("FILING_DATE")], - ) - - v(1.5em) - - // Section 1: Information on customer - text(size: 11pt, weight: "bold")[1. Information on customer#footnote[The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.]] - - v(0.5em) - - let is_natural = get("CUSTOMER_TYPE") == "NATURAL_PERSON" - let is_legal = get("CUSTOMER_TYPE") == "LEGAL_ENTITY" - - // Section 1a: Natural Person - grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(is_natural), - [a) The customer is a #underline([natural]) person:] - ) - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name\*], if is_natural { get("FULL_NAME") } else { "" }, - [Residential address\*], if is_natural { get("DOMICILE_ADDRESS") } else { "" }, - [Telephone], if is_natural { get("CONTACT_PHONE") } else { "" }, - [E-mail], if is_natural { get("CONTACT_EMAIL") } else { "" }, - [Date of birth\*], if is_natural { get("DATE_OF_BIRTH") } else { "" }, - [Nationality\*], if is_natural { get("NATIONALITY") } else { "" }, - [Identification document\*], [#checkbox(is_natural) *Copy enclosed in appendix*], - ) - #v(-1em) - #text(size: 8pt)[*\* mandatory*] - ] - v(1em) - - // Sole proprietorship section - text(weight: "bold")[For sole proprietorship (supplement to above):] - - let is_sole = is_natural and (get("CUSTOMER_IS_SOLE_PROPRIETOR") == true or get("CUSTOMER_IS_SOLE_PROPRIETOR") == "true") - - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Company name], if is_sole { get("COMPANY_NAME") } else { "" }, - [Registered office], if is_sole { get("REGISTERED_OFFICE_ADDRESS") } else { "" }, - [Company identification document], [#checkbox(is_sole) *Copy enclosed in appendix*], - ) - - v(1em) - - // Section 1b: Legal Entity - grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(is_legal), - [*b) The customer is a legal entity:*] - ) - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Company name\*], if is_legal { get("COMPANY_NAME") } else { "" }, - [Domicile\*], if is_legal { get("DOMICILE_ADDRESS") } else { "" }, - [Contact person], if is_legal { get("CONTACT_PERSON_NAME") } else { "" }, - [Telephone], if is_legal { get("CONTACT_PHONE") } else { "" }, - [E-mail], if is_legal { get("CONTACT_EMAIL") } else { "" }, - [Identification document\*\ (not older than 12 months)], [#checkbox(is_legal) *Copy enclosed in appendix*], - ) - - #v(-1em) - #text(size: 8pt)[*\* mandatory*] - ] - v(0.5em) - - // Section 2: Natural persons establishing business relationship - text(size: 11pt, weight: "bold")[2. Information on the natural persons who establish the business relationship for legal entities and partnerships] - - v(0.5em) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified. - ] - ) - - v(1em) - - let establishers = get("ESTABLISHER_LIST", default: ()) - let has_establishers = is_legal and type(establishers) == array and establishers.len() > 0 - - // Show at least 1 table for establishers to match VQF form - let num_cols = if has_establishers { calc.max(1, establishers.len()) } else { 1 } - - for col_idx in range(num_cols) { - if col_idx > 0 { - h(2em) - } - } - - // Create a table for each establisher - range(num_cols).map(col_idx => { - let establisher = if has_establishers and col_idx < establishers.len() { - establishers.at(col_idx) - } else { - (:) - } - - let get_est(key) = { - if establisher != (:) { - establisher.at(key, default: "") - } else { - "" - } - } - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name\*], - [#get_est("FULL_NAME")], - [Residential address\*], - [#get_est("DOMICILE_ADDRESS")], - [Date of birth\*], - [#get_est("DATE_OF_BIRTH")], - [Nationality\*], - [#get_est("NATIONALITY")], - [Type of authorisation\ (signatory of representation)\*], - [#get_est("SIGNING_AUTHORITY_TYPE")], - [Identification document\*], - [#checkbox(establisher != (:)) *Copy enclosed in appendix*], - [*Power of attorney arrangements\**], - [#let evidence = get_est("SIGNING_AUTHORITY_EVIDENCE") - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.3em, - checkbox(evidence == "CR"), - [CR extract], - checkbox(evidence == "MANDATE"), - [Mandate], - checkbox(evidence == "OTHER"), - [Other: #get_est("SIGNING_AUTHORITY_EVIDENCE_OTHER")], - ) - ] - ) - #v(-1em) - #text(size: 8pt)[*\* mandatory*] - ] - }).join() - - - - v(2em) - - // Section 3: Acceptance of business relationship - text(size: 11pt, weight: "bold")[3. Acceptance of business relationship] - - v(0.5em) - - let acceptance = get("ACCEPTANCE_METHOD") - let lang = get("CORRESPONDENCE_LANGUAGE") - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Date (conclusion of contract)], get("ACCEPTANCE_DATE"), - [Accepted by], - [#grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(acceptance == "FACE_TO_FACE"), - [Face-to-face meeting with customer], - checkbox(acceptance == "WAY_OF_CORRESPONDENCE"), - [Way of correspondence: - \ #v(-0.7em) #grid( - columns: (0.2em, auto), - gutter: 0.5em, - row-gutter: 0.3em, - [], - [#checkbox(acceptance == "AUTHENTICATED_COPY") - authenticated copy of identification document obtained], - [], - [#checkbox(acceptance == "RESIDENTIAL_ADDRESS_VALIDATED") - residential address validated] - ) - ] - )], - [Type of correspondence service], - [#grid( - columns: (auto), - gutter: 0.2em, - [#checkbox(true) to the customer], - [#checkbox(false) hold at bank], - [#checkbox(false) to the member], - [#checkbox(false) - to a third party (full name and address):], - )], - [Language], - [#grid( - columns: (auto), - gutter: 0.2em, - [#checkbox(lang == "de") German], - [#checkbox(lang == "en") English], - [#checkbox(lang == "fr") French], - [#checkbox(lang != "fr" and lang != "de" and lang != "en") - Other: #lang], - )], - [Further information], - [#get("ACCEPTANCE_FURTHER_INFO")] - ) - - // Section 4: Beneficial owner - text(size: 11pt, weight: "bold")[4. Information on the beneficial owner of the assets and/or controlling person] - - v(0.5em) - - let customer_type_vqf = get("CUSTOMER_TYPE_VQF") - grid( - columns: (35%,65%), - stroke: 0.5pt + black, - inset: 5pt, - [Establishment of the beneficial owner of the assets and/or controlling person.], - [The customer is: - #grid( - columns: (1.1em, auto), - inset: 5pt, - [#checkbox(customer_type_vqf == "NATURAL_PERSON")], - [a natural person and there are no doubts that this person is the sole beneficial owner of the assets], - [#checkbox(customer_type_vqf == "OPERATIONAL")], - [an operational legal entity or partnership #h(1fr) - \ $=>$ VQF doc. No. 902.11 (K)], - [#checkbox(customer_type_vqf == "FOUNDATION")], - [a foundation (or a similar construct; incl. underlying companies) - \ $=>$ VQF doc. No. 902.12 (S)], - [#checkbox(customer_type_vqf == "TRUST")], - [a trust (incl. underlying companies) - \ $=>$ VQF doc. No. 902.13 (T)], - [#checkbox(customer_type_vqf == "LIFE_INSURANCE")], - [a life insurance policy with separately managed accounts/ securities accounts (so-called insurance wrappers) - \ $=>$ VQF doc. No. 902.15 (I)], - [#checkbox(customer_type_vqf == "OTHER")], - [all other cases - $=>$ VQF doc. No. 902.9 (A)], - )] - ) - - v(2em) - - // Section 5: Embargo/terrorism evaluation - text(size: 11pt, weight: "bold")[5. Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship] - - v(0.5em) - - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Verification whether the customer, beneficial owners of the assets, controlling persons, authorised representatives or other involved persons are listed on an embargo-/terrorism list (date of verification/result)#footnote[The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.]], - [Date: #get("EMBARGO_TERRORISM_CHECK_DATE") - \ Result: #get("EMBARGO_TERRORISM_CHECK_RESULT")], - ) - - v(2em) - - // Section 6: Cash transactions - text(size: 11pt, weight: "bold")[6. In the case of cash transactions/occasional customers: Information on type and purpose of business relationship] - - v(0.5em) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that #underline([no]) customer profile (VQF doc. No. 902.5) is created - ] - ) - - v(1em) - - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Type of business relationship], - [#grid( - columns: (auto), - gutter: 0.2em, - [#checkbox(false) Money exchange], - [#checkbox(false) Money and asset transfer], - [#checkbox(false) Other cash transaction, specify?], - )], - [Purpose of the business relationship\ (purpose of service requested)], [], - ) - - // Section 7: Enclosures - text(size: 11pt, weight: "bold")[7. Enclosures] - - v(0.5em) - - grid( - columns: (auto, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - checkbox("" != get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")), [Customer identification documents (or: reference#footnote[If the identification document is lists kept in another AMLA-File (in the case of Art. 15 para. 3 SRO Regulations) a reference to the according AMLA-File is sufficient.] to AMLA File No.: #underline([#get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")]))], - checkbox("" != get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")), [Identification document of persons establishing the business relationship (or: reference to AMLA File No.: #underline([#get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")]))], - checkbox(getb("HAVE_vqf_902_9") or getb("HAVE_vqf_902_11_customer") or getb("HAVE_vqf_902_11_officer") or getb("HAVE_vqf_902_12") or getb("HAVE_vqf_902_13") or getb("HAVE_vqf_902_15")), [Establishing of the beneficial owner of the assets/controlling person (VQF Doc No. 902.15, 902.9, 902.11, 902.12 or 902.13)], - checkbox(getb("HAVE_vqf_902_5")), [Customer profile (VQF doc. No. 902.5; only in the case of permanent business relationship and regular customers)], - checkbox(getb("HAVE_vqf_902_4")), [Risk profile (VQF doc. No. 902.4)], - ) - - v(1em) - - text(size: 9pt, style: "italic")[⚠ *This form has to be updated when changes occur.*] - -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "FILED_BY_NAME": "Manuela", - "AML_STAFF_NAME": "Manuela", - "CUSTOMER_ID_AMLA_FILE_REFERENCE_NO": "4242", // optional - "ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO": "4243", // optional - "FILING_DATE": "1.4.2000", - "CUSTOMER_TYPE": "NATURAL_PERSON", - "FULL_NAME": "John Doe", - "DOMICILE_ADDRESS": "123 Main St, 8001 Zurich", - "HAVE_vqf_902_9": false, - "HAVE_vqf_902_11_customer": false, - "HAVE_vqf_902_11_officer": false, - "HAVE_vqf_902_12": false, - "HAVE_vqf_902_13": false, - "HAVE_vqf_902_15": true, - "HAVE_vqf_902_4": true, - "HAVE_vqf_902_5": true, - // ... other fields - )) diff --git a/contrib/typst/vqf_902_1/Makefile.am b/contrib/typst/vqf_902_1/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_1/0.0.0/ +typstpackage_DATA = \ + vqf_902_1.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_1/template/main.typ b/contrib/typst/vqf_902_1/template/main.typ @@ -0,0 +1,23 @@ +#import "@taler-exchange/vqf_902_1:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "FILED_BY_NAME": "Manuela", + "AML_STAFF_NAME": "Manuela", + "CUSTOMER_ID_AMLA_FILE_REFERENCE_NO": "4242", // optional + "ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO": "4243", // optional + "FILING_DATE": "1.4.2000", + "CUSTOMER_TYPE": "NATURAL_PERSON", + "FULL_NAME": "John Doe", + "DOMICILE_ADDRESS": "123 Main St, 8001 Zurich", + "HAVE_vqf_902_9": false, + "HAVE_vqf_902_11_customer": false, + "HAVE_vqf_902_11_officer": false, + "HAVE_vqf_902_12": false, + "HAVE_vqf_902_13": false, + "HAVE_vqf_902_15": true, + "HAVE_vqf_902_4": true, + "HAVE_vqf_902_5": true, + // ... other fields + )) diff --git a/contrib/typst/vqf_902_1/typst.toml b/contrib/typst/vqf_902_1/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_1" +version = "0.0.0" +entrypoint = "vqf_902_1.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "KYC"] diff --git a/contrib/typst/vqf_902_1/vqf_902_1.typ b/contrib/typst/vqf_902_1/vqf_902_1.typ @@ -0,0 +1,422 @@ +// VQF 902.1 Identification Form Template +// Pass JSON data as content dictionary +// NOTE: This is the original form. We don't use +// it as this form was split in customer + officer parts. +// Preserved here in case some auditor insists on us +// combining the two! +#import "@taler-exchange/common:0.0.0": vqflogo, pointingfinger, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.1#linebreak() + Version of 1 September 2021 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Helper function to get value or false + let getb(key, default: false) = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Identification Form]) + + v(-1em) + line(length:100%) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold. + ] + ) + + v(1em) + + text(weight: "bold")[This form was completed by:] + + v(0.3em) + + table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [Full name], [#get("FILED_BY_NAME")], + [Date], [#get("FILING_DATE")], + ) + + v(1.5em) + + // Section 1: Information on customer + text(size: 11pt, weight: "bold")[1. Information on customer#footnote[The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.]] + + v(0.5em) + + let is_natural = get("CUSTOMER_TYPE") == "NATURAL_PERSON" + let is_legal = get("CUSTOMER_TYPE") == "LEGAL_ENTITY" + + // Section 1a: Natural Person + grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(is_natural), + [a) The customer is a #underline([natural]) person:] + ) + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name\*], if is_natural { get("FULL_NAME") } else { "" }, + [Residential address\*], if is_natural { get("DOMICILE_ADDRESS") } else { "" }, + [Telephone], if is_natural { get("CONTACT_PHONE") } else { "" }, + [E-mail], if is_natural { get("CONTACT_EMAIL") } else { "" }, + [Date of birth\*], if is_natural { get("DATE_OF_BIRTH") } else { "" }, + [Nationality\*], if is_natural { get("NATIONALITY") } else { "" }, + [Identification document\*], [#checkbox(is_natural) *Copy enclosed in appendix*], + ) + #v(-1em) + #text(size: 8pt)[*\* mandatory*] + ] + v(1em) + + // Sole proprietorship section + text(weight: "bold")[For sole proprietorship (supplement to above):] + + let is_sole = is_natural and (get("CUSTOMER_IS_SOLE_PROPRIETOR") == true or get("CUSTOMER_IS_SOLE_PROPRIETOR") == "true") + + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Company name], if is_sole { get("COMPANY_NAME") } else { "" }, + [Registered office], if is_sole { get("REGISTERED_OFFICE_ADDRESS") } else { "" }, + [Company identification document], [#checkbox(is_sole) *Copy enclosed in appendix*], + ) + + v(1em) + + // Section 1b: Legal Entity + grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(is_legal), + [*b) The customer is a legal entity:*] + ) + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Company name\*], if is_legal { get("COMPANY_NAME") } else { "" }, + [Domicile\*], if is_legal { get("DOMICILE_ADDRESS") } else { "" }, + [Contact person], if is_legal { get("CONTACT_PERSON_NAME") } else { "" }, + [Telephone], if is_legal { get("CONTACT_PHONE") } else { "" }, + [E-mail], if is_legal { get("CONTACT_EMAIL") } else { "" }, + [Identification document\*\ (not older than 12 months)], [#checkbox(is_legal) *Copy enclosed in appendix*], + ) + + #v(-1em) + #text(size: 8pt)[*\* mandatory*] + ] + v(0.5em) + + // Section 2: Natural persons establishing business relationship + text(size: 11pt, weight: "bold")[2. Information on the natural persons who establish the business relationship for legal entities and partnerships] + + v(0.5em) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified. + ] + ) + + v(1em) + + let establishers = get("ESTABLISHER_LIST", default: ()) + let has_establishers = is_legal and type(establishers) == array and establishers.len() > 0 + + // Show at least 1 table for establishers to match VQF form + let num_cols = if has_establishers { calc.max(1, establishers.len()) } else { 1 } + + for col_idx in range(num_cols) { + if col_idx > 0 { + h(2em) + } + } + + // Create a table for each establisher + range(num_cols).map(col_idx => { + let establisher = if has_establishers and col_idx < establishers.len() { + establishers.at(col_idx) + } else { + (:) + } + + let get_est(key) = { + if establisher != (:) { + establisher.at(key, default: "") + } else { + "" + } + } + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name\*], + [#get_est("FULL_NAME")], + [Residential address\*], + [#get_est("DOMICILE_ADDRESS")], + [Date of birth\*], + [#get_est("DATE_OF_BIRTH")], + [Nationality\*], + [#get_est("NATIONALITY")], + [Type of authorisation\ (signatory of representation)\*], + [#get_est("SIGNING_AUTHORITY_TYPE")], + [Identification document\*], + [#checkbox(establisher != (:)) *Copy enclosed in appendix*], + [*Power of attorney arrangements\**], + [#let evidence = get_est("SIGNING_AUTHORITY_EVIDENCE") + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.3em, + checkbox(evidence == "CR"), + [CR extract], + checkbox(evidence == "MANDATE"), + [Mandate], + checkbox(evidence == "OTHER"), + [Other: #get_est("SIGNING_AUTHORITY_EVIDENCE_OTHER")], + ) + ] + ) + #v(-1em) + #text(size: 8pt)[*\* mandatory*] + ] + }).join() + + + + v(2em) + + // Section 3: Acceptance of business relationship + text(size: 11pt, weight: "bold")[3. Acceptance of business relationship] + + v(0.5em) + + let acceptance = get("ACCEPTANCE_METHOD") + let lang = get("CORRESPONDENCE_LANGUAGE") + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Date (conclusion of contract)], get("ACCEPTANCE_DATE"), + [Accepted by], + [#grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(acceptance == "FACE_TO_FACE"), + [Face-to-face meeting with customer], + checkbox(acceptance == "WAY_OF_CORRESPONDENCE"), + [Way of correspondence: + \ #v(-0.7em) #grid( + columns: (0.2em, auto), + gutter: 0.5em, + row-gutter: 0.3em, + [], + [#checkbox(acceptance == "AUTHENTICATED_COPY") + authenticated copy of identification document obtained], + [], + [#checkbox(acceptance == "RESIDENTIAL_ADDRESS_VALIDATED") + residential address validated] + ) + ] + )], + [Type of correspondence service], + [#grid( + columns: (auto), + gutter: 0.2em, + [#checkbox(true) to the customer], + [#checkbox(false) hold at bank], + [#checkbox(false) to the member], + [#checkbox(false) + to a third party (full name and address):], + )], + [Language], + [#grid( + columns: (auto), + gutter: 0.2em, + [#checkbox(lang == "de") German], + [#checkbox(lang == "en") English], + [#checkbox(lang == "fr") French], + [#checkbox(lang != "fr" and lang != "de" and lang != "en") + Other: #lang], + )], + [Further information], + [#get("ACCEPTANCE_FURTHER_INFO")] + ) + + // Section 4: Beneficial owner + text(size: 11pt, weight: "bold")[4. Information on the beneficial owner of the assets and/or controlling person] + + v(0.5em) + + let customer_type_vqf = get("CUSTOMER_TYPE_VQF") + grid( + columns: (35%,65%), + stroke: 0.5pt + black, + inset: 5pt, + [Establishment of the beneficial owner of the assets and/or controlling person.], + [The customer is: + #grid( + columns: (1.1em, auto), + inset: 5pt, + [#checkbox(customer_type_vqf == "NATURAL_PERSON")], + [a natural person and there are no doubts that this person is the sole beneficial owner of the assets], + [#checkbox(customer_type_vqf == "OPERATIONAL")], + [an operational legal entity or partnership #h(1fr) + \ $=>$ VQF doc. No. 902.11 (K)], + [#checkbox(customer_type_vqf == "FOUNDATION")], + [a foundation (or a similar construct; incl. underlying companies) + \ $=>$ VQF doc. No. 902.12 (S)], + [#checkbox(customer_type_vqf == "TRUST")], + [a trust (incl. underlying companies) + \ $=>$ VQF doc. No. 902.13 (T)], + [#checkbox(customer_type_vqf == "LIFE_INSURANCE")], + [a life insurance policy with separately managed accounts/ securities accounts (so-called insurance wrappers) + \ $=>$ VQF doc. No. 902.15 (I)], + [#checkbox(customer_type_vqf == "OTHER")], + [all other cases + $=>$ VQF doc. No. 902.9 (A)], + )] + ) + + v(2em) + + // Section 5: Embargo/terrorism evaluation + text(size: 11pt, weight: "bold")[5. Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship] + + v(0.5em) + + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Verification whether the customer, beneficial owners of the assets, controlling persons, authorised representatives or other involved persons are listed on an embargo-/terrorism list (date of verification/result)#footnote[The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.]], + [Date: #get("EMBARGO_TERRORISM_CHECK_DATE") + \ Result: #get("EMBARGO_TERRORISM_CHECK_RESULT")], + ) + + v(2em) + + // Section 6: Cash transactions + text(size: 11pt, weight: "bold")[6. In the case of cash transactions/occasional customers: Information on type and purpose of business relationship] + + v(0.5em) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that #underline([no]) customer profile (VQF doc. No. 902.5) is created + ] + ) + + v(1em) + + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Type of business relationship], + [#grid( + columns: (auto), + gutter: 0.2em, + [#checkbox(false) Money exchange], + [#checkbox(false) Money and asset transfer], + [#checkbox(false) Other cash transaction, specify?], + )], + [Purpose of the business relationship\ (purpose of service requested)], [], + ) + + // Section 7: Enclosures + text(size: 11pt, weight: "bold")[7. Enclosures] + + v(0.5em) + + grid( + columns: (auto, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + checkbox("" != get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")), [Customer identification documents (or: reference#footnote[If the identification document is lists kept in another AMLA-File (in the case of Art. 15 para. 3 SRO Regulations) a reference to the according AMLA-File is sufficient.] to AMLA File No.: #underline([#get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")]))], + checkbox("" != get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")), [Identification document of persons establishing the business relationship (or: reference to AMLA File No.: #underline([#get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")]))], + checkbox(getb("HAVE_vqf_902_9") or getb("HAVE_vqf_902_11_customer") or getb("HAVE_vqf_902_11_officer") or getb("HAVE_vqf_902_12") or getb("HAVE_vqf_902_13") or getb("HAVE_vqf_902_15")), [Establishing of the beneficial owner of the assets/controlling person (VQF Doc No. 902.15, 902.9, 902.11, 902.12 or 902.13)], + checkbox(getb("HAVE_vqf_902_5")), [Customer profile (VQF doc. No. 902.5; only in the case of permanent business relationship and regular customers)], + checkbox(getb("HAVE_vqf_902_4")), [Risk profile (VQF doc. No. 902.4)], + ) + + v(1em) + + text(size: 9pt, style: "italic")[⚠ *This form has to be updated when changes occur.*] + +} diff --git a/contrib/typst/vqf_902_11.typ b/contrib/typst/vqf_902_11.typ @@ -1,247 +0,0 @@ -// VQF 902.11 Establishing of the controlling person (K) -// Pass JSON data as content dictionary -// NOTE: This is the original form. We don't use -// it as this form was split in customer + officer parts. -// Preserved here in case some auditor insists on us -// combining the two! - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.11#linebreak() - Version of 1 December 2015 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange (K)]) - - v(0.3em) - - text(size: 9pt, style: "italic")[ - (for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners) - ] - - v(-1em) - line(length:100%) - - v(1em) - - // Section 1: Contracting Partner - text(size: 11pt, weight: "bold")[Contracting partner:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("IDENTITY_CONTRACTING_PARTNER")] - ) - - v(1em) - - // Section 2: Declaration - let control_reason = get("CONTROL_REASON") - - text()[The contracting partner hereby declares that (tick the appropriate box):] - - block(breakable: false)[ - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.8em, - checkbox(control_reason == "HAS_25_MORE_RIGHTS"), - [the person(s) listed below is/are *holding 25% or more of the contracting partner's shares (capital shares or voting rights)*; or], - - checkbox(control_reason == "OTHER_WAY"), - [if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below *is/are controlling the contracting partner in other ways*; or], - - checkbox(control_reason == "DIRECTOR"), - [in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the *managing director(s)*] - ) - ] - - v(1em) - - // Section 3: Controlling Persons - let persons = get("IDENTITY_LIST", default: ()) - let has_persons = type(persons) == array and persons.len() > 0 - - if has_persons { - for person in persons { - let get_person(key) = { - person.at(key, default: "") - } - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name:], [#get_person("FULL_NAME")], - [Actual address of domicile:], [#get_person("DOMICILE_ADDRESS")] - ) - #v(0.5em) - ] - } - } else { - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Last name(s):], [], - [First name(s):], [], - [Actual address of domicile:], [] - ) - #v(0.5em) - ] - } - - v(1.5em) - - // Section 4: Fiduciary Holding - text(size: 11pt, weight: "bold")[Fiduciary holding assets] - - v(0.5em) - - let third_party = get("THIRD_PARTY_OWNERSHIP") - - block(breakable: false)[ - #text()[Is a third person the beneficial owner of the assets held in the account/securities account?] - - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.5em, - checkbox(not third_party), [No.], - checkbox(third_party), [Yes. $=>$ The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9.] - ) - ] - - v(0.5em) - - text()[The contracting partner hereby undertakes to automatically inform of any changes to the information contained herein.] - - v(0.5em) - - // Signature Section - let submitted_by_officer = get("BY_AML_OFFICER") - - if submitted_by_officer == false { - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date:], - [], - [Signature(s):], - [#get("SIGN_DATE")], - [], - [#get("SIGNATURE")] - ) - - v(1em) - - text(size: 9pt, style: "italic")[ - It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery). - ] - } else if submitted_by_officer == true { - text(weight: "bold")[Signed declaration by the customer] - - v(0.5em) - - text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] - - v(0.5em) - - text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [Signed Document:], - [#if get("ATTACHMENT_SIGNED_DOCUMENT") != "" [Document attached] else [No document]] - ) - } else { - text(weight: "bold")[Invalid submitter (#submitted_by_officer)] - } -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", - "CONTROL_REASON": "HAS_25_MORE_RIGHTS", - "IDENTITY_LIST": ( - ( - "FULL_NAME": "Jane Smith", - "DOMICILE_ADDRESS": "Teststrasse 456\n8001 Zurich" - ), - ), - "THIRD_PARTY_OWNERSHIP": false, - "BY_AML_OFFICER": false, - "SIGNATURE": "Jane Smith", - "SIGN_DATE": "10.11.2025", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_11/Makefile.am b/contrib/typst/vqf_902_11/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_11/0.0.0/ +typstpackage_DATA = \ + vqf_902_11.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_11/template/main.typ b/contrib/typst/vqf_902_11/template/main.typ @@ -0,0 +1,18 @@ +#import "@taler-exchange/vqf_902_11:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", + "CONTROL_REASON": "HAS_25_MORE_RIGHTS", + "IDENTITY_LIST": ( + ( + "FULL_NAME": "Jane Smith", + "DOMICILE_ADDRESS": "Teststrasse 456\n8001 Zurich" + ), + ), + "THIRD_PARTY_OWNERSHIP": false, + "BY_AML_OFFICER": false, + "SIGNATURE": "Jane Smith", + "SIGN_DATE": "10.11.2025", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_11/typst.toml b/contrib/typst/vqf_902_11/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_11" +version = "0.0.0" +entrypoint = "vqf_902_11.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_11/vqf_902_11.typ b/contrib/typst/vqf_902_11/vqf_902_11.typ @@ -0,0 +1,217 @@ +// VQF 902.11 Establishing of the controlling person (K) +// Pass JSON data as content dictionary +// NOTE: This is the original form. We don't use +// it as this form was split in customer + officer parts. +// Preserved here in case some auditor insists on us +// combining the two! +#import "@taler-exchange/common:0.0.0": vqflogo, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.11#linebreak() + Version of 1 December 2015 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange (K)]) + + v(0.3em) + + text(size: 9pt, style: "italic")[ + (for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners) + ] + + v(-1em) + line(length:100%) + + v(1em) + + // Section 1: Contracting Partner + text(size: 11pt, weight: "bold")[Contracting partner:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("IDENTITY_CONTRACTING_PARTNER")] + ) + + v(1em) + + // Section 2: Declaration + let control_reason = get("CONTROL_REASON") + + text()[The contracting partner hereby declares that (tick the appropriate box):] + + block(breakable: false)[ + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.8em, + checkbox(control_reason == "HAS_25_MORE_RIGHTS"), + [the person(s) listed below is/are *holding 25% or more of the contracting partner's shares (capital shares or voting rights)*; or], + + checkbox(control_reason == "OTHER_WAY"), + [if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below *is/are controlling the contracting partner in other ways*; or], + + checkbox(control_reason == "DIRECTOR"), + [in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the *managing director(s)*] + ) + ] + + v(1em) + + // Section 3: Controlling Persons + let persons = get("IDENTITY_LIST", default: ()) + let has_persons = type(persons) == array and persons.len() > 0 + + if has_persons { + for person in persons { + let get_person(key) = { + person.at(key, default: "") + } + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name:], [#get_person("FULL_NAME")], + [Actual address of domicile:], [#get_person("DOMICILE_ADDRESS")] + ) + #v(0.5em) + ] + } + } else { + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Last name(s):], [], + [First name(s):], [], + [Actual address of domicile:], [] + ) + #v(0.5em) + ] + } + + v(1.5em) + + // Section 4: Fiduciary Holding + text(size: 11pt, weight: "bold")[Fiduciary holding assets] + + v(0.5em) + + let third_party = get("THIRD_PARTY_OWNERSHIP") + + block(breakable: false)[ + #text()[Is a third person the beneficial owner of the assets held in the account/securities account?] + + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.5em, + checkbox(not third_party), [No.], + checkbox(third_party), [Yes. $=>$ The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9.] + ) + ] + + v(0.5em) + + text()[The contracting partner hereby undertakes to automatically inform of any changes to the information contained herein.] + + v(0.5em) + + // Signature Section + let submitted_by_officer = get("BY_AML_OFFICER") + + if submitted_by_officer == false { + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date:], + [], + [Signature(s):], + [#get("SIGN_DATE")], + [], + [#get("SIGNATURE")] + ) + + v(1em) + + text(size: 9pt, style: "italic")[ + It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery). + ] + } else if submitted_by_officer == true { + text(weight: "bold")[Signed declaration by the customer] + + v(0.5em) + + text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] + + v(0.5em) + + text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [Signed Document:], + [#if get("ATTACHMENT_SIGNED_DOCUMENT") != "" [Document attached] else [No document]] + ) + } else { + text(weight: "bold")[Invalid submitter (#submitted_by_officer)] + } +} diff --git a/contrib/typst/vqf_902_11_customer.typ b/contrib/typst/vqf_902_11_customer.typ @@ -1,243 +0,0 @@ -// VQF 902.11 Establishing of the controlling person (K) -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.11#linebreak() - Version of 1 December 2015 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange (K)]) - - v(0.3em) - - text(size: 9pt, style: "italic")[ - (for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners) - ] - - v(-1em) - line(length:100%) - - v(1em) - - // Section 1: Contracting Partner - text(size: 11pt, weight: "bold")[Contracting partner:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("IDENTITY_CONTRACTING_PARTNER")] - ) - - v(1em) - - // Section 2: Declaration - let control_reason = get("CONTROL_REASON") - - text()[The contracting partner hereby declares that (tick the appropriate box):] - - block(breakable: false)[ - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.8em, - checkbox(control_reason == "HAS_25_MORE_RIGHTS"), - [the person(s) listed below is/are *holding 25% or more of the contracting partner's shares (capital shares or voting rights)*; or], - - checkbox(control_reason == "OTHER_WAY"), - [if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below *is/are controlling the contracting partner in other ways*; or], - - checkbox(control_reason == "DIRECTOR"), - [in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the *managing director(s)*] - ) - ] - - v(1em) - - // Section 3: Controlling Persons - let persons = get("IDENTITY_LIST", default: ()) - let has_persons = type(persons) == array and persons.len() > 0 - - if has_persons { - for person in persons { - let get_person(key) = { - person.at(key, default: "") - } - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name:], [#get_person("FULL_NAME")], - [Actual address of domicile:], [#get_person("DOMICILE_ADDRESS")] - ) - #v(0.5em) - ] - } - } else { - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Last name(s):], [], - [First name(s):], [], - [Actual address of domicile:], [] - ) - #v(0.5em) - ] - } - - v(1.5em) - - // Section 4: Fiduciary Holding - text(size: 11pt, weight: "bold")[Fiduciary holding assets] - - v(0.5em) - - let third_party = get("THIRD_PARTY_OWNERSHIP") - - block(breakable: false)[ - #text()[Is a third person the beneficial owner of the assets held in the account/securities account?] - - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.5em, - checkbox(not third_party), [No.], - checkbox(third_party), [Yes. $=>$ The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9.] - ) - ] - - v(0.5em) - - text()[The contracting partner hereby undertakes to automatically inform of any changes to the information contained herein.] - - v(0.5em) - - // Signature Section - let submitted_by_officer = get("BY_AML_OFFICER") - - if submitted_by_officer == false { - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date:], - [], - [Signature(s):], - [#get("SIGN_DATE")], - [], - [#get("SIGNATURE")] - ) - - v(1em) - - text(size: 9pt, style: "italic")[ - It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery). - ] - } else if submitted_by_officer == true { - text(weight: "bold")[Signed declaration by the customer] - - v(0.5em) - - text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] - - v(0.5em) - - text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [Signed Document:], - [#if get("ATTACHMENT_SIGNED_DOCUMENT") != "" [Document attached] else [No document]] - ) - } else { - text(weight: "bold")[Invalid submitter (#submitted_by_officer)] - } -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", - "CONTROL_REASON": "HAS_25_MORE_RIGHTS", - "IDENTITY_LIST": ( - ( - "FULL_NAME": "Jane Smith", - "DOMICILE_ADDRESS": "Teststrasse 456\n8001 Zurich" - ), - ), - "THIRD_PARTY_OWNERSHIP": false, - "BY_AML_OFFICER": false, - "SIGNATURE": "Jane Smith", - "SIGN_DATE": "10.11.2025", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_11_customer/Makefile.am b/contrib/typst/vqf_902_11_customer/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_11_customer/0.0.0/ +typstpackage_DATA = \ + vqf_902_11_customer.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_11_customer/template/main.typ b/contrib/typst/vqf_902_11_customer/template/main.typ @@ -0,0 +1,18 @@ +#import "@taler-exchange/vqf_902_11:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", + "CONTROL_REASON": "HAS_25_MORE_RIGHTS", + "IDENTITY_LIST": ( + ( + "FULL_NAME": "Jane Smith", + "DOMICILE_ADDRESS": "Teststrasse 456\n8001 Zurich" + ), + ), + "THIRD_PARTY_OWNERSHIP": false, + "BY_AML_OFFICER": false, + "SIGNATURE": "Jane Smith", + "SIGN_DATE": "10.11.2025", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_11_customer/typst.toml b/contrib/typst/vqf_902_11_customer/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_11_customer" +version = "0.0.0" +entrypoint = "vqf_902_11_customer.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_11_customer/vqf_902_11_customer.typ b/contrib/typst/vqf_902_11_customer/vqf_902_11_customer.typ @@ -0,0 +1,213 @@ +// VQF 902.11 Establishing of the controlling person (K) +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.11#linebreak() + Version of 1 December 2015 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange (K)]) + + v(0.3em) + + text(size: 9pt, style: "italic")[ + (for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners) + ] + + v(-1em) + line(length:100%) + + v(1em) + + // Section 1: Contracting Partner + text(size: 11pt, weight: "bold")[Contracting partner:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("IDENTITY_CONTRACTING_PARTNER")] + ) + + v(1em) + + // Section 2: Declaration + let control_reason = get("CONTROL_REASON") + + text()[The contracting partner hereby declares that (tick the appropriate box):] + + block(breakable: false)[ + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.8em, + checkbox(control_reason == "HAS_25_MORE_RIGHTS"), + [the person(s) listed below is/are *holding 25% or more of the contracting partner's shares (capital shares or voting rights)*; or], + + checkbox(control_reason == "OTHER_WAY"), + [if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below *is/are controlling the contracting partner in other ways*; or], + + checkbox(control_reason == "DIRECTOR"), + [in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the *managing director(s)*] + ) + ] + + v(1em) + + // Section 3: Controlling Persons + let persons = get("IDENTITY_LIST", default: ()) + let has_persons = type(persons) == array and persons.len() > 0 + + if has_persons { + for person in persons { + let get_person(key) = { + person.at(key, default: "") + } + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name:], [#get_person("FULL_NAME")], + [Actual address of domicile:], [#get_person("DOMICILE_ADDRESS")] + ) + #v(0.5em) + ] + } + } else { + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Last name(s):], [], + [First name(s):], [], + [Actual address of domicile:], [] + ) + #v(0.5em) + ] + } + + v(1.5em) + + // Section 4: Fiduciary Holding + text(size: 11pt, weight: "bold")[Fiduciary holding assets] + + v(0.5em) + + let third_party = get("THIRD_PARTY_OWNERSHIP") + + block(breakable: false)[ + #text()[Is a third person the beneficial owner of the assets held in the account/securities account?] + + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.5em, + checkbox(not third_party), [No.], + checkbox(third_party), [Yes. $=>$ The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9.] + ) + ] + + v(0.5em) + + text()[The contracting partner hereby undertakes to automatically inform of any changes to the information contained herein.] + + v(0.5em) + + // Signature Section + let submitted_by_officer = get("BY_AML_OFFICER") + + if submitted_by_officer == false { + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date:], + [], + [Signature(s):], + [#get("SIGN_DATE")], + [], + [#get("SIGNATURE")] + ) + + v(1em) + + text(size: 9pt, style: "italic")[ + It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery). + ] + } else if submitted_by_officer == true { + text(weight: "bold")[Signed declaration by the customer] + + v(0.5em) + + text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] + + v(0.5em) + + text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [Signed Document:], + [#if get("ATTACHMENT_SIGNED_DOCUMENT") != "" [Document attached] else [No document]] + ) + } else { + text(weight: "bold")[Invalid submitter (#submitted_by_officer)] + } +} diff --git a/contrib/typst/vqf_902_11_officer.typ b/contrib/typst/vqf_902_11_officer.typ @@ -1,244 +0,0 @@ -// VQF 902.11 Establishing of the controlling person (K) -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.11#linebreak() - Version of 1 December 2015 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange (K)]) - - v(0.3em) - - text(size: 9pt, style: "italic")[ - (for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners) - ] - - v(-1em) - line(length:100%) - - v(1em) - - // Section 1: Contracting Partner - text(size: 11pt, weight: "bold")[Contracting partner:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("IDENTITY_CONTRACTING_PARTNER")] - ) - - v(1em) - - // Section 2: Declaration - let control_reason = get("CONTROL_REASON") - - text()[The contracting partner hereby declares that (tick the appropriate box):] - - block(breakable: false)[ - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.8em, - checkbox(control_reason == "HAS_25_MORE_RIGHTS"), - [the person(s) listed below is/are *holding 25% or more of the contracting partner's shares (capital shares or voting rights)*; or], - - checkbox(control_reason == "OTHER_WAY"), - [if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below *is/are controlling the contracting partner in other ways*; or], - - checkbox(control_reason == "DIRECTOR"), - [in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the *managing director(s)*] - ) - ] - - v(1em) - - // Section 3: Controlling Persons - let persons = get("IDENTITY_LIST", default: ()) - let has_persons = type(persons) == array and persons.len() > 0 - - if has_persons { - for person in persons { - let get_person(key) = { - person.at(key, default: "") - } - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name:], [#get_person("FULL_NAME")], - [Actual address of domicile:], [#get_person("DOMICILE_ADDRESS")] - ) - #v(0.5em) - ] - } - } else { - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Last name(s):], [], - [First name(s):], [], - [Actual address of domicile:], [] - ) - #v(0.5em) - ] - } - - v(1.5em) - - // Section 4: Fiduciary Holding - text(size: 11pt, weight: "bold")[Fiduciary holding assets] - - v(0.5em) - - let third_party = get("THIRD_PARTY_OWNERSHIP") - - block(breakable: false)[ - #text()[Is a third person the beneficial owner of the assets held in the account/securities account?] - - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.5em, - checkbox(not third_party), [No.], - checkbox(third_party), [Yes. $=>$ The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9.] - ) - ] - - v(0.5em) - - text()[The contracting partner hereby undertakes to automatically inform of any changes to the information contained herein.] - - v(0.5em) - - // Signature Section - let submitted_by_officer = get("BY_AML_OFFICER") - - if submitted_by_officer == false { - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date:], - [], - [Signature(s):], - [#get("SIGN_DATE")], - [], - [#get("SIGNATURE")] - ) - - v(1em) - - text(size: 9pt, style: "italic")[ - It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery). - ] - } else if submitted_by_officer == true { - text(weight: "bold")[Signed declaration by the customer] - - v(0.5em) - - text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] - - v(0.5em) - - text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [Signed Document:], - [#if get("ATTACHMENT_SIGNED_DOCUMENT") != "" [Document attached] else [No document]] - ) - } else { - text(weight: "bold")[Invalid submitter (#submitted_by_officer)] - } -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "AML_STAFF_NAME" : "Manuela", - "FILE_NUMBER": "42", - "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", - "CONTROL_REASON": "HAS_25_MORE_RIGHTS", - "IDENTITY_LIST": ( - ( - "FULL_NAME": "Jane Smith", - "DOMICILE_ADDRESS": "Teststrasse 456\n8001 Zurich" - ), - ), - "THIRD_PARTY_OWNERSHIP": false, - "BY_AML_OFFICER": true, - "SIGNATURE": "Jane Smith", - "SIGN_DATE": "10.11.2025", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_11_officer/Makefile.am b/contrib/typst/vqf_902_11_officer/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_11_officer/0.0.0/ +typstpackage_DATA = \ + vqf_902_11_officer.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_11_officer/template/main.typ b/contrib/typst/vqf_902_11_officer/template/main.typ @@ -0,0 +1,19 @@ +#import "@taler-exchange/vqf_902_11_officer:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "AML_STAFF_NAME" : "Manuela", + "FILE_NUMBER": "42", + "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", + "CONTROL_REASON": "HAS_25_MORE_RIGHTS", + "IDENTITY_LIST": ( + ( + "FULL_NAME": "Jane Smith", + "DOMICILE_ADDRESS": "Teststrasse 456\n8001 Zurich" + ), + ), + "THIRD_PARTY_OWNERSHIP": false, + "BY_AML_OFFICER": true, + "SIGNATURE": "Jane Smith", + "SIGN_DATE": "10.11.2025", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_11_officer/typst.toml b/contrib/typst/vqf_902_11_officer/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_11_officer" +version = "0.0.0" +entrypoint = "vqf_902_11_officer.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_11_officer/vqf_902_11_officer.typ b/contrib/typst/vqf_902_11_officer/vqf_902_11_officer.typ @@ -0,0 +1,213 @@ +// VQF 902.11 Establishing of the controlling person (K) +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.11#linebreak() + Version of 1 December 2015 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange (K)]) + + v(0.3em) + + text(size: 9pt, style: "italic")[ + (for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners) + ] + + v(-1em) + line(length:100%) + + v(1em) + + // Section 1: Contracting Partner + text(size: 11pt, weight: "bold")[Contracting partner:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("IDENTITY_CONTRACTING_PARTNER")] + ) + + v(1em) + + // Section 2: Declaration + let control_reason = get("CONTROL_REASON") + + text()[The contracting partner hereby declares that (tick the appropriate box):] + + block(breakable: false)[ + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.8em, + checkbox(control_reason == "HAS_25_MORE_RIGHTS"), + [the person(s) listed below is/are *holding 25% or more of the contracting partner's shares (capital shares or voting rights)*; or], + + checkbox(control_reason == "OTHER_WAY"), + [if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below *is/are controlling the contracting partner in other ways*; or], + + checkbox(control_reason == "DIRECTOR"), + [in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the *managing director(s)*] + ) + ] + + v(1em) + + // Section 3: Controlling Persons + let persons = get("IDENTITY_LIST", default: ()) + let has_persons = type(persons) == array and persons.len() > 0 + + if has_persons { + for person in persons { + let get_person(key) = { + person.at(key, default: "") + } + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name:], [#get_person("FULL_NAME")], + [Actual address of domicile:], [#get_person("DOMICILE_ADDRESS")] + ) + #v(0.5em) + ] + } + } else { + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Last name(s):], [], + [First name(s):], [], + [Actual address of domicile:], [] + ) + #v(0.5em) + ] + } + + v(1.5em) + + // Section 4: Fiduciary Holding + text(size: 11pt, weight: "bold")[Fiduciary holding assets] + + v(0.5em) + + let third_party = get("THIRD_PARTY_OWNERSHIP") + + block(breakable: false)[ + #text()[Is a third person the beneficial owner of the assets held in the account/securities account?] + + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.5em, + checkbox(not third_party), [No.], + checkbox(third_party), [Yes. $=>$ The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9.] + ) + ] + + v(0.5em) + + text()[The contracting partner hereby undertakes to automatically inform of any changes to the information contained herein.] + + v(0.5em) + + // Signature Section + let submitted_by_officer = get("BY_AML_OFFICER") + + if submitted_by_officer == false { + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date:], + [], + [Signature(s):], + [#get("SIGN_DATE")], + [], + [#get("SIGNATURE")] + ) + + v(1em) + + text(size: 9pt, style: "italic")[ + It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery). + ] + } else if submitted_by_officer == true { + text(weight: "bold")[Signed declaration by the customer] + + v(0.5em) + + text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] + + v(0.5em) + + text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [Signed Document:], + [#if get("ATTACHMENT_SIGNED_DOCUMENT") != "" [Document attached] else [No document]] + ) + } else { + text(weight: "bold")[Invalid submitter (#submitted_by_officer)] + } +} diff --git a/contrib/typst/vqf_902_12.typ b/contrib/typst/vqf_902_12.typ @@ -1,402 +0,0 @@ -// VQF 902.12 Foundations (S) Template -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.12#linebreak() - Version of 1 December 2015 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - let get(key, default: "") = { - data.at(key, default: default) - } - - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Foundations (as well as similar constructs) (S)]) - - v(-1em) - line(length:100%) - - v(1em) - - text(weight: "bold")[Contracting partner:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("IDENTITY_CONTRACTING_PARTNER")] - ) - - v(1em) - - text()[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:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("ENTITY_NAME")] - ) - - v(1em) - - text()[and, such capacity, provide(s) to the best of his/her/their knowledge the following:] - - v(1.5em) - - // Section 1: Foundation Information -[= 1. Name and information pertaining to the foundation (tick the two boxes applicable):] - - v(0.5em) - - let entity_type = get("ENTITY_TYPE") - let entity_revoc = get("ENTITY_REVOCABILITY") - - block(breakable: false)[ - #grid( - columns: (auto, 1fr, auto, 1fr), - gutter: 1em, - row-gutter: 0.8em, - [Type of foundation:], - [#checkbox(entity_type == "DISCRETIONARY") Discretionary foundation], - [or], - [#checkbox(entity_type == "NON_DISCRETIONARY") Non-discretionary foundation], - [and], [], [], [], - [Revocability:], - [#checkbox(entity_revoc == "REVOCABLE") Revocable foundation], - [or], - [#checkbox(entity_revoc == "IRREVOCABLE") Irrevocable foundation], - ) - ] - - v(1.5em) - - // Section 2: Founder Information -[= 2. Information pertaining to the (ultimate economic, not fiduciary) founder (individual(s) or entity/ies):] - - v(0.5em) - - let founders = get("FOUNDER_LIST", default: ()) - let has_founders = type(founders) == array and founders.len() > 0 - - for founder in (if has_founders {founders} else {((:),)}) { - let get_founder(key) = { - if founder != (:) { - founder.at(key, default: "") - } else { - "" - } - } - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Last name(s), first name(s)/entity:], [#get_founder("PERSON_ENTITY_NAME")], - [Actual address of domicile/registered office:], [#get_founder("PERSON_DOMICILE_REGISTERED_OFFICE")], - [Country:], [#get_founder("PERSON_COUNTRY")], - [Date(s) of birth:], [#get_founder("PERSON_DATE_OF_BIRTH")], - [Nationality:], [#get_founder("PERSON_NATIONALITY")], - [Date of death (if deceased):], [#get_founder("PERSON_DATE_OF_DEATH")] - ) - ] - v(0.5em) - } - - let founder_revoc = get("FOUNDER_HAS_REVOCATION_RIGHT") - if entity_revoc == "REVOCABLE" [ - #table( - columns: (70%, 30%), - stroke: 0.5pt + black, - inset: 5pt, - [In case of a revocable foundation: does the founder have the right to revoke the foundation?], - [#grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(founder_revoc), [Yes], - checkbox(not founder_revoc), [No], - )] - ) - ] - - pagebreak() - - // Section 3: Pre-existing Foundation -[= 3. If the foundation results from the restructuring of pre-existing foundation (re-settlement) or the merger of pre-existing foundations, the following information pertaining to the (actual) founder(s) of the pre-existing foundation(s) has to be given:] - - v(0.5em) - - let has_pred = get("HAS_PREDECESSOR_ENTITY") - let pred_founders = get("PREDECESSOR_FOUNDER_LIST", default: ()) - let has_pred_founders = has_pred and type(pred_founders) == array and pred_founders.len() > 0 - - if has_pred_founders { - for pred in pred_founders { - let get_pred(key) = { - pred.at(key, default: "") - } - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Last name(s), first name(s)/entity:], [#get_pred("PERSON_ENTITY_NAME")], - [Actual address of domicile/registered office:], [#get_pred("PERSON_DOMICILE_REGISTERED_OFFICE")], - [Country:], [#get_pred("PERSON_COUNTRY")], - [Date(s) of birth:], [#get_pred("PERSON_DATE_OF_BIRTH")], - [Nationality:], [#get_pred("PERSON_NATIONALITY")], - [Date of death (if deceased):], [#get_pred("PERSON_DATE_OF_DEATH")] - ) - ] - v(0.5em) - } - } else { - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Last name(s), first name(s)/entity:], [], - [Actual address of domicile/registered office:], [], - [Country:], [], - [Date(s) of birth:], [], - [Nationality:], [], - [Date of death (if deceased):], [] - ) - } - - // Section 4: Beneficiary Information -[= 4. Information] - - v(0.5em) - -[== a) pertaining to the beneficiary/ies at the time of the signing of this form:] - - v(0.5em) - - let beneficiaries = get("BENEFICIARY_LIST", default: ()) - let has_benef = type(beneficiaries) == array and beneficiaries.len() > 0 - - if has_benef { - for benef in beneficiaries { - let get_benef(key) = { - benef.at(key, default: "") - } - let has_claim = get_benef("BENEFICIARY_HAS_CLAIM_RIGHT") - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [#get_benef("PERSON_ENTITY_NAME")], - [Actual address of domicile/registered office:], [#get_benef("PERSON_DOMICILE_REGISTERED_OFFICE")], - [Country:], [#get_benef("PERSON_COUNTRY")], - [Date of birth:], [#get_benef("PERSON_DATE_OF_BIRTH")], - [Nationality:], [#get_benef("PERSON_NATIONALITY")], - [Has the beneficiary an actual right to claim distribution?], - [#grid( - columns: (auto, auto), - stroke: 0.5pt + black, - inset: 5pt, - [#checkbox(has_claim) Yes], - [#checkbox(not has_claim) No], - )] - ) - ] - v(0.5em) - } - } else { - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [], - [Actual address of domicile/registered office:], [], - [Country:], [], - [Date of birth:], [], - [Nationality:], [], - [Has the beneficiary an actual right to claim distribution?], - [#grid( - columns: (auto, auto), - stroke: 0.5pt + black, - inset: 5pt, - [#checkbox(false) Yes], - [#checkbox(false) No], - )] - ) - } - - v(1em) - -[== b) and in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("BENEFICIARY_GROUP_DESCRIPTION")] - ) - - pagebreak() - - // Section 5: Nominating Persons -[= 5. Information pertaining to (a) further person(s) having the right to determine or nominate representatives (e.g. members of the foundation board), if these representatives may dispose over the assets or have the right to change the distribution of the assets or the nomination of beneficiaries:] - - v(0.5em) - - let nom_persons = get("NOMINATING_PERSON_LIST", default: ()) - let has_nom = type(nom_persons) == array and nom_persons.len() > 0 - - if has_nom { - for nom in nom_persons { - let get_nom(key) = { - nom.at(key, default: "") - } - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [#get_nom("PERSON_ENTITY_NAME")], - [Actual address of domicile/registered office:], [#get_nom("PERSON_DOMICILE_REGISTERED_OFFICE")], - [Country:], [#get_nom("PERSON_COUNTRY")], - [Date of birth:], [#get_nom("PERSON_DATE_OF_BIRTH")], - [Nationality:], [#get_nom("PERSON_NATIONALITY")] - ) - ] - v(0.5em) - } - } else { - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [], - [Actual address of domicile/registered office:], [], - [Country:], [], - [Date of birth:], [], - [Nationality:], [] - ) - } - - v(0.5em) - - let nom_revoc = get("EXTRA_REVOCATION_RIGHTS") - table( - columns: (70%, 30%), - stroke: 0.5pt + black, - inset: 5pt, - [In case of a revocable foundation: is/are there (a) further person(s) with the right to revoke the foundation?], - [#grid( - columns: (auto, auto), - stroke: 0.5pt + black, - inset: 5pt, - [#checkbox(nom_revoc) Yes], - [#checkbox(not nom_revoc) No], - )] - ) - - v(1.5em) - - text()[The contracting partner(s) hereby undertake(s) to automatically inform of any changes to the information contained herein.] - - v(1.5em) - - // Signature - table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - [Date:], [Signature(s):], - [#get("SIGN_DATE")], [#get("SIGNATURE")] - ) - - v(1em) - - text(size: 9pt, style: "italic")[ - It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery). - ] -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "IDENTITY_CONTRACTING_PARTNER": "Foundation Board AG\nExample Street 1\n8001 Zurich", - "ENTITY_NAME": "Example Foundation", - "ENTITY_TYPE": "DISCRETIONARY", - "ENTITY_REVOCABILITY": "IRREVOCABLE", - "EXTRA_REVOCATION_RIGHTS": false, - "FOUNDER_LIST": ( - ( - "PERSON_ENTITY_NAME": "John Doe", - "PERSON_DOMICILE_REGISTERED_OFFICE": "Main St 123\n8001 Zurich", - "PERSON_COUNTRY": "CH", - "PERSON_DATE_OF_BIRTH": "01.01.1950", - "PERSON_NATIONALITY": "CH", - ), - ), - "HAS_PREDECESSOR_ENTITY": false, - "SIGNATURE": "Board Member", - "SIGN_DATE": "10.11.2025", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_12/Makefile.am b/contrib/typst/vqf_902_12/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_12/0.0.0/ +typstpackage_DATA = \ + vqf_902_12.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_12/template/main.typ b/contrib/typst/vqf_902_12/template/main.typ @@ -0,0 +1,23 @@ +#import "@taler-exchange/vqf_902_12:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "IDENTITY_CONTRACTING_PARTNER": "Foundation Board AG\nExample Street 1\n8001 Zurich", + "ENTITY_NAME": "Example Foundation", + "ENTITY_TYPE": "DISCRETIONARY", + "ENTITY_REVOCABILITY": "IRREVOCABLE", + "EXTRA_REVOCATION_RIGHTS": false, + "FOUNDER_LIST": ( + ( + "PERSON_ENTITY_NAME": "John Doe", + "PERSON_DOMICILE_REGISTERED_OFFICE": "Main St 123\n8001 Zurich", + "PERSON_COUNTRY": "CH", + "PERSON_DATE_OF_BIRTH": "01.01.1950", + "PERSON_NATIONALITY": "CH", + ), + ), + "HAS_PREDECESSOR_ENTITY": false, + "SIGNATURE": "Board Member", + "SIGN_DATE": "10.11.2025", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_12/typst.toml b/contrib/typst/vqf_902_12/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_12" +version = "0.0.0" +entrypoint = "vqf_902_12.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_12/vqf_902_12.typ b/contrib/typst/vqf_902_12/vqf_902_12.typ @@ -0,0 +1,368 @@ +// VQF 902.12 Foundations (S) Template +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.12#linebreak() + Version of 1 December 2015 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Foundations (as well as similar constructs) (S)]) + + v(-1em) + line(length:100%) + + v(1em) + + text(weight: "bold")[Contracting partner:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("IDENTITY_CONTRACTING_PARTNER")] + ) + + v(1em) + + text()[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:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("ENTITY_NAME")] + ) + + v(1em) + + text()[and, such capacity, provide(s) to the best of his/her/their knowledge the following:] + + v(1.5em) + + // Section 1: Foundation Information +[= 1. Name and information pertaining to the foundation (tick the two boxes applicable):] + + v(0.5em) + + let entity_type = get("ENTITY_TYPE") + let entity_revoc = get("ENTITY_REVOCABILITY") + + block(breakable: false)[ + #grid( + columns: (auto, 1fr, auto, 1fr), + gutter: 1em, + row-gutter: 0.8em, + [Type of foundation:], + [#checkbox(entity_type == "DISCRETIONARY") Discretionary foundation], + [or], + [#checkbox(entity_type == "NON_DISCRETIONARY") Non-discretionary foundation], + [and], [], [], [], + [Revocability:], + [#checkbox(entity_revoc == "REVOCABLE") Revocable foundation], + [or], + [#checkbox(entity_revoc == "IRREVOCABLE") Irrevocable foundation], + ) + ] + + v(1.5em) + + // Section 2: Founder Information +[= 2. Information pertaining to the (ultimate economic, not fiduciary) founder (individual(s) or entity/ies):] + + v(0.5em) + + let founders = get("FOUNDER_LIST", default: ()) + let has_founders = type(founders) == array and founders.len() > 0 + + for founder in (if has_founders {founders} else {((:),)}) { + let get_founder(key) = { + if founder != (:) { + founder.at(key, default: "") + } else { + "" + } + } + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Last name(s), first name(s)/entity:], [#get_founder("PERSON_ENTITY_NAME")], + [Actual address of domicile/registered office:], [#get_founder("PERSON_DOMICILE_REGISTERED_OFFICE")], + [Country:], [#get_founder("PERSON_COUNTRY")], + [Date(s) of birth:], [#get_founder("PERSON_DATE_OF_BIRTH")], + [Nationality:], [#get_founder("PERSON_NATIONALITY")], + [Date of death (if deceased):], [#get_founder("PERSON_DATE_OF_DEATH")] + ) + ] + v(0.5em) + } + + let founder_revoc = get("FOUNDER_HAS_REVOCATION_RIGHT") + if entity_revoc == "REVOCABLE" [ + #table( + columns: (70%, 30%), + stroke: 0.5pt + black, + inset: 5pt, + [In case of a revocable foundation: does the founder have the right to revoke the foundation?], + [#grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(founder_revoc), [Yes], + checkbox(not founder_revoc), [No], + )] + ) + ] + + pagebreak() + + // Section 3: Pre-existing Foundation +[= 3. If the foundation results from the restructuring of pre-existing foundation (re-settlement) or the merger of pre-existing foundations, the following information pertaining to the (actual) founder(s) of the pre-existing foundation(s) has to be given:] + + v(0.5em) + + let has_pred = get("HAS_PREDECESSOR_ENTITY") + let pred_founders = get("PREDECESSOR_FOUNDER_LIST", default: ()) + let has_pred_founders = has_pred and type(pred_founders) == array and pred_founders.len() > 0 + + if has_pred_founders { + for pred in pred_founders { + let get_pred(key) = { + pred.at(key, default: "") + } + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Last name(s), first name(s)/entity:], [#get_pred("PERSON_ENTITY_NAME")], + [Actual address of domicile/registered office:], [#get_pred("PERSON_DOMICILE_REGISTERED_OFFICE")], + [Country:], [#get_pred("PERSON_COUNTRY")], + [Date(s) of birth:], [#get_pred("PERSON_DATE_OF_BIRTH")], + [Nationality:], [#get_pred("PERSON_NATIONALITY")], + [Date of death (if deceased):], [#get_pred("PERSON_DATE_OF_DEATH")] + ) + ] + v(0.5em) + } + } else { + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Last name(s), first name(s)/entity:], [], + [Actual address of domicile/registered office:], [], + [Country:], [], + [Date(s) of birth:], [], + [Nationality:], [], + [Date of death (if deceased):], [] + ) + } + + // Section 4: Beneficiary Information +[= 4. Information] + + v(0.5em) + +[== a) pertaining to the beneficiary/ies at the time of the signing of this form:] + + v(0.5em) + + let beneficiaries = get("BENEFICIARY_LIST", default: ()) + let has_benef = type(beneficiaries) == array and beneficiaries.len() > 0 + + if has_benef { + for benef in beneficiaries { + let get_benef(key) = { + benef.at(key, default: "") + } + let has_claim = get_benef("BENEFICIARY_HAS_CLAIM_RIGHT") + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [#get_benef("PERSON_ENTITY_NAME")], + [Actual address of domicile/registered office:], [#get_benef("PERSON_DOMICILE_REGISTERED_OFFICE")], + [Country:], [#get_benef("PERSON_COUNTRY")], + [Date of birth:], [#get_benef("PERSON_DATE_OF_BIRTH")], + [Nationality:], [#get_benef("PERSON_NATIONALITY")], + [Has the beneficiary an actual right to claim distribution?], + [#grid( + columns: (auto, auto), + stroke: 0.5pt + black, + inset: 5pt, + [#checkbox(has_claim) Yes], + [#checkbox(not has_claim) No], + )] + ) + ] + v(0.5em) + } + } else { + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [], + [Actual address of domicile/registered office:], [], + [Country:], [], + [Date of birth:], [], + [Nationality:], [], + [Has the beneficiary an actual right to claim distribution?], + [#grid( + columns: (auto, auto), + stroke: 0.5pt + black, + inset: 5pt, + [#checkbox(false) Yes], + [#checkbox(false) No], + )] + ) + } + + v(1em) + +[== b) and in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("BENEFICIARY_GROUP_DESCRIPTION")] + ) + + pagebreak() + + // Section 5: Nominating Persons +[= 5. Information pertaining to (a) further person(s) having the right to determine or nominate representatives (e.g. members of the foundation board), if these representatives may dispose over the assets or have the right to change the distribution of the assets or the nomination of beneficiaries:] + + v(0.5em) + + let nom_persons = get("NOMINATING_PERSON_LIST", default: ()) + let has_nom = type(nom_persons) == array and nom_persons.len() > 0 + + if has_nom { + for nom in nom_persons { + let get_nom(key) = { + nom.at(key, default: "") + } + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [#get_nom("PERSON_ENTITY_NAME")], + [Actual address of domicile/registered office:], [#get_nom("PERSON_DOMICILE_REGISTERED_OFFICE")], + [Country:], [#get_nom("PERSON_COUNTRY")], + [Date of birth:], [#get_nom("PERSON_DATE_OF_BIRTH")], + [Nationality:], [#get_nom("PERSON_NATIONALITY")] + ) + ] + v(0.5em) + } + } else { + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [], + [Actual address of domicile/registered office:], [], + [Country:], [], + [Date of birth:], [], + [Nationality:], [] + ) + } + + v(0.5em) + + let nom_revoc = get("EXTRA_REVOCATION_RIGHTS") + table( + columns: (70%, 30%), + stroke: 0.5pt + black, + inset: 5pt, + [In case of a revocable foundation: is/are there (a) further person(s) with the right to revoke the foundation?], + [#grid( + columns: (auto, auto), + stroke: 0.5pt + black, + inset: 5pt, + [#checkbox(nom_revoc) Yes], + [#checkbox(not nom_revoc) No], + )] + ) + + v(1.5em) + + text()[The contracting partner(s) hereby undertake(s) to automatically inform of any changes to the information contained herein.] + + v(1.5em) + + // Signature + table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + [Date:], [Signature(s):], + [#get("SIGN_DATE")], [#get("SIGNATURE")] + ) + + v(1em) + + text(size: 9pt, style: "italic")[ + It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery). + ] +} diff --git a/contrib/typst/vqf_902_13.typ b/contrib/typst/vqf_902_13.typ @@ -1,495 +0,0 @@ -// VQF 902.13 Declaration for trusts (T) Template -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.13#linebreak() - Version of 1 September 2021 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - let get(key, default: "") = { - data.at(key, default: default) - } - - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Declaration for trusts (T)]) - - v(-1em) - line(length:100%) - - v(1em) - - text(weight: "bold")[Contracting partner:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("IDENTITY_CONTRACTING_PARTNER")] - ) - - v(1em) - - text()[The undersigned hereby declare(s) that as trustee or a member of highest supervisory body of an underlying company of a trust known as:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("ENTITY_NAME")] - ) - - v(1em) - - text()[and, such capacity, provide(s) to best of his/her/their knowledge the following information:] - - v(1.5em) - - // Section 1: Trust Information -[= 1. Name and information pertaining to the trust (tick the two boxes applicable):] - - v(0.5em) - - let entity_type = get("ENTITY_TYPE") - let entity_revoc = get("ENTITY_REVOCABILITY") - - block(breakable: false)[ - #grid( - columns: (auto, 1fr, auto, 1fr), - gutter: 1em, - row-gutter: 0.8em, - [Type of trust:], - [#checkbox(entity_type == "DISCRETIONARY") Discretionary trust], - [or], - [#checkbox(entity_type == "NON_DISCRETIONARY") Non-discretionary trust], - [and], [], [], [], - [Revocability:], - [#checkbox(entity_revoc == "REVOCABLE") Revocable trust], - [or], - [#checkbox(entity_revoc == "IRREVOCABLE") Irrevocable trust], - ) - ] - - v(1.5em) - - // Section 2: Settlor Information -[= 2. Information pertaining to the (ultimate economic, not fiduciary) settlor of the trust (individual(s) or entity/-ies):] - - v(0.5em) - - let settlors = get("FOUNDER_LIST", default: ()) - let has_settlors = type(settlors) == array and settlors.len() > 0 - - for settlor in (if has_settlors {settlors} else {((:),)}) { - let get_settlor(key) = { - if settlor != (:) { - settlor.at(key, default: "") - } else { - "" - } - } - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [#get_settlor("PERSON_ENTITY_NAME")], - [Actual address of domicile/registered office:], [#get_settlor("PERSON_DOMICILE_REGISTERED_OFFICE")], - [Country:], [#get_settlor("PERSON_COUNTRY")], - [Date of birth:], [#get_settlor("PERSON_DATE_OF_BIRTH")], - [Nationality:], [#get_settlor("PERSON_NATIONALITY")], - [Date of death (if deceased):], [#get_settlor("PERSON_DATE_OF_DEATH")] - ) - ] - let settlor_revoc = get_settlor("FOUNDER_HAS_REVOCATION_RIGHT") - table( - columns: (80%, 20%), - stroke: 0.5pt + black, - inset: 5pt, - [In case of a revocable trust: - \ Does the settlor have the right to revoke the trust?], - [#grid( - columns: (auto, auto), - stroke: 0.5pt + black, - inset: 5pt, - [#checkbox((entity_revoc == "REVOCABLE") and settlor_revoc) Yes], - [#checkbox((entity_revoc == "REVOCABLE") and not settlor_revoc) No], - )] - ) - v(0.5em) - - } - - - v(1.5em) - - // Section 3: Pre-existing Trust -[= 3. If the trust results from a restructuring of a pre-existing trust (re-settlement) or a merger of pre-existing trusts, the following information pertaining to the (actual) settlor of the pre-existing trust(s) has to be given:] - - v(0.5em) - - let has_pred = get("HAS_PREDECESSOR_ENTITY") - let pred_settlors = get("PREDECESSOR_FOUNDER_LIST", default: ()) - let has_pred_settlors = has_pred and type(pred_settlors) == array and pred_settlors.len() > 0 - - if has_pred_settlors { - for pred in pred_settlors { - let get_pred(key) = { - pred.at(key, default: "") - } - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [#get_pred("PERSON_ENTITY_NAME")], - [Actual address of domicile/registered office:], [#get_pred("PERSON_DOMICILE_REGISTERED_OFFICE")], - [Country:], [#get_pred("PERSON_COUNTRY")], - [Date of birth:], [#get_pred("PERSON_DATE_OF_BIRTH")], - [Nationality:], [#get_pred("PERSON_NATIONALITY")], - [Date of death (if deceased):], [#get_pred("PERSON_DATE_OF_DEATH")] - ) - ] - v(0.5em) - } - } else { - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [], - [Actual address of domicile/registered office:], [], - [Country:], [], - [Date of birth:], [], - [Nationality:], [], - [Date of death (if deceased):], [] - ) - } - - // Section 4: Beneficiary Information -[= 4. Information] - - v(0.5em) - -[== a) pertaining to the beneficiary/-ies at the time of the signing of this form:] - - v(0.5em) - - let beneficiaries = get("BENEFICIARY_LIST", default: ()) - let has_benef = type(beneficiaries) == array and beneficiaries.len() > 0 - - if has_benef { - for benef in beneficiaries { - let get_benef(key) = { - benef.at(key, default: "") - } - let has_claim = get_benef("BENEFICIARY_HAS_CLAIM_RIGHT") - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [#get_benef("PERSON_ENTITY_NAME")], - [Actual address of domicile/registered office:], [#get_benef("PERSON_DOMICILE_REGISTERED_OFFICE")], - [Country:], [#get_benef("PERSON_COUNTRY")], - [Date of birth:], [#get_benef("PERSON_DATE_OF_BIRTH")], - [Nationality:], [#get_benef("PERSON_NATIONALITY")] - ) - #table( - columns: (80%, 20%), - stroke: 0.5pt + black, - inset: 5pt, - [Has the beneficiary an actual right to claim a distribution?], - [#grid( - columns: (auto, auto), - stroke: 0.5pt + black, - inset: 5pt, - [#checkbox(has_claim) Yes], - [#checkbox(not has_claim) No], - )] - ) - ] - v(0.5em) - } - } else { - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [], - [Actual address of domicile/registered office:], [], - [Country:], [], - [Date of birth:], [], - [Nationality:], [] - ) - table( - columns: (80%, 20%), - stroke: 0.5pt + black, - inset: 5pt, - [Has the beneficiary an actual right to claim a distribution?], - [#grid( - columns: (auto, auto), - stroke: 0.5pt + black, - inset: 5pt, - [#checkbox(false) Yes], - [#checkbox(false) No], - )] - ) - } - - v(1em) - -[== b) and in addition to certain beneficiaries or if no beneficiary/-ies has/have been determined, pertaining to (a) group(s) of beneficiaries (e.g. descendants of the settlor) known at the time of the signing of this form:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("BENEFICIARY_GROUP_DESCRIPTION")] - ) - - v(1.5em) - - // Section 5: Protectors and Other Persons -[= 5. Information pertaining to the protector(s) as well as (a) further person(s) having the right to revoke the trust (in case of revocable trusts) or to appoint the trustee of a trust:] - - v(0.5em) - -[== a) Information pertaining to the protector(s)] - - v(0.5em) - - let protectors = get("PROTECTOR_LIST", default: ()) - let has_prot = type(protectors) == array and protectors.len() > 0 - - if has_prot { - for prot in protectors { - let get_prot(key) = { - prot.at(key, default: "") - } - - let prot_revoc = get_prot("PROTECTOR_HAS_REVOCATION_RIGHT") - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name)/entity:], [#get_prot("PERSON_ENTITY_NAME")], - [Actual address of domicile/registered office:], [#get_prot("PERSON_DOMICILE_REGISTERED_OFFICE")], - [Country:], [#get_prot("PERSON_COUNTRY")], - [Date of birth:], [#get_prot("PERSON_DATE_OF_BIRTH")], - [Nationality:], [#get_prot("PERSON_NATIONALITY")] - ) - #table( - columns: (80%, 20%), - stroke: 0.5pt + black, - inset: 5pt, - [In case of a revocable trust: - \ Does the protector have the right to revoke the trust?], - [#grid( - columns: (auto, auto), - stroke: 0.5pt + black, - inset: 5pt, - [#checkbox(prot_revoc) Yes], - [#checkbox(not prot_revoc) No], - )] - ) - ] - v(0.5em) - } - } else { - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name/entity:], [], - [Actual address of domicile/registered office:], [], - [Country:], [], - [Date of birth:], [], - [Nationality:], [] - ) - #table( - columns: (80%, 20%), - stroke: 0.5pt + black, - inset: 5pt, - [In case of a revocable trust: - \ Does the protector have the right to revoke the trust?], - [#grid( - columns: (auto, auto), - stroke: 0.5pt + black, - inset: 5pt, - [#checkbox(false) Yes], - [#checkbox(false) No], - )] - )] - } - - v(0.5em) - - v(1em) - -[== b) Information pertaining to (a) further person(s)] - - v(0.5em) - - let others = get("OTHER_PERSON_LIST", default: ()) - let has_others = type(others) == array and others.len() > 0 - - if has_others { - for other in others { - let get_other(key) = { - other.at(key, default: "") - } - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Last name(s), first name(s)/entity:], [#get_other("PERSON_ENTITY_NAME")], - [Actual address of domicile/registered office:], [#get_other("PERSON_DOMICILE_REGISTERED_OFFICE")], - [Country:], [#get_other("PERSON_COUNTRY")], - [Date(s) of birth:], [#get_other("PERSON_DATE_OF_BIRTH")], - [Nationality:], [#get_other("PERSON_NATIONALITY")] - ) - ] - v(0.5em) - } - } else { - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Last name(s), first name(s)/entity:], [], - [Actual address of domicile/registered office:], [], - [Country:], [], - [Date(s) of birth:], [], - [Nationality:], [] - ) - } - - v(0.5em) - - let other_revoc = get("OTHER_PERSON_HAS_REVOCATION_RIGHT") - if entity_revoc == "REVOCABLE" [ - #table( - columns: (80%, 20%), - stroke: 0.5pt + black, - inset: 5pt, - [In case of a revocable trust: Has/have this/these further person(s) the right to revoke the trust?], - [#grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(other_revoc), [Yes], - checkbox(not other_revoc), [No], - )] - ) - ] - - v(1.5em) - - text()[The contracting partner(s) hereby declare(s) to be entitled to open a business relationship for the trust above or its underlying company.] - - v(0.5em) - - text()[The contracting partner(s) hereby undertake(s) to automatically inform of any changes to the information contained herein.] - - v(1.5em) - - // Signature - table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - [Date:], [Signature(s):], - [#get("SIGN_DATE")], [#get("SIGNATURE")] - ) - - v(1em) - - text(size: 9pt, style: "italic")[ - It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery). - ] -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "IDENTITY_CONTRACTING_PARTNER": "Trustee Company AG\nExample Street 1\n8001 Zurich", - "ENTITY_NAME": "Example Trust", - "ENTITY_TYPE": "DISCRETIONARY", - "ENTITY_REVOCABILITY": "IRREVOCABLE", - "FOUNDER_LIST": ( - ( - "PERSON_ENTITY_NAME": "Jane Smith", - "PERSON_DOMICILE_REGISTERED_OFFICE": "Main St 123\n8001 Zurich", - "PERSON_COUNTRY": "CH", - "PERSON_DATE_OF_BIRTH": "01.01.1955", - "PERSON_NATIONALITY": "CH", - ), - ), - "HAS_PREDECESSOR_ENTITY": false, - "SIGNATURE": "Trustee", - "SIGN_DATE": "10.11.2025", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_13/Makefile.am b/contrib/typst/vqf_902_13/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_13/0.0.0/ +typstpackage_DATA = \ + vqf_902_13.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_13/template/main.typ b/contrib/typst/vqf_902_13/template/main.typ @@ -0,0 +1,22 @@ +#import "@taler-exchange/vqf_902_13:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "IDENTITY_CONTRACTING_PARTNER": "Trustee Company AG\nExample Street 1\n8001 Zurich", + "ENTITY_NAME": "Example Trust", + "ENTITY_TYPE": "DISCRETIONARY", + "ENTITY_REVOCABILITY": "IRREVOCABLE", + "FOUNDER_LIST": ( + ( + "PERSON_ENTITY_NAME": "Jane Smith", + "PERSON_DOMICILE_REGISTERED_OFFICE": "Main St 123\n8001 Zurich", + "PERSON_COUNTRY": "CH", + "PERSON_DATE_OF_BIRTH": "01.01.1955", + "PERSON_NATIONALITY": "CH", + ), + ), + "HAS_PREDECESSOR_ENTITY": false, + "SIGNATURE": "Trustee", + "SIGN_DATE": "10.11.2025", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_13/typst.toml b/contrib/typst/vqf_902_13/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_13" +version = "0.0.0" +entrypoint = "vqf_902_13.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_13/vqf_902_13.typ b/contrib/typst/vqf_902_13/vqf_902_13.typ @@ -0,0 +1,462 @@ +// VQF 902.13 Declaration for trusts (T) Template +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.13#linebreak() + Version of 1 September 2021 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Declaration for trusts (T)]) + + v(-1em) + line(length:100%) + + v(1em) + + text(weight: "bold")[Contracting partner:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("IDENTITY_CONTRACTING_PARTNER")] + ) + + v(1em) + + text()[The undersigned hereby declare(s) that as trustee or a member of highest supervisory body of an underlying company of a trust known as:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("ENTITY_NAME")] + ) + + v(1em) + + text()[and, such capacity, provide(s) to best of his/her/their knowledge the following information:] + + v(1.5em) + + // Section 1: Trust Information +[= 1. Name and information pertaining to the trust (tick the two boxes applicable):] + + v(0.5em) + + let entity_type = get("ENTITY_TYPE") + let entity_revoc = get("ENTITY_REVOCABILITY") + + block(breakable: false)[ + #grid( + columns: (auto, 1fr, auto, 1fr), + gutter: 1em, + row-gutter: 0.8em, + [Type of trust:], + [#checkbox(entity_type == "DISCRETIONARY") Discretionary trust], + [or], + [#checkbox(entity_type == "NON_DISCRETIONARY") Non-discretionary trust], + [and], [], [], [], + [Revocability:], + [#checkbox(entity_revoc == "REVOCABLE") Revocable trust], + [or], + [#checkbox(entity_revoc == "IRREVOCABLE") Irrevocable trust], + ) + ] + + v(1.5em) + + // Section 2: Settlor Information +[= 2. Information pertaining to the (ultimate economic, not fiduciary) settlor of the trust (individual(s) or entity/-ies):] + + v(0.5em) + + let settlors = get("FOUNDER_LIST", default: ()) + let has_settlors = type(settlors) == array and settlors.len() > 0 + + for settlor in (if has_settlors {settlors} else {((:),)}) { + let get_settlor(key) = { + if settlor != (:) { + settlor.at(key, default: "") + } else { + "" + } + } + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [#get_settlor("PERSON_ENTITY_NAME")], + [Actual address of domicile/registered office:], [#get_settlor("PERSON_DOMICILE_REGISTERED_OFFICE")], + [Country:], [#get_settlor("PERSON_COUNTRY")], + [Date of birth:], [#get_settlor("PERSON_DATE_OF_BIRTH")], + [Nationality:], [#get_settlor("PERSON_NATIONALITY")], + [Date of death (if deceased):], [#get_settlor("PERSON_DATE_OF_DEATH")] + ) + ] + let settlor_revoc = get_settlor("FOUNDER_HAS_REVOCATION_RIGHT") + table( + columns: (80%, 20%), + stroke: 0.5pt + black, + inset: 5pt, + [In case of a revocable trust: + \ Does the settlor have the right to revoke the trust?], + [#grid( + columns: (auto, auto), + stroke: 0.5pt + black, + inset: 5pt, + [#checkbox((entity_revoc == "REVOCABLE") and settlor_revoc) Yes], + [#checkbox((entity_revoc == "REVOCABLE") and not settlor_revoc) No], + )] + ) + v(0.5em) + + } + + + v(1.5em) + + // Section 3: Pre-existing Trust +[= 3. If the trust results from a restructuring of a pre-existing trust (re-settlement) or a merger of pre-existing trusts, the following information pertaining to the (actual) settlor of the pre-existing trust(s) has to be given:] + + v(0.5em) + + let has_pred = get("HAS_PREDECESSOR_ENTITY") + let pred_settlors = get("PREDECESSOR_FOUNDER_LIST", default: ()) + let has_pred_settlors = has_pred and type(pred_settlors) == array and pred_settlors.len() > 0 + + if has_pred_settlors { + for pred in pred_settlors { + let get_pred(key) = { + pred.at(key, default: "") + } + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [#get_pred("PERSON_ENTITY_NAME")], + [Actual address of domicile/registered office:], [#get_pred("PERSON_DOMICILE_REGISTERED_OFFICE")], + [Country:], [#get_pred("PERSON_COUNTRY")], + [Date of birth:], [#get_pred("PERSON_DATE_OF_BIRTH")], + [Nationality:], [#get_pred("PERSON_NATIONALITY")], + [Date of death (if deceased):], [#get_pred("PERSON_DATE_OF_DEATH")] + ) + ] + v(0.5em) + } + } else { + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [], + [Actual address of domicile/registered office:], [], + [Country:], [], + [Date of birth:], [], + [Nationality:], [], + [Date of death (if deceased):], [] + ) + } + + // Section 4: Beneficiary Information +[= 4. Information] + + v(0.5em) + +[== a) pertaining to the beneficiary/-ies at the time of the signing of this form:] + + v(0.5em) + + let beneficiaries = get("BENEFICIARY_LIST", default: ()) + let has_benef = type(beneficiaries) == array and beneficiaries.len() > 0 + + if has_benef { + for benef in beneficiaries { + let get_benef(key) = { + benef.at(key, default: "") + } + let has_claim = get_benef("BENEFICIARY_HAS_CLAIM_RIGHT") + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [#get_benef("PERSON_ENTITY_NAME")], + [Actual address of domicile/registered office:], [#get_benef("PERSON_DOMICILE_REGISTERED_OFFICE")], + [Country:], [#get_benef("PERSON_COUNTRY")], + [Date of birth:], [#get_benef("PERSON_DATE_OF_BIRTH")], + [Nationality:], [#get_benef("PERSON_NATIONALITY")] + ) + #table( + columns: (80%, 20%), + stroke: 0.5pt + black, + inset: 5pt, + [Has the beneficiary an actual right to claim a distribution?], + [#grid( + columns: (auto, auto), + stroke: 0.5pt + black, + inset: 5pt, + [#checkbox(has_claim) Yes], + [#checkbox(not has_claim) No], + )] + ) + ] + v(0.5em) + } + } else { + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [], + [Actual address of domicile/registered office:], [], + [Country:], [], + [Date of birth:], [], + [Nationality:], [] + ) + table( + columns: (80%, 20%), + stroke: 0.5pt + black, + inset: 5pt, + [Has the beneficiary an actual right to claim a distribution?], + [#grid( + columns: (auto, auto), + stroke: 0.5pt + black, + inset: 5pt, + [#checkbox(false) Yes], + [#checkbox(false) No], + )] + ) + } + + v(1em) + +[== b) and in addition to certain beneficiaries or if no beneficiary/-ies has/have been determined, pertaining to (a) group(s) of beneficiaries (e.g. descendants of the settlor) known at the time of the signing of this form:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("BENEFICIARY_GROUP_DESCRIPTION")] + ) + + v(1.5em) + + // Section 5: Protectors and Other Persons +[= 5. Information pertaining to the protector(s) as well as (a) further person(s) having the right to revoke the trust (in case of revocable trusts) or to appoint the trustee of a trust:] + + v(0.5em) + +[== a) Information pertaining to the protector(s)] + + v(0.5em) + + let protectors = get("PROTECTOR_LIST", default: ()) + let has_prot = type(protectors) == array and protectors.len() > 0 + + if has_prot { + for prot in protectors { + let get_prot(key) = { + prot.at(key, default: "") + } + + let prot_revoc = get_prot("PROTECTOR_HAS_REVOCATION_RIGHT") + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name)/entity:], [#get_prot("PERSON_ENTITY_NAME")], + [Actual address of domicile/registered office:], [#get_prot("PERSON_DOMICILE_REGISTERED_OFFICE")], + [Country:], [#get_prot("PERSON_COUNTRY")], + [Date of birth:], [#get_prot("PERSON_DATE_OF_BIRTH")], + [Nationality:], [#get_prot("PERSON_NATIONALITY")] + ) + #table( + columns: (80%, 20%), + stroke: 0.5pt + black, + inset: 5pt, + [In case of a revocable trust: + \ Does the protector have the right to revoke the trust?], + [#grid( + columns: (auto, auto), + stroke: 0.5pt + black, + inset: 5pt, + [#checkbox(prot_revoc) Yes], + [#checkbox(not prot_revoc) No], + )] + ) + ] + v(0.5em) + } + } else { + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name/entity:], [], + [Actual address of domicile/registered office:], [], + [Country:], [], + [Date of birth:], [], + [Nationality:], [] + ) + #table( + columns: (80%, 20%), + stroke: 0.5pt + black, + inset: 5pt, + [In case of a revocable trust: + \ Does the protector have the right to revoke the trust?], + [#grid( + columns: (auto, auto), + stroke: 0.5pt + black, + inset: 5pt, + [#checkbox(false) Yes], + [#checkbox(false) No], + )] + )] + } + + v(0.5em) + + v(1em) + +[== b) Information pertaining to (a) further person(s)] + + v(0.5em) + + let others = get("OTHER_PERSON_LIST", default: ()) + let has_others = type(others) == array and others.len() > 0 + + if has_others { + for other in others { + let get_other(key) = { + other.at(key, default: "") + } + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Last name(s), first name(s)/entity:], [#get_other("PERSON_ENTITY_NAME")], + [Actual address of domicile/registered office:], [#get_other("PERSON_DOMICILE_REGISTERED_OFFICE")], + [Country:], [#get_other("PERSON_COUNTRY")], + [Date(s) of birth:], [#get_other("PERSON_DATE_OF_BIRTH")], + [Nationality:], [#get_other("PERSON_NATIONALITY")] + ) + ] + v(0.5em) + } + } else { + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Last name(s), first name(s)/entity:], [], + [Actual address of domicile/registered office:], [], + [Country:], [], + [Date(s) of birth:], [], + [Nationality:], [] + ) + } + + v(0.5em) + + let other_revoc = get("OTHER_PERSON_HAS_REVOCATION_RIGHT") + if entity_revoc == "REVOCABLE" [ + #table( + columns: (80%, 20%), + stroke: 0.5pt + black, + inset: 5pt, + [In case of a revocable trust: Has/have this/these further person(s) the right to revoke the trust?], + [#grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(other_revoc), [Yes], + checkbox(not other_revoc), [No], + )] + ) + ] + + v(1.5em) + + text()[The contracting partner(s) hereby declare(s) to be entitled to open a business relationship for the trust above or its underlying company.] + + v(0.5em) + + text()[The contracting partner(s) hereby undertake(s) to automatically inform of any changes to the information contained herein.] + + v(1.5em) + + // Signature + table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + [Date:], [Signature(s):], + [#get("SIGN_DATE")], [#get("SIGNATURE")] + ) + + v(1em) + + text(size: 9pt, style: "italic")[ + It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery). + ] +} diff --git a/contrib/typst/vqf_902_14.typ b/contrib/typst/vqf_902_14.typ @@ -1,241 +0,0 @@ -// VQF 902.14 Special Clarifications Template -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.14#linebreak() - Version of 1 December 2015 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Special Clarifications]) - - v(-1em) - line(length:100%) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - When a business relationship or transaction is associated with increased risk, appears unusual or evidence exists that the assets are the proceeds of a felony or a qualified tax offence, the member has to perform additional clarifications. - ] - ) - - v(1em) - - text(weight: "bold")[Customer:#footnote[Pursuant Identification form (VQF doc. No. 902.1) numeral 1.]] - - v(0.3em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("CUSTOMER_NAME")] - ) - - v(0.5em) - - text(weight: "bold")[This form was completed by:] - - v(0.3em) - - table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [Full name], [#get("AML_STAFF_NAME")], - [Date], [#get("FILING_DATE")], - ) - - v(1.5em) - - // Section 1: Reason for Special Clarifications - text(size: 11pt, weight: "bold")[1. Reason for special clarifications] - - v(0.5em) - - table( - columns: (45%,55%), - stroke: 0.5pt + black, - inset: 5pt, - [Description of the circumstances/transactions, which triggered the special clarifications], - [#get("INCRISK_REASON")] - ) - - v(1.5em) - - // Section 2: Used Means of Clarification - text(size: 11pt, weight: "bold")[2. Used means of clarification] - - v(0.5em) - - let means = get("INCRISK_MEANS") - - grid( - columns: (auto), - inset: 5pt, - stroke: 0.5pt + black, - [#block(breakable: false)[ - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.5em, - checkbox(means == "GATHERING"), - [Gathering of information from the customer, beneficial owner of the assets, controlling person], - - checkbox(means == "CONSULTATION"), - [Consultation of generally accessible sources and databases], - - checkbox(means == "ENQUIRIES"), - [Enquiries with trustworthy persons (e.g. custodian bank)], - - checkbox(means == "OTHER"), - [Other, which? #if means == "OTHER" [#get("INCRISK_MEANS_OTHER")]] - ) - ]]) - - v(1.5em) - - // Section 3: Supplemental Files - text(size: 11pt, weight: "bold")[3. Summary and plausibility check of the gathered information] - - v(0.3em) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - The results of the clarifications have to be documented and their plausibility has to be checked. - ] - ) - - v(1.5em) - - table( - columns: (100%), - stroke: 0.5pt + black, - inset: 5pt, - [#get("INCRISK_SUMMARY")] - ) - - v(1em) - - table( - columns: (auto, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - [Gathered/Consulted documents], - [#get("INCRISK_DOCUMENTS")] - ) - - - - - // Section 4: Result - text(size: 11pt, weight: "bold")[4. Result of the special clarification] - - v(0.5em) - - let result = get("INCRISK_RESULT") - - grid( - columns: (auto), - inset: 5pt, - stroke: 0.5pt + black, - block(breakable: false)[ - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.8em, - - checkbox(result == "NO_SUSPICION"), - [The plausibility of the circumstances could be checked, no reasonable suspicion pursuant to Art. 9 AMLA (possibly update of customer profile (VQF doc. No. 902.5) and/or risk profile (VQF doc. No. 902.4))], - - checkbox(result == "REASONABLE_SUSPICION"), - [Reasonable suspicion pursuant to Art. 9 AMLA, duty to file a report with MROS], - - checkbox(result == "SIMPLE_SUSPICION"), - [Simple suspicion pursuant to Art. 305#super[ter] Para. 2 StGB, right to notify MROS], - - checkbox(result == "OTHER"), - [Other, what? #if result == "OTHER" [#get("INCRISK_RESULT_OTHER")]] - ) - ]) - -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "CUSTOMER_NAME": "John Doe", - "FILED_BY_NAME": "Jane Smith", - "FILING_DATE": "10.11.2025", - "INCRISK_REASON": "Unusual transaction pattern detected", - "INCRISK_MEANS": "GATHERING", - "INCRISK_SUMMARY": "Customer provided satisfactory explanation", - "INCRISK_DOCUMENTS": "Bank statements, invoices", - "INCRISK_RESULT": "NO_SUSPICION", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_14/Makefile.am b/contrib/typst/vqf_902_14/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_14/0.0.0/ +typstpackage_DATA = \ + vqf_902_14.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_14/template/main.typ b/contrib/typst/vqf_902_14/template/main.typ @@ -0,0 +1,14 @@ +#import "@taler-exchange/vqf_902_14:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "CUSTOMER_NAME": "John Doe", + "FILED_BY_NAME": "Jane Smith", + "FILING_DATE": "10.11.2025", + "INCRISK_REASON": "Unusual transaction pattern detected", + "INCRISK_MEANS": "GATHERING", + "INCRISK_SUMMARY": "Customer provided satisfactory explanation", + "INCRISK_DOCUMENTS": "Bank statements, invoices", + "INCRISK_RESULT": "NO_SUSPICION", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_14/typst.toml b/contrib/typst/vqf_902_14/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_14" +version = "0.0.0" +entrypoint = "vqf_902_14.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_14/vqf_902_14.typ b/contrib/typst/vqf_902_14/vqf_902_14.typ @@ -0,0 +1,215 @@ +// VQF 902.14 Special Clarifications Template +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo, pointingfinger, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.14#linebreak() + Version of 1 December 2015 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Special Clarifications]) + + v(-1em) + line(length:100%) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + When a business relationship or transaction is associated with increased risk, appears unusual or evidence exists that the assets are the proceeds of a felony or a qualified tax offence, the member has to perform additional clarifications. + ] + ) + + v(1em) + + text(weight: "bold")[Customer:#footnote[Pursuant Identification form (VQF doc. No. 902.1) numeral 1.]] + + v(0.3em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("CUSTOMER_NAME")] + ) + + v(0.5em) + + text(weight: "bold")[This form was completed by:] + + v(0.3em) + + table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [Full name], [#get("AML_STAFF_NAME")], + [Date], [#get("FILING_DATE")], + ) + + v(1.5em) + + // Section 1: Reason for Special Clarifications + text(size: 11pt, weight: "bold")[1. Reason for special clarifications] + + v(0.5em) + + table( + columns: (45%,55%), + stroke: 0.5pt + black, + inset: 5pt, + [Description of the circumstances/transactions, which triggered the special clarifications], + [#get("INCRISK_REASON")] + ) + + v(1.5em) + + // Section 2: Used Means of Clarification + text(size: 11pt, weight: "bold")[2. Used means of clarification] + + v(0.5em) + + let means = get("INCRISK_MEANS") + + grid( + columns: (auto), + inset: 5pt, + stroke: 0.5pt + black, + [#block(breakable: false)[ + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.5em, + checkbox(means == "GATHERING"), + [Gathering of information from the customer, beneficial owner of the assets, controlling person], + + checkbox(means == "CONSULTATION"), + [Consultation of generally accessible sources and databases], + + checkbox(means == "ENQUIRIES"), + [Enquiries with trustworthy persons (e.g. custodian bank)], + + checkbox(means == "OTHER"), + [Other, which? #if means == "OTHER" [#get("INCRISK_MEANS_OTHER")]] + ) + ]]) + + v(1.5em) + + // Section 3: Supplemental Files + text(size: 11pt, weight: "bold")[3. Summary and plausibility check of the gathered information] + + v(0.3em) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + The results of the clarifications have to be documented and their plausibility has to be checked. + ] + ) + + v(1.5em) + + table( + columns: (100%), + stroke: 0.5pt + black, + inset: 5pt, + [#get("INCRISK_SUMMARY")] + ) + + v(1em) + + table( + columns: (auto, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + [Gathered/Consulted documents], + [#get("INCRISK_DOCUMENTS")] + ) + + + + + // Section 4: Result + text(size: 11pt, weight: "bold")[4. Result of the special clarification] + + v(0.5em) + + let result = get("INCRISK_RESULT") + + grid( + columns: (auto), + inset: 5pt, + stroke: 0.5pt + black, + block(breakable: false)[ + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.8em, + + checkbox(result == "NO_SUSPICION"), + [The plausibility of the circumstances could be checked, no reasonable suspicion pursuant to Art. 9 AMLA (possibly update of customer profile (VQF doc. No. 902.5) and/or risk profile (VQF doc. No. 902.4))], + + checkbox(result == "REASONABLE_SUSPICION"), + [Reasonable suspicion pursuant to Art. 9 AMLA, duty to file a report with MROS], + + checkbox(result == "SIMPLE_SUSPICION"), + [Simple suspicion pursuant to Art. 305#super[ter] Para. 2 StGB, right to notify MROS], + + checkbox(result == "OTHER"), + [Other, what? #if result == "OTHER" [#get("INCRISK_RESULT_OTHER")]] + ) + ]) + +} diff --git a/contrib/typst/vqf_902_15.typ b/contrib/typst/vqf_902_15.typ @@ -1,320 +0,0 @@ -// VQF 902.15 Life Insurance Policies (I) Template -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.15#linebreak() - Version of 22 January 2021 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - let get(key, default: "") = { - data.at(key, default: default) - } - - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Information on life insurance policies with separately managed accounts/securities accounts (so-called insurance wrappers) (I)]) - - v(-1em) - line(length:100%) - - v(1em) - - text(weight: "bold")[Contracting partner:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("IDENTITY_CONTRACTING_PARTNER")] - ) - - v(1em) - - text(weight: "bold")[Name or number of the contractual relationship between the contracting party and the financial intermediary:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("CONTRACTUAL_RELATIONSHIP_NAME")] - ) - - v(1em) - - text(weight: "bold")[Insurance policy:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("INSURANCE_POLICY_DETAILS")] - ) - - v(1em) - - text()[The contracting partner confirms in accordance with Art. 41a SRO Regulations that it is a licensed and state-supervised insurance company and that it has entered into the above-mentioned contractual relationship the assets connected to the life insurance policy also mentioned above.] - - v(0.5em) - - text()[In relation with the above insurance policy, the contracting partner gives the following further details:] - - v(1.5em) - - // Section 1: Policy Holder -[= 1. Policy holder] - - v(0.5em) - - let policy_holders = get("POLICY_HOLDER_LIST", default: ()) - let has_holders = type(policy_holders) == array and policy_holders.len() > 0 - - for holder in (if has_holders {policy_holders} else {((:),)}) { - let get_holder(key) = { - if holder != (:) { - holder.at(key, default: "") - } else { - "" - } - } - - text()[Full name/entity:] - - v(0.3em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get_holder("PERSON_ENTITY_NAME")] - ) - - v(0.5em) - - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date of birth:], - [], - [Nationality:], - [#get_holder("PERSON_DATE_OF_BIRTH")], - [], - [#get_holder("PERSON_NATIONALITY")] - ) - - v(0.5em) - - text()[Actual address of domicile/registered office (incl. country):] - - v(0.3em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get_holder("PERSON_DOMICILE_REGISTERED_OFFICE")] - ) - - v(1em) - } - - v(0.5em) - - // Section 2: Premium Payer -[= 2. Person actually (not in a fiduciary capacity) paying the premiums (to be filled in if not identical with point 1 above):] - - v(0.5em) - - let same_payer = get("PREMIUM_PAYER_SAME_AS_HOLDER") - let premium_payers = get("PREMIUM_PAYER_LIST", default: ()) - let has_payers = not same_payer and type(premium_payers) == array and premium_payers.len() > 0 - - if has_payers { - for payer in premium_payers { - let get_payer(key) = { - payer.at(key, default: "") - } - - text()[Full anme/entity:] - - v(0.3em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get_payer("PERSON_ENTITY_NAME")] - ) - - v(0.5em) - - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date of birth:], - [], - [Nationality:], - [#get_payer("PERSON_DATE_OF_BIRTH")], - [], - [#get_payer("PERSON_NATIONALITY")] - ) - - v(0.5em) - - text()[Actual address of domicile/registered office (incl. country):] - - v(0.3em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get_payer("PERSON_DOMICILE_REGISTERED_OFFICE")] - ) - - v(1em) - } - } else { - text()[Full name/entity:] - - v(0.3em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [] - ) - - v(0.5em) - - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date of birth:], - [], - [Nationality:], - [], - [], - [] - ) - - v(0.5em) - - text()[Actual address of domicile/registered office (incl. country):] - - v(0.3em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [] - ) - - v(1em) - } - - v(1em) - - text()[The contracting partner hereby undertakes to automatically inform the financial intermediary of any changes. The contracting partner hereby also declares having been given permission by the above individuals and/or entities to transmit their data to the financial intermediary.] - - v(1.5em) - - // Signature - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date:], - [], - [Signature(s):], - [#get("SIGN_DATE")], - [], - [#get("SIGNATURE")] - ) - - v(1em) - - text(size: 9pt, style: "italic")[ - It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery). - ] -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "IDENTITY_CONTRACTING_PARTNER": "Insurance Company AG\nInsurance Street 1\n8001 Zurich\nSwitzerland", - "CONTRACTUAL_RELATIONSHIP_NAME": "Policy #12345-ABC", - "INSURANCE_POLICY_DETAILS": "Life insurance policy with separate account management\nPolicy Number: LI-2025-001\nIssued: 01.01.2025", - "POLICY_HOLDER_LIST": ( - ( - "PERSON_ENTITY_NAME": "John Doe", - "PERSON_DATE_OF_BIRTH": "01.01.1970", - "PERSON_NATIONALITY": "CH", - "PERSON_DOMICILE_REGISTERED_OFFICE": "Main Street 123\n8001 Zurich\nSwitzerland" - ), - ), - "PREMIUM_PAYER_SAME_AS_HOLDER": true, - "SIGNATURE": "Insurance Company Representative", - "SIGN_DATE": "10.11.2025", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_15/Makefile.am b/contrib/typst/vqf_902_15/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_15/0.0.0/ +typstpackage_DATA = \ + vqf_902_15.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_15/template/main.typ b/contrib/typst/vqf_902_15/template/main.typ @@ -0,0 +1,20 @@ +#import "@taler-exchange/vqf_902_15:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "IDENTITY_CONTRACTING_PARTNER": "Insurance Company AG\nInsurance Street 1\n8001 Zurich\nSwitzerland", + "CONTRACTUAL_RELATIONSHIP_NAME": "Policy #12345-ABC", + "INSURANCE_POLICY_DETAILS": "Life insurance policy with separate account management\nPolicy Number: LI-2025-001\nIssued: 01.01.2025", + "POLICY_HOLDER_LIST": ( + ( + "PERSON_ENTITY_NAME": "John Doe", + "PERSON_DATE_OF_BIRTH": "01.01.1970", + "PERSON_NATIONALITY": "CH", + "PERSON_DOMICILE_REGISTERED_OFFICE": "Main Street 123\n8001 Zurich\nSwitzerland" + ), + ), + "PREMIUM_PAYER_SAME_AS_HOLDER": true, + "SIGNATURE": "Insurance Company Representative", + "SIGN_DATE": "10.11.2025", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_15/typst.toml b/contrib/typst/vqf_902_15/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_15" +version = "0.0.0" +entrypoint = "vqf_902_15.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_15/vqf_902_15.typ b/contrib/typst/vqf_902_15/vqf_902_15.typ @@ -0,0 +1,289 @@ +// VQF 902.15 Life Insurance Policies (I) Template +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.15#linebreak() + Version of 22 January 2021 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Information on life insurance policies with separately managed accounts/securities accounts (so-called insurance wrappers) (I)]) + + v(-1em) + line(length:100%) + + v(1em) + + text(weight: "bold")[Contracting partner:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("IDENTITY_CONTRACTING_PARTNER")] + ) + + v(1em) + + text(weight: "bold")[Name or number of the contractual relationship between the contracting party and the financial intermediary:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("CONTRACTUAL_RELATIONSHIP_NAME")] + ) + + v(1em) + + text(weight: "bold")[Insurance policy:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("INSURANCE_POLICY_DETAILS")] + ) + + v(1em) + + text()[The contracting partner confirms in accordance with Art. 41a SRO Regulations that it is a licensed and state-supervised insurance company and that it has entered into the above-mentioned contractual relationship the assets connected to the life insurance policy also mentioned above.] + + v(0.5em) + + text()[In relation with the above insurance policy, the contracting partner gives the following further details:] + + v(1.5em) + + // Section 1: Policy Holder +[= 1. Policy holder] + + v(0.5em) + + let policy_holders = get("POLICY_HOLDER_LIST", default: ()) + let has_holders = type(policy_holders) == array and policy_holders.len() > 0 + + for holder in (if has_holders {policy_holders} else {((:),)}) { + let get_holder(key) = { + if holder != (:) { + holder.at(key, default: "") + } else { + "" + } + } + + text()[Full name/entity:] + + v(0.3em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get_holder("PERSON_ENTITY_NAME")] + ) + + v(0.5em) + + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date of birth:], + [], + [Nationality:], + [#get_holder("PERSON_DATE_OF_BIRTH")], + [], + [#get_holder("PERSON_NATIONALITY")] + ) + + v(0.5em) + + text()[Actual address of domicile/registered office (incl. country):] + + v(0.3em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get_holder("PERSON_DOMICILE_REGISTERED_OFFICE")] + ) + + v(1em) + } + + v(0.5em) + + // Section 2: Premium Payer +[= 2. Person actually (not in a fiduciary capacity) paying the premiums (to be filled in if not identical with point 1 above):] + + v(0.5em) + + let same_payer = get("PREMIUM_PAYER_SAME_AS_HOLDER") + let premium_payers = get("PREMIUM_PAYER_LIST", default: ()) + let has_payers = not same_payer and type(premium_payers) == array and premium_payers.len() > 0 + + if has_payers { + for payer in premium_payers { + let get_payer(key) = { + payer.at(key, default: "") + } + + text()[Full anme/entity:] + + v(0.3em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get_payer("PERSON_ENTITY_NAME")] + ) + + v(0.5em) + + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date of birth:], + [], + [Nationality:], + [#get_payer("PERSON_DATE_OF_BIRTH")], + [], + [#get_payer("PERSON_NATIONALITY")] + ) + + v(0.5em) + + text()[Actual address of domicile/registered office (incl. country):] + + v(0.3em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get_payer("PERSON_DOMICILE_REGISTERED_OFFICE")] + ) + + v(1em) + } + } else { + text()[Full name/entity:] + + v(0.3em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [] + ) + + v(0.5em) + + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date of birth:], + [], + [Nationality:], + [], + [], + [] + ) + + v(0.5em) + + text()[Actual address of domicile/registered office (incl. country):] + + v(0.3em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [] + ) + + v(1em) + } + + v(1em) + + text()[The contracting partner hereby undertakes to automatically inform the financial intermediary of any changes. The contracting partner hereby also declares having been given permission by the above individuals and/or entities to transmit their data to the financial intermediary.] + + v(1.5em) + + // Signature + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date:], + [], + [Signature(s):], + [#get("SIGN_DATE")], + [], + [#get("SIGNATURE")] + ) + + v(1em) + + text(size: 9pt, style: "italic")[ + It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery). + ] +} diff --git a/contrib/typst/vqf_902_1_customer.typ b/contrib/typst/vqf_902_1_customer.typ @@ -1,422 +0,0 @@ -// VQF 902.1 Identification Form Template -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.1-customer#linebreak() - Version of 1 September 2021 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function to get value or false - let getb(key, default: false) = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Identification Form]) - - v(-1em) - line(length:100%) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold. - ] - ) - - v(1em) - - text(weight: "bold")[This form was completed by:] - - v(0.3em) - - table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [Full name], [#get("SIGNATURE")], - [Date], [#get("FILING_DATE")], - ) - - v(1.5em) - - // Section 1: Information on customer - text(size: 11pt, weight: "bold")[1. Information on customer#footnote[The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.]] - - v(0.5em) - - let is_natural = get("CUSTOMER_TYPE") == "NATURAL_PERSON" - let is_legal = get("CUSTOMER_TYPE") == "LEGAL_ENTITY" - - // Section 1a: Natural Person - grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(is_natural), - [a) The customer is a #underline([natural]) person:] - ) - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name\*], if is_natural { get("FULL_NAME") } else { "" }, - [Residential address\*], if is_natural { get("DOMICILE_ADDRESS") } else { "" }, - [Telephone], if is_natural { get("CONTACT_PHONE") } else { "" }, - [E-mail], if is_natural { get("CONTACT_EMAIL") } else { "" }, - [Date of birth\*], if is_natural { get("DATE_OF_BIRTH") } else { "" }, - [Nationality\*], if is_natural { get("NATIONALITY") } else { "" }, - [Identification document\*], [#checkbox(is_natural) *Copy enclosed in appendix*], - ) - #v(-1em) - #text(size: 8pt)[*\* mandatory*] - ] - v(1em) - - // Sole proprietorship section - text(weight: "bold")[For sole proprietorship (supplement to above):] - - let is_sole = is_natural and (get("CUSTOMER_IS_SOLE_PROPRIETOR") == true or get("CUSTOMER_IS_SOLE_PROPRIETOR") == "true") - - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Company name], if is_sole { get("COMPANY_NAME") } else { "" }, - [Registered office], if is_sole { get("REGISTERED_OFFICE_ADDRESS") } else { "" }, - [Company identification document], [#checkbox(is_sole) *Copy enclosed in appendix*], - ) - - v(1em) - - // Section 1b: Legal Entity - grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(is_legal), - [*b) The customer is a legal entity:*] - ) - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Company name\*], if is_legal { get("COMPANY_NAME") } else { "" }, - [Domicile\*], if is_legal { get("DOMICILE_ADDRESS") } else { "" }, - [Contact person], if is_legal { get("CONTACT_PERSON_NAME") } else { "" }, - [Telephone], if is_legal { get("CONTACT_PHONE") } else { "" }, - [E-mail], if is_legal { get("CONTACT_EMAIL") } else { "" }, - [Identification document\*\ (not older than 12 months)], [#checkbox(is_legal) *Copy enclosed in appendix*], - ) - - #v(-1em) - #text(size: 8pt)[*\* mandatory*] - ] - v(0.5em) - - // Section 2: Natural persons establishing business relationship - text(size: 11pt, weight: "bold")[2. Information on the natural persons who establish the business relationship for legal entities and partnerships] - - v(0.5em) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified. - ] - ) - - v(1em) - - let establishers = get("ESTABLISHER_LIST", default: ()) - let has_establishers = is_legal and type(establishers) == array and establishers.len() > 0 - - // Show at least 1 table for establishers to match VQF form - let num_cols = if has_establishers { calc.max(1, establishers.len()) } else { 1 } - - for col_idx in range(num_cols) { - if col_idx > 0 { - h(2em) - } - } - - // Create a table for each establisher - range(num_cols).map(col_idx => { - let establisher = if has_establishers and col_idx < establishers.len() { - establishers.at(col_idx) - } else { - (:) - } - - let get_est(key) = { - if establisher != (:) { - establisher.at(key, default: "") - } else { - "" - } - } - - block(breakable: false)[ - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Full name\*], - [#get_est("FULL_NAME")], - [Residential address\*], - [#get_est("DOMICILE_ADDRESS")], - [Date of birth\*], - [#get_est("DATE_OF_BIRTH")], - [Nationality\*], - [#get_est("NATIONALITY")], - [Type of authorisation\ (signatory of representation)\*], - [#get_est("SIGNING_AUTHORITY_TYPE")], - [Identification document\*], - [#checkbox(establisher != (:)) *Copy enclosed in appendix*], - [*Power of attorney arrangements\**], - [#let evidence = get_est("SIGNING_AUTHORITY_EVIDENCE") - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - row-gutter: 0.3em, - checkbox(evidence == "CR"), - [CR extract], - checkbox(evidence == "MANDATE"), - [Mandate], - checkbox(evidence == "OTHER"), - [Other: #get_est("SIGNING_AUTHORITY_EVIDENCE_OTHER")], - ) - ] - ) - #v(-1em) - #text(size: 8pt)[*\* mandatory*] - ] - }).join() - - - // Section 3: Acceptance of business relationship - text(size: 11pt, weight: "bold")[3. Acceptance of business relationship] - - v(0.5em) - - let lang = get("CORRESPONDENCE_LANGUAGE") - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Type of correspondence service], - [#grid( - columns: (auto), - gutter: 0.2em, - [#checkbox(true) to the customer], - [#checkbox(false) hold at bank], - [#checkbox(false) to the member], - [#checkbox(false) - to a third party (full name and address):], - )], - [Language], - [#grid( - columns: (auto), - gutter: 0.2em, - [#checkbox(lang == "de") German], - [#checkbox(lang == "en") English], - [#checkbox(lang == "fr") French], - [#checkbox(lang != "fr" and lang != "de" and lang != "en") - Other: #lang], - )], - ) - - - // Section 4: Beneficial owner - text(size: 11pt, weight: "bold")[4. Information on the beneficial owner of the assets and/or controlling person] - - v(0.5em) - - let customer_type_vqf = get("CUSTOMER_TYPE_VQF") - grid( - columns: (35%,65%), - stroke: 0.5pt + black, - inset: 5pt, - [Establishment of the beneficial owner of the assets and/or controlling person.], - [The customer is: - #grid( - columns: (1.1em, auto), - inset: 5pt, - [#checkbox(customer_type_vqf == "NATURAL_PERSON")], - [a natural person and there are no doubts that this person is the sole beneficial owner of the assets], - [#checkbox(customer_type_vqf == "OPERATIONAL")], - [an operational legal entity or partnership #h(1fr) - \ $=>$ VQF doc. No. 902.11 (K)], - [#checkbox(customer_type_vqf == "FOUNDATION")], - [a foundation (or a similar construct; incl. underlying companies) - \ $=>$ VQF doc. No. 902.12 (S)], - [#checkbox(customer_type_vqf == "TRUST")], - [a trust (incl. underlying companies) - \ $=>$ VQF doc. No. 902.13 (T)], - [#checkbox(customer_type_vqf == "LIFE_INSURANCE")], - [a life insurance policy with separately managed accounts/ securities accounts (so-called insurance wrappers) - \ $=>$ VQF doc. No. 902.15 (I)], - [#checkbox(customer_type_vqf == "OTHER")], - [all other cases - $=>$ VQF doc. No. 902.9 (A)], - )] - ) - - v(2em) - - // Section 5: Embargo/terrorism evaluation - text(size: 11pt, weight: "bold")[5. Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship] - - v(0.5em) - - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Verification whether the customer, beneficial owners of the assets, controlling persons, authorised representatives or other involved persons are listed on an embargo-/terrorism list (date of verification/result)#footnote[The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.]], - [Date: #get("EMBARGO_TERRORISM_CHECK_DATE") - \ Result: #get("EMBARGO_TERRORISM_CHECK_RESULT")], - ) - - v(2em) - - // Section 6: Cash transactions - text(size: 11pt, weight: "bold")[6. In the case of cash transactions/occasional customers: Information on type and purpose of business relationship] - - v(0.5em) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that #underline([no]) customer profile (VQF doc. No. 902.5) is created - ] - ) - - v(1em) - - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Type of business relationship], - [#grid( - columns: (auto), - gutter: 0.2em, - [#checkbox(false) Money exchange], - [#checkbox(false) Money and asset transfer], - [#checkbox(false) Other cash transaction, specify?], - )], - [Purpose of the business relationship\ (purpose of service requested)], [], - ) - - // Section 7: Enclosures - text(size: 11pt, weight: "bold")[7. Enclosures] - - v(0.5em) - - grid( - columns: (auto, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - checkbox("" != get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")), [Customer identification documents (or: reference#footnote[If the identification document is lists kept in another AMLA-File (in the case of Art. 15 para. 3 SRO Regulations) a reference to the according AMLA-File is sufficient.] to AMLA File No.: #underline([#get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")]))], - checkbox("" != get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")), [Identification document of persons establishing the business relationship (or: reference to AMLA File No.: #underline([#get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")]))], - checkbox(getb("HAVE_vqf_902_9") or getb("HAVE_vqf_902_11_customer") or getb("HAVE_vqf_902_11_officer") or getb("HAVE_vqf_902_12") or getb("HAVE_vqf_902_13") or getb("HAVE_vqf_902_15")), [Establishing of the beneficial owner of the assets/controlling person (VQF Doc No. 902.15, 902.9, 902.11, 902.12 or 902.13)], - checkbox(getb("HAVE_vqf_902_5")), [Customer profile (VQF doc. No. 902.5; only in the case of permanent business relationship and regular customers)], - checkbox(getb("HAVE_vqf_902_4")), [Risk profile (VQF doc. No. 902.4)], - ) - - v(1em) - - text(size: 9pt, style: "italic")[⚠ *This form has to be updated when changes occur.*] - -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "FILED_BY_NAME": "Manuela", - "FILING_DATE": "2000-4-1", - "CUSTOMER_TYPE": "NATURAL_PERSON", - "FULL_NAME": "John Doe", - "DOMICILE_ADDRESS": "123 Main St, 8001 Zurich", - "HAVE_vqf_902_9": false, - "HAVE_vqf_902_11": false, - "HAVE_vqf_902_12": false, - "HAVE_vqf_902_13": false, - "HAVE_vqf_902_15": true, - "HAVE_vqf_902_4": true, - "HAVE_vqf_902_5": true, - // ... other fields - )) diff --git a/contrib/typst/vqf_902_1_customer/Makefile.am b/contrib/typst/vqf_902_1_customer/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_1_customer/0.0.0/ +typstpackage_DATA = \ + vqf_902_1_customer.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_1_customer/template/main.typ b/contrib/typst/vqf_902_1_customer/template/main.typ @@ -0,0 +1,20 @@ +#import "@taler-exchange/vqf_902_1_customer:0.0.0": form + +// Example usage: +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "FILED_BY_NAME": "Manuela", + "FILING_DATE": "2000-4-1", + "CUSTOMER_TYPE": "NATURAL_PERSON", + "FULL_NAME": "John Doe", + "DOMICILE_ADDRESS": "123 Main St, 8001 Zurich", + "HAVE_vqf_902_9": false, + "HAVE_vqf_902_11": false, + "HAVE_vqf_902_12": false, + "HAVE_vqf_902_13": false, + "HAVE_vqf_902_15": true, + "HAVE_vqf_902_4": true, + "HAVE_vqf_902_5": true, + // ... other fields + )) diff --git a/contrib/typst/vqf_902_1_customer/typst.toml b/contrib/typst/vqf_902_1_customer/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_1_customer" +version = "0.0.0" +entrypoint = "vqf_902_1_customer.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "KYC"] diff --git a/contrib/typst/vqf_902_1_customer/vqf_902_1_customer.typ b/contrib/typst/vqf_902_1_customer/vqf_902_1_customer.typ @@ -0,0 +1,391 @@ +// VQF 902.1 Identification Form Template +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo, pointingfinger, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.1-customer#linebreak() + Version of 1 September 2021 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Helper function to get value or false + let getb(key, default: false) = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Identification Form]) + + v(-1em) + line(length:100%) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold. + ] + ) + + v(1em) + + text(weight: "bold")[This form was completed by:] + + v(0.3em) + + table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [Full name], [#get("SIGNATURE")], + [Date], [#get("FILING_DATE")], + ) + + v(1.5em) + + // Section 1: Information on customer + text(size: 11pt, weight: "bold")[1. Information on customer#footnote[The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.]] + + v(0.5em) + + let is_natural = get("CUSTOMER_TYPE") == "NATURAL_PERSON" + let is_legal = get("CUSTOMER_TYPE") == "LEGAL_ENTITY" + + // Section 1a: Natural Person + grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(is_natural), + [a) The customer is a #underline([natural]) person:] + ) + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name\*], if is_natural { get("FULL_NAME") } else { "" }, + [Residential address\*], if is_natural { get("DOMICILE_ADDRESS") } else { "" }, + [Telephone], if is_natural { get("CONTACT_PHONE") } else { "" }, + [E-mail], if is_natural { get("CONTACT_EMAIL") } else { "" }, + [Date of birth\*], if is_natural { get("DATE_OF_BIRTH") } else { "" }, + [Nationality\*], if is_natural { get("NATIONALITY") } else { "" }, + [Identification document\*], [#checkbox(is_natural) *Copy enclosed in appendix*], + ) + #v(-1em) + #text(size: 8pt)[*\* mandatory*] + ] + v(1em) + + // Sole proprietorship section + text(weight: "bold")[For sole proprietorship (supplement to above):] + + let is_sole = is_natural and (get("CUSTOMER_IS_SOLE_PROPRIETOR") == true or get("CUSTOMER_IS_SOLE_PROPRIETOR") == "true") + + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Company name], if is_sole { get("COMPANY_NAME") } else { "" }, + [Registered office], if is_sole { get("REGISTERED_OFFICE_ADDRESS") } else { "" }, + [Company identification document], [#checkbox(is_sole) *Copy enclosed in appendix*], + ) + + v(1em) + + // Section 1b: Legal Entity + grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(is_legal), + [*b) The customer is a legal entity:*] + ) + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Company name\*], if is_legal { get("COMPANY_NAME") } else { "" }, + [Domicile\*], if is_legal { get("DOMICILE_ADDRESS") } else { "" }, + [Contact person], if is_legal { get("CONTACT_PERSON_NAME") } else { "" }, + [Telephone], if is_legal { get("CONTACT_PHONE") } else { "" }, + [E-mail], if is_legal { get("CONTACT_EMAIL") } else { "" }, + [Identification document\*\ (not older than 12 months)], [#checkbox(is_legal) *Copy enclosed in appendix*], + ) + + #v(-1em) + #text(size: 8pt)[*\* mandatory*] + ] + v(0.5em) + + // Section 2: Natural persons establishing business relationship + text(size: 11pt, weight: "bold")[2. Information on the natural persons who establish the business relationship for legal entities and partnerships] + + v(0.5em) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified. + ] + ) + + v(1em) + + let establishers = get("ESTABLISHER_LIST", default: ()) + let has_establishers = is_legal and type(establishers) == array and establishers.len() > 0 + + // Show at least 1 table for establishers to match VQF form + let num_cols = if has_establishers { calc.max(1, establishers.len()) } else { 1 } + + for col_idx in range(num_cols) { + if col_idx > 0 { + h(2em) + } + } + + // Create a table for each establisher + range(num_cols).map(col_idx => { + let establisher = if has_establishers and col_idx < establishers.len() { + establishers.at(col_idx) + } else { + (:) + } + + let get_est(key) = { + if establisher != (:) { + establisher.at(key, default: "") + } else { + "" + } + } + + block(breakable: false)[ + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Full name\*], + [#get_est("FULL_NAME")], + [Residential address\*], + [#get_est("DOMICILE_ADDRESS")], + [Date of birth\*], + [#get_est("DATE_OF_BIRTH")], + [Nationality\*], + [#get_est("NATIONALITY")], + [Type of authorisation\ (signatory of representation)\*], + [#get_est("SIGNING_AUTHORITY_TYPE")], + [Identification document\*], + [#checkbox(establisher != (:)) *Copy enclosed in appendix*], + [*Power of attorney arrangements\**], + [#let evidence = get_est("SIGNING_AUTHORITY_EVIDENCE") + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + row-gutter: 0.3em, + checkbox(evidence == "CR"), + [CR extract], + checkbox(evidence == "MANDATE"), + [Mandate], + checkbox(evidence == "OTHER"), + [Other: #get_est("SIGNING_AUTHORITY_EVIDENCE_OTHER")], + ) + ] + ) + #v(-1em) + #text(size: 8pt)[*\* mandatory*] + ] + }).join() + + + // Section 3: Acceptance of business relationship + text(size: 11pt, weight: "bold")[3. Acceptance of business relationship] + + v(0.5em) + + let lang = get("CORRESPONDENCE_LANGUAGE") + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Type of correspondence service], + [#grid( + columns: (auto), + gutter: 0.2em, + [#checkbox(true) to the customer], + [#checkbox(false) hold at bank], + [#checkbox(false) to the member], + [#checkbox(false) + to a third party (full name and address):], + )], + [Language], + [#grid( + columns: (auto), + gutter: 0.2em, + [#checkbox(lang == "de") German], + [#checkbox(lang == "en") English], + [#checkbox(lang == "fr") French], + [#checkbox(lang != "fr" and lang != "de" and lang != "en") + Other: #lang], + )], + ) + + + // Section 4: Beneficial owner + text(size: 11pt, weight: "bold")[4. Information on the beneficial owner of the assets and/or controlling person] + + v(0.5em) + + let customer_type_vqf = get("CUSTOMER_TYPE_VQF") + grid( + columns: (35%,65%), + stroke: 0.5pt + black, + inset: 5pt, + [Establishment of the beneficial owner of the assets and/or controlling person.], + [The customer is: + #grid( + columns: (1.1em, auto), + inset: 5pt, + [#checkbox(customer_type_vqf == "NATURAL_PERSON")], + [a natural person and there are no doubts that this person is the sole beneficial owner of the assets], + [#checkbox(customer_type_vqf == "OPERATIONAL")], + [an operational legal entity or partnership #h(1fr) + \ $=>$ VQF doc. No. 902.11 (K)], + [#checkbox(customer_type_vqf == "FOUNDATION")], + [a foundation (or a similar construct; incl. underlying companies) + \ $=>$ VQF doc. No. 902.12 (S)], + [#checkbox(customer_type_vqf == "TRUST")], + [a trust (incl. underlying companies) + \ $=>$ VQF doc. No. 902.13 (T)], + [#checkbox(customer_type_vqf == "LIFE_INSURANCE")], + [a life insurance policy with separately managed accounts/ securities accounts (so-called insurance wrappers) + \ $=>$ VQF doc. No. 902.15 (I)], + [#checkbox(customer_type_vqf == "OTHER")], + [all other cases + $=>$ VQF doc. No. 902.9 (A)], + )] + ) + + v(2em) + + // Section 5: Embargo/terrorism evaluation + text(size: 11pt, weight: "bold")[5. Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship] + + v(0.5em) + + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Verification whether the customer, beneficial owners of the assets, controlling persons, authorised representatives or other involved persons are listed on an embargo-/terrorism list (date of verification/result)#footnote[The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.]], + [Date: #get("EMBARGO_TERRORISM_CHECK_DATE") + \ Result: #get("EMBARGO_TERRORISM_CHECK_RESULT")], + ) + + v(2em) + + // Section 6: Cash transactions + text(size: 11pt, weight: "bold")[6. In the case of cash transactions/occasional customers: Information on type and purpose of business relationship] + + v(0.5em) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that #underline([no]) customer profile (VQF doc. No. 902.5) is created + ] + ) + + v(1em) + + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Type of business relationship], + [#grid( + columns: (auto), + gutter: 0.2em, + [#checkbox(false) Money exchange], + [#checkbox(false) Money and asset transfer], + [#checkbox(false) Other cash transaction, specify?], + )], + [Purpose of the business relationship\ (purpose of service requested)], [], + ) + + // Section 7: Enclosures + text(size: 11pt, weight: "bold")[7. Enclosures] + + v(0.5em) + + grid( + columns: (auto, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + checkbox("" != get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")), [Customer identification documents (or: reference#footnote[If the identification document is lists kept in another AMLA-File (in the case of Art. 15 para. 3 SRO Regulations) a reference to the according AMLA-File is sufficient.] to AMLA File No.: #underline([#get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")]))], + checkbox("" != get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")), [Identification document of persons establishing the business relationship (or: reference to AMLA File No.: #underline([#get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")]))], + checkbox(getb("HAVE_vqf_902_9") or getb("HAVE_vqf_902_11_customer") or getb("HAVE_vqf_902_11_officer") or getb("HAVE_vqf_902_12") or getb("HAVE_vqf_902_13") or getb("HAVE_vqf_902_15")), [Establishing of the beneficial owner of the assets/controlling person (VQF Doc No. 902.15, 902.9, 902.11, 902.12 or 902.13)], + checkbox(getb("HAVE_vqf_902_5")), [Customer profile (VQF doc. No. 902.5; only in the case of permanent business relationship and regular customers)], + checkbox(getb("HAVE_vqf_902_4")), [Risk profile (VQF doc. No. 902.4)], + ) + + v(1em) + + text(size: 9pt, style: "italic")[⚠ *This form has to be updated when changes occur.*] + +} diff --git a/contrib/typst/vqf_902_1_officer.typ b/contrib/typst/vqf_902_1_officer.typ @@ -1,233 +0,0 @@ -// VQF 902.1 Identification Form Template -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.1-officer#linebreak() - Version of 1 September 2021 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function to get value or false - let getb(key, default: false) = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Identification Form]) - - v(-1em) - line(length:100%) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold. - ] - ) - - v(1em) - - text(weight: "bold")[This form was completed by:] - - v(0.3em) - - table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [Full name], [#get("AML_STAFF_NAME")], - [Date], [#get("FILING_DATE")], - ) - - v(1.5em) - - // Section 3: Acceptance of business relationship - text(size: 11pt, weight: "bold")[3. Acceptance of business relationship] - - v(0.5em) - - let acceptance = get("ACCEPTANCE_METHOD") - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Date (conclusion of contract)], get("ACCEPTANCE_DATE"), - [Accepted by], - [#grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(acceptance == "FACE_TO_FACE"), - [Face-to-face meeting with customer], - checkbox(acceptance == "WAY_OF_CORRESPONDENCE"), - [Way of correspondence: - \ #v(-0.7em) #grid( - columns: (0.2em, auto), - gutter: 0.5em, - row-gutter: 0.3em, - [], - [#checkbox(acceptance == "AUTHENTICATED_COPY") - authenticated copy of identification document obtained], - [], - [#checkbox(acceptance == "RESIDENTIAL_ADDRESS_VALIDATED") - residential address validated] - ) - ] - )], - [Type of correspondence service], - [#grid( - columns: (auto), - gutter: 0.2em, - [#checkbox(true) to the customer], - [#checkbox(false) hold at bank], - [#checkbox(false) to the member], - [#checkbox(false) - to a third party (full name and address):], - )], - [Further information], - [#get("ACCEPTANCE_FURTHER_INFO")] - ) - - v(2em) - - // Section 5: Embargo/terrorism evaluation - text(size: 11pt, weight: "bold")[5. Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship] - - v(0.5em) - - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Verification whether the customer, beneficial owners of the assets, controlling persons, authorised representatives or other involved persons are listed on an embargo-/terrorism list (date of verification/result)#footnote[The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.]], - [Date: #get("EMBARGO_TERRORISM_CHECK_DATE") - \ Result: #get("EMBARGO_TERRORISM_CHECK_RESULT")], - ) - - v(2em) - - // Section 6: Cash transactions - text(size: 11pt, weight: "bold")[6. In the case of cash transactions/occasional customers: Information on type and purpose of business relationship] - - v(0.5em) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that #underline([no]) customer profile (VQF doc. No. 902.5) is created - ] - ) - - v(1em) - - table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Type of business relationship], - [#grid( - columns: (auto), - gutter: 0.2em, - [#checkbox(false) Money exchange], - [#checkbox(false) Money and asset transfer], - [#checkbox(false) Other cash transaction, specify?], - )], - [Purpose of the business relationship\ (purpose of service requested)], [], - ) - - // Section 7: Enclosures - text(size: 11pt, weight: "bold")[7. Enclosures] - - v(0.5em) - - grid( - columns: (auto, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - checkbox("" != get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")), [Customer identification documents (or: reference#footnote[If the identification document is lists kept in another AMLA-File (in the case of Art. 15 para. 3 SRO Regulations) a reference to the according AMLA-File is sufficient.] to AMLA File No.: #underline([#get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")]))], - checkbox("" != get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")), [Identification document of persons establishing the business relationship (or: reference to AMLA File No.: #underline([#get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")]))], - checkbox(getb("HAVE_vqf_902_9") or getb("HAVE_vqf_902_11_customer") or getb("HAVE_vqf_902_11_officer") or getb("HAVE_vqf_902_12") or getb("HAVE_vqf_902_13") or getb("HAVE_vqf_902_15")), [Establishing of the beneficial owner of the assets/controlling person (VQF Doc No. 902.15, 902.9, 902.11, 902.12 or 902.13)], - checkbox(getb("HAVE_vqf_902_5")), [Customer profile (VQF doc. No. 902.5; only in the case of permanent business relationship and regular customers)], - checkbox(getb("HAVE_vqf_902_4")), [Risk profile (VQF doc. No. 902.4)], - ) - - v(1em) - - text(size: 9pt, style: "italic")[⚠ *This form has to be updated when changes occur.*] - -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "AML_STAFF_NAME": "Manuela", - "FILING_DATE": "2000-4-1", - "ACCEPTANCE_DATE": "2000-4-1", - "ACCEPTANCE_METHOD" : "FACE_TO_FACE", - // ... other fields - )) diff --git a/contrib/typst/vqf_902_1_officer/Makefile.am b/contrib/typst/vqf_902_1_officer/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_1_officer/0.0.0/ +typstpackage_DATA = \ + vqf_902_1_officer.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_1_officer/template/main.typ b/contrib/typst/vqf_902_1_officer/template/main.typ @@ -0,0 +1,11 @@ +#import "@taler-exchange/vqf_902_1_officer:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "AML_STAFF_NAME": "Manuela", + "FILING_DATE": "2000-4-1", + "ACCEPTANCE_DATE": "2000-4-1", + "ACCEPTANCE_METHOD" : "FACE_TO_FACE", + // ... other fields + )) diff --git a/contrib/typst/vqf_902_1_officer/typst.toml b/contrib/typst/vqf_902_1_officer/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_1_officer" +version = "0.0.0" +entrypoint = "vqf_902_1_officer.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_1_officer/vqf_902_1_officer.typ b/contrib/typst/vqf_902_1_officer/vqf_902_1_officer.typ @@ -0,0 +1,210 @@ +// VQF 902.1 Identification Form Template +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo, pointingfinger, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.1-officer#linebreak() + Version of 1 September 2021 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Helper function to get value or false + let getb(key, default: false) = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Identification Form]) + + v(-1em) + line(length:100%) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold. + ] + ) + + v(1em) + + text(weight: "bold")[This form was completed by:] + + v(0.3em) + + table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [Full name], [#get("AML_STAFF_NAME")], + [Date], [#get("FILING_DATE")], + ) + + v(1.5em) + + // Section 3: Acceptance of business relationship + text(size: 11pt, weight: "bold")[3. Acceptance of business relationship] + + v(0.5em) + + let acceptance = get("ACCEPTANCE_METHOD") + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Date (conclusion of contract)], get("ACCEPTANCE_DATE"), + [Accepted by], + [#grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(acceptance == "FACE_TO_FACE"), + [Face-to-face meeting with customer], + checkbox(acceptance == "WAY_OF_CORRESPONDENCE"), + [Way of correspondence: + \ #v(-0.7em) #grid( + columns: (0.2em, auto), + gutter: 0.5em, + row-gutter: 0.3em, + [], + [#checkbox(acceptance == "AUTHENTICATED_COPY") + authenticated copy of identification document obtained], + [], + [#checkbox(acceptance == "RESIDENTIAL_ADDRESS_VALIDATED") + residential address validated] + ) + ] + )], + [Type of correspondence service], + [#grid( + columns: (auto), + gutter: 0.2em, + [#checkbox(true) to the customer], + [#checkbox(false) hold at bank], + [#checkbox(false) to the member], + [#checkbox(false) + to a third party (full name and address):], + )], + [Further information], + [#get("ACCEPTANCE_FURTHER_INFO")] + ) + + v(2em) + + // Section 5: Embargo/terrorism evaluation + text(size: 11pt, weight: "bold")[5. Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship] + + v(0.5em) + + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Verification whether the customer, beneficial owners of the assets, controlling persons, authorised representatives or other involved persons are listed on an embargo-/terrorism list (date of verification/result)#footnote[The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.]], + [Date: #get("EMBARGO_TERRORISM_CHECK_DATE") + \ Result: #get("EMBARGO_TERRORISM_CHECK_RESULT")], + ) + + v(2em) + + // Section 6: Cash transactions + text(size: 11pt, weight: "bold")[6. In the case of cash transactions/occasional customers: Information on type and purpose of business relationship] + + v(0.5em) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that #underline([no]) customer profile (VQF doc. No. 902.5) is created + ] + ) + + v(1em) + + table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Type of business relationship], + [#grid( + columns: (auto), + gutter: 0.2em, + [#checkbox(false) Money exchange], + [#checkbox(false) Money and asset transfer], + [#checkbox(false) Other cash transaction, specify?], + )], + [Purpose of the business relationship\ (purpose of service requested)], [], + ) + + // Section 7: Enclosures + text(size: 11pt, weight: "bold")[7. Enclosures] + + v(0.5em) + + grid( + columns: (auto, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + checkbox("" != get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")), [Customer identification documents (or: reference#footnote[If the identification document is lists kept in another AMLA-File (in the case of Art. 15 para. 3 SRO Regulations) a reference to the according AMLA-File is sufficient.] to AMLA File No.: #underline([#get("CUSTOMER_ID_AMLA_FILE_REFERENCE_NO")]))], + checkbox("" != get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")), [Identification document of persons establishing the business relationship (or: reference to AMLA File No.: #underline([#get("ESTABLISHER_ID_AMLA_FILE_REFERENCE_NO")]))], + checkbox(getb("HAVE_vqf_902_9") or getb("HAVE_vqf_902_11_customer") or getb("HAVE_vqf_902_11_officer") or getb("HAVE_vqf_902_12") or getb("HAVE_vqf_902_13") or getb("HAVE_vqf_902_15")), [Establishing of the beneficial owner of the assets/controlling person (VQF Doc No. 902.15, 902.9, 902.11, 902.12 or 902.13)], + checkbox(getb("HAVE_vqf_902_5")), [Customer profile (VQF doc. No. 902.5; only in the case of permanent business relationship and regular customers)], + checkbox(getb("HAVE_vqf_902_4")), [Risk profile (VQF doc. No. 902.4)], + ) + + v(1em) + + text(size: 9pt, style: "italic")[⚠ *This form has to be updated when changes occur.*] + +} diff --git a/contrib/typst/vqf_902_4.typ b/contrib/typst/vqf_902_4.typ @@ -1,668 +0,0 @@ -// VQF 902.4 Risk Profile AMLA Template -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. No. 902.4#linebreak() - Version of 17 July 2020 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Risk Profile AMLA]) - - v(0.3em) - - text(size: 9pt, style: "italic")[Evaluation of business relationship with increased risk and definition of criteria for transaction monitoring] - - v(-1em) - line(length:100%) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - The member performs additional clarifications if the business relationship or the transaction is classified as increased risk (Art. 56 SRO Regulations) - ] - ) - - v(1em) - - table( - columns: (20%, 80%), - stroke: 0.5pt + black, - inset: 5pt, - [Customer#footnote([Pursuant identification form (VQF doc. Nr. 902.1) numeral 1.]):], - [#get("CUSTOMER_NAME")] - ) - - v(0.5em) - - text(weight: "bold")[This form was completed by:] - - v(0.3em) - - table( - columns: (20%, 80%), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [Full name], [#get("AML_STAFF_NAME")], - [Date], [#get("FILING_DATE")], - ) - - v(1.5em) - - // Section 1: PEP Check -[= 1. Evaluation of politically exposed persons (PEP-Check)] - - v(0.5em) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - This evaluation has to be completed by all members for every business relationship. - ] - ) - - v(1em) - - let pep_foreign = get("PEP_FOREIGN") - let pep_domestic = get("PEP_DOMESTIC") - let pep_intl = get("PEP_INTERNATIONAL_ORGANIZATION") - let any_pep = pep_foreign or pep_domestic or pep_intl - let pep_high_risk = get("PEP_HIGH_RISK") - - block(breakable: false)[ - #table( - columns: (40%, 60%), - stroke: 0.5pt + black, - inset: 5pt, - [#underline([Foreign PEP])#footnote[Definition see Art. 7 lit. g numeral 1 SRO Regulations.] - \ #v(1em) - Is the customer, the beneficial owner or the controlling person or authorised representative a foreign PEP or closely related to such a person?], - [#grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(not pep_foreign), [No], - checkbox(pep_foreign), [#grid( - columns: (auto, auto, auto), - gutter: 0.5em, - [Yes], - [$=>$], - [Classification of the business - \ as increased risk is #underline([compulsory]).])], - )], - - - [#underline( - [Domestic PEP#footnote[Definition see Art. 7 lit. g numeral 2 SRO Regulations.] - and PEP of International Organisations#footnote[Definition see Art. 7 lit. g numeral 3 SRO Regulations.]] - ) - \ #v(1em) - Is the customer, the beneficial owner or the controlling person or authorised representative a domestic PEP or PEP in International Organisations or closely related to such a person?], - [#grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(not (pep_domestic or pep_intl)), [No], - checkbox(pep_domestic or pep_intl), [#grid( - columns: (auto, auto, auto), - gutter: 0.5em, - [Yes], - [$=>$], - [Is a risk criterion pursuant to numeral 3 subsequently increased? - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(not pep_high_risk), [No], - checkbox(pep_high_risk), [#grid( - columns: (auto, auto, auto), - gutter: 0.5em, - [Yes], - [$=>$], - [Classification of the business relationship as increased risk is #underline([compulsory])] - )] - )], - )] - )] - ) - #if ("" == get("PEP_ACCEPTANCE_DATE")) [ - The decision of the Senior executive body on the acceptance of a business relationship - with a PEP was #underline([not]) obtained. - ] else [ - The decision of the Senior executive body on the acceptance of a business relationship - with a PEP was obtained on #underline([#get("PEP_ACCEPTANCE_DATE")]). - ] - ] - - v(2em) - pagebreak() - // Section 2: High Risk Country -[= 2. Evaluation "high risk" or non-cooperative country] - - v(0.5em) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - This evaluation has to be completed by all members for each business relationship. - ] - ) - - v(1em) - - let high_risk = get("HIGH_RISK_COUNTRY") - - block(breakable: false)[ - #table( - columns: (40%, 60%), - stroke: 0.5pt + black, - inset: 5pt, - [#underline(["High risk" or non-cooperative country]) - #v(1em) - Is the customer, the beneficial owner or the controlling person or authorised representative in a country considered by the FATF *"high risk"* or *non-cooperative* and for which FATF requires *increased diligence*?], - [#grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(not high_risk), [No], - checkbox(pep_foreign), [#grid( - columns: (auto, auto, auto), - gutter: 0.5em, - [Yes], - [$=>$], - [Classification of the business - \ as increased risk is #underline([compulsory]).])], - )], - ) - #if ("" == get("COUNTRY_RISK_ACCEPTANCE_DATE")) [ - The decision of the Senior executive body on the acceptance of such a business relationship - was #underline([not]) obtained. - ] else [ - The decision of the Senior executive body on the acceptance of a such a business relationship - was obtained on #underline([#get("COUNTRY_RISK_ACCEPTANCE_DATE")]). - ] - ] - - v(2em) - - // Section 3: Business Relationship Risk -[= 3. Evaluation of business relationship risk] - - v(0.5em) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - This evaluation has to be completed by all members who have in total more than 20 customers for every business relationship. At least two risk categories have to be chosen and assessed. - ] - ) - - v(1em) - - // Country Risk (Nationality) - let nat_types = get("COUNTRY_RISK_NATIONALITY_TYPE", default: ()) - let nat_level = get("COUNTRY_RISK_NATIONALITY_LEVEL") - - block(breakable: false)[ - #text(size: 10pt, weight: "bold")[a) Country risk#footnote[Risk 0 = Risk Classification "Low"; Risk 1 = Risk Classification "Medium"; Risk 2 = Risk Classification "High".] (nationality)] - - #v(0.5em) - - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [#checkbox(false) *Domicile/residential address:* - #grid( - columns: (1em, auto, 1fr), - inset: 1pt, - [], - checkbox(nat_types.contains("DOMICILE_CUSTOMER")), - [Customer], - [], - checkbox(nat_types.contains("DOMICILE_OWNER")), - [Beneficial owner of the assets], - [], - checkbox(nat_types.contains("DOMICILE_CONTROLLING")), - [Controlling person], - ) - \ #checkbox(false) *Nationality:* - #grid( - columns: (1em, auto, 1fr), - gutter: 0.3em, - [], - checkbox(nat_types.contains("NATIONALITY_CUSTOMER")), - [Customer], - [], - checkbox(nat_types.contains("NATIONALITY_OWNER")), - [Beneficial owner of the assets], - )], - [#grid( - columns: (auto, 1fr), - inset: 1em, - stroke: 0.5pt + black, - [0], - [#checkbox(nat_level == "LOW") - Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)], - [1], - [#checkbox(nat_level == "MEDIUM") - Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)], - [2], - [#checkbox(nat_level == "HIGH") - Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)], - )] - )] - - v(1em) - - // Country Risk (Business) - let bus_types = get("COUNTRY_RISK_BUSINESS_TYPE", default: ()) - let bus_level = get("COUNTRY_RISK_BUSINESS_LEVEL") - - block(breakable: false)[ - #text(size: 10pt, weight: "bold")[b) Country risk (business activity)] - - #v(0.5em) - - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [*Place of business activity:* - #grid( - columns: (1em, auto, 1fr), - gutter: 0.3em, - [], - checkbox(bus_types.contains("CUSTOMER")), - [Customer], - [], - checkbox(bus_types.contains("OWNER")), - [Beneficial owner of the assets], - )], - [#grid( - columns: (auto, 1fr), - inset: 1em, - stroke: 0.5pt + black, - [0], - [#checkbox(bus_level == "LOW") - Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)], - [1], - [#checkbox(bus_level == "MEDIUM") - Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)], - [2], - [#checkbox(bus_level == "HIGH") - Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)], - )] - )] - - v(1em) - - // Country Risk (Payments) - let pay_types = get("COUNTRY_RISK_PAYMENT_TYPE", default: ()) - let pay_level = get("COUNTRY_RISK_PAYMENTS_LEVEL") - - block(breakable: false)[ - #text(size: 10pt, weight: "bold")[c) Country risk (payments)] - - #v(0.5em) - - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [*Country of origin and destination of frequent payments (if known):* - #grid( - columns: (1em, auto, 1fr), - gutter: 0.3em, - [], - checkbox(bus_types.contains("CUSTOMER")), - [Customer], - [], - checkbox(bus_types.contains("OWNER")), - [Beneficial owner of the assets], - )], - [#grid( - columns: (auto, 1fr), - inset: 1em, - stroke: 0.5pt + black, - [0], - [#checkbox(pay_level == "LOW") - Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)], - [1], - [#checkbox(pay_level == "MEDIUM") - Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)], - [2], - [#checkbox(pay_level == "HIGH") - Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)], - )] - )] - - v(1em) - - - - // Industry Risk - let ind_types = get("INDUSTRY_RISK_TYPE", default: ()) - let ind_level = get("INDUSTRY_RISK_LEVEL") - - block(breakable: false)[ - #text(size: 10pt, weight: "bold")[d) Industry risk] - - #v(0.5em) - - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [*Nature of customer's business activity:* - #grid( - columns: (1em, auto, 1fr), - gutter: 0.3em, - [], - checkbox(ind_types.contains("CUSTOMER")), - [Customer], - [], - checkbox(ind_types.contains("OWNER")), - [Beneficial owner of the assets], - )], - [#grid( - columns: (auto, 1fr), - inset: 0.5em, - stroke: 0.5pt + black, - [0], - [#grid( - columns: (auto, 1fr), - inset: 0.2em, - [#checkbox(ind_level == "TRANSPARENT")], - [Clearly defined, transparent, easily comprehensible business activity well known to the member])], - [1], - [#grid( - columns: (auto, 1fr), - inset: 0.2em, - [#checkbox(ind_level == "HIGH_CASH_TRANSACTION")], - [Business activity with a high level of cash transactions], - [#checkbox(ind_level == "NOT_WELL_KNOWN")], - [Business activity not well known to the member])], - [2], - [#grid( - columns: (auto, 1fr), - inset: 0.2em, - [#checkbox(ind_level == "HIGH_RISK_TRADE")], - [Trade in munitions/arms, raw gem stones/diamonds, jewellery, international trade in exotic animals, casino and lottery business, trade in erotic wares], - [#checkbox(ind_level == "UNKNOWN_INDUSTRY")], - [Member has no personal knowledge of the customer's industry])] - )] - )] - - v(1em) - - // Contact Risk - let contact_level = get("CONTACT_RISK_LEVEL") - block(breakable: false)[ - #text(size: 10pt, weight: "bold")[e) Contact risk] - - #v(0.3em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [*Type of contact to the customer/beneficial owner of the assets.*], - [#grid( - columns: (auto, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - [0], - [#grid( - columns: (auto, 1fr), - inset: 0.2em, - [#checkbox(contact_level == "LOW")], - [Personal acquaintance between member and customer/beneficial owner of the assets over several years (at least 2) prior to entering into the business relationship.], - )], - [1], - [#grid( - columns: (auto, 1fr), - inset: 0.2em, - [#checkbox(contact_level == "MEDIUM")], - [The customer/beneficial owner was not personally known to the member for several years (at least 2) prior to entering into the business relationship; however (a) no business was entered into in the absence of the customer/beneficial owner, or (b) the customer was at least introduced/brokered by a trusted third party.], - )], - [2], - [#grid( - columns: (auto, 1fr), - inset: 0.2em, - [#checkbox(contact_level == "HIGH")], - [The customer/beneficial owner was not personally known to the member and business was entered into in the absence of the former (relationship by correspondence) and the customer was not introduced/brokered by a trusted third party.], - )] - )] - )] - - v(2em) - - // Product Risk - let prod_level = get("PRODUCT_RISK_LEVEL", default: ()) - block(breakable: false)[ - #text(size: 10pt, weight: "bold")[f) Product risk] - - #v(0.3em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [*Nature of services and products requested by the customer*], - [#grid( - columns: (auto, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - [0], - [#grid( - columns: (auto, 1fr), - inset: 0.2em, - [#checkbox(prod_level.contains("EASY"))], - [Easy to understand, transparent services and products whose financial background is easy to comprehend and verify.], - )], - [1], - [#grid( - columns: (auto, 1fr), - inset: 0.2em, - [#checkbox(prod_level.contains("SOPHISTICATED"))], - [More sophisticated services/products whose financial background is not readily easy to comprehend and verify.], - )], - [2], - [#grid( - columns: (auto, 1fr), - inset: 0.2em, - [#checkbox(prod_level.contains("HIGH_OFFSHORE"))], - [Main focus on offshore business (especially: relationships with domiciliary companies or other such offshore organisations) #v(0.5em)], - [#checkbox(prod_level.contains("HIGH_COMPLEX"))], - [Complex structures in particular by using a domiciliary company with fiduciary shareholders in a non-transparent jurisdiction, without comprehensible reason or for the purpose of short-term asset placement #v(0.5em)], - [#checkbox(prod_level.contains("HIGH_PASSTHROUGH"))], - [The customer or beneficial owner of the assets has a large number of accounts with pass-through transactions (pass-through accounts) #v(0.5em)], - [#checkbox(prod_level.contains("HIGH_BACKGROUND"))], - [Complex services/products whose financial background can’t be understood or verified with considerable effort. #v(0.5em)], - [#checkbox(prod_level.contains("HIGH_FREQUENT_TMER"))], - [Frequent transactions with increased risks #v(0.5em)], - )] - )] - )] - - v(2em) - - - // Summary Evaluation - text(size: 11pt, weight: "bold")[Overall assessment of the business relationship] - - v(0.5em) - - block( - fill: rgb("#f0f0f0"), - inset: 10pt, - width: 100%, - )[ - A business relationship is classified as increased risk if: - - - Business relationship with PEP pursuant to numeral 1 (no exception possible) - - Relationship with a person from a "high risk" or non-cooperative country according to numeral 2 (no exceptions possible) - - Min. #underline[one] criterion pursuant to numeral 3 was assessed with #underline[risk 2] or min. #underline[two] criteria pursuant to numeral 3 were assessed with risk 1 (exception: justification by the member below why the business relationship overall does not have to be classified as increased risk despite the fact that individual risk criteria are increased). - ] - - v(1em) - - let risk_class = get("RISK_CLASSIFICATION_LEVEL") - let risk_class_low = "NO_HIGH_RISK" == risk_class - - table( - columns: (40%, 60%), - stroke: 0.5pt + black, - inset: 5pt, - [Justification for differing risk assessment], - [#get("RISK_RATIONALE")], - [Risk classified], - [#grid( - columns: (auto), - gutter: 1em, - [#checkbox(risk_class_low) *Business relationship #underline[without] increased risk*], - [#checkbox(not risk_class_low) *Business relationship #underline[with] increased risk*], - )] - ) - - v(0.5em) - - if ("" == get("HIGH_RISK_ACCEPTANCE_DATE")) [ - The decision of the Senior executive body on the acceptance of a business relationship with increased risk was #underline([not]) obtained. - ] else [ - The decision of the Senior executive body on the acceptance of a business relationship with increased risk was obtained on #underline([#get("RISK_ACCEPTANCE_DATE")]). - ] - -pagebreak() - -[ -= 4. Criteria for identification of increased risk transactions (transaction monitoring) - -== 4.1 Criteria - -Classification as as increased risk is compulsory if: - -+ Transactions for which assets with an equivalent value of CHF 100'000.- or more are physically introduced at the beginning of the business relationship, either at once or in a staggered manner -+ Money and asset transfers ("money transfer") whereby a single transaction or multiple transactions which appear to be related reach or exceed the amount of CHF 5,000.- -+ Payments from or to a country that is considered to be "high risk" or non-cooperative by the FATF and for which increased diligence is required - -== 4.2 Additional criteria defined by the member - - #grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - All members have to define min. 1 additional criterion for every business relationship to identify unusual transactions#footnote[ - Possible criteria (Art. 59 para. 2 SRO Regulations): -- the amount of inflowing and outflowing assets; *or* -- type, volume and frequency of transactions usual to the business relationship (considerable variance would be unusual); *or* -- type, volume and frequency of transactions usual to comparable business relationships (considerable variance would be unusual); *or* -- description of expected transaction patterns which the client notify the member of (considerable variance would be unusual); *or* -- The country of origin or destination of payments, especially in the case of payments from or to a country considered by the FATF as "high risk" or non-cooperative.] - ] - ) - -] - - grid( - columns: (100%), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [#get("TRANSACTION_RISK_RULE")] - ) - -v(2em) - -text(size: 9pt, style: "italic")[⚠ *This form has to be updated immediately if changes occur.*] -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "CUSTOMER_NAME": "Example Company AG", - "FILED_BY_NAME": "Jane Smith", - "FILING_DATE": "10.11.2025", - "PEP_FOREIGN": false, - "PEP_DOMESTIC": false, - "PEP_HIGH_RISK": false, - "PEP_ACCEPTANCE_DATE": "2024-02-13", - "ANY_RISK_CRITERION": false, - "PEP_INTERNATIONAL_ORGANIZATION": false, - "HIGH_RISK_COUNTRY": false, - "COUNTRY_RISK_ACCEPTANCE_DATE": "2024-2-13", - "COUNTRY_RISK_NATIONALITY_LEVEL": "LOW", - "INDUSTRY_RISK_LEVEL": "TRANSPARENT", - "CONTACT_RISK_LEVEL": "LOW", - "RISK_CLASSIFICATION_LEVEL": "NO_HIGH_RISK", - "TRANSACTION_RISK_RULE": "more than 50k/month", - "HIGH_RISK_ACCEPTANCE_DATE": "2024-3-20", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_4/Makefile.am b/contrib/typst/vqf_902_4/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_4/0.0.0/ +typstpackage_DATA = \ + vqf_902_4.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_4/template/main.typ b/contrib/typst/vqf_902_4/template/main.typ @@ -0,0 +1,23 @@ +#import "@taler-exchange/vqf_902_4:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "CUSTOMER_NAME": "Example Company AG", + "FILED_BY_NAME": "Jane Smith", + "FILING_DATE": "10.11.2025", + "PEP_FOREIGN": false, + "PEP_DOMESTIC": false, + "PEP_HIGH_RISK": false, + "PEP_ACCEPTANCE_DATE": "2024-02-13", + "ANY_RISK_CRITERION": false, + "PEP_INTERNATIONAL_ORGANIZATION": false, + "HIGH_RISK_COUNTRY": false, + "COUNTRY_RISK_ACCEPTANCE_DATE": "2024-2-13", + "COUNTRY_RISK_NATIONALITY_LEVEL": "LOW", + "INDUSTRY_RISK_LEVEL": "TRANSPARENT", + "CONTACT_RISK_LEVEL": "LOW", + "RISK_CLASSIFICATION_LEVEL": "NO_HIGH_RISK", + "TRANSACTION_RISK_RULE": "more than 50k/month", + "HIGH_RISK_ACCEPTANCE_DATE": "2024-3-20", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_4/typst.toml b/contrib/typst/vqf_902_4/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_4" +version = "0.0.0" +entrypoint = "vqf_902_4.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_4/vqf_902_4.typ b/contrib/typst/vqf_902_4/vqf_902_4.typ @@ -0,0 +1,633 @@ +// VQF 902.4 Risk Profile AMLA Template +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo, pointingfinger, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. No. 902.4#linebreak() + Version of 17 July 2020 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Risk Profile AMLA]) + + v(0.3em) + + text(size: 9pt, style: "italic")[Evaluation of business relationship with increased risk and definition of criteria for transaction monitoring] + + v(-1em) + line(length:100%) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + The member performs additional clarifications if the business relationship or the transaction is classified as increased risk (Art. 56 SRO Regulations) + ] + ) + + v(1em) + + table( + columns: (20%, 80%), + stroke: 0.5pt + black, + inset: 5pt, + [Customer#footnote([Pursuant identification form (VQF doc. Nr. 902.1) numeral 1.]):], + [#get("CUSTOMER_NAME")] + ) + + v(0.5em) + + text(weight: "bold")[This form was completed by:] + + v(0.3em) + + table( + columns: (20%, 80%), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [Full name], [#get("AML_STAFF_NAME")], + [Date], [#get("FILING_DATE")], + ) + + v(1.5em) + + // Section 1: PEP Check +[= 1. Evaluation of politically exposed persons (PEP-Check)] + + v(0.5em) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + This evaluation has to be completed by all members for every business relationship. + ] + ) + + v(1em) + + let pep_foreign = get("PEP_FOREIGN") + let pep_domestic = get("PEP_DOMESTIC") + let pep_intl = get("PEP_INTERNATIONAL_ORGANIZATION") + let any_pep = pep_foreign or pep_domestic or pep_intl + let pep_high_risk = get("PEP_HIGH_RISK") + + block(breakable: false)[ + #table( + columns: (40%, 60%), + stroke: 0.5pt + black, + inset: 5pt, + [#underline([Foreign PEP])#footnote[Definition see Art. 7 lit. g numeral 1 SRO Regulations.] + \ #v(1em) + Is the customer, the beneficial owner or the controlling person or authorised representative a foreign PEP or closely related to such a person?], + [#grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(not pep_foreign), [No], + checkbox(pep_foreign), [#grid( + columns: (auto, auto, auto), + gutter: 0.5em, + [Yes], + [$=>$], + [Classification of the business + \ as increased risk is #underline([compulsory]).])], + )], + + + [#underline( + [Domestic PEP#footnote[Definition see Art. 7 lit. g numeral 2 SRO Regulations.] + and PEP of International Organisations#footnote[Definition see Art. 7 lit. g numeral 3 SRO Regulations.]] + ) + \ #v(1em) + Is the customer, the beneficial owner or the controlling person or authorised representative a domestic PEP or PEP in International Organisations or closely related to such a person?], + [#grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(not (pep_domestic or pep_intl)), [No], + checkbox(pep_domestic or pep_intl), [#grid( + columns: (auto, auto, auto), + gutter: 0.5em, + [Yes], + [$=>$], + [Is a risk criterion pursuant to numeral 3 subsequently increased? + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(not pep_high_risk), [No], + checkbox(pep_high_risk), [#grid( + columns: (auto, auto, auto), + gutter: 0.5em, + [Yes], + [$=>$], + [Classification of the business relationship as increased risk is #underline([compulsory])] + )] + )], + )] + )] + ) + #if ("" == get("PEP_ACCEPTANCE_DATE")) [ + The decision of the Senior executive body on the acceptance of a business relationship + with a PEP was #underline([not]) obtained. + ] else [ + The decision of the Senior executive body on the acceptance of a business relationship + with a PEP was obtained on #underline([#get("PEP_ACCEPTANCE_DATE")]). + ] + ] + + v(2em) + pagebreak() + // Section 2: High Risk Country +[= 2. Evaluation "high risk" or non-cooperative country] + + v(0.5em) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + This evaluation has to be completed by all members for each business relationship. + ] + ) + + v(1em) + + let high_risk = get("HIGH_RISK_COUNTRY") + + block(breakable: false)[ + #table( + columns: (40%, 60%), + stroke: 0.5pt + black, + inset: 5pt, + [#underline(["High risk" or non-cooperative country]) + #v(1em) + Is the customer, the beneficial owner or the controlling person or authorised representative in a country considered by the FATF *"high risk"* or *non-cooperative* and for which FATF requires *increased diligence*?], + [#grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(not high_risk), [No], + checkbox(pep_foreign), [#grid( + columns: (auto, auto, auto), + gutter: 0.5em, + [Yes], + [$=>$], + [Classification of the business + \ as increased risk is #underline([compulsory]).])], + )], + ) + #if ("" == get("COUNTRY_RISK_ACCEPTANCE_DATE")) [ + The decision of the Senior executive body on the acceptance of such a business relationship + was #underline([not]) obtained. + ] else [ + The decision of the Senior executive body on the acceptance of a such a business relationship + was obtained on #underline([#get("COUNTRY_RISK_ACCEPTANCE_DATE")]). + ] + ] + + v(2em) + + // Section 3: Business Relationship Risk +[= 3. Evaluation of business relationship risk] + + v(0.5em) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + This evaluation has to be completed by all members who have in total more than 20 customers for every business relationship. At least two risk categories have to be chosen and assessed. + ] + ) + + v(1em) + + // Country Risk (Nationality) + let nat_types = get("COUNTRY_RISK_NATIONALITY_TYPE", default: ()) + let nat_level = get("COUNTRY_RISK_NATIONALITY_LEVEL") + + block(breakable: false)[ + #text(size: 10pt, weight: "bold")[a) Country risk#footnote[Risk 0 = Risk Classification "Low"; Risk 1 = Risk Classification "Medium"; Risk 2 = Risk Classification "High".] (nationality)] + + #v(0.5em) + + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [#checkbox(false) *Domicile/residential address:* + #grid( + columns: (1em, auto, 1fr), + inset: 1pt, + [], + checkbox(nat_types.contains("DOMICILE_CUSTOMER")), + [Customer], + [], + checkbox(nat_types.contains("DOMICILE_OWNER")), + [Beneficial owner of the assets], + [], + checkbox(nat_types.contains("DOMICILE_CONTROLLING")), + [Controlling person], + ) + \ #checkbox(false) *Nationality:* + #grid( + columns: (1em, auto, 1fr), + gutter: 0.3em, + [], + checkbox(nat_types.contains("NATIONALITY_CUSTOMER")), + [Customer], + [], + checkbox(nat_types.contains("NATIONALITY_OWNER")), + [Beneficial owner of the assets], + )], + [#grid( + columns: (auto, 1fr), + inset: 1em, + stroke: 0.5pt + black, + [0], + [#checkbox(nat_level == "LOW") + Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)], + [1], + [#checkbox(nat_level == "MEDIUM") + Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)], + [2], + [#checkbox(nat_level == "HIGH") + Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)], + )] + )] + + v(1em) + + // Country Risk (Business) + let bus_types = get("COUNTRY_RISK_BUSINESS_TYPE", default: ()) + let bus_level = get("COUNTRY_RISK_BUSINESS_LEVEL") + + block(breakable: false)[ + #text(size: 10pt, weight: "bold")[b) Country risk (business activity)] + + #v(0.5em) + + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [*Place of business activity:* + #grid( + columns: (1em, auto, 1fr), + gutter: 0.3em, + [], + checkbox(bus_types.contains("CUSTOMER")), + [Customer], + [], + checkbox(bus_types.contains("OWNER")), + [Beneficial owner of the assets], + )], + [#grid( + columns: (auto, 1fr), + inset: 1em, + stroke: 0.5pt + black, + [0], + [#checkbox(bus_level == "LOW") + Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)], + [1], + [#checkbox(bus_level == "MEDIUM") + Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)], + [2], + [#checkbox(bus_level == "HIGH") + Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)], + )] + )] + + v(1em) + + // Country Risk (Payments) + let pay_types = get("COUNTRY_RISK_PAYMENT_TYPE", default: ()) + let pay_level = get("COUNTRY_RISK_PAYMENTS_LEVEL") + + block(breakable: false)[ + #text(size: 10pt, weight: "bold")[c) Country risk (payments)] + + #v(0.5em) + + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [*Country of origin and destination of frequent payments (if known):* + #grid( + columns: (1em, auto, 1fr), + gutter: 0.3em, + [], + checkbox(bus_types.contains("CUSTOMER")), + [Customer], + [], + checkbox(bus_types.contains("OWNER")), + [Beneficial owner of the assets], + )], + [#grid( + columns: (auto, 1fr), + inset: 1em, + stroke: 0.5pt + black, + [0], + [#checkbox(pay_level == "LOW") + Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)], + [1], + [#checkbox(pay_level == "MEDIUM") + Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)], + [2], + [#checkbox(pay_level == "HIGH") + Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)], + )] + )] + + v(1em) + + + + // Industry Risk + let ind_types = get("INDUSTRY_RISK_TYPE", default: ()) + let ind_level = get("INDUSTRY_RISK_LEVEL") + + block(breakable: false)[ + #text(size: 10pt, weight: "bold")[d) Industry risk] + + #v(0.5em) + + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [*Nature of customer's business activity:* + #grid( + columns: (1em, auto, 1fr), + gutter: 0.3em, + [], + checkbox(ind_types.contains("CUSTOMER")), + [Customer], + [], + checkbox(ind_types.contains("OWNER")), + [Beneficial owner of the assets], + )], + [#grid( + columns: (auto, 1fr), + inset: 0.5em, + stroke: 0.5pt + black, + [0], + [#grid( + columns: (auto, 1fr), + inset: 0.2em, + [#checkbox(ind_level == "TRANSPARENT")], + [Clearly defined, transparent, easily comprehensible business activity well known to the member])], + [1], + [#grid( + columns: (auto, 1fr), + inset: 0.2em, + [#checkbox(ind_level == "HIGH_CASH_TRANSACTION")], + [Business activity with a high level of cash transactions], + [#checkbox(ind_level == "NOT_WELL_KNOWN")], + [Business activity not well known to the member])], + [2], + [#grid( + columns: (auto, 1fr), + inset: 0.2em, + [#checkbox(ind_level == "HIGH_RISK_TRADE")], + [Trade in munitions/arms, raw gem stones/diamonds, jewellery, international trade in exotic animals, casino and lottery business, trade in erotic wares], + [#checkbox(ind_level == "UNKNOWN_INDUSTRY")], + [Member has no personal knowledge of the customer's industry])] + )] + )] + + v(1em) + + // Contact Risk + let contact_level = get("CONTACT_RISK_LEVEL") + block(breakable: false)[ + #text(size: 10pt, weight: "bold")[e) Contact risk] + + #v(0.3em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [*Type of contact to the customer/beneficial owner of the assets.*], + [#grid( + columns: (auto, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + [0], + [#grid( + columns: (auto, 1fr), + inset: 0.2em, + [#checkbox(contact_level == "LOW")], + [Personal acquaintance between member and customer/beneficial owner of the assets over several years (at least 2) prior to entering into the business relationship.], + )], + [1], + [#grid( + columns: (auto, 1fr), + inset: 0.2em, + [#checkbox(contact_level == "MEDIUM")], + [The customer/beneficial owner was not personally known to the member for several years (at least 2) prior to entering into the business relationship; however (a) no business was entered into in the absence of the customer/beneficial owner, or (b) the customer was at least introduced/brokered by a trusted third party.], + )], + [2], + [#grid( + columns: (auto, 1fr), + inset: 0.2em, + [#checkbox(contact_level == "HIGH")], + [The customer/beneficial owner was not personally known to the member and business was entered into in the absence of the former (relationship by correspondence) and the customer was not introduced/brokered by a trusted third party.], + )] + )] + )] + + v(2em) + + // Product Risk + let prod_level = get("PRODUCT_RISK_LEVEL", default: ()) + block(breakable: false)[ + #text(size: 10pt, weight: "bold")[f) Product risk] + + #v(0.3em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [*Nature of services and products requested by the customer*], + [#grid( + columns: (auto, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + [0], + [#grid( + columns: (auto, 1fr), + inset: 0.2em, + [#checkbox(prod_level.contains("EASY"))], + [Easy to understand, transparent services and products whose financial background is easy to comprehend and verify.], + )], + [1], + [#grid( + columns: (auto, 1fr), + inset: 0.2em, + [#checkbox(prod_level.contains("SOPHISTICATED"))], + [More sophisticated services/products whose financial background is not readily easy to comprehend and verify.], + )], + [2], + [#grid( + columns: (auto, 1fr), + inset: 0.2em, + [#checkbox(prod_level.contains("HIGH_OFFSHORE"))], + [Main focus on offshore business (especially: relationships with domiciliary companies or other such offshore organisations) #v(0.5em)], + [#checkbox(prod_level.contains("HIGH_COMPLEX"))], + [Complex structures in particular by using a domiciliary company with fiduciary shareholders in a non-transparent jurisdiction, without comprehensible reason or for the purpose of short-term asset placement #v(0.5em)], + [#checkbox(prod_level.contains("HIGH_PASSTHROUGH"))], + [The customer or beneficial owner of the assets has a large number of accounts with pass-through transactions (pass-through accounts) #v(0.5em)], + [#checkbox(prod_level.contains("HIGH_BACKGROUND"))], + [Complex services/products whose financial background can’t be understood or verified with considerable effort. #v(0.5em)], + [#checkbox(prod_level.contains("HIGH_FREQUENT_TMER"))], + [Frequent transactions with increased risks #v(0.5em)], + )] + )] + )] + + v(2em) + + + // Summary Evaluation + text(size: 11pt, weight: "bold")[Overall assessment of the business relationship] + + v(0.5em) + + block( + fill: rgb("#f0f0f0"), + inset: 10pt, + width: 100%, + )[ + A business relationship is classified as increased risk if: + + - Business relationship with PEP pursuant to numeral 1 (no exception possible) + - Relationship with a person from a "high risk" or non-cooperative country according to numeral 2 (no exceptions possible) + - Min. #underline[one] criterion pursuant to numeral 3 was assessed with #underline[risk 2] or min. #underline[two] criteria pursuant to numeral 3 were assessed with risk 1 (exception: justification by the member below why the business relationship overall does not have to be classified as increased risk despite the fact that individual risk criteria are increased). + ] + + v(1em) + + let risk_class = get("RISK_CLASSIFICATION_LEVEL") + let risk_class_low = "NO_HIGH_RISK" == risk_class + + table( + columns: (40%, 60%), + stroke: 0.5pt + black, + inset: 5pt, + [Justification for differing risk assessment], + [#get("RISK_RATIONALE")], + [Risk classified], + [#grid( + columns: (auto), + gutter: 1em, + [#checkbox(risk_class_low) *Business relationship #underline[without] increased risk*], + [#checkbox(not risk_class_low) *Business relationship #underline[with] increased risk*], + )] + ) + + v(0.5em) + + if ("" == get("HIGH_RISK_ACCEPTANCE_DATE")) [ + The decision of the Senior executive body on the acceptance of a business relationship with increased risk was #underline([not]) obtained. + ] else [ + The decision of the Senior executive body on the acceptance of a business relationship with increased risk was obtained on #underline([#get("RISK_ACCEPTANCE_DATE")]). + ] + +pagebreak() + +[ += 4. Criteria for identification of increased risk transactions (transaction monitoring) + +== 4.1 Criteria + +Classification as as increased risk is compulsory if: + ++ Transactions for which assets with an equivalent value of CHF 100'000.- or more are physically introduced at the beginning of the business relationship, either at once or in a staggered manner ++ Money and asset transfers ("money transfer") whereby a single transaction or multiple transactions which appear to be related reach or exceed the amount of CHF 5,000.- ++ Payments from or to a country that is considered to be "high risk" or non-cooperative by the FATF and for which increased diligence is required + +== 4.2 Additional criteria defined by the member + + #grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + All members have to define min. 1 additional criterion for every business relationship to identify unusual transactions#footnote[ + Possible criteria (Art. 59 para. 2 SRO Regulations): +- the amount of inflowing and outflowing assets; *or* +- type, volume and frequency of transactions usual to the business relationship (considerable variance would be unusual); *or* +- type, volume and frequency of transactions usual to comparable business relationships (considerable variance would be unusual); *or* +- description of expected transaction patterns which the client notify the member of (considerable variance would be unusual); *or* +- The country of origin or destination of payments, especially in the case of payments from or to a country considered by the FATF as "high risk" or non-cooperative.] + ] + ) + +] + + grid( + columns: (100%), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [#get("TRANSACTION_RISK_RULE")] + ) + +v(2em) + +text(size: 9pt, style: "italic")[⚠ *This form has to be updated immediately if changes occur.*] +} diff --git a/contrib/typst/vqf_902_5.typ b/contrib/typst/vqf_902_5.typ @@ -1,248 +0,0 @@ -// VQF 902.5 Customer Profile Template -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.5#linebreak() - Version of 1 December 2015 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Customer Profile]) - - v(0.3em) - - text(size: 9pt, style: "italic")[For permanent business relationship and regular customers] - - v(-1em) - line(length:100%) - - grid( - columns: (auto, 1fr), - gutter: 0.5em, - align: (left, left), - image("pointing_finger.svg", height: 2em), - text(size: 9pt)[ - The information below has to refer to the persons from whom the assets originate ultimately (e.g. beneficial owner of the assets, founder/creator of a trust or foundation). Is the customer an operational legal entity or partnership the information may refer to the entity itself (not to the controlling person), unless the entity holds the assets in trust for a third party. - ] - ) - - v(1em) - - v(0.3em) - - table( - columns: (20%,80%), - stroke: 0.5pt + black, - inset: 5pt, - [Customer:#footnote[Pursuant Identification Form (VQF doc. No. 902.1) numeral 1.]], - [#get("CUSTOMER_NAME")] - ) - - v(0.5em) - - text(weight: "bold")[This form was completed by:] - - v(0.3em) - - table( - columns: (20%, 80%), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [Full name], [#get("AML_STAFF_NAME")], - [Date], [#get("FILING_DATE")], - ) - - v(1.5em) - - // Section 1: Business Activity - text(size: 11pt, weight: "bold")[1. Business activity] - - v(0.5em) - - table( - columns: (40%, 60%), - stroke: 0.5pt + black, - inset: 5pt, - [Profession, business activities etc. (former, current, potentially planned)], - [#get("BIZREL_PROFESSION")] - ) - - v(1.5em) - - // Section 2: Financial Circumstances - text(size: 11pt, weight: "bold")[2. Financial circumstances] - - v(0.5em) - - table( - columns: (40%, 60%), - stroke: 0.5pt + black, - inset: 5pt, - [Income and assets, liabilities (estimated)], - [#get("BIZREL_INCOME")] - ) - - v(1.5em) - - // Section 3: Origin of Deposited Assets - text(size: 11pt, weight: "bold")[3. Origin of the deposited assets involved] - - v(0.5em) - - let origin_cat = get("BIZREL_ORIGIN_CATEGORY") - - block(breakable: false)[ - #table( - columns: (40%, 60%), - stroke: 0.5pt + black, - inset: 5pt, - [Nature, amount and currency of the involved assets], - [#get("BIZREL_ORIGIN_NATURE")], - [Category], - [#grid( - columns: (auto, 1fr), - gutter: 0.5em, - checkbox(origin_cat == "SAVINGS"), [Savings], - checkbox(origin_cat == "OWN_BUSINESS"), [Own business operations], - checkbox(origin_cat == "INHERITANCE"), [Inheritance], - checkbox(origin_cat == "OTHER"), [Other, what? #get("BIZREL_ORIGIN_CATEGORY_OTHER")], - )], - [Detailed description of the origins/economical background of the assets involved in the business relationship], - [#get("BIZREL_ORIGIN_DETAIL")] - ) - ] - - v(1.5em) - pagebreak() - - // Section 4: Nature and Purpose - text(size: 11pt, weight: "bold")[4. Nature and purpose of the business relationship] - - v(0.5em) - - table( - columns: (40%, 60%), - stroke: 0.5pt + black, - inset: 5pt, - [Purpose of the business relationship], - [#get("BIZREL_PURPOSE")], - [Information on the planned development of the business relationship and the assets], - [#get("BIZREL_DEVELOPMENT")], - [Especially in the case of cash or money and asset transfer transactions with regular customers: - \ - Details on usual business volume - \ - Information on the beneficiaries - \ (Full name, address, bank account)], - [#get("BIZREL_FINANCIAL_VOLUME")] - ) - - v(1.5em) - - // Section 5: Relationship with Third Parties - text(size: 11pt, weight: "bold")[5. Relationship with third parties] - - v(0.5em) - - table( - columns: (40%, 60%), - stroke: 0.5pt + black, - inset: 5pt, - [Relation of the customer to the beneficial owner, controlling persons, authorised signatories and other persons involved in the business relationship], - [#get("BIZREL_THIRDPARTY_RELATIONSHIP")], - [Relation to other AMLA-Files], - [#get("BIZREL_THIRDPARTY_AMLA_FILES")], - [Introducer / agents / references], - [#get("BIZREL_THIRDPARTY_REFERENCES")] - ) - - v(1.5em) - - // Section 6: Further Information - text(size: 11pt, weight: "bold")[6. Further information] - - v(0.5em) - - table( - columns: (40%, 60%), - stroke: 0.5pt + black, - inset: 5pt, - [Other relevant information], - [#get("BIZREL_FURTHER_INFO")] - ) - - v(2em) - - text(size: 9pt, style: "italic")[⚠ *This form has to be updated when changes occur.*] -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "CUSTOMER_NAME": "John Doe", - "FILED_BY_NAME": "Jane Smith", - "FILING_DATE": "10.11.2025", - "BIZREL_PROFESSION": "Software Engineer", - "BIZREL_INCOME": "Annual income CHF 150,000", - "BIZREL_ORIGIN_NATURE": "CHF 50,000 in cash", - "BIZREL_ORIGIN_CATEGORY": "SAVINGS", - "BIZREL_PURPOSE": "Payment services", - "BIZREL_DEVELOPMENT": "Regular transactions expected", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_5/Makefile.am b/contrib/typst/vqf_902_5/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_5/0.0.0/ +typstpackage_DATA = \ + vqf_902_5.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_5/template/main.typ b/contrib/typst/vqf_902_5/template/main.typ @@ -0,0 +1,15 @@ +#import "@taler-exchange/vqf_902_5:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "CUSTOMER_NAME": "John Doe", + "FILED_BY_NAME": "Jane Smith", + "FILING_DATE": "10.11.2025", + "BIZREL_PROFESSION": "Software Engineer", + "BIZREL_INCOME": "Annual income CHF 150,000", + "BIZREL_ORIGIN_NATURE": "CHF 50,000 in cash", + "BIZREL_ORIGIN_CATEGORY": "SAVINGS", + "BIZREL_PURPOSE": "Payment services", + "BIZREL_DEVELOPMENT": "Regular transactions expected", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_5/typst.toml b/contrib/typst/vqf_902_5/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_5" +version = "0.0.0" +entrypoint = "vqf_902_5.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_5/vqf_902_5.typ b/contrib/typst/vqf_902_5/vqf_902_5.typ @@ -0,0 +1,221 @@ +// VQF 902.5 Customer Profile Template +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo, pointingfinger, checkbox + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.5#linebreak() + Version of 1 December 2015 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Customer Profile]) + + v(0.3em) + + text(size: 9pt, style: "italic")[For permanent business relationship and regular customers] + + v(-1em) + line(length:100%) + + grid( + columns: (auto, 1fr), + gutter: 0.5em, + align: (left, left), + pointingfinger(), + text(size: 9pt)[ + The information below has to refer to the persons from whom the assets originate ultimately (e.g. beneficial owner of the assets, founder/creator of a trust or foundation). Is the customer an operational legal entity or partnership the information may refer to the entity itself (not to the controlling person), unless the entity holds the assets in trust for a third party. + ] + ) + + v(1em) + + v(0.3em) + + table( + columns: (20%,80%), + stroke: 0.5pt + black, + inset: 5pt, + [Customer:#footnote[Pursuant Identification Form (VQF doc. No. 902.1) numeral 1.]], + [#get("CUSTOMER_NAME")] + ) + + v(0.5em) + + text(weight: "bold")[This form was completed by:] + + v(0.3em) + + table( + columns: (20%, 80%), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [Full name], [#get("AML_STAFF_NAME")], + [Date], [#get("FILING_DATE")], + ) + + v(1.5em) + + // Section 1: Business Activity + text(size: 11pt, weight: "bold")[1. Business activity] + + v(0.5em) + + table( + columns: (40%, 60%), + stroke: 0.5pt + black, + inset: 5pt, + [Profession, business activities etc. (former, current, potentially planned)], + [#get("BIZREL_PROFESSION")] + ) + + v(1.5em) + + // Section 2: Financial Circumstances + text(size: 11pt, weight: "bold")[2. Financial circumstances] + + v(0.5em) + + table( + columns: (40%, 60%), + stroke: 0.5pt + black, + inset: 5pt, + [Income and assets, liabilities (estimated)], + [#get("BIZREL_INCOME")] + ) + + v(1.5em) + + // Section 3: Origin of Deposited Assets + text(size: 11pt, weight: "bold")[3. Origin of the deposited assets involved] + + v(0.5em) + + let origin_cat = get("BIZREL_ORIGIN_CATEGORY") + + block(breakable: false)[ + #table( + columns: (40%, 60%), + stroke: 0.5pt + black, + inset: 5pt, + [Nature, amount and currency of the involved assets], + [#get("BIZREL_ORIGIN_NATURE")], + [Category], + [#grid( + columns: (auto, 1fr), + gutter: 0.5em, + checkbox(origin_cat == "SAVINGS"), [Savings], + checkbox(origin_cat == "OWN_BUSINESS"), [Own business operations], + checkbox(origin_cat == "INHERITANCE"), [Inheritance], + checkbox(origin_cat == "OTHER"), [Other, what? #get("BIZREL_ORIGIN_CATEGORY_OTHER")], + )], + [Detailed description of the origins/economical background of the assets involved in the business relationship], + [#get("BIZREL_ORIGIN_DETAIL")] + ) + ] + + v(1.5em) + pagebreak() + + // Section 4: Nature and Purpose + text(size: 11pt, weight: "bold")[4. Nature and purpose of the business relationship] + + v(0.5em) + + table( + columns: (40%, 60%), + stroke: 0.5pt + black, + inset: 5pt, + [Purpose of the business relationship], + [#get("BIZREL_PURPOSE")], + [Information on the planned development of the business relationship and the assets], + [#get("BIZREL_DEVELOPMENT")], + [Especially in the case of cash or money and asset transfer transactions with regular customers: + \ - Details on usual business volume + \ - Information on the beneficiaries + \ (Full name, address, bank account)], + [#get("BIZREL_FINANCIAL_VOLUME")] + ) + + v(1.5em) + + // Section 5: Relationship with Third Parties + text(size: 11pt, weight: "bold")[5. Relationship with third parties] + + v(0.5em) + + table( + columns: (40%, 60%), + stroke: 0.5pt + black, + inset: 5pt, + [Relation of the customer to the beneficial owner, controlling persons, authorised signatories and other persons involved in the business relationship], + [#get("BIZREL_THIRDPARTY_RELATIONSHIP")], + [Relation to other AMLA-Files], + [#get("BIZREL_THIRDPARTY_AMLA_FILES")], + [Introducer / agents / references], + [#get("BIZREL_THIRDPARTY_REFERENCES")] + ) + + v(1.5em) + + // Section 6: Further Information + text(size: 11pt, weight: "bold")[6. Further information] + + v(0.5em) + + table( + columns: (40%, 60%), + stroke: 0.5pt + black, + inset: 5pt, + [Other relevant information], + [#get("BIZREL_FURTHER_INFO")] + ) + + v(2em) + + text(size: 9pt, style: "italic")[⚠ *This form has to be updated when changes occur.*] +} diff --git a/contrib/typst/vqf_902_9.typ b/contrib/typst/vqf_902_9.typ @@ -1,210 +0,0 @@ -// VQF 902.9 Declaration of identity of the beneficial owner (A) -// Pass JSON data as content dictionary -// NOTE: This is the original form. We don't use -// it as this form was split in customer + officer parts. -// Preserved here in case some auditor insists on us -// combining the two! - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.9#linebreak() - Version of 1 December 2015 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Declaration of identity of the beneficial owner (A)]) - - v(-1em) - line(length:100%) - - v(1em) - - // Section 1: Contracting Partner - text(size: 11pt, weight: "bold")[Contracting partner:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("IDENTITY_CONTRACTING_PARTNER")] - ) - - v(1em) - - text()[The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below] - - v(1em) - - // Section 2: Beneficial Owners - let owners = get("IDENTITY_LIST", default: ()) - let has_owners = type(owners) == array and owners.len() > 0 - - if has_owners { - for owner in owners { - let get_owner(key) = { - owner.at(key, default: "") - } - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Fullname:], [#get_owner("FULL_NAME")], - [Date of birth:], [#get_owner("DATE_OF_BIRTH")], - [Nationality:], [#get_owner("NATIONALITY")], - [Actual address of domicile:], [#get_owner("DOMICILE_ADDRESS")] - ) - #v(0.5em) - ] - } - } else { - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Surname(s):], [], - [First name(s):], [], - [Date(s) of birth:], [], - [Nationality:], [], - [Actual address of domicile:], [] - ) - #v(0.5em) - ] - } - - v(1em) - - text()[The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein.] - - v(1.5em) - - // Signature Section - let submitted_by_officer = get("BY_AML_OFFICER") - - if submitted_by_officer == false { - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date:], - [], - [Signature(s):], - [#get("SIGN_DATE")], - [], - [#get("SIGNATURE")] - ) - - v(1em) - - text(size: 9pt, style: "italic")[ - It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery) - ] - } else if submitted_by_officer == true { - text(weight: "bold")[Signed declaration by the customer] - - v(0.5em) - - text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] - - v(0.5em) - - text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [Signed Document:], - [#if (get("ATTACHMENT_SIGNED_DOCUMENT") != ""){ - [Document attached] - } else { - [No document] - } - ] - ) - } else { - text(weight: "bold")[Invalid submitter (#submitted_by_officer)] - } -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", - "IDENTITY_LIST": ( - ( - "FULL_NAME": "John Doe", - "DATE_OF_BIRTH": "01.01.1980", - "NATIONALITY": "CH", - "DOMICILE_ADDRESS": "Teststrasse 123\n8001 Zurich" - ), - ), - "BY_AML_OFFICER": false, - "SIGNATURE": "John Doe", - "SIGN_DATE": "10.11.2025", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_9/Makefile.am b/contrib/typst/vqf_902_9/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_9/0.0.0/ +typstpackage_DATA = \ + vqf_902_9.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_9/template/main.typ b/contrib/typst/vqf_902_9/template/main.typ @@ -0,0 +1,18 @@ +#import "@taler-exchange/vqf_902_9:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", + "IDENTITY_LIST": ( + ( + "FULL_NAME": "John Doe", + "DATE_OF_BIRTH": "01.01.1980", + "NATIONALITY": "CH", + "DOMICILE_ADDRESS": "Teststrasse 123\n8001 Zurich" + ), + ), + "BY_AML_OFFICER": false, + "SIGNATURE": "John Doe", + "SIGN_DATE": "10.11.2025", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_9/typst.toml b/contrib/typst/vqf_902_9/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_9" +version = "0.0.0" +entrypoint = "vqf_902_9.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_9/vqf_902_9.typ b/contrib/typst/vqf_902_9/vqf_902_9.typ @@ -0,0 +1,180 @@ +// VQF 902.9 Declaration of identity of the beneficial owner (A) +// Pass JSON data as content dictionary +// NOTE: This is the original form. We don't use +// it as this form was split in customer + officer parts. +// Preserved here in case some auditor insists on us +// combining the two! +#import "@taler-exchange/common:0.0.0": vqflogo + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.9#linebreak() + Version of 1 December 2015 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Declaration of identity of the beneficial owner (A)]) + + v(-1em) + line(length:100%) + + v(1em) + + // Section 1: Contracting Partner + text(size: 11pt, weight: "bold")[Contracting partner:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("IDENTITY_CONTRACTING_PARTNER")] + ) + + v(1em) + + text()[The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below] + + v(1em) + + // Section 2: Beneficial Owners + let owners = get("IDENTITY_LIST", default: ()) + let has_owners = type(owners) == array and owners.len() > 0 + + if has_owners { + for owner in owners { + let get_owner(key) = { + owner.at(key, default: "") + } + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Fullname:], [#get_owner("FULL_NAME")], + [Date of birth:], [#get_owner("DATE_OF_BIRTH")], + [Nationality:], [#get_owner("NATIONALITY")], + [Actual address of domicile:], [#get_owner("DOMICILE_ADDRESS")] + ) + #v(0.5em) + ] + } + } else { + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Surname(s):], [], + [First name(s):], [], + [Date(s) of birth:], [], + [Nationality:], [], + [Actual address of domicile:], [] + ) + #v(0.5em) + ] + } + + v(1em) + + text()[The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein.] + + v(1.5em) + + // Signature Section + let submitted_by_officer = get("BY_AML_OFFICER") + + if submitted_by_officer == false { + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date:], + [], + [Signature(s):], + [#get("SIGN_DATE")], + [], + [#get("SIGNATURE")] + ) + + v(1em) + + text(size: 9pt, style: "italic")[ + It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery) + ] + } else if submitted_by_officer == true { + text(weight: "bold")[Signed declaration by the customer] + + v(0.5em) + + text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] + + v(0.5em) + + text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [Signed Document:], + [#if (get("ATTACHMENT_SIGNED_DOCUMENT") != ""){ + [Document attached] + } else { + [No document] + } + ] + ) + } else { + text(weight: "bold")[Invalid submitter (#submitted_by_officer)] + } +} diff --git a/contrib/typst/vqf_902_9_customer.typ b/contrib/typst/vqf_902_9_customer.typ @@ -1,206 +0,0 @@ -// VQF 902.9 Declaration of identity of the beneficial owner (A) -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.9#linebreak() - Version of 1 December 2015 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Declaration of identity of the beneficial owner (A)]) - - v(-1em) - line(length:100%) - - v(1em) - - // Section 1: Contracting Partner - text(size: 11pt, weight: "bold")[Contracting partner:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("IDENTITY_CONTRACTING_PARTNER")] - ) - - v(1em) - - text()[The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below] - - v(1em) - - // Section 2: Beneficial Owners - let owners = get("IDENTITY_LIST", default: ()) - let has_owners = type(owners) == array and owners.len() > 0 - - if has_owners { - for owner in owners { - let get_owner(key) = { - owner.at(key, default: "") - } - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Fullname:], [#get_owner("FULL_NAME")], - [Date of birth:], [#get_owner("DATE_OF_BIRTH")], - [Nationality:], [#get_owner("NATIONALITY")], - [Actual address of domicile:], [#get_owner("DOMICILE_ADDRESS")] - ) - #v(0.5em) - ] - } - } else { - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Surname(s):], [], - [First name(s):], [], - [Date(s) of birth:], [], - [Nationality:], [], - [Actual address of domicile:], [] - ) - #v(0.5em) - ] - } - - v(1em) - - text()[The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein.] - - v(1.5em) - - // Signature Section - let submitted_by_officer = get("BY_AML_OFFICER") - - if submitted_by_officer == false { - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date:], - [], - [Signature(s):], - [#get("SIGN_DATE")], - [], - [#get("SIGNATURE")] - ) - - v(1em) - - text(size: 9pt, style: "italic")[ - It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery) - ] - } else if submitted_by_officer == true { - text(weight: "bold")[Signed declaration by the customer] - - v(0.5em) - - text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] - - v(0.5em) - - text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [Signed Document:], - [#if (get("ATTACHMENT_SIGNED_DOCUMENT") != ""){ - [Document attached] - } else { - [No document] - } - ] - ) - } else { - text(weight: "bold")[Invalid submitter (#submitted_by_officer)] - } -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "FILE_NUMBER": "42", - "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", - "IDENTITY_LIST": ( - ( - "FULL_NAME": "John Doe", - "DATE_OF_BIRTH": "01.01.1980", - "NATIONALITY": "CH", - "DOMICILE_ADDRESS": "Teststrasse 123\n8001 Zurich" - ), - ), - "BY_AML_OFFICER": false, - "SIGNATURE": "John Doe", - "SIGN_DATE": "10.11.2025", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_9_customer/Makefile.am b/contrib/typst/vqf_902_9_customer/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_9_customer/0.0.0/ +typstpackage_DATA = \ + vqf_902_9_customer.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_9_customer/template/main.typ b/contrib/typst/vqf_902_9_customer/template/main.typ @@ -0,0 +1,18 @@ +#import "@taler-exchange/vqf_902_9_customer:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "FILE_NUMBER": "42", + "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", + "IDENTITY_LIST": ( + ( + "FULL_NAME": "John Doe", + "DATE_OF_BIRTH": "01.01.1980", + "NATIONALITY": "CH", + "DOMICILE_ADDRESS": "Teststrasse 123\n8001 Zurich" + ), + ), + "BY_AML_OFFICER": false, + "SIGNATURE": "John Doe", + "SIGN_DATE": "10.11.2025", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_9_customer/typst.toml b/contrib/typst/vqf_902_9_customer/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_9_customer" +version = "0.0.0" +entrypoint = "vqf_902_9_customer.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_9_customer/vqf_902_9_customer.typ b/contrib/typst/vqf_902_9_customer/vqf_902_9_customer.typ @@ -0,0 +1,176 @@ +// VQF 902.9 Declaration of identity of the beneficial owner (A) +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.9#linebreak() + Version of 1 December 2015 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Declaration of identity of the beneficial owner (A)]) + + v(-1em) + line(length:100%) + + v(1em) + + // Section 1: Contracting Partner + text(size: 11pt, weight: "bold")[Contracting partner:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("IDENTITY_CONTRACTING_PARTNER")] + ) + + v(1em) + + text()[The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below] + + v(1em) + + // Section 2: Beneficial Owners + let owners = get("IDENTITY_LIST", default: ()) + let has_owners = type(owners) == array and owners.len() > 0 + + if has_owners { + for owner in owners { + let get_owner(key) = { + owner.at(key, default: "") + } + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Fullname:], [#get_owner("FULL_NAME")], + [Date of birth:], [#get_owner("DATE_OF_BIRTH")], + [Nationality:], [#get_owner("NATIONALITY")], + [Actual address of domicile:], [#get_owner("DOMICILE_ADDRESS")] + ) + #v(0.5em) + ] + } + } else { + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Surname(s):], [], + [First name(s):], [], + [Date(s) of birth:], [], + [Nationality:], [], + [Actual address of domicile:], [] + ) + #v(0.5em) + ] + } + + v(1em) + + text()[The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein.] + + v(1.5em) + + // Signature Section + let submitted_by_officer = get("BY_AML_OFFICER") + + if submitted_by_officer == false { + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date:], + [], + [Signature(s):], + [#get("SIGN_DATE")], + [], + [#get("SIGNATURE")] + ) + + v(1em) + + text(size: 9pt, style: "italic")[ + It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery) + ] + } else if submitted_by_officer == true { + text(weight: "bold")[Signed declaration by the customer] + + v(0.5em) + + text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] + + v(0.5em) + + text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [Signed Document:], + [#if (get("ATTACHMENT_SIGNED_DOCUMENT") != ""){ + [Document attached] + } else { + [No document] + } + ] + ) + } else { + text(weight: "bold")[Invalid submitter (#submitted_by_officer)] + } +} diff --git a/contrib/typst/vqf_902_9_officer.typ b/contrib/typst/vqf_902_9_officer.typ @@ -1,207 +0,0 @@ -// VQF 902.9 Declaration of identity of the beneficial owner (A) -// Pass JSON data as content dictionary - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[ - VQF doc. Nr. 902.9#linebreak() - Version of 1 December 2015 - ], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - set text(font: "Liberation Sans", size: 10pt) - set par(justify: false, leading: 0.65em) - - // Helper function to get value or empty string - let get(key, default: "") = { - data.at(key, default: default) - } - - // Helper function for checkbox - let checkbox(checked) = { - box( - width: 3mm, - height: 3mm, - stroke: 0.5pt + black, - inset: 0.3mm, - if checked == true or checked == "true" { - place(center + horizon, text(size: 8pt, sym.checkmark)) - } - ) - } - - // Header - align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) - - v(0.5em) - - grid( - columns: (50%, 50%), - gutter: 1em, - image("vss_vqf_verein.png", width: 80%), - align(right)[ - #table( - columns: (1fr, 1fr), - stroke: 0.5pt + black, - inset: 5pt, - align: (left, left), - [VQF member no.], [AMLA File No.], - [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] - ) - ] - ) - - v(1em) - - align(left, text(size: 14pt, weight: "bold")[Declaration of identity of the beneficial owner (A)]) - - v(-1em) - line(length:100%) - - v(1em) - - // Section 1: Contracting Partner - text(size: 11pt, weight: "bold")[Contracting partner:] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [#get("IDENTITY_CONTRACTING_PARTNER")] - ) - - v(1em) - - text()[The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below] - - v(1em) - - // Section 2: Beneficial Owners - let owners = get("IDENTITY_LIST", default: ()) - let has_owners = type(owners) == array and owners.len() > 0 - - if has_owners { - for owner in owners { - let get_owner(key) = { - owner.at(key, default: "") - } - - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Fullname:], [#get_owner("FULL_NAME")], - [Date of birth:], [#get_owner("DATE_OF_BIRTH")], - [Nationality:], [#get_owner("NATIONALITY")], - [Actual address of domicile:], [#get_owner("DOMICILE_ADDRESS")] - ) - #v(0.5em) - ] - } - } else { - block(breakable: false)[ - #v(0.5em) - #table( - columns: (35%, 65%), - stroke: 0.5pt + black, - inset: 5pt, - [Surname(s):], [], - [First name(s):], [], - [Date(s) of birth:], [], - [Nationality:], [], - [Actual address of domicile:], [] - ) - #v(0.5em) - ] - } - - v(1em) - - text()[The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein.] - - v(1.5em) - - // Signature Section - let submitted_by_officer = get("BY_AML_OFFICER") - - if submitted_by_officer == false { - table( - columns: (40%, 10%, 50%), - stroke: 0.5pt + black, - inset: 5pt, - [Date:], - [], - [Signature(s):], - [#get("SIGN_DATE")], - [], - [#get("SIGNATURE")] - ) - - v(1em) - - text(size: 9pt, style: "italic")[ - It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery) - ] - } else if submitted_by_officer == true { - text(weight: "bold")[Signed declaration by the customer] - - v(0.5em) - - text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] - - v(0.5em) - - text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] - - v(0.5em) - - table( - columns: (1fr), - stroke: 0.5pt + black, - inset: 5pt, - [Signed Document:], - [#if (get("ATTACHMENT_SIGNED_DOCUMENT") != ""){ - [Document attached] - } else { - [No document] - } - ] - ) - } else { - text(weight: "bold")[Invalid submitter (#submitted_by_officer)] - } -} - -// Example usage: -#form(( - "VQF_MEMBER_NUMBER": "12345", - "AML_STAFF_NAME" : "Manuela", - "FILE_NUMBER": "42", - "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", - "IDENTITY_LIST": ( - ( - "FULL_NAME": "John Doe", - "DATE_OF_BIRTH": "01.01.1980", - "NATIONALITY": "CH", - "DOMICILE_ADDRESS": "Teststrasse 123\n8001 Zurich" - ), - ), - "BY_AML_OFFICER": true, - "SIGNATURE": "John Doe", - "SIGN_DATE": "10.11.2025", -)) -\ No newline at end of file diff --git a/contrib/typst/vqf_902_9_officer/Makefile.am b/contrib/typst/vqf_902_9_officer/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = . + +typstpackagedir = $(prefix)/.local/share/typst/packages/taler-exchange/vqf_902_9_officer/0.0.0/ +typstpackage_DATA = \ + vqf_902_9_officer.typ \ + typst.toml + +EXTRA_DIST = \ + template/main.typ \ + $(typstpackage_DATA) diff --git a/contrib/typst/vqf_902_9_officer/template/main.typ b/contrib/typst/vqf_902_9_officer/template/main.typ @@ -0,0 +1,19 @@ +#import "@taler-exchange/vqf_902_9_officer:0.0.0": form + +#form(( + "VQF_MEMBER_NUMBER": "12345", + "AML_STAFF_NAME" : "Manuela", + "FILE_NUMBER": "42", + "IDENTITY_CONTRACTING_PARTNER": "Example Company AG\nBahnhofstrasse 1\n8001 Zurich\nSwitzerland", + "IDENTITY_LIST": ( + ( + "FULL_NAME": "John Doe", + "DATE_OF_BIRTH": "01.01.1980", + "NATIONALITY": "CH", + "DOMICILE_ADDRESS": "Teststrasse 123\n8001 Zurich" + ), + ), + "BY_AML_OFFICER": true, + "SIGNATURE": "John Doe", + "SIGN_DATE": "10.11.2025", +)) +\ No newline at end of file diff --git a/contrib/typst/vqf_902_9_officer/typst.toml b/contrib/typst/vqf_902_9_officer/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "vqf_902_9_officer" +version = "0.0.0" +entrypoint = "vqf_902_9_officer.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler exchange PDF generation in the AML SPA" +repository = "git://git.taler.net/exchange" +keywords = ["VQF", "AML", "KYC"] diff --git a/contrib/typst/vqf_902_9_officer/vqf_902_9_officer.typ b/contrib/typst/vqf_902_9_officer/vqf_902_9_officer.typ @@ -0,0 +1,176 @@ +// VQF 902.9 Declaration of identity of the beneficial owner (A) +// Pass JSON data as content dictionary +#import "@taler-exchange/common:0.0.0": vqflogo + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[ + VQF doc. Nr. 902.9#linebreak() + Version of 1 December 2015 + ], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + set text(font: "Liberation Sans", size: 10pt) + set par(justify: false, leading: 0.65em) + + // Helper function to get value or empty string + let get(key, default: "") = { + data.at(key, default: default) + } + + // Header + align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL]) + + v(0.5em) + + grid( + columns: (50%, 50%), + gutter: 1em, + vqflogo(), + align(right)[ + #table( + columns: (1fr, 1fr), + stroke: 0.5pt + black, + inset: 5pt, + align: (left, left), + [VQF member no.], [AMLA File No.], + [#get("VQF_MEMBER_NUMBER")], [#get("FILE_NUMBER")] + ) + ] + ) + + v(1em) + + align(left, text(size: 14pt, weight: "bold")[Declaration of identity of the beneficial owner (A)]) + + v(-1em) + line(length:100%) + + v(1em) + + // Section 1: Contracting Partner + text(size: 11pt, weight: "bold")[Contracting partner:] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [#get("IDENTITY_CONTRACTING_PARTNER")] + ) + + v(1em) + + text()[The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below] + + v(1em) + + // Section 2: Beneficial Owners + let owners = get("IDENTITY_LIST", default: ()) + let has_owners = type(owners) == array and owners.len() > 0 + + if has_owners { + for owner in owners { + let get_owner(key) = { + owner.at(key, default: "") + } + + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Fullname:], [#get_owner("FULL_NAME")], + [Date of birth:], [#get_owner("DATE_OF_BIRTH")], + [Nationality:], [#get_owner("NATIONALITY")], + [Actual address of domicile:], [#get_owner("DOMICILE_ADDRESS")] + ) + #v(0.5em) + ] + } + } else { + block(breakable: false)[ + #v(0.5em) + #table( + columns: (35%, 65%), + stroke: 0.5pt + black, + inset: 5pt, + [Surname(s):], [], + [First name(s):], [], + [Date(s) of birth:], [], + [Nationality:], [], + [Actual address of domicile:], [] + ) + #v(0.5em) + ] + } + + v(1em) + + text()[The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein.] + + v(1.5em) + + // Signature Section + let submitted_by_officer = get("BY_AML_OFFICER") + + if submitted_by_officer == false { + table( + columns: (40%, 10%, 50%), + stroke: 0.5pt + black, + inset: 5pt, + [Date:], + [], + [Signature(s):], + [#get("SIGN_DATE")], + [], + [#get("SIGNATURE")] + ) + + v(1em) + + text(size: 9pt, style: "italic")[ + It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery) + ] + } else if submitted_by_officer == true { + text(weight: "bold")[Signed declaration by the customer] + + v(0.5em) + + text(size: 9pt)[This form was submitted by #get("AML_STAFF_NAME").] + + v(0.5em) + + text(size: 9pt)[The attachment contains the customer's signature on the beneficial owner declaration.] + + v(0.5em) + + table( + columns: (1fr), + stroke: 0.5pt + black, + inset: 5pt, + [Signed Document:], + [#if (get("ATTACHMENT_SIGNED_DOCUMENT") != ""){ + [Document attached] + } else { + [No document] + } + ] + ) + } else { + text(weight: "bold")[Invalid submitter (#submitted_by_officer)] + } +} diff --git a/debian/control b/debian/control @@ -130,6 +130,15 @@ Description: Tools for managing the GNU Taler exchange offline keys. from the exchange, create signatures, and upload the resulting signatures to the exchange. +Package: taler-exchange-typst +Architecture: any +Recommends: + pdftk +Description: Typst packages for GNU Taler exchange. + This package contains Typst packages used by the + exchange for PDF generation. It should be installed + alongside Typst and pdftk for PDF generation. + Package: taler-auditor Architecture: any Pre-Depends: diff --git a/debian/taler-exchange-typst.install b/debian/taler-exchange-typst.install @@ -0,0 +1,4 @@ +# The local Typst packages must actually be in the $HOME +# of the user, not in the PREFIX. Move them to the right +# location. +usr/.local/share/typst/packages/taler-exchange/* /var/lib/taler-exchange/.local/share/typst/packages/taler-exchange/ diff --git a/debian/taler-exchange.tmpfiles b/debian/taler-exchange.tmpfiles @@ -3,6 +3,7 @@ d /run/taler-exchange/secmod-rsa 0755 taler-exchange-secmod-rsa taler-exchange-s d /run/taler-exchange/secmod-cs 0755 taler-exchange-secmod-cs taler-exchange-secmod - - d /run/taler-exchange/secmod-eddsa 0755 taler-exchange-secmod-eddsa taler-exchange-secmod - - d /run/taler-exchange/httpd 0750 taler-exchange-httpd www-data - - +d /var/lib/taler-exchange/.cache 0700 taler-exchange-httpd www-data - - d /var/lib/taler-exchange/secmod-cs 0700 taler-exchange-secmod-cs taler-exchange-secmod - - d /var/lib/taler-exchange/secmod-rsa 0700 taler-exchange-secmod-rsa taler-exchange-secmod - - d /var/lib/taler-exchange/secmod-eddsa 0700 taler-exchange-secmod-eddsa taler-exchange-secmod - -