Sign in

    moonpay_flow

    MoonBit ISO 20022 payment lifecycle validator and reconciliation toolkit.

    moonbit
    iso20022
    payments
    validator
    reconciliation
    finance
    Download zip
    Author
    Version
    0.6.1
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    25
    CI

    #MoonPay Flow

    MoonBit ISO 20022 payment lifecycle validator and reconciliation toolkit.

    #Implementation

    • Project-local XML pull reader in xml_reader.mbt
    • Project-local fixed-point decimal module in decimal.mbt
    • Flat XML field extractor in flat_xml.mbt
    • Multi-message validation in message.mbt
    • Payment reconciliation in reconcile.mbt
    • Bank statement balance and duplicate audit in statement_audit.mbt
    • File input through moonbitlang/x/fs
    • CLI in cmd/main
    • 67 unit tests and 15 fixture messages

    #Status

    Version: 0.6.1

    Current support: pain.001, pain.002, pacs.008, pacs.002 and camt.053.

    #Run

    scripts/check.sh # Or run the commands directly: moon check moon test moon run cmd/main -- fixtures/valid_pain001.xml moon run cmd/main -- --format json fixtures/valid_pain001.xml moon run cmd/main -- --format markdown fixtures/invalid_pain001.xml moon run cmd/main -- fixtures/valid_pain002.xml moon run cmd/main -- fixtures/valid_camt053.xml moon run cmd/main -- reconcile fixtures/valid_pain001.xml fixtures/valid_pain002.xml moon run cmd/main -- --list-rules moon run cmd/main -- lifecycle fixtures/lifecycle_pain001.xml fixtures/lifecycle_pain002.xml fixtures/lifecycle_camt053.xml moon run cmd/main -- audit fixtures/balanced_camt053.xml moon run cmd/main -- audit fixtures/balance_mismatch_camt053.xml --format json moon run cmd/main -- audit fixtures/duplicate_camt053.xml --format markdown

    #Rules

    • Message ID is required.
    • Creation date time is required.
    • At least one payment instruction is required.
    • Payment instruction ID is required.
    • Requested execution date is required.
    • End-to-end ID is required, must not exceed 35 characters and must be unique.
    • Payment instruction ID must not exceed 35 characters and must be unique.
    • Instructed amount and currency are required.
    • NbOfTxs must be an unsigned integer and match the parsed transaction count.
    • Date, datetime, currency and amount formats are checked.
    • Payment method and charge bearer values are checked.
    • IBAN checksum and BIC format are checked when present.
    • CtrlSum must match the sum of instructed amounts when present.
    • Status reports validate OrgnlEndToEndId, TxSts and duplicate status entries.
    • pacs.008 validates end-to-end IDs, settlement amounts and currencies.
    • camt.053 validates statement entries, amounts and currencies.
    • reconcile matches pain.001 transactions with status report entries.
    • camt.053 validates the CdtDbtInd credit/debit indicator.
    • --list-rules prints the rule catalog with codes and descriptions.
    • lifecycle classifies payment state across initiation, status and statement messages, including account ownership, debit/credit direction, refunds, reversals and net posted amount.
    • audit verifies that closing balance equals opening balance plus credits minus debits.
    • Balance audit accepts OPBD/PRCD and CLBD/ITBD, checks balance currency and statement account scope, and reports the exact closing difference.
    • Exact duplicate statement entries are grouped by account, end-to-end ID, amount, currency, direction and reversal flag.
    • audit emits text, JSON or Markdown and keeps all monetary values as fixed-point strings.
    • Version boundaries, lifecycle and balance audit examples are documented in docs/version-scope-and-cases.md and docs/statement-audit-cases.md.

    #License

    Apache-2.0

    #Project description

    XmlParseError

    pub(all) suberror XmlParseError {
    XmlParseError(String)
    }

    XML parsing error used by the project-local reader.

    Decimal

    pub struct Decimal {
    minor_units : Int
    scale : Int
    }

    Non-negative fixed-point decimal with a maximum scale of 2.

    Decimal::add

    fn Decimal::add(self : Decimal, other : Decimal) -> Decimal

    Decimal::compare

    fn Decimal::compare(self : Decimal, other : Decimal) -> Int

    Decimal::is_positive

    fn Decimal::is_positive(self : Decimal) -> Bool

    Decimal::parse

    fn Decimal::parse(value : String) -> Decimal?

    Decimal::subtract

    fn Decimal::subtract(self : Decimal, other : Decimal) -> Decimal

    Decimal::to_string

    fn Decimal::to_string(self : Decimal) -> String

    Decimal::zero

    fn Decimal::zero() -> Decimal

    Diagnostic

    pub struct Diagnostic {
    code : String
    path : String
    message : String
    }

    Validation result for one pain.001 document.

    DuplicateStatementEntry

    pub struct DuplicateStatementEntry {
    end_to_end_id : String
    amount : Decimal
    currency : String
    account_iban : String?
    credit_debit : String
    reversal : Bool
    occurrences : Int
    }

    Exact duplicate candidates grouped by account, transaction ID, amount, currency, direction and reversal flag.

    Event

    pub struct Event {
    kind : EventKind
    }

    EventKind

    pub(all) enum EventKind {
    Start(XmlElement)
    Empty(XmlElement)
    Text(String)
    End(String)
    Eof
    }

    LifecycleCase

    pub struct LifecycleCase {
    end_to_end_id : String
    amount : Decimal
    currency : String
    debtor_iban : String?
    creditor_iban : String?
    status : String?
    statement_role : String
    net_posted : Decimal?
    reversal_count : Int
    state : LifecycleState
    action : String
    }

    LifecycleReport

    pub struct LifecycleReport {
    cases : Array[LifecycleCase]
    orphan_statuses : Array[String]
    orphan_statements : Array[String]
    }

    LifecycleReport::to_json

    fn LifecycleReport::to_json(self : LifecycleReport) -> String

    LifecycleReport::to_markdown

    fn LifecycleReport::to_markdown(self : LifecycleReport) -> String

    LifecycleReport::to_text

    fn LifecycleReport::to_text(self : LifecycleReport) -> String

    LifecycleState

    pub(all) enum LifecycleState {
    Settled
    SettledNotPosted
    Rejected
    Pending
    MissingStatus
    StatementMismatch
    StatementBeforeFinalStatus
    PartiallyRefunded
    FullyRefunded
    AccountUnmatched
    }

    LifecycleState::name

    fn LifecycleState::name(self : LifecycleState) -> String

    MessageInfo

    pub struct MessageInfo {
    kind : MessageKind
    message_id : String?
    creation_datetime : String?
    original_message_id : String?
    entry_count : Int
    }

    MessageKind

    pub(all) enum MessageKind {
    Pain001
    Pain002
    Pacs008
    Pacs002
    Camt053
    Unknown
    }

    MessageKind::name

    fn MessageKind::name(self : MessageKind) -> String

    MessageValidation

    pub struct MessageValidation {
    info : MessageInfo
    diagnostics : Array[Diagnostic]
    }

    MessageValidation::is_valid

    fn MessageValidation::is_valid(self : MessageValidation) -> Bool

    MessageValidation::to_json

    fn MessageValidation::to_json(self : MessageValidation) -> String

    MessageValidation::to_markdown

    fn MessageValidation::to_markdown(self : MessageValidation) -> String

    MessageValidation::to_text

    fn MessageValidation::to_text(self : MessageValidation) -> String

    Pain001Summary

    pub struct Pain001Summary {
    message_id : String?
    creation_datetime : String?
    payment_instruction_count : Int
    transaction_count : Int
    }

    Basic information extracted from an ISO 20022 pain.001 message.

    Pain001Summary::to_text

    fn Pain001Summary::to_text(self : Pain001Summary) -> String

    PaymentRecord

    pub struct PaymentRecord {
    end_to_end_id : String
    amount : Decimal
    currency : String
    debtor_iban : String?
    creditor_iban : String?
    }

    ReconciliationReport

    pub struct ReconciliationReport {
    matched : Int
    missing_status : Array[String]
    missing_payment : Array[String]
    status_counts : Map[String, Int]
    }

    ReconciliationReport::to_json

    fn ReconciliationReport::to_json(self : ReconciliationReport) -> String

    ReconciliationReport::to_markdown

    fn ReconciliationReport::to_markdown(self : ReconciliationReport) -> String

    ReconciliationReport::to_text

    fn ReconciliationReport::to_text(self : ReconciliationReport) -> String

    RuleInfo

    pub struct RuleInfo {
    code : String
    description : String
    }

    StatementAuditReport

    pub struct StatementAuditReport {
    account_iban : String?
    account_count : Int
    entry_count : Int
    total_credits : Decimal
    total_debits : Decimal
    net_movement : Decimal
    opening_balance : StatementBalance?
    closing_balance : StatementBalance?
    expected_closing : Decimal?
    closing_difference : Decimal?
    balance_status : StatementBalanceStatus
    duplicate_entries : Array[DuplicateStatementEntry]
    diagnostics : Array[Diagnostic]
    }

    Independent audit result for one camt.053 bank statement.

    StatementAuditReport::is_valid

    fn StatementAuditReport::is_valid(self : StatementAuditReport) -> Bool

    StatementAuditReport::to_json

    fn StatementAuditReport::to_json(self : StatementAuditReport) -> String

    StatementAuditReport::to_markdown

    fn StatementAuditReport::to_markdown(self : StatementAuditReport) -> String

    StatementAuditReport::to_text

    fn StatementAuditReport::to_text(self : StatementAuditReport) -> String

    StatementBalance

    pub struct StatementBalance {
    code : String
    amount : Decimal
    currency : String
    credit_debit : String
    signed_amount : Decimal
    }

    One opening, closing, interim or previously closed balance record.

    StatementBalanceStatus

    pub(all) enum StatementBalanceStatus {
    Balanced
    BalanceMismatch
    BalanceIncomplete
    }

    Balance result for one camt.053 statement.

    StatementBalanceStatus::name

    StatementRecord

    pub struct StatementRecord {
    end_to_end_id : String
    amount : Decimal
    currency : String
    account_iban : String?
    credit_debit : String
    reversal : Bool
    }

    StatusRecord

    pub struct StatusRecord {
    end_to_end_id : String
    status : String
    }

    ValidationReport

    pub struct ValidationReport {
    summary : Pain001Summary
    diagnostics : Array[Diagnostic]
    }

    ValidationReport::is_valid

    fn ValidationReport::is_valid(self : ValidationReport) -> Bool

    ValidationReport::to_json

    fn ValidationReport::to_json(self : ValidationReport) -> String

    ValidationReport::to_markdown

    fn ValidationReport::to_markdown(self : ValidationReport) -> String

    ValidationReport::to_text

    fn ValidationReport::to_text(self : ValidationReport) -> String

    XmlElement

    pub struct XmlElement {
    name : String
    attributes : Array[(String, String)]
    }

    XmlElement::get

    fn XmlElement::get(self : XmlElement, name : String) -> String?

    XmlField

    pub struct XmlField {
    path : String
    attributes : Array[(String, String)]
    text : String?
    }

    One element or text event with its full XML path.

    XmlField::get_attribute

    fn XmlField::get_attribute(self : XmlField, name : String) -> String?

    XmlReader

    pub struct XmlReader {
    input : Array[Char]
    pos : Int
    line : Int
    column : Int
    tag_stack : Array[String]
    saw_root : Bool
    root_closed : Bool
    }

    XmlReader::from_string

    fn XmlReader::from_string(text : String) -> XmlReader

    XmlReader::read_event

    fn XmlReader::read_event(self : XmlReader) -> Event raise XmlParseError

    detect_message_kind

    fn detect_message_kind(xml_text : String) -> MessageKind raise XmlParseError

    elements_by_suffix

    fn elements_by_suffix(fields : Array[XmlField], suffix : String) -> Array[XmlField]

    fields_by_suffix

    fn fields_by_suffix(fields : Array[XmlField], suffix : String) -> Array[XmlField]

    first_text_by_suffix

    fn first_text_by_suffix(fields : Array[XmlField], suffix : String) -> String?

    flatten_xml

    fn flatten_xml(xml_text : String) -> Array[XmlField] raise XmlParseError

    lifecycle_report

    fn lifecycle_report(pain_xml : String, status_xml : String, statement_xml : String) -> LifecycleReport raise XmlParseError

    pain001_payment_records

    fn pain001_payment_records(xml_text : String) -> Array[PaymentRecord] raise XmlParseError

    reconcile_pain001_status

    fn reconcile_pain001_status(pain_xml : String, status_xml : String) -> ReconciliationReport raise XmlParseError

    rule_catalog

    fn rule_catalog() -> Array[RuleInfo]

    statement_audit

    fn statement_audit(xml_text : String) -> StatementAuditReport raise XmlParseError

    Audit statement balances, movement totals, account scope and exact duplicates.

    statement_records

    fn statement_records(xml_text : String) -> Array[StatementRecord] raise XmlParseError

    status_records

    fn status_records(xml_text : String) -> Array[StatusRecord] raise XmlParseError

    summarize_pain001

    fn summarize_pain001(xml_text : String) -> Pain001Summary raise XmlParseError

    texts_by_suffix

    fn texts_by_suffix(fields : Array[XmlField], suffix : String) -> Array[String]

    validate_message

    fn validate_message(xml_text : String) -> MessageValidation raise XmlParseError

    validate_pain001

    fn validate_pain001(xml_text : String) -> ValidationReport raise XmlParseError

    version

    fn version() -> String

    Public version marker used by the CLI and tests.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io