1use crate::error::{Ap2Error, Result};
4use crate::types::{
5 CartContents, CartMandate, IntentMandate, PaymentCurrencyAmount, PaymentDetailsInit,
6 PaymentItem, PaymentMandate, PaymentMandateContents, PaymentMethodData, PaymentReceipt,
7 PaymentRequest, PaymentResponse, PaymentShippingOption,
8};
9
10pub trait Validate {
12 fn validate(&self) -> Result<()>;
14}
15
16impl Validate for PaymentCurrencyAmount {
17 fn validate(&self) -> Result<()> {
18 if self.currency.len() != 3 || !self.currency.chars().all(|c| c.is_ascii_uppercase()) {
19 return Err(Ap2Error::ValidationError {
20 field: "currency".into(),
21 message: format!("must be 3-letter ISO 4217 code, got {:?}", self.currency),
22 });
23 }
24 Ok(())
25 }
26}
27
28impl Validate for PaymentItem {
29 fn validate(&self) -> Result<()> {
30 if self.label.trim().is_empty() {
31 return Err(Ap2Error::MissingField("PaymentItem.label".into()));
32 }
33 self.amount.validate()?;
34 if self.refund_period < 0 {
35 return Err(Ap2Error::ValidationError {
36 field: "refund_period".into(),
37 message: "must be non-negative".into(),
38 });
39 }
40 Ok(())
41 }
42}
43
44impl Validate for PaymentShippingOption {
45 fn validate(&self) -> Result<()> {
46 if self.id.trim().is_empty() {
47 return Err(Ap2Error::MissingField("PaymentShippingOption.id".into()));
48 }
49 if self.label.trim().is_empty() {
50 return Err(Ap2Error::MissingField("PaymentShippingOption.label".into()));
51 }
52 self.amount.validate()
53 }
54}
55
56impl Validate for PaymentMethodData {
57 fn validate(&self) -> Result<()> {
58 if self.supported_methods.trim().is_empty() {
59 return Err(Ap2Error::MissingField(
60 "PaymentMethodData.supported_methods".into(),
61 ));
62 }
63 Ok(())
64 }
65}
66
67impl Validate for PaymentDetailsInit {
68 fn validate(&self) -> Result<()> {
69 if self.id.trim().is_empty() {
70 return Err(Ap2Error::MissingField("PaymentDetailsInit.id".into()));
71 }
72 self.total.validate()?;
73 for item in &self.display_items {
74 item.validate()?;
75 }
76 Ok(())
77 }
78}
79
80impl Validate for PaymentRequest {
81 fn validate(&self) -> Result<()> {
82 if self.method_data.is_empty() {
83 return Err(Ap2Error::MissingField("PaymentRequest.method_data".into()));
84 }
85 for md in &self.method_data {
86 md.validate()?;
87 }
88 self.details.validate()
89 }
90}
91
92impl Validate for PaymentResponse {
93 fn validate(&self) -> Result<()> {
94 if self.request_id.trim().is_empty() {
95 return Err(Ap2Error::MissingField("PaymentResponse.request_id".into()));
96 }
97 if self.method_name.trim().is_empty() {
98 return Err(Ap2Error::MissingField("PaymentResponse.method_name".into()));
99 }
100 Ok(())
101 }
102}
103
104impl Validate for IntentMandate {
105 fn validate(&self) -> Result<()> {
106 if self.natural_language_description.trim().is_empty() {
107 return Err(Ap2Error::MissingField(
108 "IntentMandate.natural_language_description".into(),
109 ));
110 }
111 if self.intent_expiry.trim().is_empty() {
112 return Err(Ap2Error::MissingField("IntentMandate.intent_expiry".into()));
113 }
114 Ok(())
115 }
116}
117
118impl Validate for CartContents {
119 fn validate(&self) -> Result<()> {
120 if self.id.trim().is_empty() {
121 return Err(Ap2Error::MissingField("CartContents.id".into()));
122 }
123 if self.cart_expiry.trim().is_empty() {
124 return Err(Ap2Error::MissingField("CartContents.cart_expiry".into()));
125 }
126 if self.merchant_name.trim().is_empty() {
127 return Err(Ap2Error::MissingField("CartContents.merchant_name".into()));
128 }
129 self.payment_request.validate()
130 }
131}
132
133impl Validate for CartMandate {
134 fn validate(&self) -> Result<()> {
135 self.contents.validate()
136 }
137}
138
139impl Validate for PaymentMandateContents {
140 fn validate(&self) -> Result<()> {
141 if self.payment_mandate_id.trim().is_empty() {
142 return Err(Ap2Error::MissingField(
143 "PaymentMandateContents.payment_mandate_id".into(),
144 ));
145 }
146 if self.payment_details_id.trim().is_empty() {
147 return Err(Ap2Error::MissingField(
148 "PaymentMandateContents.payment_details_id".into(),
149 ));
150 }
151 if self.merchant_agent.trim().is_empty() {
152 return Err(Ap2Error::MissingField(
153 "PaymentMandateContents.merchant_agent".into(),
154 ));
155 }
156 if self.timestamp.trim().is_empty() {
157 return Err(Ap2Error::MissingField(
158 "PaymentMandateContents.timestamp".into(),
159 ));
160 }
161 self.payment_details_total.validate()?;
162 self.payment_response.validate()
163 }
164}
165
166impl Validate for PaymentMandate {
167 fn validate(&self) -> Result<()> {
168 self.payment_mandate_contents.validate()
169 }
170}
171
172impl Validate for PaymentReceipt {
173 fn validate(&self) -> Result<()> {
174 if self.payment_mandate_id.trim().is_empty() {
175 return Err(Ap2Error::MissingField(
176 "PaymentReceipt.payment_mandate_id".into(),
177 ));
178 }
179 if self.payment_id.trim().is_empty() {
180 return Err(Ap2Error::MissingField("PaymentReceipt.payment_id".into()));
181 }
182 if self.timestamp.trim().is_empty() {
183 return Err(Ap2Error::MissingField("PaymentReceipt.timestamp".into()));
184 }
185 self.amount.validate()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::types::{PaymentCurrencyAmount, PaymentStatus, Success};
193
194 #[test]
195 fn valid_currency() {
196 let amt = PaymentCurrencyAmount {
197 currency: "USD".into(),
198 value: 10.0,
199 };
200 assert!(amt.validate().is_ok());
201 }
202
203 #[test]
204 fn invalid_currency() {
205 let amt = PaymentCurrencyAmount {
206 currency: "us".into(),
207 value: 10.0,
208 };
209 assert!(amt.validate().is_err());
210 }
211
212 #[test]
213 fn empty_label_fails() {
214 let item = PaymentItem {
215 label: "".into(),
216 amount: PaymentCurrencyAmount {
217 currency: "USD".into(),
218 value: 1.0,
219 },
220 pending: None,
221 refund_period: 30,
222 };
223 assert!(item.validate().is_err());
224 }
225
226 #[test]
227 fn valid_intent_mandate() {
228 let im = IntentMandate {
229 user_cart_confirmation_required: true,
230 natural_language_description: "Buy shoes".into(),
231 merchants: None,
232 skus: None,
233 requires_refundability: None,
234 intent_expiry: "2026-12-31T23:59:59Z".into(),
235 };
236 assert!(im.validate().is_ok());
237 }
238
239 #[test]
240 fn empty_description_fails() {
241 let im = IntentMandate {
242 user_cart_confirmation_required: true,
243 natural_language_description: " ".into(),
244 merchants: None,
245 skus: None,
246 requires_refundability: None,
247 intent_expiry: "2026-12-31T23:59:59Z".into(),
248 };
249 assert!(im.validate().is_err());
250 }
251
252 #[test]
253 fn valid_receipt() {
254 let receipt = PaymentReceipt {
255 payment_mandate_id: "pm_1".into(),
256 timestamp: "2025-09-16T12:00:00Z".into(),
257 payment_id: "pay_1".into(),
258 amount: PaymentCurrencyAmount {
259 currency: "EUR".into(),
260 value: 50.0,
261 },
262 payment_status: PaymentStatus::Success(Success {
263 merchant_confirmation_id: "mc_1".into(),
264 psp_confirmation_id: None,
265 network_confirmation_id: None,
266 }),
267 payment_method_details: None,
268 };
269 assert!(receipt.validate().is_ok());
270 }
271}