diff --git a/Cargo.lock b/Cargo.lock index 7e3d576..50d56ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2135,7 +2135,7 @@ checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" [[package]] name = "pay_u" -version = "0.1.6" +version = "0.1.7" dependencies = [ "chrono", "derive_more", diff --git a/api/src/actors/payment_manager.rs b/api/src/actors/payment_manager.rs index 14b58a8..5283258 100644 --- a/api/src/actors/payment_manager.rs +++ b/api/src/actors/payment_manager.rs @@ -153,10 +153,14 @@ pub(crate) async fn request_payment( client .lock() .create_order( - OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency) - .with_description(msg.description) - .with_notify_url(msg.redirect_uri) - .with_products(msg.products.into_iter().map(Into::into)), + OrderCreateRequest::build( + msg.buyer.into(), + msg.customer_ip, + msg.currency, + msg.description, + )? + .with_notify_url(msg.redirect_uri) + .with_products(msg.products.into_iter().map(Into::into)), ) .await? }; diff --git a/pay_u/Cargo.toml b/pay_u/Cargo.toml index 6df8610..9ab6bb9 100644 --- a/pay_u/Cargo.toml +++ b/pay_u/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pay_u" description = "PayU Rest API wrapper" -version = "0.1.6" +version = "0.1.7" edition = "2021" license = "MIT" diff --git a/pay_u/README.md b/pay_u/README.md index 925674e..084f6f5 100644 --- a/pay_u/README.md +++ b/pay_u/README.md @@ -17,11 +17,13 @@ async fn usage() { client.authorize().await.expect("Invalid credentials"); let _res = client.create_order( - OrderCreateRequest::new( + OrderCreateRequest::build( Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"), "127.0.0.1", "PLN", + "Some description" ) + .expect("All required fields must be valid") // Endpoint which will be requested by PayU with payment status update .with_notify_url("https://your.eshop.com/notify") // payment description (MANDATORY) @@ -94,6 +96,10 @@ async fn handle_notification(path: Path, Json(notify): Json, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Postal box number + postal_box: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Postal code + postal_code: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// City + city: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Province + state: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Two-letter country code compliant with ISO-3166. + country_code: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Address description + name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Recipient’s name + recipient_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Recipient’s e-mail address + recipient_email: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Recipient’s phone number + recipient_phone: Option, +} + +#[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, +} + +impl BuyerShippingAddress { + pub fn new_with_delivery(delivery: Delivery) -> Self { + Self { + delivery: Some(delivery), + } + } +} + #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct Buyer { /// Required customer e-mail - email: String, + #[serde(skip_serializing_if = "Option::is_none")] + email: Option, /// Required customer phone number - phone: String, + #[serde(skip_serializing_if = "Option::is_none")] + phone: Option, /// Required customer first name - first_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + first_name: Option, /// Required customer last name - last_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + last_name: Option, /// Required customer language - language: String, + #[serde(skip_serializing_if = "Option::is_none")] + language: Option, + #[serde(skip_serializing_if = "Option::is_none")] + delivery: Option, } impl Buyer { @@ -229,62 +296,68 @@ impl Buyer { Language: Into, { Self { - email: email.into(), - phone: phone.into(), - first_name: first_name.into(), - last_name: last_name.into(), - language: lang.into(), + email: Some(email.into()), + phone: Some(phone.into()), + first_name: Some(first_name.into()), + last_name: Some(last_name.into()), + language: Some(lang.into()), + delivery: None, } } pub fn email(&self) -> &str { - &self.email + self.email.as_deref().unwrap_or_default() } pub fn with_email(mut self, email: S) -> Self where S: Into, { - self.email = email.into(); + self.email = Some(email.into()); self } pub fn phone(&self) -> &str { - &self.phone + self.phone.as_deref().unwrap_or_default() } pub fn with_phone(mut self, phone: S) -> Self where S: Into, { - self.phone = phone.into(); + self.phone = Some(phone.into()); self } pub fn first_name(&self) -> &str { - &self.first_name + self.first_name.as_deref().unwrap_or_default() } pub fn with_first_name(mut self, first_name: S) -> Self where S: Into, { - self.first_name = first_name.into(); + self.first_name = Some(first_name.into()); self } pub fn last_name(&self) -> &str { - &self.last_name + self.last_name.as_deref().unwrap_or_default() } pub fn with_last_name(mut self, last_name: S) -> Self where S: Into, { - self.last_name = last_name.into(); + self.last_name = Some(last_name.into()); self } pub fn language(&self) -> &str { - &self.language + self.language.as_deref().unwrap_or_default() } pub fn with_language(mut self, language: S) -> Self where S: Into, { - 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 } } @@ -306,6 +379,14 @@ pub struct Product { deserialize_with = "deserialize::deserialize_u32" )] 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, + /// 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, } impl Product { @@ -314,8 +395,238 @@ impl Product { name: name.into(), unit_price, 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, + /// Section containing data about ordered products. + /// > Note: product objects in the section do not + /// > have a listingDate field + #[serde(skip_serializing_if = "Option::is_none")] + products: Option>, + /// 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(ext_customer_id: ExtCustomerId) -> Self + where + ExtCustomerId: Into, + { + Self { + shopping_method: None, + ext_customer_id: ext_customer_id.into(), + products: None, + } + } + + pub fn new_with_products( + ext_customer_id: ExtCustomerId, + products: Products, + ) -> Self + where + ExtCustomerId: Into, + Products: Iterator, + { + Self { + shopping_method: None, + ext_customer_id: ext_customer_id.into(), + products: Some(products.map(Product::erase_listing_date).collect()), + } + } + + pub fn with_products(mut self, products: Products) -> Self + where + Products: Iterator, + { + self.products = Some(products.map(Product::erase_listing_date).collect()); + self + } + + /// Section containing data of shipping method. + pub fn shopping_method(&self) -> &Option { + &self.shopping_method + } + + /// Section containing data about ordered products. + /// > Note: product objects in the section do not + /// > have a listingDate field + pub fn products(&self) -> &Option> { + &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, + /// Street name, possibly including house and flat number. + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub street: Option, + /// Street number + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub street_no: Option, + /// Flat number + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub flat_no: Option, + /// Postal code + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub postal_code: Option, + /// City + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, + /// Two-letter country code compliant with ISO-3166 + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub country_code: Option, +} + +#[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, + /// Shipping cost + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub price: Option, + /// Section containing data about shipping address. + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + 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 @@ -504,9 +815,12 @@ pub struct OrderCreateRequest { /// 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 { @@ -542,6 +856,7 @@ impl OrderCreateRequest { visible_description: None, statement_description: None, muct: None, + credit: None, }) } @@ -560,26 +875,6 @@ impl OrderCreateRequest { 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 - } - pub fn with_multi_use_token( mut self, recurring: muct::Recurring, @@ -613,6 +908,9 @@ impl OrderCreateRequest { self } + /// Description of the an order. + /// + /// > This method will override initial description! pub fn with_description(mut self, desc: Description) -> Self where Description: Into, @@ -621,6 +919,38 @@ impl OrderCreateRequest { 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 /// @@ -635,6 +965,28 @@ impl OrderCreateRequest { 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. /// @@ -675,25 +1027,30 @@ impl OrderCreateRequest { &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 }