1use std::collections::BTreeMap;
2
3use url::Url;
4use uuid::Uuid;
5
6use crate::{
7 Customer, IdempotencyKey, MerchantReference, Money, NextAction, PaymentError, PaymentId,
8 PaymentMethod, PaymentProvider, PaymentStatus, ProviderReference,
9};
10
11pub type Metadata = BTreeMap<String, String>;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct CreatePaymentRequest {
17 amount: Money,
18 reference: MerchantReference,
19 description: Option<String>,
20 customer: Option<Customer>,
21 payment_method: PaymentMethod,
22 callback_url: Option<Url>,
23 return_url: Option<Url>,
24 cancel_url: Option<Url>,
25 idempotency_key: Option<IdempotencyKey>,
26 metadata: Metadata,
27}
28
29impl CreatePaymentRequest {
30 #[inline]
32 pub fn builder() -> CreatePaymentRequestBuilder {
33 CreatePaymentRequestBuilder::default()
34 }
35
36 #[inline]
38 #[must_use]
39 pub const fn amount(&self) -> &Money {
40 &self.amount
41 }
42
43 #[inline]
45 #[must_use]
46 pub const fn reference(&self) -> &MerchantReference {
47 &self.reference
48 }
49
50 #[inline]
52 #[must_use]
53 pub fn description(&self) -> Option<&str> {
54 self.description.as_deref()
55 }
56
57 #[inline]
59 #[must_use]
60 pub const fn customer(&self) -> Option<&Customer> {
61 self.customer.as_ref()
62 }
63
64 #[inline]
66 #[must_use]
67 pub const fn payment_method(&self) -> &PaymentMethod {
68 &self.payment_method
69 }
70
71 #[inline]
73 #[must_use]
74 pub const fn callback_url(&self) -> Option<&Url> {
75 self.callback_url.as_ref()
76 }
77
78 #[inline]
80 #[must_use]
81 pub const fn return_url(&self) -> Option<&Url> {
82 self.return_url.as_ref()
83 }
84
85 #[inline]
87 #[must_use]
88 pub const fn cancel_url(&self) -> Option<&Url> {
89 self.cancel_url.as_ref()
90 }
91
92 #[inline]
94 #[must_use]
95 pub const fn idempotency_key(&self) -> Option<&IdempotencyKey> {
96 self.idempotency_key.as_ref()
97 }
98
99 #[inline]
101 #[must_use]
102 pub const fn metadata(&self) -> &Metadata {
103 &self.metadata
104 }
105}
106
107#[derive(Debug, Default, Clone)]
109#[must_use]
110pub struct CreatePaymentRequestBuilder {
111 amount: Option<Money>,
112 reference: Option<MerchantReference>,
113 description: Option<String>,
114 customer: Option<Customer>,
115 payment_method: Option<PaymentMethod>,
116 callback_url: Option<Url>,
117 return_url: Option<Url>,
118 cancel_url: Option<Url>,
119 idempotency_key: Option<IdempotencyKey>,
120 metadata: Metadata,
121}
122
123impl CreatePaymentRequestBuilder {
124 pub fn amount(mut self, amount: Money) -> Self {
126 self.amount = Some(amount);
127 self
128 }
129
130 pub fn reference(mut self, reference: impl AsRef<str>) -> Result<Self, PaymentError> {
136 self.reference = Some(MerchantReference::new(reference)?);
137 Ok(self)
138 }
139
140 pub fn description(mut self, description: impl Into<String>) -> Self {
142 self.description = Some(description.into());
143 self
144 }
145
146 pub fn customer(mut self, customer: Customer) -> Self {
148 self.customer = Some(customer);
149 self
150 }
151
152 pub fn payment_method(mut self, payment_method: PaymentMethod) -> Self {
154 self.payment_method = Some(payment_method);
155 self
156 }
157
158 pub fn callback_url(mut self, url: impl AsRef<str>) -> Result<Self, PaymentError> {
164 self.callback_url = Some(parse_url(url.as_ref())?);
165 Ok(self)
166 }
167
168 pub fn return_url(mut self, url: impl AsRef<str>) -> Result<Self, PaymentError> {
174 self.return_url = Some(parse_url(url.as_ref())?);
175 Ok(self)
176 }
177
178 pub fn cancel_url(mut self, url: impl AsRef<str>) -> Result<Self, PaymentError> {
184 self.cancel_url = Some(parse_url(url.as_ref())?);
185 Ok(self)
186 }
187
188 pub fn idempotency_key(mut self, key: impl AsRef<str>) -> Result<Self, PaymentError> {
194 self.idempotency_key = Some(IdempotencyKey::new(key)?);
195 Ok(self)
196 }
197
198 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
200 self.metadata.insert(key.into(), value.into());
201 self
202 }
203
204 pub fn build(self) -> Result<CreatePaymentRequest, PaymentError> {
210 Ok(CreatePaymentRequest {
211 amount: self
212 .amount
213 .ok_or(PaymentError::MissingRequiredField("amount"))?,
214 reference: self
215 .reference
216 .ok_or(PaymentError::MissingRequiredField("reference"))?,
217 description: self.description,
218 customer: self.customer,
219 payment_method: self
220 .payment_method
221 .ok_or(PaymentError::MissingRequiredField("payment_method"))?,
222 callback_url: self.callback_url,
223 return_url: self.return_url,
224 cancel_url: self.cancel_url,
225 idempotency_key: self.idempotency_key,
226 metadata: self.metadata,
227 })
228 }
229}
230
231fn parse_url(value: &str) -> Result<Url, PaymentError> {
232 Url::parse(value).map_err(|error| PaymentError::InvalidUrl(error.to_string()))
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct PaymentSession {
238 pub payment_id: PaymentId,
240 pub provider: PaymentProvider,
242 pub provider_reference: ProviderReference,
244 pub merchant_reference: MerchantReference,
246 pub status: PaymentStatus,
248 pub next_action: Option<NextAction>,
250}
251
252impl PaymentSession {
253 pub fn new(
259 provider: PaymentProvider,
260 provider_reference: ProviderReference,
261 merchant_reference: MerchantReference,
262 status: PaymentStatus,
263 next_action: Option<NextAction>,
264 ) -> Result<Self, PaymentError> {
265 Ok(Self {
266 payment_id: PaymentId::new(format!("pay_{}", Uuid::new_v4()))?,
267 provider,
268 provider_reference,
269 merchant_reference,
270 status,
271 next_action,
272 })
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct PaymentStatusResponse {
279 pub provider: PaymentProvider,
281 pub provider_reference: ProviderReference,
283 pub status: PaymentStatus,
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn builder_requires_fields() {
293 assert!(matches!(
294 CreatePaymentRequest::builder().build(),
295 Err(PaymentError::MissingRequiredField("amount"))
296 ));
297 }
298
299 #[test]
300 fn builder_creates_request() {
301 let request = CreatePaymentRequest::builder()
302 .amount(Money::new_minor(1_000, "USD").expect("money should be valid"))
303 .reference("ORDER-1")
304 .expect("reference should be valid")
305 .description("Order 1")
306 .customer(Customer::new().with_email("customer@example.com"))
307 .payment_method(PaymentMethod::card())
308 .callback_url("https://example.com/webhook")
309 .expect("url should be valid")
310 .return_url("https://example.com/success")
311 .expect("url should be valid")
312 .cancel_url("https://example.com/cancel")
313 .expect("url should be valid")
314 .idempotency_key("ORDER-1:create")
315 .expect("key should be valid")
316 .metadata("cart", "primary")
317 .build()
318 .expect("request should be valid");
319
320 assert_eq!(request.reference().as_str(), "ORDER-1");
321 assert_eq!(request.description(), Some("Order 1"));
322 assert_eq!(
323 request
324 .customer()
325 .expect("customer should be present")
326 .email(),
327 Some("customer@example.com")
328 );
329 assert!(request.callback_url().is_some());
330 assert!(request.return_url().is_some());
331 assert!(request.cancel_url().is_some());
332 assert_eq!(
333 request
334 .idempotency_key()
335 .expect("key should be present")
336 .as_str(),
337 "ORDER-1:create"
338 );
339 assert_eq!(
340 request
341 .metadata()
342 .get("cart")
343 .expect("metadata should exist"),
344 "primary"
345 );
346 }
347
348 #[test]
349 fn payment_session_new_generates_payment_id() {
350 let session = PaymentSession::new(
351 PaymentProvider::Stripe,
352 ProviderReference::new("provider-ref").expect("reference should be valid"),
353 MerchantReference::new("ORDER-1").expect("reference should be valid"),
354 PaymentStatus::Created,
355 None,
356 )
357 .expect("session should be valid");
358
359 assert!(session.payment_id.as_str().starts_with("pay_"));
360 assert_eq!(session.provider, PaymentProvider::Stripe);
361 }
362}