mod deserialize;
mod serialize;
use std::sync::Arc;
use reqwest::redirect;
use serde::{Deserialize, Serialize};
use crate::res::OrdersInfo;
macro_rules! get_client {
($self:expr) => {{
#[cfg(feature = "single-client")]
{
$self.client.clone()
}
#[cfg(not(feature = "single-client"))]
{
Client::build_client()
}
}};
}
pub static SUCCESS: &str = "SUCCESS";
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Client is not authorized. No bearer token available")]
NoToken,
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("Total value is not sum of products price")]
IncorrectTotal,
#[error("{0}")]
Reqwest(#[from] reqwest::Error),
#[error("Buyer is required to place an order")]
NoBuyer,
#[error("Description is required to place an order")]
NoDescription,
#[error("Client is not authorized")]
Unauthorized,
#[error("Refund returned invalid response")]
Refund,
#[error("Create order returned invalid response")]
CreateOrder,
#[error("Failed to fetch order transactions")]
OrderTransactions,
#[error("Failed to fetch order details")]
OrderDetails,
#[error("Failed to fetch order refunds")]
OrderRefunds,
#[error("PayU rejected to create order with status {status_code:?}")]
CreateFailed {
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
},
#[error("PayU rejected to perform refund with status {status_code:?}")]
RefundFailed {
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
},
#[error("PayU rejected order details request with status {status_code:?}")]
OrderDetailsFailed {
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
},
#[error("PayU rejected order transactions details request with status {status_code:?}")]
OrderTransactionsFailed {
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
},
#[error("PayU returned order details but without any order")]
NoOrderInDetails,
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(
Debug,
Clone,
serde::Deserialize,
serde::Serialize,
derive_more::Display,
derive_more::From,
derive_more::Deref,
)]
#[serde(transparent)]
pub struct OrderId(pub String);
impl OrderId {
pub fn new<S: Into<String>>(id: S) -> Self {
Self(id.into())
}
}
#[derive(
Debug,
serde::Deserialize,
serde::Serialize,
Copy,
Clone,
derive_more::Display,
derive_more::From,
derive_more::Deref,
derive_more::Constructor,
)]
#[serde(transparent)]
pub struct MerchantPosId(pub i32);
#[derive(
Debug,
Clone,
serde::Deserialize,
serde::Serialize,
derive_more::Display,
derive_more::From,
derive_more::Deref,
)]
#[serde(transparent)]
pub struct ClientId(pub String);
impl ClientId {
pub fn new<S: Into<String>>(id: S) -> Self {
Self(id.into())
}
}
#[derive(
Debug,
Clone,
serde::Deserialize,
serde::Serialize,
derive_more::Display,
derive_more::From,
derive_more::Deref,
)]
#[serde(transparent)]
pub struct ClientSecret(pub String);
impl ClientSecret {
pub fn new<S: Into<String>>(id: S) -> Self {
Self(id.into())
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PaymentStatus {
Pending,
WaitingForConfirmation,
Completed,
Canceled,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RefundStatus {
Finalized,
Canceled,
Pending,
WaitingForConfirmation,
Completed,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct Buyer {
email: String,
phone: String,
first_name: String,
last_name: String,
language: String,
}
impl Buyer {
pub fn new<Email, Phone, FirstName, LastName, Language>(
email: Email,
phone: Phone,
first_name: FirstName,
last_name: LastName,
lang: Language,
) -> Self
where
Email: Into<String>,
Phone: Into<String>,
FirstName: Into<String>,
LastName: Into<String>,
Language: Into<String>,
{
Self {
email: email.into(),
phone: phone.into(),
first_name: first_name.into(),
last_name: last_name.into(),
language: lang.into(),
}
}
pub fn email(&self) -> &str {
&self.email
}
pub fn with_email<S>(mut self, email: S) -> Self
where
S: Into<String>,
{
self.email = email.into();
self
}
pub fn phone(&self) -> &str {
&self.phone
}
pub fn with_phone<S>(mut self, phone: S) -> Self
where
S: Into<String>,
{
self.phone = phone.into();
self
}
pub fn first_name(&self) -> &str {
&self.first_name
}
pub fn with_first_name<S>(mut self, first_name: S) -> Self
where
S: Into<String>,
{
self.first_name = first_name.into();
self
}
pub fn last_name(&self) -> &str {
&self.last_name
}
pub fn with_last_name<S>(mut self, last_name: S) -> Self
where
S: Into<String>,
{
self.last_name = last_name.into();
self
}
pub fn language(&self) -> &str {
&self.language
}
pub fn with_language<S>(mut self, language: S) -> Self
where
S: Into<String>,
{
self.language = language.into();
self
}
}
pub type Price = i32;
pub type Quantity = u32;
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Product {
pub name: String,
#[serde(
serialize_with = "serialize::serialize_i32",
deserialize_with = "deserialize::deserialize_i32"
)]
pub unit_price: Price,
#[serde(
serialize_with = "serialize::serialize_u32",
deserialize_with = "deserialize::deserialize_u32"
)]
pub quantity: Quantity,
}
impl Product {
pub fn new<Name: Into<String>>(name: Name, unit_price: Price, quantity: Quantity) -> Self {
Self {
name: name.into(),
unit_price,
quantity,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrderCreateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
notify_url: Option<String>,
customer_ip: String,
#[serde(
serialize_with = "serialize::serialize_newtype",
deserialize_with = "deserialize::deserialize_i32_newtype"
)]
merchant_pos_id: MerchantPosId,
description: String,
currency_code: String,
#[serde(
serialize_with = "serialize::serialize_i32",
deserialize_with = "deserialize::deserialize_i32"
)]
total_amount: Price,
buyer: Option<Buyer>,
products: Vec<Product>,
#[serde(skip_serializing)]
order_create_date: Option<String>,
}
impl OrderCreateRequest {
pub fn new<CustomerIp, Currency>(
buyer: Buyer,
customer_ip: CustomerIp,
currency: Currency,
) -> Self
where
CustomerIp: Into<String>,
Currency: Into<String>,
{
Self {
notify_url: None,
customer_ip: customer_ip.into(),
merchant_pos_id: 0.into(),
description: String::from(""),
currency_code: currency.into(),
total_amount: 0,
buyer: Some(buyer),
products: Vec::new(),
order_create_date: None,
}
}
pub fn with_products<Products>(mut self, products: Products) -> Self
where
Products: Iterator<Item = Product>,
{
self.products.extend(products);
self.total_amount = self
.products
.iter()
.fold(0, |agg, p| agg + (p.quantity as i32 * p.unit_price as i32));
self
}
pub fn with_product(mut self, product: Product) -> Self {
self.products.push(product);
self.total_amount = self
.products
.iter()
.fold(0, |agg, p| agg + (p.quantity as i32 * p.unit_price as i32));
self
}
pub fn with_description<Description>(mut self, desc: Description) -> Self
where
Description: Into<String>,
{
self.description = String::from(desc.into().trim());
self
}
pub fn with_notify_url<NotifyUrl>(mut self, notify_url: NotifyUrl) -> Self
where
NotifyUrl: Into<String>,
{
self.notify_url = Some(notify_url.into());
self
}
pub fn notify_url(&self) -> &Option<String> {
&self.notify_url
}
pub fn customer_ip(&self) -> &String {
&self.customer_ip
}
pub fn merchant_pos_id(&self) -> MerchantPosId {
self.merchant_pos_id
}
pub fn description(&self) -> &String {
&self.description
}
pub fn currency_code(&self) -> &String {
&self.currency_code
}
pub fn total_amount(&self) -> &Price {
&self.total_amount
}
pub fn buyer(&self) -> &Option<Buyer> {
&self.buyer
}
pub fn products(&self) -> &[Product] {
&self.products
}
pub fn order_create_date(&self) -> &Option<String> {
&self.order_create_date
}
pub(crate) fn with_merchant_pos_id(mut self, merchant_pos_id: MerchantPosId) -> Self {
self.merchant_pos_id = merchant_pos_id;
self
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PaymentType {
Pbl,
CardToken,
Installments,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PayMethod {
#[serde(rename = "type")]
pub payment_type: PaymentType,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Status {
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum StatusCode {
ErrorValueMissing,
OpenpayuBusinessError,
OpenpayuErrorValueInvalid,
OpenpayuErrorInternal,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CodeLiteral {
MissingRefundSection,
TransNotEnded,
NoBalance,
AmountToBig,
AmountToSmall,
RefundDisabled,
RefundToOften,
Paid,
UnknownError,
RefundIdempotencyMismatch,
TransBillingEntriesNotCompleted,
TransTooOld,
RemainingTransAmountTooSmall,
#[serde(other)]
Unknown,
}
impl Status {
pub fn is_success(&self) -> bool {
self.status_code.as_str() == SUCCESS
}
pub fn status_code(&self) -> &str {
&self.status_code
}
pub fn status_desc(&self) -> Option<&str> {
self.status_desc.as_deref()
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Prop {
pub name: String,
pub value: String,
}
pub mod res {
use crate::{OrderId, Refund, Status};
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreateOrder {
pub status: Status,
pub redirect_uri: String,
pub order_id: OrderId,
pub ext_order_id: Option<String>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RefundDetails {
pub order_id: Option<String>,
pub refund: Option<Refund>,
pub status: Status,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Refunds {
pub refunds: Vec<Refund>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionPayMethod {
pub value: String,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CardProfile {
Consumer,
Business,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CardClassification {
Debit,
Credit,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionCartData {
pub card_number_masked: String,
pub card_scheme: String,
pub card_profile: CardProfile,
pub card_classification: CardClassification,
pub card_response_code: String,
pub card_response_code_desc: String,
pub card_eci_code: String,
pub card3ds_status: String,
pub card_bin_country: String,
pub first_transaction_id: String,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionCardInstallmentProposal {
pub proposal_id: String,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionCart {
pub cart_data: TransactionCartData,
pub card_installment_proposal: TransactionCardInstallmentProposal,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
pub pay_method: TransactionPayMethod,
pub payment_flow: String,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Transactions {
pub transactions: Vec<Transaction>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Order {
pub order_id: super::OrderId,
pub ext_order_id: Option<String>,
pub order_create_date: String,
pub notify_url: Option<String>,
pub customer_ip: String,
pub merchant_pos_id: String,
pub description: String,
pub currency_code: String,
pub total_amount: String,
pub status: String,
pub products: Vec<super::Product>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrdersInfo {
pub orders: Vec<Order>,
pub status: super::Status,
pub properties: Option<Vec<crate::Prop>>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrderInfo {
pub order: Order,
pub status: super::Status,
pub properties: Option<Vec<crate::Prop>>,
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RefundRequest {
description: String,
#[serde(skip_serializing_if = "Option::is_none")]
amount: Option<Price>,
}
impl RefundRequest {
pub fn new<Description>(description: Description, amount: Option<Price>) -> Self
where
Description: Into<String>,
{
Self {
description: description.into(),
amount,
}
}
pub fn description(&self) -> &str {
&self.description
}
pub fn amount(&self) -> Option<Price> {
self.amount
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Refund {
pub refund_id: String,
pub ext_refund_id: Option<String>,
pub amount: String,
pub currency_code: String,
pub description: String,
pub creation_date_time: String,
pub status: String,
pub status_date_time: String,
}
pub mod notify {
use serde::Deserialize;
use super::deserialize;
use crate::OrderId;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StatusUpdate {
pub order: Order,
pub local_receipt_date_time: Option<String>,
pub properties: Option<Vec<super::Prop>>,
pub status: Option<super::Status>,
}
impl StatusUpdate {
pub fn status(&self) -> super::PaymentStatus {
self.order.status
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RefundUpdate {
pub ext_order_id: String,
pub order_id: OrderId,
pub refund: Refund,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Refund {
pub refund_id: String,
pub amount: String,
pub currency_code: String,
pub status: super::RefundStatus,
pub status_date_time: String,
pub reason: String,
pub reason_description: String,
pub refund_date: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Order {
pub notify_url: Option<String>,
pub customer_ip: String,
#[serde(deserialize_with = "deserialize::deserialize_i32_newtype")]
pub merchant_pos_id: super::MerchantPosId,
pub description: String,
pub currency_code: String,
#[serde(deserialize_with = "deserialize::deserialize_i32")]
pub total_amount: super::Price,
pub buyer: Option<super::Buyer>,
pub products: Vec<super::Product>,
#[serde(skip_serializing)]
pub order_create_date: Option<String>,
pub pay_method: Option<super::PayMethod>,
pub status: super::PaymentStatus,
}
}
pub struct Client {
sandbox: bool,
merchant_pos_id: MerchantPosId,
client_id: ClientId,
client_secret: ClientSecret,
bearer: Option<String>,
bearer_expires_at: chrono::DateTime<chrono::Utc>,
#[cfg(feature = "single-client")]
client: Arc<reqwest::Client>,
}
impl Client {
pub fn new(
client_id: ClientId,
client_secret: ClientSecret,
merchant_pos_id: MerchantPosId,
) -> Self {
#[cfg(feature = "single-client")]
{
Self {
bearer: None,
sandbox: false,
merchant_pos_id,
client_id,
client_secret,
bearer_expires_at: chrono::Utc::now(),
client: Arc::new(Self::build_client()),
}
}
#[cfg(not(feature = "single-client"))]
{
Self {
bearer: None,
sandbox: false,
merchant_pos_id,
client_id,
client_secret,
bearer_expires_at: chrono::Utc::now(),
}
}
}
pub fn sandbox(mut self) -> Self {
self.sandbox = true;
self
}
pub fn with_bearer<Bearer: Into<String>>(mut self, bearer: Bearer, expires_in: i64) -> Self {
self.bearer = Some(bearer.into());
self.bearer_expires_at = chrono::Utc::now() + chrono::Duration::seconds(expires_in);
self
}
pub async fn create_order(&mut self, order: OrderCreateRequest) -> Result<res::CreateOrder> {
self.authorize().await?;
if order.total_amount
!= order
.products
.iter()
.fold(0, |memo, p| memo + (p.unit_price * p.quantity as i32))
{
return Err(Error::IncorrectTotal);
}
if order.buyer().is_none() {
return Err(Error::NoBuyer);
}
if order.description().trim().is_empty() {
return Err(Error::NoDescription);
}
let bearer = self.bearer.as_ref().cloned().unwrap_or_default();
let path = format!("{}/orders", self.base_url());
let client = get_client!(self);
let text = client
.post(path)
.bearer_auth(bearer)
.json(&order.with_merchant_pos_id(self.merchant_pos_id))
.send()
.await?
.text()
.await?;
log::trace!("Response: {}", text);
let res: res::CreateOrder = serde_json::from_str(&text).map_err(|e| {
log::error!("{e:?}");
Error::CreateOrder
})?;
if !res.status.is_success() {
let Status {
status_code,
status_desc,
code,
severity,
code_literal,
} = res.status;
return Err(Error::CreateFailed {
status_desc,
status_code,
code,
severity,
code_literal,
});
}
Ok(res)
}
pub async fn refund(
&mut self,
order_id: OrderId,
refund: RefundRequest,
) -> Result<res::RefundDetails> {
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct RefundWrapper {
refund: RefundRequest,
}
self.authorize().await?;
if refund.description().trim().is_empty() {
return Err(Error::NoDescription);
}
let bearer = self.bearer.as_ref().cloned().unwrap_or_default();
let path = format!("{}/orders/{}/refunds", self.base_url(), order_id);
let client = get_client!(self);
let text = client
.post(path)
.bearer_auth(bearer)
.json(&RefundWrapper { refund })
.send()
.await?
.text()
.await?;
log::trace!("Response: {}", text);
let res: res::RefundDetails = serde_json::from_str(&text).map_err(|e| {
log::error!("Invalid PayU response {e:?}");
Error::Refund
})?;
if !res.status.is_success() {
let Status {
status_code,
status_desc,
code,
severity,
code_literal,
} = res.status;
return Err(Error::RefundFailed {
status_desc,
status_code,
code,
severity,
code_literal,
});
}
Ok(res)
}
pub async fn order_details(&mut self, order_id: OrderId) -> Result<res::OrderInfo> {
self.authorize().await?;
let bearer = self.bearer.as_ref().cloned().unwrap_or_default();
let path = format!("{}/orders/{}", self.base_url(), order_id);
let client = get_client!(self);
let text = client
.get(path)
.bearer_auth(bearer)
.send()
.await?
.text()
.await?;
log::trace!("Response: {}", text);
dbg!(&text);
let mut res: OrdersInfo = serde_json::from_str(&text).map_err(|e| {
log::error!("{e:?}");
dbg!(e);
Error::OrderDetails
})?;
if !res.status.is_success() {
let Status {
status_code,
status_desc,
code,
severity,
code_literal,
} = res.status;
return Err(Error::OrderDetailsFailed {
status_code,
status_desc,
code,
severity,
code_literal,
});
}
Ok(res::OrderInfo {
order: if res.orders.is_empty() {
return Err(Error::NoOrderInDetails);
} else {
res.orders.remove(0)
},
status: res.status,
properties: res.properties,
})
}
pub async fn order_transactions(&mut self, order_id: OrderId) -> Result<res::Transactions> {
self.authorize().await?;
let bearer = self.bearer.as_ref().cloned().unwrap_or_default();
let path = format!("{}/orders/{}/transactions", self.base_url(), order_id);
let client = get_client!(self);
let text = client
.get(path)
.bearer_auth(bearer)
.send()
.await?
.text()
.await?;
log::trace!("Response: {}", text);
dbg!(&text);
serde_json::from_str(&text).map_err(|e| {
log::error!("{e:?}");
dbg!(e);
Error::OrderTransactions
})
}
pub async fn order_refunds(&mut self, order_id: OrderId) -> Result<Vec<Refund>> {
self.authorize().await?;
let bearer = self.bearer.as_ref().cloned().unwrap_or_default();
let path = format!("{}/orders/{}/refunds", self.base_url(), order_id);
let client = get_client!(self);
let text = client
.get(path)
.bearer_auth(bearer)
.send()
.await?
.text()
.await?;
log::trace!("Response: {}", text);
let res::Refunds { refunds, .. } = serde_json::from_str(&text).map_err(|e| {
log::error!("{e:?}");
Error::OrderRefunds
})?;
Ok(refunds)
}
pub async fn authorize(&mut self) -> Result<bool> {
use chrono::{Duration, Utc};
if Utc::now() - Duration::seconds(1) < self.bearer_expires_at {
return Ok(true);
}
#[derive(Deserialize)]
struct BearerResult {
access_token: String,
expires_in: i64,
}
let client = get_client!(self);
let res = client.post(&format!(
"https://secure.payu.com/pl/standard/user/oauth/authorize?grant_type=client_credentials&client_id={}&client_secret={}",
self.client_id,
self.client_secret
))
.send()
.await?;
let res = res.json::<BearerResult>().await.map_err(|e| {
log::error!("{e}");
Error::Unauthorized
})?;
log::trace!("Bearer is {}", res.access_token);
self.bearer_expires_at = Utc::now() + Duration::seconds(res.expires_in);
self.bearer = Some(res.access_token);
Ok(true)
}
fn base_url(&self) -> &str {
if self.sandbox {
"https://secure.snd.payu.com/api/v2_1"
} else {
"https://secure.payu.com/api/v2_1"
}
}
fn build_client() -> reqwest::Client {
reqwest::ClientBuilder::default()
.user_agent("curl/7.82.0")
.use_native_tls()
.redirect(redirect::Policy::none())
.connection_verbose(true)
.build()
.expect("Failed to create client")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::res::CreateOrder;
fn build_client() -> Client {
dotenv::dotenv().ok();
Client::new(
ClientId::new("145227"),
ClientSecret::new("12f071174cb7eb79d4aac5bc2f07563f"),
MerchantPosId::new(300746),
)
.sandbox()
.with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 999999)
}
async fn perform_create_order(client: &mut Client) -> Result<CreateOrder> {
client
.create_order(
OrderCreateRequest::new(
Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
"127.0.0.1",
"PLN",
)
.with_notify_url("https://your.eshop.com/notify")
.with_description("RTV market")
.with_products(
[
Product::new("Wireless Mouse for Laptop", 15000, 1),
Product::new("HDMI cable", 6000, 1),
]
.into_iter(),
),
)
.await
}
#[tokio::test]
async fn create_order() {
let mut client = build_client();
let res = perform_create_order(&mut client).await;
if res.is_err() {
eprintln!("create_order res is {res:?}");
}
assert!(res.is_ok());
}
#[tokio::test]
async fn partial_refund() {
let mut client = build_client();
let CreateOrder { order_id, .. } = perform_create_order(&mut client)
.await
.expect("Failed to create");
let res = client
.refund(order_id, RefundRequest::new("Refund", Some(10)))
.await;
if res.is_err() {
eprintln!("partial_refund res is {res:?}");
}
assert!(matches!(res, Err(Error::RefundFailed { .. })));
}
#[tokio::test]
async fn full_refund() {
let mut client = build_client();
let CreateOrder { order_id, .. } = perform_create_order(&mut client)
.await
.expect("Failed to create");
let res = client
.refund(order_id, RefundRequest::new("Refund", None))
.await;
if res.is_err() {
eprintln!("full_refund res is {res:?}");
}
assert!(matches!(res, Err(Error::RefundFailed { .. })));
}
#[tokio::test]
async fn order_details() {
let mut client = build_client();
let CreateOrder { order_id, .. } = perform_create_order(&mut client)
.await
.expect("Failed to create");
let res = client.order_details(order_id).await;
if res.is_err() {
eprintln!("order_details res is {res:?}");
}
assert!(matches!(res, Ok(res::OrderInfo { .. })));
}
#[tokio::test]
async fn order_transactions() {
let mut client = build_client();
let CreateOrder { order_id, .. } = perform_create_order(&mut client)
.await
.expect("Failed to create");
let res = client.order_transactions(order_id).await;
if res.is_err() {
eprintln!("order_transactions res is {res:?}");
}
assert!(matches!(res, Err(Error::OrderTransactions)));
}
#[test]
fn check_accepted_refund_json() {
let res = serde_json::from_str::<res::RefundDetails>(include_str!(
"../tests/responses/accepted_refund.json"
));
assert!(res.is_ok());
}
#[test]
fn check_cancel_json() {
let res = serde_json::from_str::<notify::StatusUpdate>(include_str!(
"../tests/responses/cancel.json"
));
assert!(res.is_ok());
}
#[test]
fn check_completed_cart_token_json() {
let res = serde_json::from_str::<notify::StatusUpdate>(include_str!(
"../tests/responses/completed_cart_token.json"
));
assert!(res.is_ok());
}
#[test]
fn check_completed_installments_json() {
let res = serde_json::from_str::<notify::StatusUpdate>(include_str!(
"../tests/responses/completed_installments.json"
));
assert!(res.is_ok());
}
#[test]
fn check_completed_pbl_json() {
let res = serde_json::from_str::<notify::StatusUpdate>(include_str!(
"../tests/responses/completed_pbl.json"
));
assert!(res.is_ok());
}
#[test]
fn check_refund_json() {
let res = serde_json::from_str::<notify::RefundUpdate>(include_str!(
"../tests/responses/refund.json"
));
assert!(res.is_ok());
}
#[test]
fn check_rejection_json() {
let res = serde_json::from_str::<res::RefundDetails>(include_str!(
"../tests/responses/rejection.json"
));
assert!(res.is_ok());
}
#[test]
fn check_custom_literal_json() {
let res = serde_json::from_str::<res::RefundDetails>(include_str!(
"../tests/responses/custom_code_literal.json"
));
assert!(res.is_ok());
}
}