Skip to main content

a2a_ap2/types/
receipt.rs

1//! AP2 payment receipt types.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use super::payment_request::PaymentCurrencyAmount;
7
8/// Details about a successful payment.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct Success {
11    /// Transaction confirmation ID at the merchant.
12    pub merchant_confirmation_id: String,
13    /// Transaction confirmation ID at the PSP.
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub psp_confirmation_id: Option<String>,
16    /// Transaction confirmation ID at the network.
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub network_confirmation_id: Option<String>,
19}
20
21/// Details about an errored payment.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct Error {
24    /// Human-readable message explaining the error.
25    pub error_message: String,
26}
27
28/// Details about a failed payment.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct Failure {
31    /// Human-readable message explaining the failure.
32    pub failure_message: String,
33}
34
35/// The status of a completed payment.
36///
37/// Each variant has a distinct set of fields, so untagged deserialization
38/// can identify the correct variant from the JSON shape.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40#[serde(untagged)]
41pub enum PaymentStatus {
42    Success(Success),
43    Error(Error),
44    Failure(Failure),
45}
46
47/// Information about the final state of a payment.
48///
49/// Exchanged via a data part with key
50/// [`PAYMENT_RECEIPT_DATA_KEY`](super::roles::PAYMENT_RECEIPT_DATA_KEY).
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52pub struct PaymentReceipt {
53    /// Reference to the processed payment mandate.
54    pub payment_mandate_id: String,
55    /// When the receipt was created, in ISO 8601 format.
56    pub timestamp: String,
57    /// Unique identifier for the payment.
58    pub payment_id: String,
59    /// Monetary amount of the payment.
60    pub amount: PaymentCurrencyAmount,
61    /// Outcome of the payment.
62    pub payment_status: PaymentStatus,
63    /// Payment-method-specific details.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub payment_method_details: Option<HashMap<String, serde_json::Value>>,
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn success_receipt_roundtrip() {
74        let receipt = PaymentReceipt {
75            payment_mandate_id: "pm_123".into(),
76            timestamp: "2025-09-16T12:00:00Z".into(),
77            payment_id: "pay_456".into(),
78            amount: PaymentCurrencyAmount {
79                currency: "USD".into(),
80                value: 120.0,
81            },
82            payment_status: PaymentStatus::Success(Success {
83                merchant_confirmation_id: "mc_789".into(),
84                psp_confirmation_id: Some("psp_101".into()),
85                network_confirmation_id: None,
86            }),
87            payment_method_details: None,
88        };
89        let json = serde_json::to_string(&receipt).unwrap();
90        let back: PaymentReceipt = serde_json::from_str(&json).unwrap();
91        assert_eq!(receipt, back);
92    }
93
94    #[test]
95    fn error_receipt() {
96        let receipt = PaymentReceipt {
97            payment_mandate_id: "pm_123".into(),
98            timestamp: "2025-09-16T12:00:00Z".into(),
99            payment_id: "pay_456".into(),
100            amount: PaymentCurrencyAmount {
101                currency: "USD".into(),
102                value: 120.0,
103            },
104            payment_status: PaymentStatus::Error(Error {
105                error_message: "Card declined".into(),
106            }),
107            payment_method_details: None,
108        };
109        let json = serde_json::to_string(&receipt).unwrap();
110        assert!(json.contains("error_message"));
111        let back: PaymentReceipt = serde_json::from_str(&json).unwrap();
112        assert_eq!(receipt, back);
113    }
114
115    #[test]
116    fn failure_receipt() {
117        let receipt = PaymentReceipt {
118            payment_mandate_id: "pm_123".into(),
119            timestamp: "2025-09-16T12:00:00Z".into(),
120            payment_id: "pay_456".into(),
121            amount: PaymentCurrencyAmount {
122                currency: "USD".into(),
123                value: 120.0,
124            },
125            payment_status: PaymentStatus::Failure(Failure {
126                failure_message: "Insufficient funds".into(),
127            }),
128            payment_method_details: None,
129        };
130        let json = serde_json::to_string(&receipt).unwrap();
131        assert!(json.contains("failure_message"));
132        let back: PaymentReceipt = serde_json::from_str(&json).unwrap();
133        assert_eq!(receipt, back);
134    }
135}