///|
/// Portfolio-level audit result for all statements in one camt.053 file.
pub struct StatementPortfolioReport {
message_id : String?
statements : Array[StatementAuditReport]
diagnostics : Array[Diagnostic]
}
///|
fn optional_value_text(value : String?) -> String {
match value {
Some(text) => text
None => ""
}
}
///|
fn statement_group_key(statement_id : String?, account_iban : String?) -> String {
optional_value_text(statement_id) + "|" + optional_value_text(account_iban)
}
///|
fn group_index(
keys : Array[String],
indexes : Map[String, Int],
key : String,
) -> Int {
match indexes.get(key) {
Some(index) => index
None => {
let index = keys.length()
keys.push(key)
indexes.set(key, index)
index
}
}
}
///|
/// Audit every Stmt in a camt.053 file separately.
pub fn statement_portfolio_audit(
xml_text : String,
) -> StatementPortfolioReport raise XmlParseError {
let fields = flatten_xml(xml_text)
let records = statement_records(xml_text)
let balance_diagnostics : Array[Diagnostic] = []
let diagnostics : Array[Diagnostic] = []
let owned_balances = parse_statement_balances_with_owner(
xml_text, balance_diagnostics,
)
let keys : Array[String] = []
let indexes : Map[String, Int] = Map([])
let statement_ids : Array[String?] = []
let accounts : Array[String?] = []
let record_groups : Array[Array[StatementRecord]] = []
let balance_groups : Array[Array[StatementBalance]] = []
for record in records {
let key = statement_group_key(record.statement_id, record.account_iban)
let index = group_index(keys, indexes, key)
if index >= statement_ids.length() {
statement_ids.push(record.statement_id)
accounts.push(record.account_iban)
record_groups.push([])
balance_groups.push([])
}
record_groups[index].push(record)
}
for owned in owned_balances {
let key = statement_group_key(owned.statement_id, owned.account_iban)
let index = group_index(keys, indexes, key)
if index >= statement_ids.length() {
statement_ids.push(owned.statement_id)
accounts.push(owned.account_iban)
record_groups.push([])
balance_groups.push([])
}
balance_groups[index].push(owned.balance)
}
let statements : Array[StatementAuditReport] = []
for i = 0; i < keys.length(); i = i + 1 {
let account_count = if accounts[i] is Some(_) { 1 } else { 0 }
statements.push(
build_statement_audit_report(
statement_ids[i],
accounts[i],
account_count,
record_groups[i],
balance_groups[i],
[],
record_groups[i].length(),
),
)
}
if statements.length() == 0 {
push_diagnostic(
diagnostics,
"STATEMENT_MISSING",
"Stmt",
"The camt.053 portfolio contains no statement.",
)
}
for diagnostic in balance_diagnostics {
diagnostics.push(diagnostic)
}
let statement_id_counts : Map[String, Int] = Map([])
for field in fields {
if field.path.has_suffix("/Stmt/Id") {
match field.text {
Some(value) =>
if value != "" {
let count = match statement_id_counts.get(value) {
Some(number) => number
None => 0
}
statement_id_counts.set(value, count + 1)
}
None => ()
}
}
}
for pair in statement_id_counts {
if pair.1 > 1 {
push_diagnostic(
diagnostics,
"STATEMENT_ID_DUPLICATE",
"Stmt/Id",
"Statement ID is repeated " + pair.1.to_string() + " times: " + pair.0 + ".",
)
}
}
{
message_id: first_text_by_suffix(fields, "/GrpHdr/MsgId"),
statements,
diagnostics,
}
}
///|
pub fn StatementPortfolioReport::is_valid(self : StatementPortfolioReport) -> Bool {
if self.diagnostics.length() > 0 {
return false
}
for statement in self.statements {
if !statement.is_valid() {
return false
}
}
true
}
///|
pub fn StatementPortfolioReport::to_text(self : StatementPortfolioReport) -> String {
let output = StringBuilder()
output.write_string("Bank statement portfolio audit\nMessage ID: ")
output.write_string(match self.message_id {
Some(value) => value
None => "(missing)"
})
output.write_string("\nStatements: ")
output.write_string(self.statements.length().to_string())
output.write_string("\nResult: ")
if self.is_valid() {
output.write_string("VALID")
} else {
output.write_string("ISSUES FOUND")
}
for diagnostic in self.diagnostics {
output.write_string("\n- [")
output.write_string(diagnostic.code)
output.write_string("] ")
output.write_string(diagnostic.path)
output.write_string(": ")
output.write_string(diagnostic.message)
}
for statement in self.statements {
output.write_string("\n\n---\n")
output.write_string(statement.to_text())
}
output.to_string()
}
///|
fn diagnostic_array_json(diagnostics : Array[Diagnostic]) -> Json {
let items : Array[Json] = []
for diagnostic in diagnostics {
items.push({
"code": diagnostic.code,
"path": diagnostic.path,
"message": diagnostic.message,
})
}
Json::array(items)
}
///|
pub fn StatementPortfolioReport::to_json(self : StatementPortfolioReport) -> String {
let output = StringBuilder()
output.write_string("{\n \"valid\": ")
output.write_string(if self.is_valid() { "true" } else { "false" })
output.write_string(",\n \"messageId\": ")
output.write_string(match self.message_id {
Some(value) => Json::string(value).stringify()
None => "null"
})
output.write_string(",\n \"statementCount\": ")
output.write_string(self.statements.length().to_string())
output.write_string(",\n \"statements\": [")
for i = 0; i < self.statements.length(); i = i + 1 {
if i > 0 {
output.write_char(',')
}
output.write_char('\n')
output.write_string(self.statements[i].to_json())
}
output.write_string("\n ],\n \"diagnostics\": ")
output.write_string(diagnostic_array_json(self.diagnostics).stringify(indent=2))
output.write_string("\n}")
output.to_string()
}
///|
pub fn StatementPortfolioReport::to_markdown(
self : StatementPortfolioReport,
) -> String {
let output = StringBuilder()
output.write_string("# Bank statement portfolio audit\n\n")
output.write_string("- Message ID: `")
output.write_string(match self.message_id {
Some(value) => value
None => "(missing)"
})
output.write_string("`\n- Statements: ")
output.write_string(self.statements.length().to_string())
output.write_string("\n- Result: ")
if self.is_valid() {
output.write_string("VALID\n")
} else {
output.write_string("ISSUES FOUND\n")
}
if self.diagnostics.length() > 0 {
output.write_string("\n## Portfolio diagnostics\n\n")
for diagnostic in self.diagnostics {
output.write_string("- `")
output.write_string(diagnostic.code)
output.write_string("`: ")
output.write_string(diagnostic.message)
output.write_string("\n")
}
}
for statement in self.statements {
output.write_string("\n---\n\n")
output.write_string(statement.to_markdown())
}
output.to_string()
}