Add many additional create order fields

This commit is contained in:
Adrian Woźniak 2022-04-28 17:54:22 +02:00
parent 9658abe3b8
commit 94e7e7eb1e
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
5 changed files with 415 additions and 48 deletions

2
Cargo.lock generated
View File

@ -2135,7 +2135,7 @@ checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc"
[[package]] [[package]]
name = "pay_u" name = "pay_u"
version = "0.1.6" version = "0.1.7"
dependencies = [ dependencies = [
"chrono", "chrono",
"derive_more", "derive_more",

View File

@ -153,10 +153,14 @@ pub(crate) async fn request_payment(
client client
.lock() .lock()
.create_order( .create_order(
OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency) OrderCreateRequest::build(
.with_description(msg.description) msg.buyer.into(),
.with_notify_url(msg.redirect_uri) msg.customer_ip,
.with_products(msg.products.into_iter().map(Into::into)), msg.currency,
msg.description,
)?
.with_notify_url(msg.redirect_uri)
.with_products(msg.products.into_iter().map(Into::into)),
) )
.await? .await?
}; };

View File

@ -1,7 +1,7 @@
[package] [package]
name = "pay_u" name = "pay_u"
description = "PayU Rest API wrapper" description = "PayU Rest API wrapper"
version = "0.1.6" version = "0.1.7"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"

View File

