PayU client
This commit is contained in:
parent
bb69885361
commit
7f83624cf7
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2138,6 +2138,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"log",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -13,6 +13,8 @@ chrono = { version = "0.4.19", features = ["serde"] }
|
|||||||
|
|
||||||
thiserror = { version = "1.0.30" }
|
thiserror = { version = "1.0.30" }
|
||||||
|
|
||||||
|
log = { version = "0.4.16" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.17.0", features = ["full"] }
|
tokio = { version = "1.17.0", features = ["full"] }
|
||||||
dotenv = { version = "0.15.0" }
|
dotenv = { version = "0.15.0" }
|
||||||
|
2
pay_u/Readme.md
Normal file
2
pay_u/Readme.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Payu REST API
|
||||||
|
|
153
pay_u/src/deserialize.rs
Normal file
153
pay_u/src/deserialize.rs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::de::{self, Error, Visitor};
|
||||||
|
|
||||||
|
pub(crate) fn deserialize_i32<'de, D>(d: D) -> std::result::Result<i32, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
d.deserialize_string(I32Visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn deserialize_u32<'de, D>(d: D) -> std::result::Result<u32, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
d.deserialize_string(U32Visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct I32Visitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for I32Visitor {
|
||||||
|
type Value = i32;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("an integer between -2^31 and 2^31")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i8<E>(self, value: i8) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(i32::from(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
use std::i32;
|
||||||
|
if value >= i64::from(i32::MIN) && value <= i64::from(i32::MAX) {
|
||||||
|
Ok(value as i32)
|
||||||
|
} else {
|
||||||
|
Err(E::custom(format!("i32 out of range: {}", value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: Error,
|
||||||
|
{
|
||||||
|
if let Ok(value) = v.parse::<i32>() {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(E::custom(format!("str does not contains valid i32: {}", v)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: Error,
|
||||||
|
{
|
||||||
|
if let Ok(value) = v.parse::<i32>() {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(E::custom(format!(
|
||||||
|
"string does not contains valid i32: {}",
|
||||||
|
v
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar for other methods:
|
||||||
|
// - visit_i16
|
||||||
|
// - visit_u8
|
||||||
|
// - visit_u16
|
||||||
|
// - visit_u32
|
||||||
|
// - visit_u64
|
||||||
|
}
|
||||||
|
|
||||||
|
struct U32Visitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for U32Visitor {
|
||||||
|
type Value = u32;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("an integer between -2^31 and 2^31")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u8<E>(self, value: u8) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(u32::from(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
use std::u32;
|
||||||
|
if value >= u64::from(u32::MIN) && value <= u64::from(u32::MAX) {
|
||||||
|
Ok(value as u32)
|
||||||
|
} else {
|
||||||
|
Err(E::custom(format!("i32 out of range: {}", value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: Error,
|
||||||
|
{
|
||||||
|
if let Ok(value) = v.parse::<u32>() {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(E::custom(format!("str does not contains valid i32: {}", v)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: Error,
|
||||||
|
{
|
||||||
|
if let Ok(value) = v.parse::<u32>() {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(E::custom(format!(
|
||||||
|
"string does not contains valid i32: {}",
|
||||||
|
v
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar for other methods:
|
||||||
|
// - visit_i16
|
||||||
|
// - visit_u8
|
||||||
|
// - visit_u16
|
||||||
|
// - visit_u32
|
||||||
|
// - visit_u64
|
||||||
|
}
|
800
pay_u/src/lib.rs
800
pay_u/src/lib.rs
@ -1,3 +1,6 @@
|
|||||||
|
mod deserialize;
|
||||||
|
mod serialize;
|
||||||
|
|
||||||
use reqwest::redirect;
|
use reqwest::redirect;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -11,6 +14,16 @@ pub enum Error {
|
|||||||
IncorrectTotal,
|
IncorrectTotal,
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Reqwest(#[from] reqwest::Error),
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
@ -37,19 +50,36 @@ pub enum PaymentStatus {
|
|||||||
Canceled,
|
Canceled,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum RefundStatus {
|
||||||
|
/// refund was completed successfully
|
||||||
|
Finalized,
|
||||||
|
/// refund was cancelled
|
||||||
|
Canceled,
|
||||||
|
/// refund in progress
|
||||||
|
Pending,
|
||||||
|
/// PayU is currently waiting for the merchant system to receive (capture)
|
||||||
|
/// the payment. This status is set if auto-receive is disabled on the
|
||||||
|
/// merchant system.
|
||||||
|
WaitingForConfirmation,
|
||||||
|
/// Payment has been accepted. PayU will pay out the funds shortly.
|
||||||
|
Completed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Buyer {
|
pub struct Buyer {
|
||||||
/// Required customer e-mail
|
/// Required customer e-mail
|
||||||
pub email: String,
|
email: String,
|
||||||
/// Required customer phone number
|
/// Required customer phone number
|
||||||
pub phone: String,
|
phone: String,
|
||||||
/// Required customer first name
|
/// Required customer first name
|
||||||
pub first_name: String,
|
first_name: String,
|
||||||
/// Required customer last name
|
/// Required customer last name
|
||||||
pub last_name: String,
|
last_name: String,
|
||||||
/// Required customer language
|
/// Required customer language
|
||||||
pub language: String,
|
language: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buyer {
|
impl Buyer {
|
||||||
@ -75,18 +105,76 @@ impl Buyer {
|
|||||||
language: lang.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 Price = i32;
|
||||||
pub type Quantity = u32;
|
pub type Quantity = u32;
|
||||||
|
pub type MerchantPosId = i32;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Product {
|
pub struct Product {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(serialize_with = "Product::serialize_unit_price")]
|
#[serde(
|
||||||
|
serialize_with = "serialize::serialize_i32",
|
||||||
|
deserialize_with = "deserialize::deserialize_i32"
|
||||||
|
)]
|
||||||
pub unit_price: Price,
|
pub unit_price: Price,
|
||||||
#[serde(serialize_with = "Product::serialize_quantity")]
|
#[serde(
|
||||||
|
serialize_with = "serialize::serialize_u32",
|
||||||
|
deserialize_with = "deserialize::deserialize_u32"
|
||||||
|
)]
|
||||||
pub quantity: Quantity,
|
pub quantity: Quantity,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,25 +186,11 @@ impl Product {
|
|||||||
quantity,
|
quantity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_unit_price<S>(v: &Price, ser: S) -> std::result::Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
ser.serialize_str(&format!("{v}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_quantity<S>(v: &Quantity, ser: S) -> std::result::Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
ser.serialize_str(&format!("{v}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Order {
|
pub struct OrderCreateRequest {
|
||||||
/// URL to which web hook will be send. It's important to return 200 to all
|
/// URL to which web hook will be send. It's important to return 200 to all
|
||||||
/// notifications.
|
/// notifications.
|
||||||
///
|
///
|
||||||
@ -149,33 +223,166 @@ pub struct Order {
|
|||||||
/// | 19| 60 hours |
|
/// | 19| 60 hours |
|
||||||
/// | 20| 72 hours |
|
/// | 20| 72 hours |
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub notify_url: Option<String>,
|
notify_url: Option<String>,
|
||||||
/// Customer client IP address
|
/// Customer client IP address
|
||||||
pub customer_ip: String,
|
customer_ip: String,
|
||||||
/// Secret pos ip. This is connected to PayU account
|
/// Secret pos ip. This is connected to PayU account
|
||||||
pub merchant_pos_id: String,
|
#[serde(
|
||||||
|
serialize_with = "serialize::serialize_i32",
|
||||||
|
deserialize_with = "deserialize::deserialize_i32"
|
||||||
|
)]
|
||||||
|
merchant_pos_id: MerchantPosId,
|
||||||
/// Transaction description
|
/// Transaction description
|
||||||
pub description: String,
|
description: String,
|
||||||
/// 3 characters currency identifier, ex. PLN
|
/// 3 characters currency identifier, ex. PLN
|
||||||
pub currency_code: String,
|
currency_code: String,
|
||||||
/// Total price of the order in pennies (e.g. 1000 is 10.00 EUR). Applies
|
/// Total price of the order in pennies (e.g. 1000 is 10.00 EUR). Applies
|
||||||
/// also to currencies without subunits (e.g. 1000 is 10 HUF).
|
/// also to currencies without subunits (e.g. 1000 is 10 HUF).
|
||||||
#[serde(serialize_with = "Order::serialize_total_amount")]
|
#[serde(
|
||||||
pub total_amount: Price,
|
serialize_with = "serialize::serialize_i32",
|
||||||
|
deserialize_with = "deserialize::deserialize_i32"
|
||||||
|
)]
|
||||||
|
total_amount: Price,
|
||||||
/// @see [Buyer]
|
/// @see [Buyer]
|
||||||
pub buyer: Buyer,
|
buyer: Option<Buyer>,
|
||||||
/// List of products
|
/// List of products
|
||||||
pub products: Vec<Product>,
|
products: Vec<Product>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub order_create_date: Option<String>,
|
order_create_date: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Order {
|
impl OrderCreateRequest {
|
||||||
fn serialize_total_amount<S>(v: &Price, ser: S) -> std::result::Result<S::Ok, S::Error>
|
pub fn new<CustomerIp, Currency>(
|
||||||
|
buyer: Buyer,
|
||||||
|
customer_ip: CustomerIp,
|
||||||
|
currency: Currency,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
S: serde::Serializer,
|
CustomerIp: Into<String>,
|
||||||
|
Currency: Into<String>,
|
||||||
{
|
{
|
||||||
ser.serialize_str(&format!("{v}"))
|
Self {
|
||||||
|
notify_url: None,
|
||||||
|
customer_ip: customer_ip.into(),
|
||||||
|
merchant_pos_id: 0,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add url to which PayU will be able to send http request with payment
|
||||||
|
/// status updates
|
||||||
|
///
|
||||||
|
/// All requests from PayU should receive 200 response!
|
||||||
|
///
|
||||||
|
/// See more [Order::notify_url]
|
||||||
|
pub fn with_notify_url<NotifyUrl>(mut self, notify_url: NotifyUrl) -> Self
|
||||||
|
where
|
||||||
|
NotifyUrl: Into<String>,
|
||||||
|
{
|
||||||
|
self.notify_url = Some(notify_url.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// URL to which web hook will be send. It's important to return 200 to all
|
||||||
|
/// notifications.
|
||||||
|
///
|
||||||
|
/// All notifications are send as POST with JSON payload
|
||||||
|
///
|
||||||
|
/// Notifications are sent immediately after a payment status changes. If
|
||||||
|
/// the notification is not received by the Shop application, it will be
|
||||||
|
/// sent again in accordance with the table below:
|
||||||
|
///
|
||||||
|
/// | Attempt | Time |
|
||||||
|
/// |---------|------|
|
||||||
|
/// | 1 | immediately |
|
||||||
|
/// | 2 | 1 minute |
|
||||||
|
/// | 3 | 2 minutes |
|
||||||
|
/// | 4 | 5 minutes |
|
||||||
|
/// | 5 | 10 minutes |
|
||||||
|
/// | 6 | 30 minutes |
|
||||||
|
/// | 7 | 1 hour |
|
||||||
|
/// | 8 | 2 hours |
|
||||||
|
/// | 9 | 3 hours |
|
||||||
|
/// | 10| 6 hours |
|
||||||
|
/// | 11| 9 hours |
|
||||||
|
/// | 12| 12 hours |
|
||||||
|
/// | 13| 15 hours |
|
||||||
|
/// | 14| 18 hours |
|
||||||
|
/// | 15| 21 hours |
|
||||||
|
/// | 16| 24 hours |
|
||||||
|
/// | 17| 36 hours |
|
||||||
|
/// | 18| 48 hours |
|
||||||
|
/// | 19| 60 hours |
|
||||||
|
/// | 20| 72 hours |
|
||||||
|
pub fn notify_url(&self) -> &Option<String> {
|
||||||
|
&self.notify_url
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Customer IP address from http request received from client
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,10 +390,22 @@ impl Order {
|
|||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
pub enum PaymentType {
|
pub enum PaymentType {
|
||||||
Pbl,
|
Pbl,
|
||||||
CartToken,
|
CardToken,
|
||||||
Installments,
|
Installments,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper around pay method. It's used only for deserializing notifications
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use pay_u::PayMethod;
|
||||||
|
/// let method: PayMethod = serde_json::from_str(r#"
|
||||||
|
/// {
|
||||||
|
/// "type": "INSTALLMENTS"
|
||||||
|
/// }
|
||||||
|
/// "#).unwrap();
|
||||||
|
/// ```
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PayMethod {
|
pub struct PayMethod {
|
||||||
@ -194,21 +413,110 @@ pub struct PayMethod {
|
|||||||
pub payment_type: PaymentType,
|
pub payment_type: PaymentType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
pub status_code: String,
|
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 {
|
||||||
|
/// Request lacks "refund" object.
|
||||||
|
MissingRefundSection,
|
||||||
|
|
||||||
|
/// Transaction has not been finalized
|
||||||
|
TransNotEnded,
|
||||||
|
|
||||||
|
/// Lack of funds in account
|
||||||
|
NoBalance,
|
||||||
|
|
||||||
|
/// Refund amount exceeds transaction amount
|
||||||
|
AmountToBig,
|
||||||
|
|
||||||
|
/// Refund value is too small
|
||||||
|
AmountToSmall,
|
||||||
|
|
||||||
|
/// Refunds have been disabled
|
||||||
|
RefundDisabled,
|
||||||
|
|
||||||
|
/// Too many refund attempts have been made
|
||||||
|
RefundToOften,
|
||||||
|
|
||||||
|
/// Refund was already created
|
||||||
|
Paid,
|
||||||
|
|
||||||
|
/// Unknown error
|
||||||
|
UnknownError,
|
||||||
|
|
||||||
|
/// extRefundId was re-used and other params do not match the values
|
||||||
|
/// sent during the first call.
|
||||||
|
RefundIdempotencyMismatch,
|
||||||
|
|
||||||
|
/// Shop billing has not yet been completed
|
||||||
|
TransBillingEntriesNotCompleted,
|
||||||
|
|
||||||
|
/// The available time for refund has passed.
|
||||||
|
TransTooOld,
|
||||||
|
|
||||||
|
/// Transaction amount that remains after refund creation will be too
|
||||||
|
/// small to make another refund.
|
||||||
|
RemainingTransAmountTooSmall,
|
||||||
|
|
||||||
|
#[serde(other)]
|
||||||
|
/// Implementation changed
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Status {
|
impl Status {
|
||||||
|
/// Check if http request was successful
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use pay_u::Status;
|
||||||
|
/// let status: Status = serde_json::from_str("{\"statusCode\":\"SUCCESS\"}").unwrap();
|
||||||
|
/// assert_eq!(status.is_success(), true);
|
||||||
|
/// ```
|
||||||
pub fn is_success(&self) -> bool {
|
pub fn is_success(&self) -> bool {
|
||||||
self.status_code.as_str() == "SUCCESS"
|
self.status_code.as_str() == "SUCCESS"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns http status
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use pay_u::Status;
|
||||||
|
/// let status: Status = serde_json::from_str("{\"statusCode\":\"SUCCESS\"}").unwrap();
|
||||||
|
/// assert_eq!(status.status_code(), "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)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CreateOrderResult {
|
pub struct CreateOrderResult {
|
||||||
|
/// Http status as a text
|
||||||
pub status: Status,
|
pub status: Status,
|
||||||
/// Client should be redirected to this URI
|
/// Client should be redirected to this URI
|
||||||
pub redirect_uri: String,
|
pub redirect_uri: String,
|
||||||
@ -218,15 +526,64 @@ pub struct CreateOrderResult {
|
|||||||
pub ext_order_id: Option<String>,
|
pub ext_order_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PartialRefundResult {
|
||||||
|
pub order_id: Option<String>,
|
||||||
|
pub refund: Option<Refund>,
|
||||||
|
pub status: Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RefundRequest {
|
||||||
|
description: String,
|
||||||
|
amount: Price,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RefundRequest {
|
||||||
|
pub fn new<Description>(description: Description, amount: Price) -> Self
|
||||||
|
where
|
||||||
|
Description: Into<String>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
description: description.into(),
|
||||||
|
amount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description(&self) -> &str {
|
||||||
|
&self.description
|
||||||
|
}
|
||||||
|
pub fn amount(&self) -> 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 {
|
pub mod notify {
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Payment notification object received on [super::Order].[notify_url]
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StatusUpdate {
|
pub struct StatusUpdate {
|
||||||
pub order: Order,
|
pub order: Order,
|
||||||
pub local_receipt_date_time: Option<String>,
|
pub local_receipt_date_time: Option<String>,
|
||||||
pub properties: Option<Vec<Prop>>,
|
pub properties: Option<Vec<Prop>>,
|
||||||
|
pub status: Option<super::Status>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusUpdate {
|
impl StatusUpdate {
|
||||||
@ -235,15 +592,45 @@ pub mod notify {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refund notification object
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RefundUpdate {
|
||||||
|
pub ext_order_id: String,
|
||||||
|
pub order_id: String,
|
||||||
|
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)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Order {
|
pub struct Order {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub order: super::Order,
|
pub order: super::OrderCreateRequest,
|
||||||
pub pay_method: Option<super::PaymentType>,
|
pub pay_method: Option<super::PayMethod>,
|
||||||
pub status: super::PaymentStatus,
|
pub status: super::PaymentStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for Order {
|
||||||
|
type Target = super::OrderCreateRequest;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Prop {
|
pub struct Prop {
|
||||||
@ -253,11 +640,48 @@ pub mod notify {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
pub bearer: Option<String>,
|
sandbox: bool,
|
||||||
pub sandbox: bool,
|
merchant_pos_id: MerchantPosId,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: String,
|
||||||
|
bearer: Option<String>,
|
||||||
|
bearer_expires_at: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
|
/// Create new PayU client
|
||||||
|
pub fn new<ClientId, ClientSecret>(
|
||||||
|
client_id: ClientId,
|
||||||
|
client_secret: ClientSecret,
|
||||||
|
merchant_pos_id: MerchantPosId,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
ClientId: Into<String>,
|
||||||
|
ClientSecret: Into<String>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
bearer: None,
|
||||||
|
sandbox: false,
|
||||||
|
merchant_pos_id,
|
||||||
|
client_id: client_id.into(),
|
||||||
|
client_secret: client_secret.into(),
|
||||||
|
bearer_expires_at: chrono::Utc::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All operation will be performed in sandbox PayU environment
|
||||||
|
pub fn sandbox(mut self) -> Self {
|
||||||
|
self.sandbox = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set your own bearer key
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
/// Create new order in PayU
|
/// Create new order in PayU
|
||||||
///
|
///
|
||||||
/// ### IMPORTANT:
|
/// ### IMPORTANT:
|
||||||
@ -270,31 +694,30 @@ impl Client {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pay_u::{Client, Order, Product, Buyer};
|
/// # use pay_u::{Client, OrderCreateRequest, Product, Buyer};
|
||||||
/// async fn pay() {
|
/// async fn pay() {
|
||||||
/// let client = Client {
|
/// let mut client = Client::new("145227", "12f071174cb7eb79d4aac5bc2f07563f", 300746)
|
||||||
/// bearer: Some(String::from("d9a4536e-62ba-4f60-8017-6053211d3f47")),
|
/// .sandbox()
|
||||||
/// sandbox: true,
|
/// .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 2000);
|
||||||
/// };
|
|
||||||
/// let res = client
|
/// let res = client
|
||||||
/// .create_order(Order {
|
/// .create_order(
|
||||||
/// notify_url: Some(String::from("https://your.eshop.com/notify")),
|
/// OrderCreateRequest::new(
|
||||||
/// customer_ip: "127.0.0.1".to_string(),
|
/// Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
|
||||||
/// merchant_pos_id: "300746".to_string(),
|
/// "127.0.0.1",
|
||||||
/// description: "RTV market".to_string(),
|
/// "PLN",
|
||||||
/// currency_code: "PLN".to_string(),
|
/// )
|
||||||
/// total_amount: 21000,
|
/// .with_notify_url("https://your.eshop.com/notify")
|
||||||
/// buyer: Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
|
/// .with_description("RTV market")
|
||||||
/// products: vec![
|
/// .with_products([
|
||||||
/// Product::new("Wireless Mouse for Laptop", 15000, 1),
|
/// Product::new("Wireless Mouse for Laptop", 15000, 1),
|
||||||
/// Product::new("HDMI cable", 6000, 1),
|
/// Product::new("HDMI cable", 6000, 1),
|
||||||
/// ],
|
/// ].into_iter()),
|
||||||
/// order_create_date: None,
|
/// )
|
||||||
/// })
|
|
||||||
/// .await;
|
/// .await;
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn create_order(&self, order: Order) -> Result<CreateOrderResult> {
|
pub async fn create_order(&mut self, order: OrderCreateRequest) -> Result<CreateOrderResult> {
|
||||||
|
self.authorize().await?;
|
||||||
if order.total_amount
|
if order.total_amount
|
||||||
!= order
|
!= order
|
||||||
.products
|
.products
|
||||||
@ -303,25 +726,128 @@ impl Client {
|
|||||||
{
|
{
|
||||||
return Err(Error::IncorrectTotal);
|
return Err(Error::IncorrectTotal);
|
||||||
}
|
}
|
||||||
let client = reqwest::ClientBuilder::default()
|
|
||||||
.user_agent("curl/7.82.0")
|
if order.buyer().is_none() {
|
||||||
.use_native_tls()
|
return Err(Error::NoBuyer);
|
||||||
// Do not follow!
|
}
|
||||||
.redirect(redirect::Policy::none())
|
|
||||||
.connection_verbose(true)
|
if order.description().trim().is_empty() {
|
||||||
.build()
|
return Err(Error::NoDescription);
|
||||||
.expect("Failed to create client");
|
}
|
||||||
|
|
||||||
|
let client = Self::build_client();
|
||||||
let bearer = self.bearer.as_ref().cloned().unwrap_or_default();
|
let bearer = self.bearer.as_ref().cloned().unwrap_or_default();
|
||||||
let path = format!("{}/orders", self.base_url());
|
let path = format!("{}/orders", self.base_url());
|
||||||
client
|
let text = client
|
||||||
.post(path)
|
.post(path)
|
||||||
.bearer_auth(bearer)
|
.bearer_auth(bearer)
|
||||||
.json(&order)
|
.json(&order.with_merchant_pos_id(self.merchant_pos_id))
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.json::<CreateOrderResult>()
|
.text()
|
||||||
.await
|
.await?;
|
||||||
.map_err(Into::into)
|
log::trace!("Response: {}", text);
|
||||||
|
serde_json::from_str(&text).map_err(|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
Error::CreateOrder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PayU system fully supports refunds for processed payments, the
|
||||||
|
/// balance of which is transferred directly to the buyer’s account.
|
||||||
|
///
|
||||||
|
/// > For „PayU | Pay later” payment method funds are transferred to a
|
||||||
|
/// > credit provider.
|
||||||
|
///
|
||||||
|
/// You can process refund requests as either full or partial. For partial
|
||||||
|
/// refunds, always specify the amount in the lowest unit of a given
|
||||||
|
/// currency, which must be the same currency as the initial order (np. for
|
||||||
|
/// Poland lowest currency unit is “grosz” so 10 pln should be given as
|
||||||
|
/// 1000).
|
||||||
|
///
|
||||||
|
/// You can send several partial refund requests for each single order. The
|
||||||
|
/// total value of the requests must not exceed the order value.
|
||||||
|
///
|
||||||
|
/// The payu system allows multiple partial refunds to be executed at the
|
||||||
|
/// same time. To do so, the extRefundId parameter must be sent in the
|
||||||
|
/// request. In situations where partial refunds will not be executed more
|
||||||
|
/// than once per second, the extRefundId parameter is not required.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use pay_u::*;
|
||||||
|
/// async fn perform_refund() {
|
||||||
|
/// let mut client = Client::new("145227", "12f071174cb7eb79d4aac5bc2f07563f", 300746)
|
||||||
|
/// .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 2000)
|
||||||
|
/// .sandbox();
|
||||||
|
/// let res = client
|
||||||
|
/// .partial_refund(
|
||||||
|
/// "H9LL64F37H160126GUEST000P01",
|
||||||
|
/// RefundRequest::new("Refund", 1000),
|
||||||
|
/// )
|
||||||
|
/// .await;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub async fn partial_refund<OrderId>(
|
||||||
|
&mut self,
|
||||||
|
order_id: OrderId,
|
||||||
|
refund: RefundRequest,
|
||||||
|
) -> Result<PartialRefundResult>
|
||||||
|
where
|
||||||
|
OrderId: std::fmt::Display,
|
||||||
|
{
|
||||||
|
self.authorize().await?;
|
||||||
|
if refund.description().trim().is_empty() {
|
||||||
|
return Err(Error::NoDescription);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = Self::build_client();
|
||||||
|
|
||||||
|
let bearer = self.bearer.as_ref().cloned().unwrap_or_default();
|
||||||
|
let path = format!("{}/orders/{}/refunds", self.base_url(), order_id);
|
||||||
|
let text = client
|
||||||
|
.post(path)
|
||||||
|
.bearer_auth(bearer)
|
||||||
|
.json(&refund)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
log::trace!("Response: {}", text);
|
||||||
|
serde_json::from_str::<'_, PartialRefundResult>(&text).map_err(|e| {
|
||||||
|
log::error!("Invalid PayU response {e:?}");
|
||||||
|
Error::Refund
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or refresh token
|
||||||
|
pub(crate) 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 res = Self::build_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 {
|
fn base_url(&self) -> &str {
|
||||||
@ -331,36 +857,122 @@ impl Client {
|
|||||||
"https://secure.payu.com/api/v2_1"
|
"https://secure.payu.com/api/v2_1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_client() -> reqwest::Client {
|
||||||
|
reqwest::ClientBuilder::default()
|
||||||
|
.user_agent("curl/7.82.0")
|
||||||
|
.use_native_tls()
|
||||||
|
// Do not follow redirect!
|
||||||
|
.redirect(redirect::Policy::none())
|
||||||
|
.connection_verbose(true)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create client")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn build_client() -> Client {
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
Client::new("145227", "12f071174cb7eb79d4aac5bc2f07563f", 300746)
|
||||||
|
.sandbox()
|
||||||
|
.with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 999999)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn create_order() {
|
async fn create_order() {
|
||||||
dotenv::dotenv().ok();
|
let res = build_client()
|
||||||
|
.create_order(
|
||||||
let client = Client {
|
OrderCreateRequest::new(
|
||||||
bearer: Some(String::from("d9a4536e-62ba-4f60-8017-6053211d3f47")),
|
Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
|
||||||
sandbox: true,
|
"127.0.0.1",
|
||||||
};
|
"PLN",
|
||||||
let res = client
|
)
|
||||||
.create_order(Order {
|
.with_notify_url("https://your.eshop.com/notify")
|
||||||
notify_url: Some(String::from("https://your.eshop.com/notify")),
|
.with_description("RTV market")
|
||||||
customer_ip: "127.0.0.1".to_string(),
|
.with_products(
|
||||||
merchant_pos_id: "300746".to_string(),
|
[
|
||||||
description: "RTV market".to_string(),
|
|
||||||
currency_code: "PLN".to_string(),
|
|
||||||
total_amount: 21000,
|
|
||||||
buyer: Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
|
|
||||||
products: vec![
|
|
||||||
Product::new("Wireless Mouse for Laptop", 15000, 1),
|
Product::new("Wireless Mouse for Laptop", 15000, 1),
|
||||||
Product::new("HDMI cable", 6000, 1),
|
Product::new("HDMI cable", 6000, 1),
|
||||||
],
|
]
|
||||||
order_create_date: None,
|
.into_iter(),
|
||||||
})
|
),
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn partial_refund() {
|
||||||
|
let res = build_client()
|
||||||
|
.partial_refund(
|
||||||
|
"H9LL64F37H160126GUEST000P01",
|
||||||
|
RefundRequest::new("Refund", 1000),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
eprintln!("{:?}", res);
|
||||||
|
assert!(matches!(res, Ok(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn check_refund() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_accepted_refund_json() {
|
||||||
|
let res = serde_json::from_str::<PartialRefundResult>(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::<PartialRefundResult>(include_str!(
|
||||||
|
"../tests/responses/rejection.json"
|
||||||
|
));
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn check_custom_literal_json() {
|
||||||
|
let res = serde_json::from_str::<PartialRefundResult>(include_str!(
|
||||||
|
"../tests/responses/custom_code_literal.json"
|
||||||
|
));
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
pay_u/src/serialize.rs
Normal file
13
pay_u/src/serialize.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
pub(crate) fn serialize_i32<S>(v: &i32, ser: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
ser.serialize_str(&format!("{v}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn serialize_u32<S>(v: &u32, ser: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
ser.serialize_str(&format!("{v}"))
|
||||||
|
}
|
17
pay_u/tests/responses/accepted_refund.json
Normal file
17
pay_u/tests/responses/accepted_refund.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"orderId": "ZXWZ53KQQM200702GUEST000P01",
|
||||||
|
"refund": {
|
||||||
|
"refundId": "5000009987",
|
||||||
|
"extRefundId": "20200702091903",
|
||||||
|
"amount": "21000",
|
||||||
|
"currencyCode": "PLN",
|
||||||
|
"description": "5000009987 Refund Accepted",
|
||||||
|
"creationDateTime": "2020-07-02T09:19:03.896+02:00",
|
||||||
|
"status": "PENDING",
|
||||||
|
"statusDateTime": "2020-07-02T09:19:04.013+02:00"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"statusCode": "SUCCESS",
|
||||||
|
"statusDesc": "Refund queued for processing"
|
||||||
|
}
|
||||||
|
}
|
21
pay_u/tests/responses/cancel.json
Normal file
21
pay_u/tests/responses/cancel.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"order": {
|
||||||
|
"orderId": "LDLW5N7MF4140324GUEST000P01",
|
||||||
|
"extOrderId": "Order id in your shop",
|
||||||
|
"orderCreateDate": "2012-12-31T12:00:00",
|
||||||
|
"notifyUrl": "http://tempuri.org/notify",
|
||||||
|
"customerIp": "127.0.0.1",
|
||||||
|
"merchantPosId": "98374",
|
||||||
|
"description": "My order description",
|
||||||
|
"currencyCode": "PLN",
|
||||||
|
"totalAmount": "200",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"name": "Product 1",
|
||||||
|
"unitPrice": "200",
|
||||||
|
"quantity": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "CANCELED"
|
||||||
|
}
|
||||||
|
}
|
38
pay_u/tests/responses/completed_cart_token.json
Normal file
38
pay_u/tests/responses/completed_cart_token.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"order": {
|
||||||
|
"orderId": "LDLW5N7MF4140324GUEST000P01",
|
||||||
|
"extOrderId": "Order id in your shop",
|
||||||
|
"orderCreateDate": "2012-12-31T12:00:00",
|
||||||
|
"notifyUrl": "http://tempuri.org/notify",
|
||||||
|
"customerIp": "127.0.0.1",
|
||||||
|
"merchantPosId": "238684",
|
||||||
|
"description": "My order description",
|
||||||
|
"currencyCode": "PLN",
|
||||||
|
"totalAmount": "200",
|
||||||
|
"buyer": {
|
||||||
|
"email": "john.doe@example.org",
|
||||||
|
"phone": "111111111",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"language": "en"
|
||||||
|
},
|
||||||
|
"payMethod": {
|
||||||
|
"type": "CARD_TOKEN"
|
||||||
|
},
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"name": "Product 1",
|
||||||
|
"unitPrice": "200",
|
||||||
|
"quantity": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "COMPLETED"
|
||||||
|
},
|
||||||
|
"localReceiptDateTime": "2016-03-02T12:58:14.828+01:00",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "PAYMENT_ID",
|
||||||
|
"value": "151471228"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
38
pay_u/tests/responses/completed_installments.json
Normal file
38
pay_u/tests/responses/completed_installments.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"order": {
|
||||||
|
"orderId": "LDLW5N7MF4140324GUEST000P01",
|
||||||
|
"extOrderId": "Order id in your shop",
|
||||||
|
"orderCreateDate": "2012-12-31T12:00:00",
|
||||||
|
"notifyUrl": "http://tempuri.org/notify",
|
||||||
|
"customerIp": "127.0.0.1",
|
||||||
|
"merchantPosId": "8236",
|
||||||
|
"description": "My order description",
|
||||||
|
"currencyCode": "PLN",
|
||||||
|
"totalAmount": "200",
|
||||||
|
"buyer": {
|
||||||
|
"email": "john.doe@example.org",
|
||||||
|
"phone": "111111111",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"language": "en"
|
||||||
|
},
|
||||||
|
"payMethod": {
|
||||||
|
"type": "INSTALLMENTS"
|
||||||
|
},
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"name": "Product 1",
|
||||||
|
"unitPrice": "200",
|
||||||
|
"quantity": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "COMPLETED"
|
||||||
|
},
|
||||||
|
"localReceiptDateTime": "2016-03-02T12:58:14.828+01:00",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "PAYMENT_ID",
|
||||||
|
"value": "151471228"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
38
pay_u/tests/responses/completed_pbl.json
Normal file
38
pay_u/tests/responses/completed_pbl.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"order": {
|
||||||
|
"orderId": "LDLW5N7MF4140324GUEST000P01",
|
||||||
|
"extOrderId": "Order id in your shop",
|
||||||
|
"orderCreateDate": "2012-12-31T12:00:00",
|
||||||
|
"notifyUrl": "http://tempuri.org/notify",
|
||||||
|
"customerIp": "127.0.0.1",
|
||||||
|
"merchantPosId": "928374",
|
||||||
|
"description": "My order description",
|
||||||
|
"currencyCode": "PLN",
|
||||||
|
"totalAmount": "200",
|
||||||
|
"buyer": {
|
||||||
|
"email": "john.doe@example.org",
|
||||||
|
"phone": "111111111",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"language": "en"
|
||||||
|
},
|
||||||
|
"payMethod": {
|
||||||
|
"type": "PBL"
|
||||||
|
},
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"name": "Product 1",
|
||||||
|
"unitPrice": "200",
|
||||||
|
"quantity": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "COMPLETED"
|
||||||
|
},
|
||||||
|
"localReceiptDateTime": "2016-03-02T12:58:14.828+01:00",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "PAYMENT_ID",
|
||||||
|
"value": "151471228"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
9
pay_u/tests/responses/custom_code_literal.json
Normal file
9
pay_u/tests/responses/custom_code_literal.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"status": {
|
||||||
|
"statusCode": "ERROR_VALUE_MISSING",
|
||||||
|
"severity": "ERROR",
|
||||||
|
"code": "8300",
|
||||||
|
"codeLiteral": "FOO_BAR",
|
||||||
|
"statusDesc": "Implementation changed"
|
||||||
|
}
|
||||||
|
}
|
14
pay_u/tests/responses/refund.json
Normal file
14
pay_u/tests/responses/refund.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"orderId": "2DVZMPMFPN140219GUEST000P01",
|
||||||
|
"extOrderId": "Order id in your shop",
|
||||||
|
"refund": {
|
||||||
|
"refundId": "912128",
|
||||||
|
"amount": "15516",
|
||||||
|
"currencyCode": "PLN",
|
||||||
|
"status": "FINALIZED",
|
||||||
|
"statusDateTime": "1395677071799",
|
||||||
|
"reason": "refund",
|
||||||
|
"reasonDescription": "on customer’s request",
|
||||||
|
"refundDate": "1395677071799"
|
||||||
|
}
|
||||||
|
}
|
9
pay_u/tests/responses/rejection.json
Normal file
9
pay_u/tests/responses/rejection.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"status": {
|
||||||
|
"statusCode": "ERROR_VALUE_MISSING",
|
||||||
|
"severity": "ERROR",
|
||||||
|
"code": "8300",
|
||||||
|
"codeLiteral": "MISSING_REFUND_SECTION",
|
||||||
|
"statusDesc": "Missing required field"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user