diff --git a/.changeset/funding-receipt-fx-refunds-txid-privacy.md b/.changeset/funding-receipt-fx-refunds-txid-privacy.md new file mode 100644 index 00000000..5a25254f --- /dev/null +++ b/.changeset/funding-receipt-fx-refunds-txid-privacy.md @@ -0,0 +1,16 @@ +--- +"@hypercerts-org/lexicon": minor +--- + +Funding receipt: reconciliation and completeness fields — `fx`, `fees`/`netAmount`, `refundOf`, `via`, `settledAt` — plus `transactionId` privacy guidance. + +**Non-breaking:** all new fields are optional; existing receipts remain valid. + +Informed by the payment models of Stripe (BalanceTransaction gross/fee/net, available_on), Wise (source/target amounts, rate), PayPal (gross/fee/net, platform_fees) and ISO 20022 pacs.008 (instructed vs settlement amount, settlement date, intermediary agents): + +- **`fx`** (new `#fxInfo` object: required `originalAmount`/`originalCurrency`, optional `rate`/`rateSource`/`rateTimestamp`) stamps the payment's original denomination and conversion rate at payment time, so receipts for one payment written by different parties in different currencies reconcile deterministically instead of re-deriving rates that drift. +- **`fees`** (array of new `#fee`: `type` with knownValues [processor, platform, network, fx, tax, other], `label`, required `amount`) and **`netAmount`** record the universal gross/fee/net triple: `amount` stays the gross the sender paid, `netAmount` is what the recipient actually received. Without them, a sender's "$100" and a recipient's "$96.80" receipt for the same payment can never reconcile or cluster. No charge-bearer field: a receipt is retrospective, the amounts already encode who bore what. +- **`refundOf`** (strongRef) marks a receipt as a partial or whole refund of the referenced receipt. Receipts are immutable once attested, so refunds are new receipts, never edits; copying the original's `fx` computes partial refunds at the original rate without forex drift; multiple partial refunds are multiple receipts, netted by consumers. +- **`via`** (same party union as `from`/`to`) names the intermediary that facilitated the payment (funding platform, payment servicer, broker) in the record itself, rather than leaving it implied by receipt authorship. +- **`settledAt`** records when funds actually settled to the recipient — may differ from `occurredAt` by days on delayed rails such as bank transfers. +- **`transactionId`** description now carries privacy guidance for public repositories: prefer opaque processor IDs or internal references, avoid bank references embedding account details, and note an onchain transaction hash publicly reveals the sending wallet — significant when `from` was omitted for sender anonymity. diff --git a/SCHEMAS.md b/SCHEMAS.md index 1c16ca94..bf75368e 100644 --- a/SCHEMAS.md +++ b/SCHEMAS.md @@ -272,20 +272,26 @@ Overall score for an evaluation on a numeric scale. #### Properties -| Property | Type | Required | Description | Comments | -| ---------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | -| `from` | `union` | ❌ | The sender of the funds (a free-text string, an account DID, or a strong reference to a record). Optional — omit to represent anonymity. | | -| `to` | `union` | ✅ | The recipient of the funds (a free-text string, an account DID, or a strong reference to a record). | | -| `amount` | `string` | ✅ | Amount of funding received as a numeric string (e.g. '1000.50'). | maxLength: 50 | -| `currency` | `string` | ✅ | Currency of the payment (e.g. EUR, USD, ETH). | maxLength: 10 | -| `paymentRail` | `string` | ❌ | How the funds were transferred (e.g. bank_transfer, credit_card, onchain, cash, check, payment_processor). | maxLength: 50 | -| `paymentNetwork` | `string` | ❌ | Optional network within the payment rail (e.g. arbitrum, ethereum, sepa, visa, paypal). | maxLength: 50 | -| `transactionId` | `string` | ❌ | Identifier of the underlying payment transaction (e.g. bank reference, onchain transaction hash, or processor-specific ID). Use paymentNetwork to specify the network where applicable. | maxLength: 256 | -| `for` | `ref` | ❌ | Optional strong reference to the activity, project, or organization this funding relates to. | | -| `notes` | `string` | ❌ | Optional notes or additional context for this funding receipt. | maxLength: 500 | -| `occurredAt` | `string` | ❌ | Timestamp when the payment occurred. | | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this receipt record was created. | | -| `signatures` | `ref` | ❌ | Optional cryptographic signatures attesting to this record's content. | | +| Property | Type | Required | Description | Comments | +| ---------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | +| `from` | `union` | ❌ | The sender of the funds (a free-text string, an account DID, or a strong reference to a record). Optional — omit to represent anonymity. | | +| `to` | `union` | ✅ | The recipient of the funds (a free-text string, an account DID, or a strong reference to a record). | | +| `via` | `union` | ❌ | Optional intermediary that facilitated or processed the payment (funding platform, payment servicer, broker) — named in the record rather than merely implied by who authored the receipt. Distinct from paymentNetwork (the scheme or chain) and from the record author; lets consumers group receipts by facilitator regardless of author. | | +| `amount` | `string` | ✅ | Amount of funding received as a numeric string (e.g. '1000.50'). | maxLength: 50 | +| `currency` | `string` | ✅ | Currency of the payment (e.g. EUR, USD, ETH). | maxLength: 10 | +| `netAmount` | `string` | ❌ | Amount the recipient actually received after all fees, as a numeric string in the receipt's currency. When present, amount is the gross amount the sender paid, and amount - sum(fees[].amount) ≈ netAmount. Recording both sides prevents honest receipts for one payment from disagreeing: the sender attests the gross, the recipient the net. | maxLength: 50 | +| `fees` | `ref[]` | ❌ | Itemized deductions between the gross amount and netAmount, in the receipt's currency (e.g. platform commission, processor fee, network/gas cost). A receipt is retrospective, so no charge-bearer field is needed — the amounts already encode who bore what: a sender covering fees simply raises the gross above the intended contribution. | | +| `fx` | `ref` | ❌ | Foreign-exchange context binding a receipt's amount/currency to the payment's original denomination at a point in time. | | +| `paymentRail` | `string` | ❌ | How the funds were transferred (e.g. bank_transfer, credit_card, onchain, cash, check, payment_processor). | maxLength: 50 | +| `paymentNetwork` | `string` | ❌ | Optional network within the payment rail (e.g. arbitrum, ethereum, sepa, visa, paypal). | maxLength: 50 | +| `transactionId` | `string` | ❌ | Identifier of the underlying payment transaction (e.g. bank reference, onchain transaction hash, or processor-specific ID). Use paymentNetwork to specify the network where applicable. Receipts are published to public repositories, so choose identifiers accordingly: prefer opaque processor IDs (dereferenceable only by the processor and the account holder) or internal payment references; avoid bank references that embed account details; and note that an onchain transaction hash publicly reveals the sending wallet — when the sender is deliberately anonymous (from omitted), consider whether publishing the hash is appropriate. | maxLength: 256 | +| `for` | `ref` | ❌ | Optional strong reference to the activity, project, or organization this funding relates to. | | +| `refundOf` | `ref` | ❌ | Optional strong reference marking this receipt as a refund — partial or whole — of the referenced receipt. Receipts are immutable once published (attestations bind to their content), so refunds are represented as new receipts rather than edits: amount/currency carry the refunded amount in the same denomination as the original (copy the original's fx block so partial refunds are computed at the original rate, without forex drift); from/to mirror the original reversed (when the original sender was anonymous, use the #text party for the refund's recipient). Multiple partial refunds are multiple refund receipts. Consumers aggregating funding should net refund receipts against the receipt they reference. | | +| `notes` | `string` | ❌ | Optional notes or additional context for this funding receipt. | maxLength: 500 | +| `occurredAt` | `string` | ❌ | Timestamp when the payment occurred. | | +| `settledAt` | `string` | ❌ | When the funds actually settled to the recipient (e.g. a processor's funds-available date, an interbank settlement date, or a block timestamp). May differ from occurredAt by days on delayed rails such as bank transfers. | | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this receipt record was created. | | +| `signatures` | `ref` | ❌ | Optional cryptographic signatures attesting to this record's content. | | #### Defs @@ -297,6 +303,28 @@ A free-text string value (e.g. a display name, wallet address, or other identifi | -------- | -------- | -------- | ----------------- | --------------- | | `value` | `string` | ✅ | The string value. | maxLength: 2048 | +##### `org.hypercerts.funding.receipt#fxInfo` + +Foreign-exchange context binding a receipt's amount/currency to the payment's original denomination at a point in time. + +| Property | Type | Required | Description | Comments | +| ------------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `originalAmount` | `string` | ✅ | Amount actually paid, as a numeric string, denominated in originalCurrency. | maxLength: 50 | +| `originalCurrency` | `string` | ✅ | Currency the payment was actually made in (e.g. EUR, CELO, USDC). | maxLength: 10 | +| `rate` | `string` | ❌ | Conversion rate applied at payment time, as a numeric string: the price of one unit of originalCurrency expressed in the receipt's currency, so amount = originalAmount x rate (up to rounding). Recording the rate explicitly removes rounding ambiguity and lets later computations (e.g. partial refunds) reuse the original rate instead of drifting with the market. | maxLength: 50 | +| `rateSource` | `string` | ❌ | Where the rate came from (e.g. stripe, ecb, coingecko). | maxLength: 50 | +| `rateTimestamp` | `string` | ❌ | When the rate was captured; usually the payment time. | | + +##### `org.hypercerts.funding.receipt#fee` + +A single fee deducted from a payment. + +| Property | Type | Required | Description | Comments | +| -------- | -------- | -------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | +| `type` | `string` | ❌ | Category of the fee. knownValues is an open, advisory vocabulary. | maxLength: 30, Known values: `processor`, `platform`, `network`, `fx`, `tax`, `other` | +| `label` | `string` | ❌ | Human-readable description (e.g. 'Stripe processing fee', 'Ma Earth platform fee', 'gas'). | maxLength: 100 | +| `amount` | `string` | ✅ | Fee amount as a numeric string, in the receipt's currency. | maxLength: 50 | + --- ### `org.hypercerts.workscope.cel` diff --git a/lexicons/org/hypercerts/funding/receipt.json b/lexicons/org/hypercerts/funding/receipt.json index e0522e7c..785eb800 100644 --- a/lexicons/org/hypercerts/funding/receipt.json +++ b/lexicons/org/hypercerts/funding/receipt.json @@ -28,6 +28,15 @@ "com.atproto.repo.strongRef" ] }, + "via": { + "type": "union", + "description": "Optional intermediary that facilitated or processed the payment (funding platform, payment servicer, broker) — named in the record rather than merely implied by who authored the receipt. Distinct from paymentNetwork (the scheme or chain) and from the record author; lets consumers group receipts by facilitator regardless of author.", + "refs": [ + "#text", + "app.certified.defs#did", + "com.atproto.repo.strongRef" + ] + }, "amount": { "type": "string", "description": "Amount of funding received as a numeric string (e.g. '1000.50').", @@ -38,6 +47,24 @@ "description": "Currency of the payment (e.g. EUR, USD, ETH).", "maxLength": 10 }, + "netAmount": { + "type": "string", + "description": "Amount the recipient actually received after all fees, as a numeric string in the receipt's currency. When present, amount is the gross amount the sender paid, and amount - sum(fees[].amount) ≈ netAmount. Recording both sides prevents honest receipts for one payment from disagreeing: the sender attests the gross, the recipient the net.", + "maxLength": 50 + }, + "fees": { + "type": "array", + "description": "Itemized deductions between the gross amount and netAmount, in the receipt's currency (e.g. platform commission, processor fee, network/gas cost). A receipt is retrospective, so no charge-bearer field is needed — the amounts already encode who bore what: a sender covering fees simply raises the gross above the intended contribution.", + "items": { + "type": "ref", + "ref": "#fee" + } + }, + "fx": { + "type": "ref", + "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": { "type": "string", "description": "How the funds were transferred (e.g. bank_transfer, credit_card, onchain, cash, check, payment_processor).", @@ -50,7 +77,7 @@ }, "transactionId": { "type": "string", - "description": "Identifier of the underlying payment transaction (e.g. bank reference, onchain transaction hash, or processor-specific ID). Use paymentNetwork to specify the network where applicable.", + "description": "Identifier of the underlying payment transaction (e.g. bank reference, onchain transaction hash, or processor-specific ID). Use paymentNetwork to specify the network where applicable. Receipts are published to public repositories, so choose identifiers accordingly: prefer opaque processor IDs (dereferenceable only by the processor and the account holder) or internal payment references; avoid bank references that embed account details; and note that an onchain transaction hash publicly reveals the sending wallet — when the sender is deliberately anonymous (from omitted), consider whether publishing the hash is appropriate.", "maxLength": 256 }, "for": { @@ -58,6 +85,11 @@ "ref": "com.atproto.repo.strongRef", "description": "Optional strong reference to the activity, project, or organization this funding relates to." }, + "refundOf": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "Optional strong reference marking this receipt as a refund — partial or whole — of the referenced receipt. Receipts are immutable once published (attestations bind to their content), so refunds are represented as new receipts rather than edits: amount/currency carry the refunded amount in the same denomination as the original (copy the original's fx block so partial refunds are computed at the original rate, without forex drift); from/to mirror the original reversed (when the original sender was anonymous, use the #text party for the refund's recipient). Multiple partial refunds are multiple refund receipts. Consumers aggregating funding should net refund receipts against the receipt they reference." + }, "notes": { "type": "string", "description": "Optional notes or additional context for this funding receipt.", @@ -68,6 +100,11 @@ "format": "datetime", "description": "Timestamp when the payment occurred." }, + "settledAt": { + "type": "string", + "format": "datetime", + "description": "When the funds actually settled to the recipient (e.g. a processor's funds-available date, an interbank settlement date, or a block timestamp). May differ from occurredAt by days on delayed rails such as bank transfers." + }, "createdAt": { "type": "string", "format": "datetime", @@ -92,6 +129,68 @@ "maxLength": 2048 } } + }, + "fxInfo": { + "type": "object", + "description": "Foreign-exchange context binding a receipt's amount/currency to the payment's original denomination at a point in time.", + "required": ["originalAmount", "originalCurrency"], + "properties": { + "originalAmount": { + "type": "string", + "description": "Amount actually paid, as a numeric string, denominated in originalCurrency.", + "maxLength": 50 + }, + "originalCurrency": { + "type": "string", + "description": "Currency the payment was actually made in (e.g. EUR, CELO, USDC).", + "maxLength": 10 + }, + "rate": { + "type": "string", + "description": "Conversion rate applied at payment time, as a numeric string: the price of one unit of originalCurrency expressed in the receipt's currency, so amount = originalAmount x rate (up to rounding). Recording the rate explicitly removes rounding ambiguity and lets later computations (e.g. partial refunds) reuse the original rate instead of drifting with the market.", + "maxLength": 50 + }, + "rateSource": { + "type": "string", + "description": "Where the rate came from (e.g. stripe, ecb, coingecko).", + "maxLength": 50 + }, + "rateTimestamp": { + "type": "string", + "format": "datetime", + "description": "When the rate was captured; usually the payment time." + } + } + }, + "fee": { + "type": "object", + "description": "A single fee deducted from a payment.", + "required": ["amount"], + "properties": { + "type": { + "type": "string", + "description": "Category of the fee. knownValues is an open, advisory vocabulary.", + "knownValues": [ + "processor", + "platform", + "network", + "fx", + "tax", + "other" + ], + "maxLength": 30 + }, + "label": { + "type": "string", + "description": "Human-readable description (e.g. 'Stripe processing fee', 'Ma Earth platform fee', 'gas').", + "maxLength": 100 + }, + "amount": { + "type": "string", + "description": "Fee amount as a numeric string, in the receipt's currency.", + "maxLength": 50 + } + } } } }