@ -17,11 +17,13 @@ async fn usage() {
client.authorize().await.expect("Invalid credentials"); client.authorize().await.expect("Invalid credentials");
let _res = client.create_order( let _res = client.create_order(
OrderCreateRequest::new( OrderCreateRequest::build(
Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"), Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
"127.0.0.1", "127.0.0.1",
"PLN", "PLN",
"Some description"
) )
.expect("All required fields must be valid")
// Endpoint which will be requested by PayU with payment status update // Endpoint which will be requested by PayU with payment status update
.with_notify_url("https://your.eshop.com/notify") .with_notify_url("https://your.eshop.com/notify")
// payment description (MANDATORY) // payment description (MANDATORY)
@ -94,6 +96,10 @@ async fn handle_notification(path: Path<i32>, Json(notify): Json<pay_u::notify::
} }
``` ```
### Releases
0.1.7 - Added credit and more create order request options like additional description, visible description.
## Bugs ## Bugs
Please report bugs here: https://todo.sr.ht/~tsumanu/payu-rs Please report bugs here: https://todo.sr.ht/~tsumanu/payu-rs

View File

@ -198,19 +198,86 @@ pub enum RefundStatus {
Completed, Completed,
} }
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct Delivery {
#[serde(skip_serializing_if = "Option::is_none")]
/// Street name
street: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Postal box number
postal_box: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Postal code
postal_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// City
city: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Province
state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Two-letter country code compliant with ISO-3166.
country_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Address description
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Recipients name
recipient_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Recipients e-mail address
recipient_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Recipients phone number
recipient_phone: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct BuyerShippingAddress {
#[serde(skip_serializing_if = "Option::is_none")]
/// stores the shipping address
delivery: Option<Delivery>,
}
impl BuyerShippingAddress {
pub fn new_with_delivery(delivery: Delivery) -> Self {
Self {
delivery: Some(delivery),
}
}
}
#[derive(Serialize, Deserialize, Debug, Default)] #[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Buyer { pub struct Buyer {
/// Required customer e-mail /// Required customer e-mail
email: String, #[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
/// Required customer phone number /// Required customer phone number
phone: String, #[serde(skip_serializing_if = "Option::is_none")]
phone: Option<String>,
/// Required customer first name /// Required customer first name
first_name: String, #[serde(skip_serializing_if = "Option::is_none")]
first_name: Option<String>,
/// Required customer last name /// Required customer last name
last_name: String, #[serde(skip_serializing_if = "Option::is_none")]
last_name: Option<String>,
/// Required customer language /// Required customer language
language: String, #[serde(skip_serializing_if = "Option::is_none")]
language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
delivery: Option<BuyerShippingAddress>,
} }
impl Buyer { impl Buyer {
@ -229,62 +296,68 @@ impl Buyer {
Language: Into<String>, Language: Into<String>,
{ {
Self { Self {
email: email.into(), email: Some(email.into()),
phone: phone.into(), phone: Some(phone.into()),
first_name: first_name.into(), first_name: Some(first_name.into()),
last_name: last_name.into(), last_name: Some(last_name.into()),
language: lang.into(), language: Some(lang.into()),
delivery: None,
} }
} }
pub fn email(&self) -> &str { pub fn email(&self) -> &str {
&self.email self.email.as_deref().unwrap_or_default()
} }
pub fn with_email<S>(mut self, email: S) -> Self pub fn with_email<S>(mut self, email: S) -> Self
where where
S: Into<String>, S: Into<String>,
{ {
self.email = email.into(); self.email = Some(email.into());
self self
} }
pub fn phone(&self) -> &str { pub fn phone(&self) -> &str {
&self.phone self.phone.as_deref().unwrap_or_default()
} }
pub fn with_phone<S>(mut self, phone: S) -> Self pub fn with_phone<S>(mut self, phone: S) -> Self
where where
S: Into<String>, S: Into<String>,
{ {
self.phone = phone.into(); self.phone = Some(phone.into());
self self
} }
pub fn first_name(&self) -> &str { pub fn first_name(&self) -> &str {
&self.first_name self.first_name.as_deref().unwrap_or_default()
} }
pub fn with_first_name<S>(mut self, first_name: S) -> Self pub fn with_first_name<S>(mut self, first_name: S) -> Self
where where
S: Into<String>, S: Into<String>,
{ {
self.first_name = first_name.into(); self.first_name = Some(first_name.into());
self self
} }
pub fn last_name(&self) -> &str { pub fn last_name(&self) -> &str {
&self.last_name self.last_name.as_deref().unwrap_or_default()
} }
pub fn with_last_name<S>(mut self, last_name: S) -> Self pub fn with_last_name<S>(mut self, last_name: S) -> Self
where where
S: Into<String>, S: Into<String>,
{ {
self.last_name = last_name.into(); self.last_name = Some(last_name.into());
self self
} }
pub fn language(&self) -> &str { pub fn language(&self) -> &str {
&self.language self.language.as_deref().unwrap_or_default()
} }
pub fn with_language<S>(mut self, language: S) -> Self pub fn with_language<S>(mut self, language: S) -> Self
where where
S: Into<String>, S: Into<String>,
{ {
self.language = language.into(); self.language = Some(language.into());
self
}
pub fn with_delivery(mut self, delivery: Delivery) -> Self {
self.delivery = Some(BuyerShippingAddress::new_with_delivery(delivery));
self self
} }
} }
@ -306,6 +379,14 @@ pub struct Product {
deserialize_with = "deserialize::deserialize_u32" deserialize_with = "deserialize::deserialize_u32"
)] )]
pub quantity: Quantity, pub quantity: Quantity,
/// Product type, which can be virtual or material; (possible values true or
/// false).
#[serde(rename = "virtual", skip_serializing_if = "Option::is_none")]
pub virtual_product: Option<bool>,
/// Marketplace date from which the product (or offer) is available, ISO
/// format applies, e.g. "2019-03-27T10:57:59.000+01:00".
#[serde(skip_serializing_if = "Option::is_none")]
pub listing_date: Option<chrono::NaiveDateTime>,
} }
impl Product { impl Product {
@ -314,8 +395,238 @@ impl Product {
name: name.into(), name: name.into(),
unit_price, unit_price,
quantity, quantity,
virtual_product: None,
listing_date: None,
} }
} }
/// Product type, which can be virtual or material; (possible values true or
/// false).
pub fn into_virtual(mut self) -> Self {
self.virtual_product = Some(true);
self
}
/// Product type, which can be virtual or material; (possible values true or
/// false).
pub fn non_virtual(mut self) -> Self {
self.virtual_product = Some(false);
self
}
/// Marketplace date from which the product (or offer) is available, ISO
/// format applies, e.g. "2019-03-27T10:57:59.000+01:00".
pub fn with_listing_date(mut self, listing_date: chrono::NaiveDateTime) -> Self {
self.listing_date = Some(listing_date);
self
}
fn erase_listing_date(mut self) -> Self {
self.listing_date = None;
self
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ShoppingCart {
/// Section containing data of shipping method.
#[serde(skip_serializing_if = "Option::is_none")]
shopping_method: Option<ShoppingMethod>,
/// Section containing data about ordered products.
/// > Note: product objects in the <shoppingCart.products> section do not
/// > have a listingDate field
#[serde(skip_serializing_if = "Option::is_none")]
products: Option<Vec<Product>>,
/// Submerchant identifier. This field should be consistent with field
/// extCustomerId in shoppingCarts section when order is placed in
/// marketplace.
ext_customer_id: String,
}
impl ShoppingCart {
pub fn new<ExtCustomerId, Products>(ext_customer_id: ExtCustomerId) -> Self
where
ExtCustomerId: Into<String>,
{
Self {
shopping_method: None,
ext_customer_id: ext_customer_id.into(),
products: None,
}
}
pub fn new_with_products<ExtCustomerId, Products>(
ext_customer_id: ExtCustomerId,
products: Products,
) -> Self
where
ExtCustomerId: Into<String>,
Products: Iterator<Item = Product>,
{
Self {
shopping_method: None,
ext_customer_id: ext_customer_id.into(),
products: Some(products.map(Product::erase_listing_date).collect()),
}
}
pub fn with_products<Products>(mut self, products: Products) -> Self
where
Products: Iterator<Item = Product>,
{
self.products = Some(products.map(Product::erase_listing_date).collect());
self
}
/// Section containing data of shipping method.
pub fn shopping_method(&self) -> &Option<ShoppingMethod> {
&self.shopping_method
}
/// Section containing data about ordered products.
/// > Note: product objects in the <shoppingCart.products> section do not
/// > have a listingDate field
pub fn products(&self) -> &Option<Vec<Product>> {
&self.products
}
/// Submerchant identifier. This field should be consistent with field
/// extCustomerId in shoppingCarts section when order is placed in
/// marketplace.
pub fn ext_customer_id(&self) -> &String {
&self.ext_customer_id
}
}
/// Type of shipment
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ShoppingMethodType {
Courier,
CollectionPointPickup,
ParcelLocker,
StorePickup,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Address {
/// The full name of the pickup point, including its unique identifier, e.g.
/// „Parcel locker POZ29A”.
#[serde(skip_serializing_if = "Option::is_none")]
pub point_id: Option<String>,
/// Street name, possibly including house and flat number.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub street: Option<String>,
/// Street number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub street_no: Option<String>,
/// Flat number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub flat_no: Option<String>,
/// Postal code
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub postal_code: Option<String>,
/// City
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub city: Option<String>,
/// Two-letter country code compliant with ISO-3166
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub country_code: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ShoppingMethod {
/// Shipping type
/// Recommended
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub shopping_type: Option<ShoppingMethodType>,
/// Shipping cost
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
/// Section containing data about shipping address.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
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 /// MultiUseCartToken
@ -504,9 +815,12 @@ pub struct OrderCreateRequest {
/// number etc) visible on card statement (max. 22 chars). The name should /// 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 /// 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. /// is not provided, static name configured by PayU will be used.
#[serde(skip_serializing_if = "Option::is_none")]
statement_description: Option<String>, statement_description: Option<String>,
#[serde(flatten, skip_serializing_if = "Option::is_none")] #[serde(flatten, skip_serializing_if = "Option::is_none")]
muct: Option<muct::MultiUseCartToken>, muct: Option<muct::MultiUseCartToken>,
#[serde(skip_serializing_if = "Option::is_none")]
credit: Option<Credit>,
} }
impl OrderCreateRequest { impl OrderCreateRequest {
@ -542,6 +856,7 @@ impl OrderCreateRequest {
visible_description: None, visible_description: None,
statement_description: None, statement_description: None,
muct: None, muct: None,
credit: None,
}) })
} }
@ -560,26 +875,6 @@ impl OrderCreateRequest {
self 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
}
pub fn with_multi_use_token( pub fn with_multi_use_token(
mut self, mut self,
recurring: muct::Recurring, recurring: muct::Recurring,
@ -613,6 +908,9 @@ impl OrderCreateRequest {
self self
} }
/// Description of the an order.
///
/// > This method will override initial description!
pub fn with_description<Description>(mut self, desc: Description) -> Self pub fn with_description<Description>(mut self, desc: Description) -> Self
where where
Description: Into<String>, Description: Into<String>,
@ -621,6 +919,38 @@ impl OrderCreateRequest {
self 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 /// Add url to which PayU will be able to send http request with payment
/// status updates /// status updates
/// ///
@ -635,6 +965,28 @@ impl OrderCreateRequest {
self 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 /// URL to which web hook will be send. It's important to return 200 to all
/// notifications. /// notifications.
/// ///
@ -675,25 +1027,30 @@ impl OrderCreateRequest {
&self.customer_ip &self.customer_ip
} }
///
pub fn merchant_pos_id(&self) -> MerchantPosId { pub fn merchant_pos_id(&self) -> MerchantPosId {
self.merchant_pos_id self.merchant_pos_id
} }
pub fn description(&self) -> &String { pub fn description(&self) -> &String {
&self.description &self.description
} }
pub fn currency_code(&self) -> &String { pub fn currency_code(&self) -> &String {
&self.currency_code &self.currency_code
} }
pub fn total_amount(&self) -> &Price { pub fn total_amount(&self) -> &Price {
&self.total_amount &self.total_amount
} }
pub fn buyer(&self) -> &Option<Buyer> { pub fn buyer(&self) -> &Option<Buyer> {
&self.buyer &self.buyer
} }
pub fn products(&self) -> &[Product] { pub fn products(&self) -> &[Product] {
&self.products &self.products
} }
pub fn order_create_date(&self) -> &Option<String> { pub fn order_create_date(&self) -> &Option<String> {
&self.order_create_date &self.order_create_date
} }