More docs, implement payment

This commit is contained in:
Adrian Woźniak 2022-04-29 17:10:04 +02:00
parent 94e7e7eb1e
commit bf7c942fed
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
10 changed files with 1033 additions and 729 deletions

View File

@ -31,7 +31,7 @@ pub(crate) async fn all_account_orders(
) -> Result<Vec<AccountOrder>> {
sqlx::query_as(
r#"
SELECT id, buyer_id, status, order_id
SELECT id, buyer_id, status, order_id, order_ext_id
FROM account_orders
"#,
)
@ -73,7 +73,7 @@ pub(crate) async fn create_account_order(
r#"
INSERT INTO account_orders (buyer_id, status)
VALUES ($1, $2, $3)
RETURNING id, buyer_id, status
RETURNING id, buyer_id, status, order_ext_id
"#,
)
.bind(msg.buyer_id)
@ -147,7 +147,7 @@ pub(crate) async fn update_account_order(
UPDATE account_orders
SET buyer_id = $2 AND status = $3 AND order_id = $4
WHERE id = $1
RETURNING id, buyer_id, status, order_id
RETURNING id, buyer_id, status, order_id, order_ext_id
"#,
)
.bind(msg.id)
@ -162,6 +162,41 @@ RETURNING id, buyer_id, status, order_id
})
}
#[derive(actix::Message)]
#[rtype(result = "Result<AccountOrder>")]
pub struct UpdateAccountOrderByExt {
pub order_ext_id: String,
pub status: OrderStatus,
}
db_async_handler!(
UpdateAccountOrderByExt,
update_account_order_by_ext,
AccountOrder
);
pub(crate) async fn update_account_order_by_ext(
msg: UpdateAccountOrderByExt,
db: PgPool,
) -> Result<AccountOrder> {
sqlx::query_as(
r#"
UPDATE account_orders
SET status = $2
WHERE order_ext_id = $1
RETURNING id, buyer_id, status, order_id, order_ext_id
"#,
)
.bind(msg.order_ext_id)
.bind(msg.status)
.fetch_one(&db)
.await
.map_err(|e| {
log::error!("{e:?}");
super::Error::AccountOrder(Error::CantCreate)
})
}
#[derive(actix::Message)]
#[rtype(result = "Result<AccountOrder>")]
pub struct FindAccountOrder {
@ -173,7 +208,7 @@ db_async_handler!(FindAccountOrder, find_account_order, AccountOrder);
pub(crate) async fn find_account_order(msg: FindAccountOrder, db: PgPool) -> Result<AccountOrder> {
sqlx::query_as(
r#"
SELECT id, buyer_id, status, order_id
SELECT id, buyer_id, status, order_id, order_ext_id
FROM account_orders
WHERE id = $1
"#,

View File

@ -2,12 +2,13 @@ use std::sync::Arc;
use actix::Addr;
use parking_lot::Mutex;
use pay_u::OrderCreateRequest;
use crate::config::SharedAppConfig;
use crate::database;
use crate::database::Database;
use crate::model::{AccountId, Price, ProductId, Quantity, QuantityUnit, ShoppingCartId};
use crate::model::{
AccountId, OrderStatus, Price, ProductId, Quantity, QuantityUnit, ShoppingCartId,
};
use crate::{database, model};
#[macro_export]
macro_rules! pay_async_handler {
@ -106,13 +107,14 @@ impl From<Product> for pay_u::Product {
#[rtype(result = "Result<pay_u::OrderId>")]
pub struct RequestPayment {
pub products: Vec<Product>,
pub redirect_uri: String,
pub currency: String,
pub description: String,
pub buyer: Buyer,
pub customer_ip: String,
pub buyer_id: AccountId,
pub shopping_cart_id: ShoppingCartId,
pub redirect_uri: String,
pub continue_uri: String,
}
pay_async_handler!(RequestPayment, request_payment, pay_u::OrderId);
@ -122,7 +124,7 @@ pub(crate) async fn request_payment(
client: PayUClient,
db: Addr<Database>,
) -> Result<pay_u::OrderId> {
let db_order = match db
let db_order: model::AccountOrder = match db
.send(database::CreateAccountOrder {
buyer_id: msg.buyer_id,
items: msg
@ -153,16 +155,55 @@ pub(crate) async fn request_payment(
client
.lock()
.create_order(
OrderCreateRequest::build(
pay_u::req::OrderCreate::build(
msg.buyer.into(),
msg.customer_ip,
msg.currency,
msg.description,
)?
.with_products(msg.products.into_iter().map(Into::into))
.with_ext_order_id(db_order.order_ext_id.to_string())
.with_notify_url(msg.redirect_uri)
.with_products(msg.products.into_iter().map(Into::into)),
.with_continue_url(msg.continue_uri),
)
.await?
};
Ok(order.order_id)
}
#[derive(Debug, serde::Deserialize)]
#[serde(transparent)]
pub struct PaymentNotification(pay_u::notify::StatusUpdate);
#[derive(Debug, actix::Message)]
#[rtype(result = "Result<()>")]
pub struct UpdatePayment {
pub notification: PaymentNotification,
}
pay_async_handler!(UpdatePayment, update_payment, ());
pub(crate) async fn update_payment(
msg: UpdatePayment,
_client: PayUClient,
db: Addr<Database>,
) -> Result<()> {
let status = msg.notification.0.status();
let order_ext_id = match msg.notification.0.order.ext_order_id {
Some(id) => id,
_ => return Ok(()),
};
let status = match status {
pay_u::PaymentStatus::Pending => return Ok(()),
pay_u::PaymentStatus::WaitingForConfirmation => return Ok(()),
pay_u::PaymentStatus::Completed => OrderStatus::Payed,
pay_u::PaymentStatus::Canceled => OrderStatus::Cancelled,
};
let _ = db
.send(database::UpdateAccountOrderByExt {
status,
order_ext_id,
})
.await;
Ok(())
}

View File

@ -503,12 +503,12 @@ pub struct Stock {
pub quantity_unit: QuantityUnit,
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Deref)]
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Display, Deref)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct AccountOrderId(RecordId);
#[derive(sqlx::Type, Serialize, Deserialize, Deref)]
#[derive(sqlx::Type, Serialize, Deserialize, Display, Deref)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct OrderId(String);
@ -519,6 +519,34 @@ pub struct AccountOrder {
pub buyer_id: AccountId,
pub status: OrderStatus,
pub order_id: Option<OrderId>,
pub order_ext_id: uuid::Uuid,
}
#[derive(sqlx::FromRow, Serialize, Deserialize)]
pub struct PublicAccountOrder {
pub id: AccountOrderId,
pub buyer_id: AccountId,
pub status: OrderStatus,
pub order_id: Option<OrderId>,
}
impl From<AccountOrder> for PublicAccountOrder {
fn from(
AccountOrder {
id,
buyer_id,
status,
order_id,
order_ext_id: _,
}: AccountOrder,
) -> Self {
Self {
id,
buyer_id,
status,
order_id,
}
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Deref)]

View File

@ -5,10 +5,11 @@ use actix_web::{get, post, HttpResponse};
use crate::database::{self, Database};
use crate::logic::validate_password;
use crate::model::{Audience, FullAccount, Token, TokenString};
use crate::payment_manager::{PaymentManager, PaymentNotification};
use crate::routes::public::Error as PublicError;
use crate::routes::{self, Result};
use crate::token_manager::TokenManager;
use crate::{public_send_db, token_manager, Login, Password};
use crate::{payment_manager, public_send_db, token_manager, Login, Password};
#[get("/products")]
async fn products(db: Data<Addr<Database>>) -> Result<HttpResponse> {
@ -84,6 +85,19 @@ async fn sign_in(
Ok(HttpResponse::Created().json(SignInOutput { token: string }))
}
#[post("/pay_u/notify")]
async fn handle_notification(
Json(notify): Json<PaymentNotification>,
payment: Data<Addr<PaymentManager>>,
) -> HttpResponse {
{
payment.do_send(payment_manager::UpdatePayment {
notification: notify,
});
}
HttpResponse::Ok().body("")
}
pub(crate) fn configure(config: &mut ServiceConfig) {
config.service(products).service(stocks).service(sign_in);
}

View File

@ -1,4 +1,9 @@
# Payu REST API
# Unofficial Payu client
<img src="https://poland.payu.com/wp-content/themes/global-website/assets/src/images/payu-logo.svg">
This client support integration with the REST API 2.1 protocol.
It presents various methods of implementing online payments via different PayU services and is dedicated primarily to developers wanting to implement the PayU payment services.
## Install
@ -10,6 +15,8 @@ cargo add pay_u
```rust
async fn usage() {
use pay_u::*;
let client_id = ClientId::new(std::env::var("PAYU_CLIENT_ID").unwrap());
let client_secret = ClientSecret::new(std::env::var("PAYU_CLIENT_SECRET").unwrap());
let merchant_id = std::env::var("PAYU_CLIENT_MERCHANT_ID").unwrap().parse::<i32>().map(MerchantPosId::from).unwrap();
@ -17,7 +24,7 @@ async fn usage() {
client.authorize().await.expect("Invalid credentials");
let _res = client.create_order(
OrderCreateRequest::build(
req::OrderCreate::build(
Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
"127.0.0.1",
"PLN",
@ -75,30 +82,36 @@ async fn checkout(session: Data<Session>, db: Data<Database>, payu: Data<Arc<Mut
let payu = payu.into_inner();
let shopping_cart = db.send(LoadShoppingCart { user_id }).await??;
let shopping_cart_id = shopping_cart.id;
let create_order_req: pay_u::OrderCreateRequest = shopping_cart.into();
let create_order_req: pay_u::req::OrderCreate = shopping_cart.into();
let pay_u::res::CreateOrder { redirect_uri, order_id, .. } = payu.create_order(create_order_req).await?;
db.send(database::CreateOrder { shopping_cart_id, order_id }).await??;
HttpResponse::SeeOther().append_header((actix_web::http::header::LOCATION, redirect_uri)).body("")
}
#[post("/pay_u/{own_order_id}/notify")]
async fn handle_notification(path: Path<i32>, Json(notify): Json<pay_u::notify::StatusUpdate>) -> HttpResponse {
async fn handle_notification(
path: Path<i32>,
Json(notify): Json<pay_u::notify::StatusUpdate>,
payment: Data<Addr<PaymentManager>>
) -> HttpResponse {
let status = notify.status();
// Create additional field which will always be unique like UUID
// Do not use record primary key!
let ext_order_id = String::from(notify.ext_order_id());
let order_id = path.into_inner();
match handle_update(order_id, status, notify) {
Ok(_) => (),
Err(e) => {
// ALWAYS SEND OK!
log::error!("{e:?}");
}
};
payment.do_send(payment_manager::Update {
status,
ext_order_id
});
HttpResponse::Ok().body("")
}
```
### Releases
0.1.7 - Added credit and more create order request options like additional description, visible description.
0.1.8 - Additional documentation, move requests to module
0.1.7 - Added credit and more create order request options like additional description, visible description.
## Bugs

77
pay_u/src/credit.rs Normal file
View File

@ -0,0 +1,77 @@
//! This module allow to create credit request during create order request
use serde::{Deserialize, Serialize};
use crate::{Address, ShoppingCart};
/// Describe customer during credit request
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Applicant {
/// Applicants email address
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
/// Applicants phone number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
phone: Option<String>,
/// Applicants first name
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
first_name: Option<String>,
/// Applicants last name
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
last_name: Option<String>,
/// Language code, ISO-639-1 compliant. Denotes the language version of
/// PayU hosted payment page and of e-mail messages sent from PayU to the
/// payer (supported values are here).
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<String>,
/// National Identification Number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
nin: Option<String>,
/// Section containing data about applicants address.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
address: Option<Address>,
/// Additional information about person applying for credit.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
additional_info: Option<ApplicantAdditionalInfo>,
}
/// Allow to create credit request
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct Credit {
/// Section containing data of the ordered products
#[serde(skip_serializing_if = "Option::is_none")]
shopping_carts: Option<Vec<ShoppingCart>>,
/// Section containing data of person applying for a credit
#[serde(skip_serializing_if = "Option::is_none")]
applicant: Option<Applicant>,
}
impl Credit {
pub fn with_shopping_carts<ShoppingCarts>(mut self, shopping_carts: ShoppingCarts) -> Self
where
ShoppingCarts: Iterator<Item = ShoppingCart>,
{
self.shopping_carts = Some(shopping_carts.collect());
self
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ApplicantAdditionalInfo {
/// Information whether there were previous, successfully completed orders
/// for applicant.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
has_successfully_finished_order_in_shop: Option<String>,
}

View File

@ -1,4 +1,13 @@
//! This is unofficial client support integration with the REST API 2.1
//! protocol. It presents various methods of implementing online payments via
//! different PayU services and is dedicated primarily to developers wanting to
//! implement the PayU payment services.
pub mod credit;
mod deserialize;
pub mod notify;
pub mod req;
pub mod res;
mod serialize;
use std::sync::Arc;
@ -90,6 +99,8 @@ pub enum Error {
pub type Result<T> = std::result::Result<T, Error>;
/// PayU internal order id
///
/// Unique order identifier
#[derive(
Debug,
Clone,
@ -108,7 +119,9 @@ impl OrderId {
}
}
/// PayU internal order id
/// PayU internal merchant id
///
/// This value is customer identifier
#[derive(
Debug,
serde::Deserialize,
@ -123,6 +136,7 @@ impl OrderId {
#[serde(transparent)]
pub struct MerchantPosId(pub i32);
/// Public payu OAuth client identifier
#[derive(
Debug,
Clone,
@ -141,6 +155,7 @@ impl ClientId {
}
}
/// Secret payu OAuth client identifier
#[derive(
Debug,
Clone,
@ -509,6 +524,7 @@ pub enum ShoppingMethodType {
StorePickup,
}
/// Delivery address
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Address {
@ -559,76 +575,6 @@ pub struct ShoppingMethod {
pub address: Option<Address>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ApplicantAdditionalInfo {
/// Information whether there were previous, successfully completed orders
/// for applicant.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
has_successfully_finished_order_in_shop: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Applicant {
/// Applicants email address
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
/// Applicants phone number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
phone: Option<String>,
/// Applicants first name
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
first_name: Option<String>,
/// Applicants last name
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
last_name: Option<String>,
/// Language code, ISO-639-1 compliant. Denotes the language version of
/// PayU hosted payment page and of e-mail messages sent from PayU to the
/// payer (supported values are here).
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<String>,
/// National Identification Number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
nin: Option<String>,
/// Section containing data about applicants address.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
address: Option<Address>,
/// Additional information about person applying for credit.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
additional_info: Option<ApplicantAdditionalInfo>,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct Credit {
/// Section containing data of the ordered products
#[serde(skip_serializing_if = "Option::is_none")]
shopping_carts: Option<Vec<ShoppingCart>>,
/// Section containing data of person applying for a credit
#[serde(skip_serializing_if = "Option::is_none")]
applicant: Option<Applicant>,
}
impl Credit {
pub fn with_shopping_carts<ShoppingCarts>(mut self, shopping_carts: ShoppingCarts) -> Self
where
ShoppingCarts: Iterator<Item = ShoppingCart>,
{
self.shopping_carts = Some(shopping_carts.collect());
self
}
}
/// MultiUseCartToken
pub mod muct {
use serde::{Deserialize, Serialize};
@ -721,346 +667,6 @@ pub mod muct {
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrderCreateRequest {
/// ID of an order used in merchant system. Order identifier assigned by the
/// merchant. It enables merchants to find a specific order in their system.
/// This value must be unique within a single POS.
ext_order_id: Option<String>,
/// 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 |
#[serde(skip_serializing_if = "Option::is_none")]
notify_url: Option<String>,
/// Address for redirecting the customer after payment is commenced. If the
/// payment has not been authorized, error=501 parameter will be added.
/// Please note that no decision regarding payment status should be made
/// depending on the presence or lack of this parameter (to get payment
/// status, wait for notification or retrieve order details).
///
/// IMPORTANT: the address must be compliant with the structure below:
/// <img src="https://developers.payu.com/images/continueUrlStructure_en.png" />
///
/// Please keep in mind:
/// * accepted schemas are http and https,
/// * such elements as port, path, query and fragment are optional,
/// * query values must be encoded.
#[serde(skip_serializing_if = "Option::is_none")]
continue_url: Option<String>,
/// Payers IP address, e.g. 123.123.123.123. Note: 0.0.0.0 is not accepted.
customer_ip: String,
/// Secret pos ip. This is connected to PayU account
#[serde(
serialize_with = "serialize::serialize_newtype",
deserialize_with = "deserialize::deserialize_i32_newtype"
)]
merchant_pos_id: MerchantPosId,
/// Transaction description
description: String,
/// 3 characters currency identifier, ex. PLN
currency_code: String,
/// 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).
#[serde(
serialize_with = "serialize::serialize_i32",
deserialize_with = "deserialize::deserialize_i32"
)]
total_amount: Price,
/// @see [Buyer]
buyer: Option<Buyer>,
/// List of products
products: Vec<Product>,
#[serde(skip_serializing)]
order_create_date: Option<String>,
/// Duration for the validity of an order (in seconds), during which time
/// payment must be made. Default value 86400.
#[serde(skip_serializing_if = "Option::is_none")]
validity_time: Option<u16>,
/// Additional description of the order.
#[serde(skip_serializing_if = "Option::is_none")]
additional_description: Option<String>,
/// Text visible on the PayU payment page (max. 80 chars).
#[serde(skip_serializing_if = "Option::is_none")]
visible_description: Option<String>,
/// Payment recipient name followed by payment description (order ID, ticket
/// number etc) visible on card statement (max. 22 chars). The name should
/// be easy to recognize by the cardholder (e.g "shop.com 124343"). If field
/// is not provided, static name configured by PayU will be used.
#[serde(skip_serializing_if = "Option::is_none")]
statement_description: Option<String>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
muct: Option<muct::MultiUseCartToken>,
#[serde(skip_serializing_if = "Option::is_none")]
credit: Option<Credit>,
}
impl OrderCreateRequest {
pub fn build<CustomerIp, Currency, Description>(
buyer: Buyer,
customer_ip: CustomerIp,
currency: Currency,
description: Description,
) -> Result<Self>
where
CustomerIp: Into<String>,
Currency: Into<String>,
Description: Into<String>,
{
let customer_ip = customer_ip.into();
if &customer_ip == "0.0.0.0" {
return Err(Error::CustomerIp);
}
Ok(Self {
ext_order_id: None,
notify_url: None,
continue_url: None,
customer_ip,
merchant_pos_id: 0.into(),
description: description.into(),
currency_code: currency.into(),
total_amount: 0,
buyer: Some(buyer),
products: Vec::new(),
order_create_date: None,
validity_time: None,
additional_description: None,
visible_description: None,
statement_description: None,
muct: None,
credit: None,
})
}
/// ID of an order used in merchant system. Order identifier assigned by the
/// merchant. It enables merchants to find a specific order in their system.
/// This value must be unique within a single POS.
pub fn with_ext_order_id<S: Into<String>>(mut self, ext_order_id: S) -> Self {
self.ext_order_id = Some(ext_order_id.into());
self
}
/// Duration for the validity of an order (in seconds), during which time
/// payment must be made. Default value 86400.
pub fn with_validity_time(mut self, validity_time: u16) -> Self {
self.validity_time = Some(validity_time);
self
}
pub fn with_multi_use_token(
mut self,
recurring: muct::Recurring,
card_on_file: muct::CardOnFile,
) -> Self {
self.muct = Some(muct::MultiUseCartToken {
recurring,
card_on_file,
});
self
}
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
}
/// Description of the an order.
///
/// > This method will override initial description!
pub fn with_description<Description>(mut self, desc: Description) -> Self
where
Description: Into<String>,
{
self.description = String::from(desc.into().trim());
self
}
/// Additional description of the order.
pub fn with_additional_description<S: Into<String>>(
mut self,
additional_description: S,
) -> Self {
self.additional_description = Some(additional_description.into());
self
}
/// Text visible on the PayU payment page (max. 80 chars).
pub fn with_visible_description(mut self, visible_description: &str) -> Self {
let visible_description = if visible_description.len() > 60 {
&visible_description[..60]
} else {
visible_description
};
self.visible_description = Some(String::from(visible_description));
self
}
/// Payment recipient name followed by payment description (order ID, ticket
/// number etc) visible on card statement (max. 22 chars). The name should
/// be easy to recognize by the cardholder (e.g "shop.com 124343"). If field
/// is not provided, static name configured by PayU will be used.
pub fn with_statement_description<Description>(mut self, desc: Description) -> Self
where
Description: Into<String>,
{
self.statement_description = Some(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
}
/// Address for redirecting the customer after payment is commenced. If the
/// payment has not been authorized, error=501 parameter will be added.
/// Please note that no decision regarding payment status should be made
/// depending on the presence or lack of this parameter (to get payment
/// status, wait for notification or retrieve order details).
pub fn with_continue_url<ContinueUrl>(mut self, continue_url: ContinueUrl) -> Self
where
ContinueUrl: Into<String>,
{
self.continue_url = Some(continue_url.into());
self
}
/// Section containing credit data. This information is not required, but it
/// is strongly recommended to include it. Otherwise the buyer will be
/// prompted to provide missing data on provider page when payment by
/// Installments or Pay later.
pub fn with_credit(mut self, credit: Credit) -> Self {
self.credit = Some(credit);
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
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PaymentType {
@ -1091,6 +697,18 @@ pub struct PayMethod {
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Status {
/// One of
/// * `PENDING`: Payment is currently being processed.
/// * `WAITING_FOR_CONFIRMATION`: 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.
/// * `COMPLETED`: Payment has been accepted. PayU will pay out the funds
/// shortly.
/// * `CANCELED`: Payment has been cancelled and the buyer has not been
/// charged (no money was taken from buyer's account).
///
/// > Too prevent sending wrong status from server to PayU this field
/// > remains String
status_code: String,
status_desc: Option<String>,
code: Option<String>,
@ -1098,6 +716,50 @@ pub struct Status {
code_literal: Option<CodeLiteral>,
}
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 {
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()
}
pub fn code(&self) -> Option<&str> {
self.code.as_deref()
}
pub fn severity(&self) -> Option<&str> {
self.severity.as_deref()
}
pub fn code_literal(&self) -> Option<&CodeLiteral> {
self.code_literal.as_ref()
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum StatusCode {
@ -1156,38 +818,6 @@ pub enum CodeLiteral {
Unknown,
}
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 {
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)]
#[serde(rename_all = "camelCase")]
pub struct Prop {
@ -1195,181 +825,6 @@ pub struct Prop {
pub value: String,
}
pub mod res {
use crate::{OrderId, Refund, Status};
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreateOrder {
/// Http status as a text
pub status: Status,
/// Client should be redirected to this URI
pub redirect_uri: String,
/// This should be connected to your own order
pub order_id: OrderId,
/// This is YOUR_EXT_ORDER_ID
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 {
/// // "543402******4014",
pub card_number_masked: String,
/// MC (MasterCard/Maestro), VS (Visa)
/// Example; "MC"
pub card_scheme: String,
pub card_profile: CardProfile,
pub card_classification: CardClassification,
/// Example: "000"
pub card_response_code: String,
/// Example: "000 - OK"
pub card_response_code_desc: String,
/// Example: "5"
pub card_eci_code: String,
/// Example: "AY",
pub card3ds_status: String,
/// Example: "PL",
pub card_bin_country: String,
/// Example: "MCC0111LL1121"
pub first_transaction_id: String,
}
/// > Installment proposal on the Sandbox environment is not related to the
/// > order amount and always returns data for 480 PLN.
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionCardInstallmentProposal {
/// Example: "5aff3ba8-0c37-4da1-ba4a-4ff24bcc2eed"
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 {
/// Example: "{orderId}",
pub order_id: super::OrderId,
/// Example: "358766",
pub ext_order_id: Option<String>,
/// Example: "2014-10-27T14:58:17.443+01:00",
pub order_create_date: String,
/// Example: "http://localhost/OrderNotify/",
pub notify_url: Option<String>,
/// Example: "127.0.0.1",
pub customer_ip: String,
/// Example: "145227",
pub merchant_pos_id: String,
/// Example: "New order",
pub description: String,
/// Example: "PLN",
pub currency_code: String,
/// Example: "3200",
pub total_amount: String,
/// Example: "NEW",
pub status: String,
/// Example: `[{"name":"Product1","unitPrice":"1000","quantity":"1"}]`
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 {
@ -1383,79 +838,6 @@ pub struct Refund {
pub status_date_time: String,
}
pub mod notify {
use serde::Deserialize;
use super::deserialize;
use crate::OrderId;
/// Payment notification object received on [super::Order].[notify_url]
#[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
}
}
/// Refund notification object
#[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>,
/// Customer client IP address
pub customer_ip: String,
/// Secret pos ip. This is connected to PayU account
#[serde(deserialize_with = "deserialize::deserialize_i32_newtype")]
pub merchant_pos_id: super::MerchantPosId,
/// Transaction description
pub description: String,
/// 3 characters currency identifier, ex. PLN
pub currency_code: String,
/// 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).
#[serde(deserialize_with = "deserialize::deserialize_i32")]
pub total_amount: super::Price,
/// @see [Buyer]
pub buyer: Option<super::Buyer>,
/// List of products
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,
@ -1500,12 +882,38 @@ impl Client {
}
/// All operation will be performed in sandbox PayU environment
///
/// Examples:
///
/// ```
/// # use pay_u::*;
///
/// async fn test() {
/// let client_id = ClientId::new(std::env::var("PAYU_CLIENT_ID").unwrap());
/// let client_secret = ClientSecret::new(std::env::var("PAYU_CLIENT_SECRET").unwrap());
/// let merchant_id = std::env::var("PAYU_CLIENT_MERCHANT_ID").unwrap().parse::<i32>().map(MerchantPosId::from).unwrap();
/// let mut client = Client::new(client_id, client_secret, merchant_id).sandbox();
/// }
/// ```
pub fn sandbox(mut self) -> Self {
self.sandbox = true;
self
}
/// Set your own bearer key
///
/// Examples:
///
/// ```
/// # use pay_u::*;
///
/// async fn test() {
/// let client_id = ClientId::new(std::env::var("PAYU_CLIENT_ID").unwrap());
/// let client_secret = ClientSecret::new(std::env::var("PAYU_CLIENT_SECRET").unwrap());
/// let merchant_id = std::env::var("PAYU_CLIENT_MERCHANT_ID").unwrap().parse::<i32>().map(MerchantPosId::from).unwrap();
/// let mut client = Client::new(client_id, client_secret, merchant_id).with_bearer("a89sdhas9d8yasd8", 9_999_999);
/// }
/// ```
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);
@ -1531,13 +939,14 @@ impl Client {
/// .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 2000);
/// let res = client
/// .create_order(
/// OrderCreateRequest::new(
/// req::OrderCreate::build(
/// Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
/// "127.0.0.1",
/// "PLN",
/// )
/// "Some description"
/// ).expect("All fields must be valid")
/// .with_notify_url("https://your.eshop.com/notify")
/// .with_description("RTV market")
/// .with_description("Replace description")
/// .with_products([
/// Product::new("Wireless Mouse for Laptop", 15000, 1),
/// Product::new("HDMI cable", 6000, 1),
@ -1546,7 +955,7 @@ impl Client {
/// .await;
/// }
/// ```
pub async fn create_order(&mut self, order: OrderCreateRequest) -> Result<res::CreateOrder> {
pub async fn create_order(&mut self, order: req::OrderCreate) -> Result<res::CreateOrder> {
self.authorize().await?;
if order.total_amount
!= order
@ -1631,7 +1040,7 @@ impl Client {
/// let res = client
/// .refund(
/// OrderId::new("H9LL64F37H160126GUEST000P01"),
/// RefundRequest::new("Refund", Some(1000)),
/// req::Refund::new("Refund", Some(1000)),
/// )
/// .await;
/// }
@ -1642,7 +1051,7 @@ impl Client {
/// let res = client
/// .refund(
/// OrderId::new("H9LL64F37H160126GUEST000P01"),
/// RefundRequest::new("Refund", None),
/// req::Refund::new("Refund", None),
/// )
/// .await;
/// }
@ -1650,12 +1059,12 @@ impl Client {
pub async fn refund(
&mut self,
order_id: OrderId,
refund: RefundRequest,
refund: req::Refund,
) -> Result<res::RefundDetails> {
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct RefundWrapper {
refund: RefundRequest,
refund: req::Refund,
}
self.authorize().await?;
@ -1918,11 +1327,12 @@ mod tests {
async fn perform_create_order(client: &mut Client) -> Result<CreateOrder> {
client
.create_order(
OrderCreateRequest::new(
req::OrderCreate::build(
Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
"127.0.0.1",
"PLN",
)
"Desc",
)?
.with_notify_url("https://your.eshop.com/notify")
.with_description("RTV market")
.with_products(
@ -1954,7 +1364,7 @@ mod tests {
.await
.expect("Failed to create");
let res = client
.refund(order_id, RefundRequest::new("Refund", Some(10)))
.refund(order_id, req::Refund::new("Refund", Some(10)))
.await;
if res.is_err() {
@ -1970,7 +1380,7 @@ mod tests {
.await
.expect("Failed to create");
let res = client
.refund(order_id, RefundRequest::new("Refund", None))
.refund(order_id, req::Refund::new("Refund", None))
.await;
if res.is_err() {

161
pay_u/src/notify.rs Normal file
View File

@ -0,0 +1,161 @@
//! Notification objects. Those objects will be send on notify_url if was given.
//!
//! To enable notifications for a specific payment, specify the notifyUrl
//! parameter in the payment form. Each payment can receive a different URL to
//! which notifications will be sent.
//!
//! Every notification is sent asynchronously. After your system receives a
//! notification with the status COMPLETED, instruct it to ignore any further
//! notifications.
//!
//! After sending a notification, PayU system requires a 200 HTTP status code in
//! response. If it receives a different status code, it will resend the
//! notification. Your system should also take account of situations where a
//! notification is sent several times with the same status. For each repeated
//! notification, response with code 200 should be sent as well.
//!
//! To ensure trusted communication between PayU and your shop, you must verify
//! the signature value available in the OpenPayu-Signature header each time
//! your system receives any notification from a PayU server. Refer to the
//! Verification of notifications signature for more information.
//!
//! Notifications are sent for orders in the following statuses: PENDING,
//! WAITING_FOR_CONFIRMATION, COMPLETED, CANCELED.
//!
//! Note: if you filter IP addresses, remember to allow IPs used by PayU to send
//! the notifications. These are:
//!
//! ### PRODUCTION
//!
//! > 185.68.12.10, 185.68.12.11, 185.68.12.12, 185.68.12.26, 185.68.12.27,
//! > 185.68.12.28
//!
//! ### SANDBOX
//!
//! > 185.68.14.10, 185.68.14.11, 185.68.14.12, 185.68.14.26, 185.68.14.27,
//! > 185.68.14.28
//!
//! ## Payment lifecycle
//!
//! You can configure a separate auto-receive / automatic collection parameter
//! for each payment method via the Management Panel.
//!
//! By default, auto-receive is enabled. The basic payment sequence is as
//! follows:
//!
//! 1) Each successfully authorized payment for an order is captured.
//! 2) The buyer is charged with the order amount.
//! 3) The shop balance is increased by the order amount.
//! 4) PayU calculates its commission to the order.
//!
//! If the auto-receive is turned off, you should capture each order using a PUT
//! method (Order capture) or cancel using DELETE method (Cancellation).
//!
//! If no such action is taken the order is auto-canceled. Automatic
//! cancellation occurs after a number of days indicated for the payment method.
//!
//! <img src="https://developers.payu.com/images/order_statusesV2-en.png" />
use serde::Deserialize;
use super::deserialize;
use crate::OrderId;
/// Payment notification object received on notify_url
///
/// See [crate::req::CreateOrder::notify_url]
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StatusUpdate {
/// Section containing order object
pub order: Order,
/// Moment of accepting the transaction and adding funds from the
/// transaction to the Shop balance. Format: "%Y-%M-%DT%h:%m:%s%z."
/// Example: "2020-06-09T17:52:04.644+02:00". If the millisecond counter
/// is "000" then milliseconds are not sent and the format changes to:
/// "%Y-%M-%DT%h:%m:%s". Is present only for the status "COMPLETED".
pub local_receipt_date_time: Option<String>,
/// Array of objects related to transaction identification. In case of
/// statuses:
/// * `"WAITING_FOR_CONFIRMATION"` and `"COMPLETED"` - Contains one element
/// with two parameters: name and value,
/// * `"PENDING"` - may contain object with aforementioned parameters or it
/// can be empty.
///
/// Also properties `name`
///
/// Static value. The payment identifier, displayed on transaction
/// statements as "Trans ID" and within the transaction search option in
/// the Management Panel.
///
/// Also properties `value`
///
/// Transaction ID in PayU system (data type - string).
pub properties: Option<Vec<super::Prop>>,
}
impl StatusUpdate {
pub fn status(&self) -> super::PaymentStatus {
self.order.status
}
/// See [crate::req::OrderCreate::ext_order_id]
pub fn ext_order_id(&self) -> &str {
self.order.ext_order_id.as_deref().unwrap_or_default()
}
pub fn has_ext_order_id(&self) -> bool {
self.order.ext_order_id.is_some()
}
}
/// Refund notification object
#[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>,
/// Customer client IP address
pub customer_ip: String,
/// Secret pos ip. This is connected to PayU account
#[serde(deserialize_with = "deserialize::deserialize_i32_newtype")]
pub merchant_pos_id: super::MerchantPosId,
/// Transaction description
pub description: String,
/// 3 characters currency identifier, ex. PLN
pub currency_code: String,
/// 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).
#[serde(deserialize_with = "deserialize::deserialize_i32")]
pub total_amount: super::Price,
/// @see [crate::Buyer]
pub buyer: Option<super::Buyer>,
/// List of products
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 ext_order_id: Option<String>,
}

381
pay_u/src/req.rs Normal file
View File

@ -0,0 +1,381 @@
//! Objects used to send requests to PayU
use serde::{Deserialize, Serialize};
use crate::credit::Credit;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Refund {
pub(crate) description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) amount: Option<super::Price>,
}
impl Refund {
pub fn new<Description>(description: Description, amount: Option<super::Price>) -> Self
where
Description: Into<String>,
{
Self {
description: description.into(),
amount,
}
}
pub fn description(&self) -> &str {
&self.description
}
pub fn amount(&self) -> Option<super::Price> {
self.amount
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrderCreate {
/// ID of an order used in merchant system. Order identifier assigned by
/// the merchant. It enables merchants to find a specific order
/// in their system. This value must be unique within a single
/// POS.
pub(crate) ext_order_id: Option<String>,
/// 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 |
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) notify_url: Option<String>,
/// Address for redirecting the customer after payment is commenced. If
/// the payment has not been authorized, error=501 parameter
/// will be added. Please note that no decision regarding
/// payment status should be made depending on the presence or
/// lack of this parameter (to get payment status, wait for
/// notification or retrieve order details).
///
/// IMPORTANT: the address must be compliant with the structure below:
/// <img src="https://developers.payu.com/images/continueUrlStructure_en.png" />
///
/// Please keep in mind:
/// * accepted schemas are http and https,
/// * such elements as port, path, query and fragment are optional,
/// * query values must be encoded.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) continue_url: Option<String>,
/// Payers IP address, e.g. 123.123.123.123. Note: 0.0.0.0 is not
/// accepted.
pub(crate) customer_ip: String,
/// Secret pos ip. This is connected to PayU account
#[serde(
serialize_with = "crate::serialize::serialize_newtype",
deserialize_with = "crate::deserialize::deserialize_i32_newtype"
)]
pub(crate) merchant_pos_id: super::MerchantPosId,
/// Transaction description
pub(crate) description: String,
/// 3 characters currency identifier, ex. PLN
pub(crate) currency_code: String,
/// 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).
#[serde(
serialize_with = "crate::serialize::serialize_i32",
deserialize_with = "crate::deserialize::deserialize_i32"
)]
pub(crate) total_amount: super::Price,
/// @see [crate::Buyer]
pub(crate) buyer: Option<super::Buyer>,
/// List of products
pub(crate) products: Vec<super::Product>,
#[serde(skip_serializing)]
pub(crate) order_create_date: Option<String>,
/// Duration for the validity of an order (in seconds), during which
/// time payment must be made. Default value 86400.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) validity_time: Option<u16>,
/// Additional description of the order.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) additional_description: Option<String>,
/// Text visible on the PayU payment page (max. 80 chars).
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) visible_description: Option<String>,
/// Payment recipient name followed by payment description (order ID,
/// ticket number etc) visible on card statement (max. 22
/// chars). The name should be easy to recognize by the
/// cardholder (e.g "shop.com 124343"). If field
/// is not provided, static name configured by PayU will be used.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) statement_description: Option<String>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub(crate) muct: Option<super::muct::MultiUseCartToken>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) credit: Option<Credit>,
}
impl OrderCreate {
pub fn build<CustomerIp, Currency, Description>(
buyer: super::Buyer,
customer_ip: CustomerIp,
currency: Currency,
description: Description,
) -> super::Result<Self>
where
CustomerIp: Into<String>,
Currency: Into<String>,
Description: Into<String>,
{
let customer_ip = customer_ip.into();
if &customer_ip == "0.0.0.0" {
return Err(super::Error::CustomerIp);
}
Ok(Self {
ext_order_id: None,
notify_url: None,
continue_url: None,
customer_ip,
merchant_pos_id: 0.into(),
description: description.into(),
currency_code: currency.into(),
total_amount: 0,
buyer: Some(buyer),
products: Vec::new(),
order_create_date: None,
validity_time: None,
additional_description: None,
visible_description: None,
statement_description: None,
muct: None,
credit: None,
})
}
/// ID of an order used in merchant system. Order identifier assigned by
/// the merchant. It enables merchants to find a specific order
/// in their system. This value must be unique within a single
/// POS.
pub fn with_ext_order_id<S: Into<String>>(mut self, ext_order_id: S) -> Self {
self.ext_order_id = Some(ext_order_id.into());
self
}
/// Duration for the validity of an order (in seconds), during which
/// time payment must be made. Default value 86400.
pub fn with_validity_time(mut self, validity_time: u16) -> Self {
self.validity_time = Some(validity_time);
self
}
pub fn with_multi_use_token(
mut self,
recurring: super::muct::Recurring,
card_on_file: super::muct::CardOnFile,
) -> Self {
self.muct = Some(super::muct::MultiUseCartToken {
recurring,
card_on_file,
});
self
}
pub fn with_products<Products>(mut self, products: Products) -> Self
where
Products: Iterator<Item = super::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: super::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
}
/// Description of the an order.
///
/// > This method will override initial description!
pub fn with_description<Description>(mut self, desc: Description) -> Self
where
Description: Into<String>,
{
self.description = String::from(desc.into().trim());
self
}
/// Additional description of the order.
pub fn with_additional_description<S: Into<String>>(
mut self,
additional_description: S,
) -> Self {
self.additional_description = Some(additional_description.into());
self
}
/// Text visible on the PayU payment page (max. 80 chars).
pub fn with_visible_description(mut self, visible_description: &str) -> Self {
let visible_description = if visible_description.len() > 60 {
&visible_description[..60]
} else {
visible_description
};
self.visible_description = Some(String::from(visible_description));
self
}
/// Payment recipient name followed by payment description (order ID,
/// ticket number etc) visible on card statement (max. 22
/// chars). The name should be easy to recognize by the
/// cardholder (e.g "shop.com 124343"). If field
/// is not provided, static name configured by PayU will be used.
pub fn with_statement_description<Description>(mut self, desc: Description) -> Self
where
Description: Into<String>,
{
self.statement_description = Some(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 [crate::res::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
}
/// Address for redirecting the customer after payment is commenced. If
/// the payment has not been authorized, error=501 parameter
/// will be added. Please note that no decision regarding
/// payment status should be made depending on the presence or
/// lack of this parameter (to get payment status, wait for
/// notification or retrieve order details).
pub fn with_continue_url<ContinueUrl>(mut self, continue_url: ContinueUrl) -> Self
where
ContinueUrl: Into<String>,
{
self.continue_url = Some(continue_url.into());
self
}
/// Section containing credit data. This information is not required,
/// but it is strongly recommended to include it. Otherwise the
/// buyer will be prompted to provide missing data on provider
/// page when payment by Installments or Pay later.
pub fn with_credit(mut self, credit: Credit) -> Self {
self.credit = Some(credit);
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) -> super::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) -> &super::Price {
&self.total_amount
}
pub fn buyer(&self) -> &Option<super::Buyer> {
&self.buyer
}
pub fn products(&self) -> &[super::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: super::MerchantPosId) -> Self {
self.merchant_pos_id = merchant_pos_id;
self
}
}

144
pay_u/src/res.rs Normal file
View File

@ -0,0 +1,144 @@
use crate::{OrderId, Refund, Status};
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreateOrder {
/// Http status as a text
pub status: Status,
/// Client should be redirected to this URI
pub redirect_uri: String,
/// This should be connected to your own order
pub order_id: OrderId,
/// This is YOUR_EXT_ORDER_ID
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 {
/// // "543402******4014",
pub card_number_masked: String,
/// MC (MasterCard/Maestro), VS (Visa)
/// Example; "MC"
pub card_scheme: String,
pub card_profile: CardProfile,
pub card_classification: CardClassification,
/// Example: "000"
pub card_response_code: String,
/// Example: "000 - OK"
pub card_response_code_desc: String,
/// Example: "5"
pub card_eci_code: String,
/// Example: "AY",
pub card3ds_status: String,
/// Example: "PL",
pub card_bin_country: String,
/// Example: "MCC0111LL1121"
pub first_transaction_id: String,
}
/// > Installment proposal on the Sandbox environment is not related to the
/// > order amount and always returns data for 480 PLN.
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionCardInstallmentProposal {
/// Example: "5aff3ba8-0c37-4da1-ba4a-4ff24bcc2eed"
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 {
/// Example: "{orderId}",
pub order_id: super::OrderId,
/// Example: "358766",
pub ext_order_id: Option<String>,
/// Example: "2014-10-27T14:58:17.443+01:00",
pub order_create_date: String,
/// Example: "http://localhost/OrderNotify/",
pub notify_url: Option<String>,
/// Example: "127.0.0.1",
pub customer_ip: String,
/// Example: "145227",
pub merchant_pos_id: String,
/// Example: "New order",
pub description: String,
/// Example: "PLN",
pub currency_code: String,
/// Example: "3200",
pub total_amount: String,
/// Example: "NEW",
pub status: String,
/// Example: `[{"name":"Product1","unitPrice":"1000","quantity":"1"}]`
pub products: Vec<super::Product>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrdersInfo {
pub orders: Vec<Order>,
pub status: Status,
pub properties: Option<Vec<crate::Prop>>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrderInfo {
pub order: Order,
pub status: Status,
pub properties: Option<Vec<crate::Prop>>,
}