open-standards/openpay/crates/op-reconciliation/src/lib.rs
Transaction Science 70ecd6caa0 Initial import — three open standards under one shelf
Transaction Science open standards monorepo. Each subdirectory is a
self-contained reference protocol with its own README and licence.

- openpay/    Apache-2.0  payment-acceptance stack (Rust, 26 crates)
- smart-byte/ CC-BY-4.0   content-addressed value carrier (spec)
- eoc/        CC-BY-4.0   energy-optimised compute (spec)

The wire format and the right to fork are public. The optional hosted
services are the offer; the protocols are not.
2026-05-23 07:58:09 -04:00

87 lines
3.6 KiB
Rust

//! # `op-reconciliation` — ledger vs. statement diffing
//!
//! Reconciliation answers one question: **does what the bank/PSP says
//! happened agree with what our ledger says happened?** Every payment
//! stack needs this because the two records drift:
//!
//! - A rail confirms settlement but the webhook never arrives, so the
//! ledger never posts.
//! - A chargeback debits the merchant account at the bank but no
//! reversal is booked.
//! - A PSP fee is netted out of a payout and the gross/fee split was
//! recorded wrong.
//!
//! Without reconciliation these are invisible until an auditor or an
//! angry customer finds them. With it, each one becomes a typed
//! [`Discrepancy`](discrepancy::Discrepancy).
//!
//! ## Inputs are pluggable
//!
//! A [`ReconciliationSource`](source::ReconciliationSource) yields a
//! stream of normalized [`StatementLine`](statement::StatementLine)s.
//! Three reference sources ship:
//!
//! - [`Camt053Source`](sources::Camt053Source) — end-of-day bank
//! statement (ISO 20022 `camt.053`). The authoritative artifact.
//! - [`Camt054Source`](sources::Camt054Source) — intra-day
//! debit/credit notifications (`camt.054`).
//! - [`WebhookEventSource`](sources::WebhookEventSource) — the
//! settlement webhooks already flowing through `op-webhook`.
//!
//! Operators with a proprietary PSP CSV implement the trait
//! themselves; the rest of the engine doesn't change. This mirrors
//! the `LedgerStore` / `WebhookStore` pluggable-driver idiom used
//! elsewhere in `OpenPay`.
//!
//! ## The ledger side is caller-selected
//!
//! [`op_ledger::LedgerStore`] deliberately exposes no "list all
//! transactions" method (a real store has millions of rows; an
//! unbounded scan is a footgun). So the [`Reconciler`](engine::Reconciler)
//! takes the ledger window as an explicit `&[Transaction]` slice the
//! caller assembled — by date range, by ledger, however their store
//! indexes. We never impose an iteration API on `op-ledger`.
//!
//! ## Outputs: both a report and (optionally) graph vertices
//!
//! [`Reconciler::reconcile`](engine::Reconciler::reconcile) returns a
//! serializable [`ReconciliationReport`](discrepancy::ReconciliationReport)
//! — matched counts plus a `Vec<Discrepancy>` an operator can feed
//! into any ticketing system. Materializing discrepancies as graph
//! vertices (so an operator can traverse from a ledger account to its
//! open reconciliation tasks) lives in `op-graph` behind a trait, so
//! this crate stays free of the MPL-2.0 `indradb` dependency.
//!
//! ## What this crate does NOT do
//!
//! - **No currency conversion.** A line in EUR against a USD tx is an
//! `AmountMismatch`, not an FX calculation.
//! - **No bipartite optimal matching.** v1 is a deterministic two-tier
//! join (external-id, then amount+window heuristic). Optimal
//! assignment is future work.
//! - **No clock.** Reconciliation windows are caller-supplied so
//! replay is deterministic.
//! - **No auto-resolution.** We detect and classify; booking the
//! correcting entry is an operator decision.
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
pub mod discrepancy;
pub mod engine;
pub mod error;
pub mod matcher;
pub mod source;
pub mod sources;
pub mod statement;
pub mod store;
pub use discrepancy::{Discrepancy, ReconciliationReport, TaskDescriptor};
pub use engine::Reconciler;
pub use error::{Error, Result};
pub use matcher::MatchedPair;
pub use source::ReconciliationSource;
pub use statement::{LineDirection, StatementLine};
pub use store::{ReconciliationStore, ReconciliationTask};