1#[macro_use]
4mod macros {
5 #[doc(hidden)]
6 #[macro_export]
7 macro_rules! concat_payment {
8 ($payment:expr) => {
9 format_args!("{id}{reference}{amount}{additional_info}{return_url}{result_url}{auth_email}{tokenize}{merchant_trace}{status}",
10 id=$payment.id,
11 reference=$payment.reference,
12 amount=$payment.amount,
13 additional_info=$payment.additional_info.unwrap_or(""),
14 return_url=$payment.return_url.map(|x| x.to_string()).unwrap_or(String::new()),
15 result_url=$payment.result_url,
16 auth_email=$payment.auth_email.unwrap_or(""),
17 tokenize=$payment.tokenize.map(|x| x.to_string()).unwrap_or(String::new()),
18 merchant_trace=$payment.merchant_trace.unwrap_or(""),
19 status=$payment.status,
20 )
21 }
22 }
23}
24
25pub mod express;
26
27use crate::{status, Client, Error, Hash, Payload};
28use async_trait::async_trait;
29use error::Error as PaymentError;
30use rust_decimal::Decimal;
31use secrecy::Secret;
32use serde::{Deserialize, Serialize};
33use url::Url;
34
35#[derive(Debug, Clone, Serialize)]
37pub struct Payment<'a> {
38 pub(crate) id: u64,
39 pub(crate) reference: &'a str,
40 pub(crate) amount: Decimal,
41 #[serde(rename = "additionalinfo")]
42 pub(crate) additional_info: Option<&'a str>,
43 #[serde(rename = "returnurl")]
44 pub(crate) return_url: Option<&'a Url>,
45 #[serde(rename = "resulturl")]
46 pub(crate) result_url: &'a Url,
47 #[serde(rename = "authemail")]
48 pub(crate) auth_email: Option<&'a str>,
49 pub(crate) tokenize: Option<bool>,
50 #[serde(rename = "merchanttrace")]
51 pub(crate) merchant_trace: Option<&'a str>,
52 pub(crate) status: status::Message,
53}
54
55impl<'a> Payment<'a> {
56 pub fn additional_info(&mut self, info: &'a str) -> &mut Self {
58 self.additional_info = Some(info);
59 self
60 }
61
62 pub fn auth_email(&mut self, email: &'a str) -> &mut Self {
64 self.auth_email = Some(email);
65 self
66 }
67
68 pub fn merchant_trace(&mut self, id: &'a str) -> &mut Self {
70 self.merchant_trace = Some(id);
71 self
72 }
73
74 pub fn tokenize(&mut self, tokenize: bool) -> &mut Self {
76 self.tokenize = Some(tokenize);
77 self
78 }
79}
80
81#[async_trait]
83pub trait Submit {
84 type Response;
85
86 async fn submit(self, client: &Client) -> Result<Self::Response, Error>;
88}
89
90#[async_trait]
91impl Submit for &'_ Payment<'_> {
92 type Response = Response;
93
94 async fn submit(self, client: &Client) -> Result<Self::Response, Error> {
95 #[derive(Debug, Clone, Serialize)]
96 struct Msg<'a> {
97 #[serde(flatten)]
98 payment: &'a Payment<'a>,
99 hash: Secret<Hash>,
100 }
101 let endpoint = client
102 .base
103 .join("initiatetransaction")
104 .map_err(Error::InvalidPaymentUrl)?;
105 let payload = Msg {
106 hash: client.hash(concat_payment!(self)),
107 payment: self,
108 };
109 let res: Response = client
110 .submit(endpoint, Payload::Form(&payload))
111 .await
112 .map_err(|err| match err {
113 Error::UnexpectedResponse(error, msg) => {
114 match serde_urlencoded::from_str::<'_, error::Response>(&msg) {
115 Ok(res) => match PaymentError::from(res) {
116 PaymentError::InvalidId => Error::InvalidId(client.id),
117 PaymentError::AmountOverflow => Error::AmountOverflow(self.amount),
118 PaymentError::InvalidAmount => Error::InvalidAmount(self.amount),
119 PaymentError::InsufficientBalance => Error::InsufficientBalance,
120 PaymentError::Response(msg) => {
121 Error::Response(reqwest::StatusCode::OK, msg)
122 }
123 },
124 Err(..) => Error::UnexpectedResponse(error, msg),
125 }
126 }
127 error => error,
128 })?;
129 client.validate_hash(
130 &res.hash,
131 format_args!(
132 "{status}{browser_url}{poll_url}",
133 status = res.status,
134 browser_url = res.browser_url,
135 poll_url = res.poll_url
136 ),
137 )?;
138 Ok(res)
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct Response {
145 status: status::Ok,
146 #[serde(rename = "browserurl")]
147 browser_url: Url,
148 #[serde(rename = "pollurl")]
149 poll_url: Url,
150 hash: Secret<Hash>,
151}
152
153impl Response {
154 #[must_use]
156 pub fn browser_url(&self) -> &Url {
157 &self.browser_url
158 }
159
160 #[must_use]
162 pub fn take_browser_url(self) -> Url {
163 self.browser_url
164 }
165
166 #[must_use]
168 pub fn poll_url(&self) -> &Url {
169 &self.poll_url
170 }
171
172 #[must_use]
174 pub fn take_poll_url(self) -> Url {
175 self.poll_url
176 }
177}
178
179pub(crate) mod error {
180 use crate::status;
181 use serde::Deserialize;
182
183 #[derive(Debug, Clone, Deserialize)]
184 pub(crate) enum Error {
185 InvalidId,
186 InvalidAmount,
187 AmountOverflow,
188 InsufficientBalance,
189 Response(String),
190 }
191
192 #[derive(Debug, Clone, Deserialize)]
193 pub(crate) struct Response {
194 #[allow(dead_code)]
195 pub(crate) status: status::Error,
196 pub(crate) error: String,
197 }
198
199 impl From<Response> for Error {
200 fn from(res: Response) -> Self {
201 match res.error.as_str() {
202 "Invalid Id." => Self::InvalidId,
203 "Invalid amount field." => Self::InvalidAmount,
204 "Conversion overflows." => Self::AmountOverflow,
205 "Insufficient balance" => Self::InsufficientBalance,
206 _ => Self::Response(res.error),
207 }
208 }
209 }
210}