feat(funding): receipt reconciliation fields — fx, fees/net, refundOf, via, settledAt; transactionId privacy guidance#233
Conversation
Three additions to org.hypercerts.funding.receipt from downstream integration (Ma Earth platform receipts for fiat + crypto donations): - fx (#fxInfo object: originalAmount/originalCurrency required; rate/rateSource/rateTimestamp optional): stamps the payment's original denomination + conversion rate at payment time. Receipts for one payment written by different parties in different currencies otherwise never reconcile, and amount/currency-based clustering cannot match them; related receipts copy the block verbatim. - refundOf (strongRef): represents partial/whole refunds as NEW receipts netted against the original by consumers — receipts are immutable once attested, so edits are not an option. Copying the original's fx lets partial refunds compute at the original rate without forex drift. - transactionId description gains privacy guidance for public repos: prefer opaque processor ids / internal references, avoid bank references embedding account details, and an onchain tx hash publicly reveals the sending wallet (significant when from was omitted for donor anonymity). Non-breaking: both fields optional; existing receipts remain valid.
|
Warning Review limit reached
Next review available in: 11 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🦋 Changeset detectedLatest commit: 1b7b6a3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Extend the receipt with the reconciliation fields every major payment model carries (Stripe BalanceTransaction, Wise transfers, PayPal capture breakdowns, ISO 20022 pacs.008): - netAmount + fees[] (#fee: type knownValues [processor, platform, network, fx, tax, other], label, amount): the universal gross/fee/net triple. amount stays the gross the sender paid; netAmount is what the recipient received. Without both, a sender's gross receipt and a recipient's net receipt for the same payment never reconcile or cluster. No charge-bearer field: receipts are retrospective, the amounts encode who bore what. - via (same party union as from/to): names the facilitating intermediary (platform / payment servicer / broker) in the record instead of implying it from receipt authorship. - settledAt: settlement time distinct from occurredAt (bank rails settle days later; Stripe available_on / ISO settlement date). All optional; existing receipts remain valid.
| }, | ||
| "originalCurrency": { | ||
| "type": "string", | ||
| "description": "Currency the payment was actually made in (e.g. EUR, CELO, USDC).", |
There was a problem hiding this comment.
Lets use ETH as an example instead of CELO
| } | ||
| } | ||
| }, | ||
| "fee": { |
There was a problem hiding this comment.
The fee could be paid in a different currency, e.g. gas for a USDC transaction on Ethereum uses ETH, cross-border fees, multi-hop conversions with hosts partners.
In most cases the fees are in the recipient currency though. So, we might want to put fxInfo into the fees array (is that nesting allowed?)
| "ref": "#fxInfo", | ||
| "description": "Optional foreign-exchange context. Stamp at payment time whenever amount/currency is not the denomination the payment was actually made in (e.g. a platform recording a USD-normalized amount for a EUR card payment or an on-chain transfer). Receipts for the same payment written by other parties should copy this block verbatim from the receipt they reference, so consumers comparing or aggregating receipts reconcile deterministically instead of re-deriving rates that drift over time." | ||
| }, | ||
| "paymentRail": { |
There was a problem hiding this comment.
When we have paymentRail = payment_processor, then we have the challenge that paymentNetwork could be Stripe and Visa. I think we have two options (claude argues for option 2 for clarity, option 1 is nice because it is one field less).
Option 1: Naming the payment orocessor in paymentRails
paymentRail: The mechanism that moved the funds. Values are either a mechanism class (bank_transfer, onchain, cash, check) or, when a commercial processor executed the payment, the named processor itself (stripe, paypal, wise, square) — naming the processor implies the payment_processor class. Use paymentNetwork for the settlement scheme the payment rode over. knownValues is an open, advisory vocabulary.
paymentNetwork: The settlement scheme or network the payment rode over (e.g. visa, mastercard, sepa, ach, ethereum, arbitrum). Distinct from paymentRail (the mechanism or processor that executed it). Omit when the rail has no distinct scheme, such as a wallet-balance transfer or cash. knownValues is an open, advisory vocabulary.
via: Intermediary(ies) that facilitated the payment — the platform or entity the funder interacted with (crowdfunding site, fiscal host, broker), named rather than merely implied by the record author. Ordered by flow where meaningful. Distinct from paymentRail (the mechanism or processor that executed the transfer) and paymentNetwork (the settlement scheme). A single facilitator is a one-element array.
Option 2: Adding another field called paymentProcessor
paymentRail: The class of mechanism that moved the funds (bank_transfer, onchain, cash, check, payment_processor): a category, never a specific provider. Use paymentProcessor to name the specific service and paymentNetwork to name the settlement scheme. knownValues is an open, advisory vocabulary.
paymentProcessor: The named commercial service whose product executed the payment (e.g. stripe, paypal, wise, square). Distinct from paymentRail (the mechanism class) and paymentNetwork (the settlement scheme). Omit when there is no processor layer, such as cash or a direct bank transfer. knownValues is an open, advisory vocabulary.
paymentNetwork: "The settlement scheme or network the payment rode over (e.g. visa, mastercard, sepa, ach, ethereum, arbitrum). Distinct from paymentProcessor (the service that executed it). Omit when the rail has no distinct scheme, such as a wallet-balance transfer or cash. knownValues is an open, advisory vocabulary.
via: Intermediary(ies) that facilitated the payment — the platform or entity the funder interacted with (crowdfunding site, fiscal host, broker), named rather than merely implied by the record author. Ordered by flow where meaningful. Distinct from paymentProcessor (backend service that executed the transfer) and paymentNetwork (the settlement scheme). A single facilitator is a one-element array.
There was a problem hiding this comment.
I'm not sure if I like these options tbh
| "com.atproto.repo.strongRef" | ||
| ] | ||
| }, | ||
| "via": { |
There was a problem hiding this comment.
Should this be an array, e.g. if the payment went via Ma Earth and a host partner?
There was a problem hiding this comment.
When it goes through a host partner, we actually don't represent it fully because it would actually be two payments, but including that seems overkill
What
Brings
org.hypercerts.funding.receiptup to parity with what every major payment model considers essential, fixing the reconciliation gaps that surfaced while integrating it downstream (Ma Earth publishing platform-authored receipts for fiat + crypto donations — first records on dev). Companion to #232 (crypto-vs-fiat classification); the two are independent and can land in either order.All new fields are optional — existing receipts remain valid. Changeset:
minor.Field survey behind this (Stripe
BalanceTransaction, Wise transfers, PayPal capture breakdowns, ISO 20022 pacs.008):amountsourceValuegross_amountamountfee,fee_details[]paypal_fee,platform_fees[]nettargetValuenet_amountexchange_raterateexchange_rateavailable_onon_behalf_ofrefunds[]1.
fees[]+netAmount— the gross/fee/net tripleProblem. The most universal invariant in payments is missing: a donor attests "I sent $100", the project attests "I received $96.80" — two honest receipts for one payment that never reconcile and never cluster on amount equality. It also hides fee flows that funding-transparency platforms want public (platform commission, processor fee, donor-covered fees).
Solution.
amountstays the gross the sender paid; optionalnetAmountis what the recipient actually received; optionalfees[]itemizes the difference (#fee:typewith knownValues[processor, platform, network, fx, tax, other],label, requiredamount, all in the receipt's currency):Deliberately no ISO-style charge-bearer field: a receipt is retrospective, so the amounts already encode who bore what — a sender covering fees simply raises the gross above the intended contribution.
2.
fx— foreign-exchange contextProblem. Receipts for the same payment may be denominated differently by different parties (donor paid €100 or 50 CELO; platform records USD-normalized; recipient books local currency). Cross-receipt math never reconciles, amount+currency clustering can't match, and a partial refund computed later at the market rate doesn't match the original proportions.
Solution. Optional
fx→ new#fxInfo, stamped at payment time: requiredoriginalAmount/originalCurrency, optionalrate(1 unit of original currency in the receipt's currency; recorded explicitly to remove rounding ambiguity),rateSource,rateTimestamp. Writers of related receipts copy the block verbatim.3.
refundOf— immutable refund representationProblem. Receipts are immutable in practice — attestations (
signatures, per the ATProto Attestation Spec) bind to record content, so editing breaks them and deleting dangles strongRefs. How do partial/whole refunds get represented?Solution. A refund is a new receipt carrying
refundOf→ the original: refunded amount in the original denomination (copy the original'sfxso partial refunds compute at the original rate, without forex drift),from/tomirrored, one receipt per partial refund; consumers net refund receipts against the referenced receipt. Chargebacks/reversals fit the same shape. Rejected alternatives: status/amountRefunded mutation (breaks attestation CIDs); a separate refund lexicon (a refund is a payment — reuse keeps rails, parties, attestations, indexing uniform).4.
via— the intermediary as a partyProblem. The platform/servicer/broker that actually moved the funds is only implied by who authored the receipt.
Solution. Optional
via, same union asfrom/to(DID / free text / strongRef), naming the facilitator in the record itself. Mirrors Stripeon_behalf_of, ISO intermediary agents, and the broker role in three-party attestation models (attested.network); lets consumers group receipts by facilitator regardless of author.5.
settledAtSettlement time distinct from
occurredAt— bank rails settle days later (Stripeavailable_on, ISO settlement date; block timestamp for onchain).6.
transactionIdprivacy guidance (description only)Receipts publish payment identifiers into public repos, and identifier classes carry different risk: opaque processor IDs are dereferenceable only by the processor/account holder; bank references may embed account details; an onchain tx hash publicly reveals the sending wallet — which matters specifically when
fromwas omitted to keep a donor anonymous (cf. Stripe's transaction-ID guidance).Deliberately excluded
status/lifecycle — a receipt attests a settled fact; pending payments shouldn't get receipts.CHARetc.) — regulatory plumbing;for+ the workscope ecosystem carry semantics.metadata— untyped bags kill interop.Notes for reviewers
npm run checkgreen (validate + gen + typecheck + build + 182 tests);SCHEMAS.mdregenerated;minorchangeset included.forat the funded activity (keeping them visible in activity funding views for transparent netting), or omitforso activity views show gross funding? The description currently leaves this to writers.