moonpay_flow

    MoonBit ISO 20022 validation, status timeline, statement audit and reconciliation toolkit.

    moonbit
    iso20022
    payments
    reconciliation
    statement-audit
    status-timeline
    Download zip
    Author
    Version
    0.8.2
    License
    Apache-2.0
    Last updated
    7 days ago
    Downloads
    80

    #MoonPay Flow

    CI License Mooncakes

    MoonPay Flow is a MoonBit-native ISO 20022 validation, status timeline, statement audit and reconciliation toolkit. It connects payment instructions, status reports and bank statement postings while making incomplete evidence explicit.

    #Highlights

    • Project-local XML pull reader and fixed-point decimal implementation.
    • Validation for pain.001, pain.002, pacs.008, pacs.002 and camt.053.
    • Multi-report status timelines with terminal-state and conflict detection.
    • Balance, movement, currency, account and duplicate-entry audit for camt.053.
    • Per-Stmt, per-account portfolio audit.
    • Cross-period refund and reversal ledger with explicit evidence boundaries.
    • Text, JSON and Markdown reports.
    • 112 tests, 25 synthetic fixtures and 82 queryable rules.

    #Install

    moon add cstoooo/moonpay_flow@0.8.2

    Add the package alias to the consuming package's moon.pkg:

    import { "cstoooo/moonpay_flow" @moonpay_flow, }

    #Quick start

    Run from a clone of this repository:

    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 -- --list-rules

    Full acceptance-style verification:

    scripts/check.sh

    #CLI

    CommandPurpose
    <message.xml>Validate one supported ISO 20022 message
    reconcile <pain.001> <status>Match payment instructions with status entries
    lifecycle <payment> <status> <statement>Compact payment, status and posting summary
    audit <camt.053>Single-account balance and duplicate-entry audit
    audit-portfolio <camt.053>Audit every Stmt and account separately
    timeline <payment> [status ...]Merge multiple status reports by timestamp
    return-ledger <payment> <statement ...>Authoritative cross-period refund/reversal ledger
    --list-rulesPrint all 82 rule codes and descriptions

    Report formats:

    moon run cmd/main -- --format text fixtures/valid_pain001.xml moon run cmd/main -- --format json fixtures/valid_pain001.xml moon run cmd/main -- --format markdown fixtures/invalid_pain001.xml

    #Return and reversal example

    moon run cmd/main -- return-ledger \ fixtures/returns_pain001.xml \ fixtures/returns_day1.xml \ fixtures/returns_day2_partial.xml \ fixtures/returns_day3_final.xml \ fixtures/returns_day4_refund_reversal.xml

    The example crosses four booking days:

    2026-10-01 DBIT 100.00 original payment 2026-10-02 CRDT 20.00 partial refund 2026-10-03 CRDT 80.00 final refund 2026-10-04 DBIT 80.00 reversal of the final refund

    Result:

    state=PARTIALLY_RETURNED Posted payment: 100.00 Effective refund: 20.00 Remaining: 80.00

    If only the refund statement is supplied, the result is EVIDENCE_INCOMPLETE; postedPayment, effectiveRefund and remaining are null. The tool does not guess a partial or full return from net amount alone.

    #Library API

    let validation = @moonpay_flow.validate_message(xml_text)
    let timeline = @moonpay_flow.status_timeline_report(payment_xml, status_xmls)
    let portfolio = @moonpay_flow.statement_portfolio_audit(camt_xml)
    let ledger = @moonpay_flow.return_ledger_report(payment_xml, statement_xmls)

    All major reports provide:

    • structured case/leg objects;
    • stable diagnostic codes;
    • is_valid() for automated gates;
    • to_text(), to_json() and to_markdown().

    See the usage guide for complete API examples.

    #Reliability model

    1. XML parsing and decimal arithmetic are implemented in MoonBit.
    2. JSON monetary values are serialized as fixed-point strings.
    3. Reversals are linked by OrgnlNtryRef, OrgnlTxId, UETR, or a unique compatible candidate.
    4. Ambiguous reversal targets produce an error instead of an arbitrary match.
    5. Refund states require the original payment posting in the supplied history.
    6. Incomplete history produces EVIDENCE_INCOMPLETE, not a guessed conclusion.
    7. Multi-account audits never combine movements across account scopes.

    #Supported versions

    MessageSupported version
    pain.001pain.001.001.03
    pain.002pain.002.001.03
    pacs.008pacs.008.001.08
    pacs.002pacs.002.001.10
    camt.053camt.053.001.08

    The project implements common fields and explicit business rules, not complete ISO 20022 XSD validation or every bank-specific implementation guide. See version scope.

    #Quality and compliance

    • Tests: 112 passing.
    • Fixtures: 25 synthetic ISO 20022 messages.
    • Rule catalog: 82 codes.
    • CI: GitHub Actions runs moon check, moon test and a validation example.
    • License: Apache-2.0.
    • Data policy and security reporting: SECURITY.md.
    • Contribution rules: CONTRIBUTING.md.
    • Copyright and third-party notice: NOTICE.
    • Release history: CHANGELOG.md.

    #Documentation

    #License

    Licensed under the Apache License, Version 2.0. See LICENSE.

    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
    }

    LedgerIssue

    pub struct LedgerIssue {
    code : String
    severity : String
    message : String
    }

    LedgerLegKind

    pub(all) enum LedgerLegKind {
    OriginalPayment
    RefundCredit
    ReversalOfPayment
    ReversalOfRefund
    UnlinkedReversal
    }

    LedgerLegKind::name

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

    LedgerLinkQuality

    pub(all) enum LedgerLinkQuality {
    LedgerExplicitLink
    LedgerInferredLink
    LedgerAmbiguousLink
    LedgerMissingLink
    }

    LedgerLinkQuality::name

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

    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

    ReturnLedgerCase

    pub struct ReturnLedgerCase {
    end_to_end_id : String
    declared_amount : Decimal
    currency : String
    account_role : String
    state : ReturnLedgerState
    observed_payment : Decimal
    observed_refund : Decimal
    posted_payment : Decimal?
    effective_refund : Decimal?
    remaining : Decimal?
    crossed_period : Bool
    legs : Array[ReturnLedgerLeg]
    issues : Array[LedgerIssue]
    action : String
    }

    ReturnLedgerLeg

    pub struct ReturnLedgerLeg {
    statement_id : String?
    booking_date : String?
    entry_reference : String?
    transaction_id : String?
    uetr : String?
    original_entry_reference : String?
    original_transaction_id : String?
    amount : Decimal
    currency : String
    credit_debit : String
    reversal : Bool
    kind : LedgerLegKind
    linked_to : String?
    link_quality : LedgerLinkQuality
    }

    ReturnLedgerReport

    pub struct ReturnLedgerReport {
    statement_count : Int
    cases : Array[ReturnLedgerCase]
    orphan_entries : Array[ReturnLedgerLeg]
    diagnostics : Array[Diagnostic]
    }

    ReturnLedgerReport::is_valid

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

    ReturnLedgerReport::to_json

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

    ReturnLedgerReport::to_markdown

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

    ReturnLedgerReport::to_text

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

    ReturnLedgerState

    pub(all) enum ReturnLedgerState {
    LedgerNotPosted
    LedgerPaid
    LedgerPartiallyReturned
    LedgerFullyReturned
    LedgerPaymentReversed
    LedgerEvidenceIncomplete
    LedgerConflict
    }

    Authoritative post-posting state for refunds and reversals.

    ReturnLedgerState::name

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

    RuleInfo

    pub struct RuleInfo {
    code : String
    description : String
    }

    StatementAuditReport

    pub struct StatementAuditReport {
    statement_id : String?
    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

    StatementPortfolioReport

    pub struct StatementPortfolioReport {
    message_id : String?
    statements : Array[StatementAuditReport]
    diagnostics : Array[Diagnostic]
    }

    Portfolio-level audit result for all statements in one camt.053 file.

    StatementPortfolioReport::is_valid

    StatementPortfolioReport::to_json

    StatementPortfolioReport::to_markdown

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

    StatementPortfolioReport::to_text

    StatementRecord

    pub struct StatementRecord {
    statement_id : String?
    entry_reference : String?
    transaction_id : String?
    uetr : String?
    original_entry_reference : String?
    original_transaction_id : String?
    booking_date : String?
    end_to_end_id : String
    amount : Decimal
    currency : String
    account_iban : String?
    credit_debit : String
    reversal : Bool
    }

    StatusEventRecord

    pub struct StatusEventRecord {
    message_id : String?
    creation_datetime : String?
    end_to_end_id : String
    status : String
    reason_code : String?
    original_amount : Decimal?
    original_currency : String?
    }

    One transaction-level status notification.

    StatusRecord

    pub struct StatusRecord {
    end_to_end_id : String
    status : String
    }

    StatusTimelineCase

    pub struct StatusTimelineCase {
    end_to_end_id : String
    amount : Decimal
    currency : String
    events : Array[StatusEventRecord]
    latest_status : String?
    state : TimelineState
    issues : Array[TimelineIssue]
    action : String
    }

    Merged history and resolved state for one payment.

    StatusTimelineReport

    pub struct StatusTimelineReport {
    message_count : Int
    event_count : Int
    cases : Array[StatusTimelineCase]
    orphan_status_ids : Array[String]
    orphan_events : Array[StatusEventRecord]
    diagnostics : Array[Diagnostic]
    }

    Result of merging one payment source with multiple status reports.

    StatusTimelineReport::is_valid

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

    StatusTimelineReport::to_json

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

    StatusTimelineReport::to_markdown

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

    StatusTimelineReport::to_text

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

    TimelineIssue

    pub struct TimelineIssue {
    code : String
    severity : TimelineSeverity
    message : String
    }

    A transition or data-quality issue found in a status history.

    TimelineSeverity

    pub(all) enum TimelineSeverity {
    TimelineInfo
    TimelineWarning
    TimelineError
    }

    TimelineSeverity::name

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

    TimelineState

    pub(all) enum TimelineState {
    TimelineAwaiting
    TimelineInProgress
    TimelineSettled
    TimelineRejected
    TimelineConflict
    TimelineUnknown
    }

    Resolution state after merging multiple payment status reports.

    TimelineState::name

    fn TimelineState::name(self : TimelineState) -> 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

    pacs008_payment_records

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

    Extract payment records from a pacs.008 customer credit transfer.

    pain001_payment_records

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

    payment_records_auto

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

    Extract payment records from either pain.001 or pacs.008.

    reconcile_pain001_status

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

    return_ledger_report

    fn return_ledger_report(payment_xml : String, statement_xmls : Array[String]) -> ReturnLedgerReport raise XmlParseError

    Build a cross-statement return/reversal ledger for every payment.

    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_portfolio_audit

    fn statement_portfolio_audit(xml_text : String) -> StatementPortfolioReport raise XmlParseError

    Audit every Stmt in a camt.053 file separately.

    statement_records

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

    status_events

    fn status_events(xml_text : String) -> Array[StatusEventRecord] raise XmlParseError

    Parse all transaction status events from pain.002 or pacs.002.

    status_records

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

    status_timeline_report

    fn status_timeline_report(payment_xml : String, status_xmls : Array[String]) -> StatusTimelineReport raise XmlParseError

    Merge multiple status reports and detect conflicting or regressive histories.

    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.