OpenPay (1 crate, ~2940 LOC, 40 tests)
op-statements Statement generation + reconciliation + fee accounting.
Render targets: PDF (template), CSV, JSON, 94-char
NACHA fixed-width. ISO 20022 camt.053 with full
BkToCstmrStmt envelope (GrpHdr/Stmt/Bal/Ntry). BAI2
with 01/02/03/16/49/98/99 records. MT940/MT942
SWIFT with :20:/:25:/:28C:/:60F:/:61:/:86:/:62F:
+ :90C:/:90D: counters. FeeBucket (interchange/scheme/
acquirer/fx/settlement-network/bnpl) + FeeSchedule
with bps + flat rules. Cadence (daily/weekly/monthly/
custom). Two-tier Reconciler (external_id → window
heuristic).
Smart Byte (1 crate, 3032 LOC, 39 tests)
smart-byte-activitypub ActivityPub W3C Rec (2018). AS2 vocab + Public
addressing; Actor (Person/Application/Group/
Organization/Service) with publicKey + shared-inbox;
Object (Note/Article/Image/Video/Tombstone) with
Mastodon extensions (sensitive, CW summary,
conversation, blurhash); 12 Activity types
(Create/Update/Delete/Follow/Accept/Reject/Announce/
Like/Undo/Move/Block/Flag) with side-effect rules;
OrderedCollection + Page; Webfinger (RFC 7033);
HTTP Signatures (cavage draft-12 + RFC 9421);
SHA-256 Digest; Ed25519 sign/verify.
EOC (1 crate, 2759 LOC, 20 tests)
eoc-observability OpenTelemetry-compatible tracing + metrics + logs.
W3C tracecontext + baggage. Span / SpanKind / Status.
Samplers: AlwaysOn/Off/RatioBased/ParentBased. Sync
BatchSpanProcessor + Exporter trait. Counter / Gauge /
Histogram. Prometheus text exposition (HELP/TYPE/
_bucket/_sum/_count/+Inf). StatsD / DogStatsD lines.
GenAI semconv (gen_ai.{operation,system,request.*,
response.*,usage.*}) + EOC extensions (eoc.joules.*,
eoc.co2e_grams, eoc.cost.usd). USD + gCO2e attribution
with PUE. LangSmith + LangFuse export adapters.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
102 lines
3.1 KiB
Rust
102 lines
3.1 KiB
Rust
//! Integration: end-to-end reconciliation of a statement against a
|
|
//! ledger-side view containing one match, one ledger-only, and one
|
|
//! statement-only entry.
|
|
|
|
use op_core::{Currency, Money};
|
|
use op_statements::{
|
|
LedgerEntry, Period, Reconciler, Statement, StatementLine, StatementLineKind,
|
|
};
|
|
use op_statements::reconcile::{LedgerDirection, ReconStatus};
|
|
|
|
#[test]
|
|
fn three_way_reconciliation() {
|
|
let mut s = Statement::new(
|
|
"STMT-RECON-1",
|
|
"MERCHANT-RECON",
|
|
Period::new(0, 86_400).expect("period"),
|
|
Currency::USD,
|
|
)
|
|
.expect("statement");
|
|
|
|
// Line 1: matches ledger via external_id.
|
|
s.push_line(
|
|
StatementLine::new(
|
|
"l1",
|
|
StatementLineKind::GrossCapture,
|
|
Money::from_minor(2_500, Currency::USD),
|
|
1_000,
|
|
)
|
|
.with_external_id("ord-1"),
|
|
)
|
|
.expect("push 1");
|
|
|
|
// Line 2: present on statement only — bank double-posted or the
|
|
// ledger never recorded it.
|
|
s.push_line(StatementLine::new(
|
|
"l2",
|
|
StatementLineKind::Fee,
|
|
Money::from_minor(50, Currency::USD),
|
|
2_000,
|
|
))
|
|
.expect("push 2");
|
|
|
|
s.aggregate().expect("aggregate");
|
|
|
|
let ledger = vec![
|
|
// Counter-match for l1.
|
|
LedgerEntry {
|
|
id: "tx-1".into(),
|
|
external_id: Some("ord-1".into()),
|
|
amount: Money::from_minor(2_500, Currency::USD),
|
|
direction: LedgerDirection::Credit,
|
|
posted_at_unix_secs: 1_010,
|
|
},
|
|
// Ledger-only entry: posted internally, not yet on the bank
|
|
// statement.
|
|
LedgerEntry {
|
|
id: "tx-2".into(),
|
|
external_id: Some("ord-2".into()),
|
|
amount: Money::from_minor(7_777, Currency::USD),
|
|
direction: LedgerDirection::Credit,
|
|
posted_at_unix_secs: 3_000,
|
|
},
|
|
];
|
|
|
|
let records = Reconciler::with_window(86_400).reconcile(&s, &ledger).expect("reconcile");
|
|
assert_eq!(records.len(), 3);
|
|
|
|
// Find each by status.
|
|
let matched = records.iter().filter(|r| r.status == ReconStatus::Matched).count();
|
|
let missing_ledger = records
|
|
.iter()
|
|
.filter(|r| r.status == ReconStatus::MissingInLedger)
|
|
.count();
|
|
let missing_stmt = records
|
|
.iter()
|
|
.filter(|r| r.status == ReconStatus::MissingOnStatement)
|
|
.count();
|
|
assert_eq!(matched, 1);
|
|
assert_eq!(missing_ledger, 1);
|
|
assert_eq!(missing_stmt, 1);
|
|
|
|
// The l1 match is zero-delta.
|
|
let m = records
|
|
.iter()
|
|
.find(|r| r.statement_line_id.as_deref() == Some("l1"))
|
|
.expect("l1 record");
|
|
assert_eq!(m.delta_minor_units, 0);
|
|
|
|
// The l2 missing-in-ledger carries the line magnitude as delta.
|
|
let ml = records
|
|
.iter()
|
|
.find(|r| r.statement_line_id.as_deref() == Some("l2"))
|
|
.expect("l2 record");
|
|
assert_eq!(ml.delta_minor_units, 50);
|
|
|
|
// The tx-2 missing-on-statement carries a negative delta.
|
|
let ms = records
|
|
.iter()
|
|
.find(|r| r.ledger_entry_id.as_deref() == Some("tx-2"))
|
|
.expect("tx-2 record");
|
|
assert_eq!(ms.delta_minor_units, -7_777);
|
|
}
|