use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::instrument;
use crate::{
Amount, Error, Gateway, Result, StartRequest, StartResponse, VerifyRequest, VerifyResponse,
};
const PROVIDER: &str = "zarinpal";
const PROD_API: &str = "https://payment.zarinpal.com";
const PROD_PAY: &str = "https://www.zarinpal.com";
const SANDBOX_API: &str = "https://sandbox.zarinpal.com";
const SANDBOX_PAY: &str = "https://sandbox.zarinpal.com";
pub struct ZarinPal {
merchant_id: String,
api_base: String,
pay_base: String,
client: reqwest::Client,
}
impl ZarinPal {
#[must_use]
pub fn new(merchant_id: impl Into<String>) -> Self {
Self {
merchant_id: merchant_id.into(),
api_base: PROD_API.into(),
pay_base: PROD_PAY.into(),
client: reqwest::Client::new(),
}
}
#[must_use]
pub fn sandbox(mut self) -> Self {
self.api_base = SANDBOX_API.into();
self.pay_base = SANDBOX_PAY.into();
self
}
#[must_use]
pub fn with_api_base(mut self, url: impl Into<String>) -> Self {
self.api_base = url.into();
self
}
#[must_use]
pub fn with_pay_base(mut self, url: impl Into<String>) -> Self {
self.pay_base = url.into();
self
}
#[must_use]
pub fn with_client(mut self, client: reqwest::Client) -> Self {
self.client = client;
self
}
}
#[async_trait]
impl Gateway for ZarinPal {
fn name(&self) -> &'static str {
PROVIDER
}
#[instrument(skip(self, req), fields(provider = PROVIDER, amount_rials = req.amount.as_rials()))]
async fn start_payment(&self, req: &StartRequest) -> Result<StartResponse> {
let url = format!("{}/pg/v4/payment/request.json", self.api_base);
let body = json!({
"merchant_id": self.merchant_id,
"amount": req.amount.as_rials(),
"callback_url": req.callback_url,
"description": req.description,
"metadata": {
"email": req.email,
"mobile": req.mobile,
"order_id": req.order_id,
}
});
let raw: serde_json::Value = self
.client
.post(&url)
.json(&body)
.send()
.await
.map_err(|e| Error::http(PROVIDER, e))?
.json()
.await
.map_err(|e| Error::http(PROVIDER, e))?;
let parsed: ZpResp<ZpStartData> = serde_json::from_value(raw.clone())
.map_err(|e| Error::decode(PROVIDER, format!("start: {e}")))?;
check_zp_errors(&parsed)?;
let data = parsed
.data
.ok_or_else(|| Error::decode(PROVIDER, "start: missing `data`"))?;
if data.code != 100 {
return Err(Error::Gateway {
provider: PROVIDER,
code: data.code,
message: data.message,
});
}
let payment_url = format!("{}/pg/StartPay/{}", self.pay_base, data.authority);
Ok(StartResponse {
authority: data.authority,
payment_url,
provider: PROVIDER,
raw,
})
}
#[instrument(skip(self, req), fields(provider = PROVIDER, authority = %req.authority))]
async fn verify_payment(&self, req: &VerifyRequest) -> Result<VerifyResponse> {
let url = format!("{}/pg/v4/payment/verify.json", self.api_base);
let body = json!({
"merchant_id": self.merchant_id,
"authority": req.authority,
"amount": req.amount.as_rials(),
});
let raw: serde_json::Value = self
.client
.post(&url)
.json(&body)
.send()
.await
.map_err(|e| Error::http(PROVIDER, e))?
.json()
.await
.map_err(|e| Error::http(PROVIDER, e))?;
let parsed: ZpResp<ZpVerifyData> = serde_json::from_value(raw.clone())
.map_err(|e| Error::decode(PROVIDER, format!("verify: {e}")))?;
check_zp_errors(&parsed)?;
let data = parsed
.data
.ok_or_else(|| Error::decode(PROVIDER, "verify: missing `data`"))?;
if data.code != 100 && data.code != 101 {
return Err(Error::Gateway {
provider: PROVIDER,
code: data.code,
message: data.message,
});
}
Ok(VerifyResponse {
transaction_id: data.ref_id.to_string(),
authority: req.authority.clone(),
amount: req.amount,
card_pan: data.card_pan,
card_hash: data.card_hash,
fee: data.fee.map(Amount::rial),
provider: PROVIDER,
raw,
})
}
}
#[derive(Debug, Deserialize, Serialize)]
struct ZpResp<D> {
data: Option<D>,
#[serde(default, deserialize_with = "deser_errors_loose")]
errors: Vec<ZpError>,
}
#[derive(Debug, Deserialize, Serialize)]
struct ZpError {
code: i64,
message: String,
}
#[derive(Debug, Deserialize, Serialize)]
struct ZpStartData {
code: i64,
#[serde(default)]
message: String,
authority: String,
}
#[derive(Debug, Deserialize, Serialize)]
struct ZpVerifyData {
code: i64,
#[serde(default)]
message: String,
ref_id: i64,
#[serde(default)]
card_pan: Option<String>,
#[serde(default)]
card_hash: Option<String>,
#[serde(default)]
fee: Option<i64>,
}
fn deser_errors_loose<'de, D>(d: D) -> std::result::Result<Vec<ZpError>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let v = serde_json::Value::deserialize(d)?;
match v {
serde_json::Value::Array(a) => {
serde_json::from_value(serde_json::Value::Array(a)).map_err(D::Error::custom)
}
serde_json::Value::Object(o) if !o.is_empty() => {
let one: ZpError =
serde_json::from_value(serde_json::Value::Object(o)).map_err(D::Error::custom)?;
Ok(vec![one])
}
_ => Ok(Vec::new()),
}
}
fn check_zp_errors<D>(resp: &ZpResp<D>) -> Result<()> {
if let Some(first) = resp.errors.first() {
return Err(Error::Gateway {
provider: PROVIDER,
code: first.code,
message: first.message.clone(),
});
}
Ok(())
}