exchange

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

_cover_.typ (9081B)


      1 // Cover page for AML files.
      2 // Renders all account properties, current rules, etc.
      3 
      4 #let form(data) = {
      5   set page(
      6     paper: "a4",
      7     margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm),
      8     footer: context [
      9       #grid(
     10         columns: (1fr, 1fr),
     11         align: (left, right),
     12         text(size: 8pt)[
     13         ],
     14         text(size: 8pt)[
     15           Page #here().page() of #counter(page).final().first()
     16         ]
     17       )
     18     ]
     19   )
     20 
     21   set text(font: "Liberation Sans", size: 10pt)
     22   set par(justify: false, leading: 0.65em)
     23 
     24   // Helper function to get value or empty string
     25   let get(key, default: "") = {
     26     data.at(key, default: default)
     27   }
     28 
     29   // Helper function for checkbox
     30   let checkbox(checked) = {
     31     box(
     32       width: 3mm,
     33       height: 3mm,
     34       stroke: 0.5pt + black,
     35       inset: 0.3mm,
     36       if checked == true or checked == "true" {
     37         place(center + horizon, text(size: 8pt, sym.checkmark))
     38       }
     39     )
     40   }
     41 
     42   // Helper function to get nice labels for standard properties
     43   let get_property_label(key) = {
     44     if key == "pep" { "Politically exposed person (PEP)" }
     45     else if key == "sanctioned" { "Sanctioned account" }
     46     else if key == "high_risk" { "High risk account" }
     47     else if key == "business_domain" { "Business domain" }
     48     else if key == "is_frozen" { "Account frozen" }
     49     else if key == "was_reported" { "Reported to authorities" }
     50     else { key }
     51   }
     52 
     53   // Helper function to format timeframe
     54   let format_timeframe(d_us) = {
     55     if d_us == "forever" {
     56       "forever"
     57     } else {
     58       let us = int(d_us)
     59       let s = calc.quo(us, 1000000)
     60       let m = calc.quo(s, 60)
     61       let h = calc.quo(m, 60)
     62       let d = calc.quo(h, 24)
     63 
     64       if calc.rem(us, 1000000) == 0 {
     65         if calc.rem(s, 60) == 0 {
     66           if calc.rem(m, 60) == 0 {
     67             if calc.rem(h, 24) == 0 {
     68               str(d) + " day" + if d != 1 { "s" } else { "" }
     69             } else {
     70               str(h) + " hour" + if h != 1 { "s" } else { "" }
     71             }
     72           } else {
     73             str(m) + " minute" + if m != 1 { "s" } else { "" }
     74           }
     75         } else {
     76           str(s) + " s"
     77         }
     78       } else {
     79         str(us) + " μs"
     80       }
     81     }
     82   }
     83 
     84 
     85   // Helper function to format timestamp; ignores leap seconds (too variable)
     86   let format_timestamp(ts) = {
     87     if type(ts) == dictionary and "t_s" in ts {
     88       let t_s = ts.t_s
     89       if t_s == "never" {
     90         "never"
     91       } else {
     92         // Convert Unix timestamp to human-readable format
     93         let seconds = int(t_s)
     94         let days_since_epoch = calc.quo(seconds, 86400)
     95         let remaining_seconds = calc.rem(seconds, 86400)
     96         let hours = calc.quo(remaining_seconds, 3600)
     97         let minutes = calc.quo(calc.rem(remaining_seconds, 3600), 60)
     98         let secs = calc.rem(remaining_seconds, 60)
     99 
    100         // Helper to check if year is leap year
    101         let is_leap(y) = {
    102           calc.rem(y, 4) == 0 and (calc.rem(y, 100) != 0 or calc.rem(y, 400) == 0)
    103         }
    104 
    105         // Calculate year, month, day
    106         let year = 1970
    107         let days_left = days_since_epoch
    108 
    109         // Find the year
    110         let done = false
    111         while not done {
    112           let days_in_year = if is_leap(year) { 366 } else { 365 }
    113           if days_left >= days_in_year {
    114             days_left = days_left - days_in_year
    115             year = year + 1
    116           } else {
    117             done = true
    118           }
    119         }
    120 
    121         // Days in each month
    122         let days_in_months = if is_leap(year) {
    123           (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
    124         } else {
    125           (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
    126         }
    127 
    128         // Find month and day
    129         let month = 1
    130         for days_in_month in days_in_months {
    131           if days_left >= days_in_month {
    132             days_left = days_left - days_in_month
    133             month = month + 1
    134           } else {
    135             break
    136           }
    137         }
    138         let day = days_left + 1
    139 
    140         // Format with leading zeros
    141         let m_str = if month < 10 { "0" + str(month) } else { str(month) }
    142         let d_str = if day < 10 { "0" + str(day) } else { str(day) }
    143         let h_str = if hours < 10 { "0" + str(hours) } else { str(hours) }
    144         let min_str = if minutes < 10 { "0" + str(minutes) } else { str(minutes) }
    145         let s_str = if secs < 10 { "0" + str(secs) } else { str(secs) }
    146 
    147         str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + ":" + s_str + " UTC"
    148       }
    149     } else {
    150       str(ts)
    151     }
    152   }
    153 
    154 
    155 
    156   // Header
    157   align(center, text(size: 11pt, weight: "bold")[CONFIDENTIAL])
    158 
    159   v(0.5em)
    160 
    161   grid(
    162     columns: (50%, 50%),
    163     gutter: 1em,
    164     image("taler-logo.svg", width: 80%),
    165     align(right)[
    166       #table(
    167         columns: (1fr, 1fr),
    168         stroke: 0.5pt + black,
    169         inset: 5pt,
    170         align: (left, left),
    171         [AMLA File No.], [#get("FILE_NUMBER")],
    172         [Account open?], [#checkbox(get("is_active"))],
    173         [Active investigation?], [#checkbox(get("to_investigate"))],
    174       )
    175     ]
    176   )
    177 
    178   v(1em)
    179 
    180   // Section 1: Properties
    181   text(size: 11pt, weight: "bold")[Properties:]
    182 
    183   v(0.5em)
    184 
    185   block(breakable: false)[
    186     #v(0.5em)
    187     #let props = get("properties", default: (:))
    188     #let standard_props = ("pep", "sanctioned", "high_risk", "business_domain", "is_frozen", "was_reported")
    189     #let all_keys = props.keys()
    190 
    191     #table(
    192       columns: (35%, 65%),
    193       stroke: 0.5pt + black,
    194       inset: 5pt,
    195       align: (left, left),
    196       ..for key in all_keys {
    197         let value = props.at(key)
    198         let label = get_property_label(key)
    199 
    200         // Render based on value type
    201         if type(value) == bool {
    202           ([#label], [#checkbox(value)])
    203         } else {
    204           ([#label], [#value])
    205         }
    206       }
    207     )
    208     #v(0.5em)
    209   ]
    210 
    211   // Section 2: Rules
    212   let rules_data = get("rules", default: none)
    213 
    214   if rules_data != none {
    215     text(size: 11pt, weight: "bold")[
    216       Rules
    217       #if "expiration_time" in rules_data {
    218         [ (expires: #format_timestamp(rules_data.expiration_time))]
    219       }
    220       :
    221     ]
    222 
    223     v(0.5em)
    224 
    225     let rules = rules_data.at("rules", default: ())
    226 
    227     if rules.len() > 0 {
    228       block(breakable: true)[
    229         #table(
    230           columns: (17%, 13%, 13%, 20%, 17%, 10%, 10%),
    231           stroke: 0.5pt + black,
    232           inset: 4pt,
    233           align: (left, left, left, left, left, center, center),
    234           table.header(
    235             [*Operation*],
    236             [*Threshold*],
    237             [*Timeframe*],
    238             [*Rule Name*],
    239             [*Measures*],
    240             [*Exposed*],
    241             [*Verboten*]
    242           ),
    243           ..for rule in rules {
    244             let op_type = rule.at("operation_type", default: "")
    245             let threshold = rule.at("threshold", default: "")
    246             let timeframe_raw = rule.at("timeframe", default: (:))
    247             let timeframe = if "d_us" in timeframe_raw {
    248               format_timeframe(timeframe_raw.d_us)
    249             } else { "" }
    250             let rule_name = rule.at("rule_name", default: "")
    251             let measures = rule.at("measures", default: ())
    252             let exposed = rule.at("exposed", default: false)
    253             let is_verboten = if type(measures) == array { "verboten" in measures } else { "verboten" == measures }
    254             let measures_text = if type(measures) == array {
    255               measures.filter(m => m != "verboten").map(m => str(m)).join(", ")
    256             } else if measures != "verboten" {
    257               str(measures)
    258             } else {
    259               ""
    260             }
    261 
    262             (
    263               [#op_type],
    264               [#threshold],
    265               [#timeframe],
    266               [#rule_name],
    267               [#measures_text],
    268               [#checkbox(exposed)],
    269               [#checkbox(is_verboten)]
    270             )
    271           }
    272         )
    273       ]
    274     } else {
    275       text(style: "italic")[No rules defined.]
    276     }
    277   }
    278 }
    279 
    280 // Example usage:
    281 #form((
    282   "FILE_NUMBER": "42",
    283   "is_active": true,
    284   "to_investigate": false,
    285   "properties": (
    286     "pep": false,
    287     "sanctioned": false,
    288     "high_risk": true,
    289     "business_domain": "Financial services",
    290     "is_frozen": false,
    291     "was_reported": false,
    292     "custom_field": "Custom value"
    293   ),
    294   "rules": (
    295     "expiration_time": ("t_s": 1764967786),  // Fri Dec 5 20:49:46 UTC 2025
    296     "rules": (
    297       (
    298         "operation_type": "WITHDRAW",
    299         "rule_name": "large_withdrawal",
    300         "threshold": "EUR:10000",
    301         "timeframe": ("d_us": 86400000000),
    302         "measures": ("kyc_review"),
    303         "display_priority": 10,
    304         "exposed": true
    305       ),
    306       (
    307         "operation_type": "DEPOSIT",
    308         "rule_name": "suspicious_deposit",
    309         "threshold": "EUR:50000",
    310         "timeframe": ("d_us": 604800000000),
    311         "measures": ("verboten"),
    312         "display_priority": 20,
    313         "exposed": false
    314       ),
    315       (
    316         "operation_type": "BALANCE",
    317         "threshold": "EUR:5000",
    318         "timeframe": ("d_us": 3600000000),
    319         "measures": ("aml_check", "manager_approval"),
    320         "display_priority": 5,
    321         "exposed": true
    322       )
    323     )
    324   )
    325 ))