_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 }