paynow/payment/
mod.rs

1//! Payment messages
2
3#[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/// Payment
36#[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    /// Set additional info
57    pub fn additional_info(&mut self, info: &'a str) -> &mut Self {
58        self.additional_info = Some(info);
59        self
60    }
61
62    /// Set auth email
63    pub fn auth_email(&mut self, email: &'a str) -> &mut Self {
64        self.auth_email = Some(email);
65        self
66    }
67
68    /// Set merchant trace
69    pub fn merchant_trace(&mut self, id: &'a str) -> &mut Self {
70        self.merchant_trace = Some(id);
71        self
72    }
73
74    /// Set tokenize
75    pub fn tokenize(&mut self, tokenize: bool) -> &mut Self {
76        self.tokenize = Some(tokenize);
77        self
78    }
79}
80
81/// Message that can be submitted to Paynow
82#[async_trait]
83pub trait Submit {
84    type Response;
85
86    /// Submits the message to Paynow
87    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/// Paynow response
143#[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    /// Get a reference to the browser URL
155    #[must_use]
156    pub fn browser_url(&self) -> &Url {
157        &self.browser_url
158    }
159
160    /// Consume browser URL
161    #[must_use]
162    pub fn take_browser_url(self) -> Url {
163        self.browser_url
164    }
165
166    /// Get reference to poll URL
167    #[must_use]
168    pub fn poll_url(&self) -> &Url {
169        &self.poll_url
170    }
171
172    /// Consume poll URL
173    #[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}