///|
pub struct PaymentRecord {
end_to_end_id : String
amount : Decimal
currency : String
debtor_iban : String?
creditor_iban : String?
}
///|
pub struct StatusRecord {
end_to_end_id : String
status : String
}
///|
pub struct ReconciliationReport {
matched : Int
missing_status : Array[String]
missing_payment : Array[String]
status_counts : Map[String, Int]
}
///|
pub fn pain001_payment_records(
xml_text : String,
) -> Array[PaymentRecord] raise XmlParseError {
let data = parse_pain001_data(xml_text)
let records : Array[PaymentRecord] = []
for i = 0; i < data.end_to_end_ids.length(); i = i + 1 {
match data.end_to_end_ids[i] {
Some(id) =>
match data.instruction_amounts[i] {
Some(amount_text) =>
match data.instruction_currencies[i] {
Some(currency) =>
match Decimal::parse(amount_text) {
Some(amount) => records.push({
end_to_end_id: id,
amount,
currency,
debtor_iban: data.transaction_debtor_ibans[i],
creditor_iban: data.transaction_creditor_ibans[i],
})
None => ()
}
None => ()
}
None => ()
}
None => ()
}
}
records
}
///|
pub fn status_records(xml_text : String) -> Array[StatusRecord] raise XmlParseError {
let fields = flatten_xml(xml_text)
let ids = texts_by_suffix(fields, "/TxInfAndSts/OrgnlEndToEndId")
let statuses = texts_by_suffix(fields, "/TxInfAndSts/TxSts")
let count = if ids.length() < statuses.length() {
ids.length()
} else {
statuses.length()
}
let records : Array[StatusRecord] = []
for i = 0; i < count; i = i + 1 {
records.push({ end_to_end_id: ids[i], status: statuses[i], })
}
records
}
///|
pub fn reconcile_pain001_status(
pain_xml : String,
status_xml : String,
) -> ReconciliationReport raise XmlParseError {
let payments = pain001_payment_records(pain_xml)
let statuses = status_records(status_xml)
let payment_ids : Map[String, Bool] = Map([])
for payment in payments {
if !payment_ids.contains(payment.end_to_end_id) {
payment_ids.set(payment.end_to_end_id, true)
}
}
let status_map : Map[String, String] = Map([])
let status_counts : Map[String, Int] = Map([])
for record in statuses {
status_map.set(record.end_to_end_id, record.status)
let current = match status_counts.get(record.status) {
Some(value) => value
None => 0
}
status_counts.set(record.status, current + 1)
}
let missing_status : Array[String] = []
let missing_payment : Array[String] = []
let mut matched = 0
for payment in payments {
if status_map.contains(payment.end_to_end_id) {
matched += 1
} else {
missing_status.push(payment.end_to_end_id)
}
}
for record in statuses {
if !payment_ids.contains(record.end_to_end_id) {
missing_payment.push(record.end_to_end_id)
}
}
{ matched, missing_status, missing_payment, status_counts, }
}
///|
pub fn ReconciliationReport::to_text(self : ReconciliationReport) -> String {
let output = StringBuilder()
output.write_string("Reconciliation report\n")
output.write_string("Matched transactions: ")
output.write_string(self.matched.to_string())
output.write_string("\nMissing status entries: ")
output.write_string(self.missing_status.length().to_string())
output.write_string("\nMissing payment entries: ")
output.write_string(self.missing_payment.length().to_string())
if self.missing_status.length() > 0 {
output.write_string("\nMissing status IDs:")
for id in self.missing_status {
output.write_string("\n- ")
output.write_string(id)
}
}
if self.missing_payment.length() > 0 {
output.write_string("\nMissing payment IDs:")
for id in self.missing_payment {
output.write_string("\n- ")
output.write_string(id)
}
}
output.write_string("\nStatus counts:")
for pair in self.status_counts {
output.write_string("\n- ")
output.write_string(pair.0)
output.write_string(": ")
output.write_string(pair.1.to_string())
}
output.to_string()
}
///|
pub fn ReconciliationReport::to_json(self : ReconciliationReport) -> String {
let missing_status : Array[Json] = []
for id in self.missing_status {
missing_status.push(Json::string(id))
}
let missing_payment : Array[Json] = []
for id in self.missing_payment {
missing_payment.push(Json::string(id))
}
let counts = Map([])
for pair in self.status_counts {
counts.set(pair.0, Json::number(pair.1.to_double()))
}
let root = Map([])
root.set("matched", Json::number(self.matched.to_double()))
root.set("missingStatus", Json::array(missing_status))
root.set("missingPayment", Json::array(missing_payment))
root.set("statusCounts", Json::object(counts))
Json::object(root).stringify(indent=2)
}
///|
pub fn ReconciliationReport::to_markdown(self : ReconciliationReport) -> String {
let output = StringBuilder()
output.write_string("# MoonPay Flow reconciliation report\n\n")
output.write_string("- Matched transactions: ")
output.write_string(self.matched.to_string())
output.write_string("\n- Missing status entries: ")
output.write_string(self.missing_status.length().to_string())
output.write_string("\n- Missing payment entries: ")
output.write_string(self.missing_payment.length().to_string())
output.write_string("\n\n## Missing status IDs\n\n")
if self.missing_status.length() == 0 {
output.write_string("None.\n")
} else {
for id in self.missing_status {
output.write_string("- ")
output.write_string(id)
output.write_string("\n")
}
}
output.write_string("\n## Missing payment IDs\n\n")
if self.missing_payment.length() == 0 {
output.write_string("None.\n")
} else {
for id in self.missing_payment {
output.write_string("- ")
output.write_string(id)
output.write_string("\n")
}
}
output.write_string("\n## Status counts\n\n")
if self.status_counts.length() == 0 {
output.write_string("None.\n")
} else {
for pair in self.status_counts {
output.write_string("- `")
output.write_string(pair.0)
output.write_string("`: ")
output.write_string(pair.1.to_string())
output.write_string("\n")
}
}
output.to_string()
}