exchange

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

commit 3214c7ce39492541244ac35b6756fc2decfc0864
parent cd8f204566fd75adaf8e2ab0d783c76b02a4dc13
Author: Christian Grothoff <christian@grothoff.org>
Date:   Fri,  5 Dec 2025 21:53:11 +0100

add Typst for cover page

Diffstat:
Mcontrib/typst/Makefile.am | 1+
Mcontrib/typst/_cover_.typ | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Acontrib/typst/taler-logo.svg | 10++++++++++
3 files changed, 257 insertions(+), 12 deletions(-)

diff --git a/contrib/typst/Makefile.am b/contrib/typst/Makefile.am @@ -5,6 +5,7 @@ dist_formdata_DATA = \ _cover_.typ \ challenger_postal.typ \ challenger_sms.typ \ + taler-logo.svg \ pointing_finger.svg \ vqf_902_1_customer.typ \ vqf_902_4.typ \ diff --git a/contrib/typst/_cover_.typ b/contrib/typst/_cover_.typ @@ -39,6 +39,120 @@ ) } + // 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]) @@ -47,45 +161,165 @@ grid( columns: (50%, 50%), gutter: 1em, - image("vss_vqf_verein.png", width: 80%), + 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")] + [AMLA File No.], [#get("FILE_NUMBER")], + [Account open?], [#checkbox(get("is_active"))], + [Active investigation?], [#checkbox(get("to_investigate"))], ) ] ) v(1em) - // Section 1: FIXME -- Validated address - text(size: 11pt, weight: "bold")[Validated address:] + // 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, - [Contact name:], [#get("CONTACT_NAME")], - [Address:], [#get("ADDRESS_LINES").split("\n").join(linebreak())], - [Country:], [#get("ADDRESS_COUNTRY")], + 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: (14%, 13%, 13%, 20%, 20%, 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", - "properties": { "PROPA" : "VALUEA", "PROPB":"VALUEB" }, // FIXME: better example... - "rules": { }, // FIXME: object? - "is_active" : false, - "to_investigate" : false, + "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/taler-logo.svg b/contrib/typst/taler-logo.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90"> + <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3"> + <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" /> + <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" /> + <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" /> + </g> + <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" /> +</svg> +\ No newline at end of file