exchange

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

_cover_.typ (7623B)


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