diff --git a/api/src/actors/database/account_orders.rs b/api/src/actors/database/account_orders.rs index 5ebbdf1..a29f248 100644 --- a/api/src/actors/database/account_orders.rs +++ b/api/src/actors/database/account_orders.rs @@ -31,7 +31,7 @@ pub(crate) async fn all_account_orders( ) -> Result> { 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")] +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 { + 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")] 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 { 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 "#, diff --git a/api/src/actors/payment_manager.rs b/api/src/actors/payment_manager.rs index 5283258..5db43fe 100644 --- a/api/src/actors/payment_manager.rs +++ b/api/src/actors/payment_manager.rs @@ -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 for pay_u::Product { #[rtype(result = "Result")] pub struct RequestPayment { pub products: Vec, - 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, ) -> Result { - 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, +) -> 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(()) +} diff --git a/api/src/model.rs b/api/src/model.rs index e482b49..c7b7969 100644 --- a/api/src/model.rs +++ b/api/src/model.rs @@ -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, + 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, +} + +impl From 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)] diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/api/src/routes/public/api_v1/unrestricted.rs index 6bd5532..881b961 100644 --- a/api/src/routes/public/api_v1/unrestricted.rs +++ b/api/src/routes/public/api_v1/unrestricted.rs @@ -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>) -> Result { @@ -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, + payment: Data>, +) -> 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); } diff --git a/pay_u/README.md b/pay_u/README.md index 084f6f5..fabdd8f 100644 --- a/pay_u/README.md +++ b/pay_u/README.md @@ -1,4 +1,9 @@ -# Payu REST API +# Unofficial Payu client + + + +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::().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, db: Data, payu: Data, Json(notify): Json) -> HttpResponse { +async fn handle_notification( + path: Path, + Json(notify): Json, + payment: Data> +) -> 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 diff --git a/pay_u/src/credit.rs b/pay_u/src/credit.rs new file mode 100644 index 0000000..3fa8b7c --- /dev/null +++ b/pay_u/src/credit.rs @@ -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 { + /// Applicant’s email address + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + email: Option, + /// Applicant’s phone number + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + phone: Option, + /// Applicant’s first name + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + first_name: Option, + /// Applicant’s last name + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + last_name: Option, + /// 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, + /// National Identification Number + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + nin: Option, + /// Section containing data about applicant’s address. + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + address: Option
, + /// Additional information about person applying for credit. + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + additional_info: Option, +} + +/// 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>, + /// Section containing data of person applying for a credit + #[serde(skip_serializing_if = "Option::is_none")] + applicant: Option, +} + +impl Credit { + pub fn with_shopping_carts(mut self, shopping_carts: ShoppingCarts) -> Self + where + ShoppingCarts: Iterator, + { + 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, +} diff --git a/pay_u/src/lib.rs b/pay_u/src/lib.rs index 317411b..1a9080f 100644 --- a/pay_u/src/lib.rs +++ b/pay_u/src/lib.rs @@ -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 = std::result::Result; /// 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
, } -#[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, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Applicant { - /// Applicant’s email address - /// Recommended - #[serde(skip_serializing_if = "Option::is_none")] - email: Option, - /// Applicant’s phone number - /// Recommended - #[serde(skip_serializing_if = "Option::is_none")] - phone: Option, - /// Applicant’s first name - /// Recommended - #[serde(skip_serializing_if = "Option::is_none")] - first_name: Option, - /// Applicant’s last name - /// Recommended - #[serde(skip_serializing_if = "Option::is_none")] - last_name: Option, - /// 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, - /// National Identification Number - /// Recommended - #[serde(skip_serializing_if = "Option::is_none")] - nin: Option, - /// Section containing data about applicant’s address. - /// Recommended - #[serde(skip_serializing_if = "Option::is_none")] - address: Option
, - /// Additional information about person applying for credit. - /// Recommended - #[serde(skip_serializing_if = "Option::is_none")] - additional_info: Option, -} - -#[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>, - /// Section containing data of person applying for a credit - #[serde(skip_serializing_if = "Option::is_none")] - applicant: Option, -} - -impl Credit { - pub fn with_shopping_carts(mut self, shopping_carts: ShoppingCarts) -> Self - where - ShoppingCarts: Iterator, - { - 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, - /// 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, - /// 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: - /// - /// - /// 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, - /// Payer’s 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, - /// List of products - products: Vec, - #[serde(skip_serializing)] - order_create_date: Option, - /// 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, - /// Additional description of the order. - #[serde(skip_serializing_if = "Option::is_none")] - additional_description: Option, - /// Text visible on the PayU payment page (max. 80 chars). - #[serde(skip_serializing_if = "Option::is_none")] - visible_description: Option, - /// 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, - #[serde(flatten, skip_serializing_if = "Option::is_none")] - muct: Option, - #[serde(skip_serializing_if = "Option::is_none")] - credit: Option, -} - -impl OrderCreateRequest { - pub fn build( - buyer: Buyer, - customer_ip: CustomerIp, - currency: Currency, - description: Description, - ) -> Result - where - CustomerIp: Into, - Currency: Into, - Description: Into, - { - 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>(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(mut self, products: Products) -> Self - where - Products: Iterator, - { - 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(mut self, desc: Description) -> Self - where - Description: Into, - { - self.description = String::from(desc.into().trim()); - self - } - - /// Additional description of the order. - pub fn with_additional_description>( - 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(mut self, desc: Description) -> Self - where - Description: Into, - { - 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(mut self, notify_url: NotifyUrl) -> Self - where - NotifyUrl: Into, - { - 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(mut self, continue_url: ContinueUrl) -> Self - where - ContinueUrl: Into, - { - 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 { - &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 { - &self.buyer - } - - pub fn products(&self) -> &[Product] { - &self.products - } - - pub fn order_create_date(&self) -> &Option { - &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, code: Option, @@ -1098,6 +716,50 @@ pub struct Status { code_literal: Option, } +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, - } - - #[derive(serde::Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - pub struct RefundDetails { - pub order_id: Option, - pub refund: Option, - pub status: Status, - } - - #[derive(serde::Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - pub struct Refunds { - pub refunds: Vec, - } - - #[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, - } - - #[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, - /// Example: "2014-10-27T14:58:17.443+01:00", - pub order_create_date: String, - /// Example: "http://localhost/OrderNotify/", - pub notify_url: Option, - /// 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, - } - - #[derive(serde::Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - pub struct OrdersInfo { - pub orders: Vec, - pub status: super::Status, - pub properties: Option>, - } - - #[derive(serde::Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - pub struct OrderInfo { - pub order: Order, - pub status: super::Status, - pub properties: Option>, - } -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct RefundRequest { - description: String, - #[serde(skip_serializing_if = "Option::is_none")] - amount: Option, -} - -impl RefundRequest { - pub fn new(description: Description, amount: Option) -> Self - where - Description: Into, - { - Self { - description: description.into(), - amount, - } - } - - pub fn description(&self) -> &str { - &self.description - } - - pub fn amount(&self) -> Option { - 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, - pub properties: Option>, - pub status: Option, - } - - 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, - /// 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, - /// List of products - pub products: Vec, - #[serde(skip_serializing)] - pub order_create_date: Option, - pub pay_method: Option, - 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::().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::().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>(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 { + pub async fn create_order(&mut self, order: req::OrderCreate) -> Result { 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 { #[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 { 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() { diff --git a/pay_u/src/notify.rs b/pay_u/src/notify.rs new file mode 100644 index 0000000..cc118aa --- /dev/null +++ b/pay_u/src/notify.rs @@ -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. +//! +//! + +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, + /// 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>, +} + +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, + /// 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, + /// List of products + pub products: Vec, + #[serde(skip_serializing)] + pub order_create_date: Option, + pub pay_method: Option, + pub status: super::PaymentStatus, + pub ext_order_id: Option, +} diff --git a/pay_u/src/req.rs b/pay_u/src/req.rs new file mode 100644 index 0000000..d05e05f --- /dev/null +++ b/pay_u/src/req.rs @@ -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, +} + +impl Refund { + pub fn new(description: Description, amount: Option) -> Self + where + Description: Into, + { + Self { + description: description.into(), + amount, + } + } + + pub fn description(&self) -> &str { + &self.description + } + + pub fn amount(&self) -> Option { + 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, + /// 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, + /// 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: + /// + /// + /// 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, + /// Payer’s 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, + /// List of products + pub(crate) products: Vec, + #[serde(skip_serializing)] + pub(crate) order_create_date: Option, + /// 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, + /// Additional description of the order. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) additional_description: Option, + /// Text visible on the PayU payment page (max. 80 chars). + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) visible_description: Option, + /// 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, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub(crate) muct: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) credit: Option, +} + +impl OrderCreate { + pub fn build( + buyer: super::Buyer, + customer_ip: CustomerIp, + currency: Currency, + description: Description, + ) -> super::Result + where + CustomerIp: Into, + Currency: Into, + Description: Into, + { + 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>(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(mut self, products: Products) -> Self + where + Products: Iterator, + { + 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(mut self, desc: Description) -> Self + where + Description: Into, + { + self.description = String::from(desc.into().trim()); + self + } + + /// Additional description of the order. + pub fn with_additional_description>( + 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(mut self, desc: Description) -> Self + where + Description: Into, + { + 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(mut self, notify_url: NotifyUrl) -> Self + where + NotifyUrl: Into, + { + 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(mut self, continue_url: ContinueUrl) -> Self + where + ContinueUrl: Into, + { + 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 { + &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 { + &self.buyer + } + + pub fn products(&self) -> &[super::Product] { + &self.products + } + + pub fn order_create_date(&self) -> &Option { + &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 + } +} diff --git a/pay_u/src/res.rs b/pay_u/src/res.rs new file mode 100644 index 0000000..1fcf1d2 --- /dev/null +++ b/pay_u/src/res.rs @@ -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, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RefundDetails { + pub order_id: Option, + pub refund: Option, + pub status: Status, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Refunds { + pub refunds: Vec, +} + +#[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, +} + +#[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, + /// Example: "2014-10-27T14:58:17.443+01:00", + pub order_create_date: String, + /// Example: "http://localhost/OrderNotify/", + pub notify_url: Option, + /// 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, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OrdersInfo { + pub orders: Vec, + pub status: Status, + pub properties: Option>, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OrderInfo { + pub order: Order, + pub status: Status, + pub properties: Option>, +}