From b9845d284ff059622ae96d1c75a2d77be3311ac7 Mon Sep 17 00:00:00 2001 From: Edgar Date: Wed, 10 Jun 2020 12:29:13 +0200 Subject: [PATCH] progress with orders --- Cargo.toml | 3 +- README.md | 2 +- rustfmt.toml | 1 + src/errors.rs | 2 +- src/lib.rs | 55 ++++++--- src/orders.rs | 321 ++++++++++++++++++++++++++++++++++++-------------- src/tests.rs | 17 ++- 7 files changed, 288 insertions(+), 113 deletions(-) create mode 100644 rustfmt.toml diff --git a/Cargo.toml b/Cargo.toml index dda66e6..b4e266a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "paypal-rs" -version = "0.0.4" +version = "0.0.5" authors = ["Edgar "] description = "A library that wraps the paypal api asynchronously." repository = "https://github.com/edg-l/paypal-rs/" @@ -16,6 +16,7 @@ edition = "2018" reqwest = { version = "0.10", features = ["json"] } tokio = { version = "0.2", features = ["full"] } serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" jsonwebtoken = "7" base64 = "0.12" log = "0.4" diff --git a/README.md b/README.md index a2029ac..f8f6b23 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Rust](https://github.com/edg-l/paypal-rs/workflows/Rust/badge.svg) ![Docs](https://docs.rs/paypal-rs/badge.svg) -A rust library that wraps the [paypal api](https://developer.paypal.com/docs/api) asynchronously. +A rust library that wraps the [paypal api](https://developer.paypal.com/docs/api) asynchronously in a strongly typed manner. Crate: https://crates.io/crates/paypal-rs diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..866c756 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs index 7582f0b..52852d0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,5 +5,5 @@ pub enum Errors { #[error("failed to get access token")] GetAccessTokenFailure, #[error("failure when calling the paypal api")] - ApiCallFailure, + ApiCallFailure(String), } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 2636d2f..79e695b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,10 @@ extern crate chrono; pub mod errors; pub mod orders; -use serde::{Serialize, Deserialize}; -use std::time::{Duration, Instant}; -use reqwest::header::HeaderMap; use reqwest::header; +use reqwest::header::HeaderMap; +use serde::{Deserialize, Serialize}; +use std::time::{Duration, Instant}; pub const LIVE_ENDPOINT: &str = "https://api.paypal.com"; pub const SANDBOX_ENDPOINT: &str = "https://api.sandbox.paypal.com"; @@ -94,7 +94,7 @@ pub struct Query { /// When results are paged, you can use the next_id value as the start_id to continue with the next set of results. #[serde(skip_serializing_if = "Option::is_none")] pub start_id: Option, - /// The start index of the payments to list. Typically, you use the start_index to jump to a specific position in the resource history based on its cart. + /// The start index of the payments to list. Typically, you use the start_index to jump to a specific position in the resource history based on its cart. /// For example, to start at the second item in a list of results, specify start_index=2. #[serde(skip_serializing_if = "Option::is_none")] pub start_index: Option, @@ -104,7 +104,7 @@ pub struct Query { // TODO: Use https://github.com/samscott89/serde_qs } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub enum Prefer { /// The server returns a minimal response to optimize communication between the API caller and the server. /// A minimal response includes the id, status and HATEOAS links. @@ -124,11 +124,21 @@ impl Default for Prefer { /// https://developer.paypal.com/docs/api/reference/api-requests/#paypal-auth-assertion #[derive(Debug, Default)] pub struct HeaderParams { - pub merchant_payer_id: Option, + /// The merchant payer id used on PayPal-Auth-Assertion + pub merchant_payer_id: Option, + /// Verifies that the payment originates from a valid, user-consented device and application. + /// Reduces fraud and decreases declines. Transactions that do not include a client metadata ID are not eligible for PayPal Seller Protection. pub client_metadata_id: Option, + /// Identifies the caller as a PayPal partner. To receive revenue attribution, specify a unique build notation (BN) code. + /// BN codes provide tracking on all transactions that originate or are associated with a particular partner. pub partner_attribution_id: Option, + /// Contains a unique user-generated ID that the server stores for a period of time. Use this header to enforce idempotency on REST API POST calls. + /// You can make these calls any number of times without concern that the server creates or completes an action on a resource more than once. + /// You can retry calls that fail with network timeouts or the HTTP 500 status code. You can retry calls for as long as the server stores the ID. pub request_id: Option, + /// The preferred server response upon successful completion of the request. pub prefer: Option, + /// The media type. Required for operations with a request body. pub content_type: Option, } @@ -182,26 +192,40 @@ impl Client { headers.append(header::ACCEPT, "application/json".parse().unwrap()); if let Some(token) = &self.auth.access_token { - headers.append(header::AUTHORIZATION, format!("Bearer {}", token.access_token).as_str().parse().unwrap()); + headers.append( + header::AUTHORIZATION, + format!("Bearer {}", token.access_token).as_str().parse().unwrap(), + ); } if let Some(merchant_payer_id) = header_params.merchant_payer_id { let claims = AuthAssertionClaims { iss: self.auth.client_id.clone(), - payer_id: merchant_payer_id + payer_id: merchant_payer_id, }; let jwt_header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256); - let token = jsonwebtoken::encode(&jwt_header, &claims, &jsonwebtoken::EncodingKey::from_secret(self.auth.secret.as_ref())).unwrap(); + let token = jsonwebtoken::encode( + &jwt_header, + &claims, + &jsonwebtoken::EncodingKey::from_secret(self.auth.secret.as_ref()), + ) + .unwrap(); let encoded_token = base64::encode(token); headers.append("PayPal-Auth-Assertion", encoded_token.as_str().parse().unwrap()); } if let Some(client_metadata_id) = header_params.client_metadata_id { - headers.append("PayPal-Client-Metadata-Id", client_metadata_id.as_str().parse().unwrap()); + headers.append( + "PayPal-Client-Metadata-Id", + client_metadata_id.as_str().parse().unwrap(), + ); } if let Some(partner_attribution_id) = header_params.partner_attribution_id { - headers.append("PayPal-Partner-Attribution-Id", partner_attribution_id.as_str().parse().unwrap()); + headers.append( + "PayPal-Partner-Attribution-Id", + partner_attribution_id.as_str().parse().unwrap(), + ); } if let Some(request_id) = header_params.request_id { @@ -219,7 +243,7 @@ impl Client { headers.append(header::CONTENT_TYPE, content_type.as_str().parse().unwrap()); } - builder.headers(headers) + builder.headers(headers) } /// Gets a access token used in all the api calls. @@ -227,10 +251,7 @@ impl Client { let res = self .client .post(format!("{}/v1/oauth2/token", self.endpoint()).as_str()) - .basic_auth( - self.auth.client_id.as_str(), - Some(self.auth.secret.as_str()), - ) + .basic_auth(self.auth.client_id.as_str(), Some(self.auth.secret.as_str())) .header("Content-Type", "x-www-form-urlencoded") .header("Accept", "application/json") .body("grant_type=client_credentials") @@ -242,7 +263,7 @@ impl Client { self.auth.expires = Some((Instant::now(), Duration::new(token.expires_in, 0))); self.auth.access_token = Some(token); } else { - return Err(Box::new(errors::Errors::GetAccessTokenFailure)); + return Err(Box::new(errors::Errors::ApiCallFailure(res.text().await?))); } Ok(()) diff --git a/src/orders.rs b/src/orders.rs index f907b97..d1142dc 100644 --- a/src/orders.rs +++ b/src/orders.rs @@ -1,8 +1,8 @@ -use serde::{Serialize, Deserialize}; use crate::errors; +use serde::{Deserialize, Serialize}; /// The intent to either capture payment immediately or authorize a payment for an order after order creation. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum Intent { /// The merchant intends to capture payment immediately after the customer makes a payment. @@ -24,7 +24,7 @@ impl Default for Intent { /// Represents a payer name. /// /// https://developer.paypal.com/docs/api/orders/v2/#definition-payer.name -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct PayerName { /// When the party is a person, the party's given, or first, name. pub given_name: String, @@ -36,7 +36,7 @@ pub struct PayerName { /// The phone type. /// /// https://developer.paypal.com/docs/api/orders/v2/#definition-phone_with_type -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum PhoneType { Fax, @@ -46,7 +46,7 @@ pub enum PhoneType { Pager, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct PhoneNumber { /// The national number, in its canonical international E.164 numbering plan format. /// The combined length of the country calling code (CC) and the national number must not be greater than 15 digits. @@ -63,7 +63,7 @@ pub struct Phone { pub phone_number: PhoneNumber, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[allow(non_camel_case_types)] pub enum TaxIdType { @@ -134,7 +134,7 @@ pub struct Payer { pub address: Option
, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Money { /// The [three-character ISO-4217 currency code](https://developer.paypal.com/docs/integration/direct/rest/currency-codes/) that identifies the currency. pub currency_code: String, @@ -216,12 +216,12 @@ pub struct PlatformFee { pub payee: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] pub enum DisbursementMode { /// The funds are released to the merchant immediately. Instant, - /// The funds are held for a finite number of days. The actual duration depends on the region and type of integration. - /// You can release the funds through a referenced payout. + /// The funds are held for a finite number of days. The actual duration depends on the region and type of integration. + /// You can release the funds through a referenced payout. /// Otherwise, the funds disbursed automatically after the specified duration. Delayed, } @@ -234,19 +234,19 @@ impl Default for DisbursementMode { #[derive(Debug, Default, Serialize, Deserialize)] pub struct PaymentInstruction { - /// An array of various fees, commissions, tips, or donations. + /// An array of various fees, commissions, tips, or donations. #[serde(skip_serializing_if = "Option::is_none")] pub platform_fees: Option>, /// The funds that are held on behalf of the merchant. #[serde(skip_serializing_if = "Option::is_none")] - pub disbursement_mode: Option + pub disbursement_mode: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ItemCategoryType { /// Goods that are stored, delivered, and used in their electronic format. - /// This value is not currently supported for API callers that leverage + /// This value is not currently supported for API callers that leverage /// the [PayPal for Commerce Platform](https://www.paypal.com/us/webapps/mpp/commerce-platform) product. Digital, /// A tangible item that can be shipped with proof of delivery. @@ -272,17 +272,17 @@ pub struct Item { /// The item name or title. pub name: String, /// The item price or rate per unit. - /// If you specify unit_amount, purchase_units[].amount.breakdown.item_total is required. Must equal unit_amount * quantity for all items. + /// If you specify unit_amount, purchase_units[].amount.breakdown.item_total is required. Must equal unit_amount * quantity for all items. pub unit_amount: Money, - /// The item tax for each unit. If tax is specified, purchase_units[].amount.breakdown.tax_total is required. Must equal tax * quantity for all items. + /// The item tax for each unit. If tax is specified, purchase_units[].amount.breakdown.tax_total is required. Must equal tax * quantity for all items. #[serde(skip_serializing_if = "Option::is_none")] pub tax: Option, - /// The item quantity. Must be a whole number. + /// The item quantity. Must be a whole number. pub quantity: String, - /// The detailed item description. + /// The detailed item description. #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, - /// The stock keeping unit (SKU) for the item. + /// The stock keeping unit (SKU) for the item. #[serde(skip_serializing_if = "Option::is_none")] pub sku: Option, /// The item category type @@ -290,7 +290,7 @@ pub struct Item { pub category: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum AuthorizationStatus { /// The authorized payment is created. No captured payments have been made for this authorized payment. @@ -311,22 +311,22 @@ pub enum AuthorizationStatus { Pending, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum AuthorizationStatusDetails { /// Authorization is pending manual review. PendingReview, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct AuthorizationWithData { /// The status for the authorized payment. pub status: AuthorizationStatus, - /// The details of the authorized order pending status. + /// The details of the authorized order pending status. pub status_details: AuthorizationStatusDetails, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CaptureStatus { /// The funds for this captured payment were credited to the payee's PayPal account. @@ -341,12 +341,12 @@ pub enum CaptureStatus { Refunded, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CaptureStatusDetails { /// The payer initiated a dispute for this captured payment with PayPal. BuyerComplaint, - /// The captured funds were reversed in response to the payer disputing this captured payment with + /// The captured funds were reversed in response to the payer disputing this captured payment with /// the issuer of the financial instrument used to pay for this captured payment. Chargeback, /// The payer paid by an eCheck that has not yet cleared. @@ -359,7 +359,7 @@ pub enum CaptureStatusDetails { PendingReview, /// The payee has not yet set up appropriate receiving preferences for their account. /// For more information about how to accept or deny this payment, visit your account online. - /// This reason is typically offered in scenarios such as when the currency of the captured + /// This reason is typically offered in scenarios such as when the currency of the captured /// payment is different from the primary holding currency of the payee. ReceivingPreferenceMandatesManualAction, /// The captured funds were refunded. @@ -372,15 +372,15 @@ pub enum CaptureStatusDetails { VerificationRequired, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Capture { /// The status of the captured payment. status: CaptureStatus, - /// The details of the captured payment status. + /// The details of the captured payment status. status_details: CaptureStatusDetails, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum RefundStatus { /// The refund was cancelled. @@ -392,7 +392,7 @@ pub enum RefundStatus { } /// The reason why the refund has the PENDING or FAILED status. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum RefundStatusDetails { /// The customer's account is funded through an eCheck, which has not yet cleared. @@ -401,19 +401,19 @@ pub enum RefundStatusDetails { #[derive(Debug, Serialize, Deserialize)] pub struct Refund { - /// The status of the refund. + /// The status of the refund. pub status: RefundStatus, - /// The details of the refund status. + /// The details of the refund status. pub status_details: RefundStatusDetails, } #[derive(Debug, Serialize, Deserialize)] pub struct PaymentCollection { - /// An array of authorized payments for a purchase unit. A purchase unit can have zero or more authorized payments. + /// An array of authorized payments for a purchase unit. A purchase unit can have zero or more authorized payments. pub authorizations: Vec, - /// An array of captured payments for a purchase unit. A purchase unit can have zero or more captured payments. + /// An array of captured payments for a purchase unit. A purchase unit can have zero or more captured payments. pub captures: Vec, - /// An array of refunds for a purchase unit. A purchase unit can have zero or more refunds. + /// An array of refunds for a purchase unit. A purchase unit can have zero or more refunds. pub refunds: Vec, } @@ -436,24 +436,24 @@ pub struct PurchaseUnit { pub payee: Option, /// Any additional payment instructions for PayPal Commerce Platform customers. /// Enables features for the PayPal Commerce Platform, such as delayed disbursement and collection of a platform fee. - /// Applies during order creation for captured payments or during capture of authorized payments. + /// Applies during order creation for captured payments or during capture of authorized payments. #[serde(skip_serializing_if = "Option::is_none")] pub payment_instruction: Option, /// The purchase description. #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// The API caller-provided external ID. Used to reconcile client transactions with PayPal transactions. - /// Appears in transaction and settlement reports but is not visible to the payer. + /// Appears in transaction and settlement reports but is not visible to the payer. #[serde(skip_serializing_if = "Option::is_none")] pub custom_id: Option, /// The API caller-provided external invoice number for this order. - /// Appears in both the payer's transaction history and the emails that the payer receives. + /// Appears in both the payer's transaction history and the emails that the payer receives. #[serde(skip_serializing_if = "Option::is_none")] pub invoice_id: Option, /// The PayPal-generated ID for the purchase unit. /// This ID appears in both the payer's transaction history and the emails that the payer receives. - /// In addition, this ID is available in transaction and settlement reports that merchants and API callers can use to reconcile transactions. - /// This ID is only available when an order is saved by calling v2/checkout/orders/id/save. + /// In addition, this ID is available in transaction and settlement reports that merchants and API callers can use to reconcile transactions. + /// This ID is only available when an order is saved by calling v2/checkout/orders/id/save. #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, /// The soft descriptor is the dynamic text used to construct the statement descriptor that appears on a payer's card statement. @@ -461,10 +461,10 @@ pub struct PurchaseUnit { /// More info here: https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit_request #[serde(skip_serializing_if = "Option::is_none")] pub soft_descriptor: Option, - /// An array of items that the customer purchases from the merchant. + /// An array of items that the customer purchases from the merchant. #[serde(skip_serializing_if = "Option::is_none")] pub items: Option>, - /// The name and address of the person to whom to ship the items. + /// The name and address of the person to whom to ship the items. #[serde(skip_serializing_if = "Option::is_none")] pub shipping: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -480,17 +480,17 @@ impl PurchaseUnit { } } -/// The type of landing page to show on the PayPal site for customer checkout. -#[derive(Debug, Serialize, Deserialize)] +/// The type of landing page to show on the PayPal site for customer checkout. +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum LandingPage { /// When the customer clicks PayPal Checkout, the customer is redirected to a page to log in to PayPal and approve the payment. Login, - /// When the customer clicks PayPal Checkout, the customer is redirected to a page + /// When the customer clicks PayPal Checkout, the customer is redirected to a page /// to enter credit or debit card and other relevant billing information required to complete the purchase. Billing, - /// When the customer clicks PayPal Checkout, the customer is redirected to either a page to log in to PayPal and approve - /// the payment or to a page to enter credit or debit card and other relevant billing information required to complete the purchase, + /// When the customer clicks PayPal Checkout, the customer is redirected to either a page to log in to PayPal and approve + /// the payment or to a page to enter credit or debit card and other relevant billing information required to complete the purchase, /// depending on their previous interaction with PayPal. NoPreference, } @@ -502,7 +502,7 @@ impl Default for LandingPage { } /// The shipping preference -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ShippingPreference { /// Use the customer-provided shipping address on the PayPal site. @@ -519,7 +519,7 @@ impl Default for ShippingPreference { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum UserAction { /// After you redirect the customer to the PayPal payment page, a Continue button appears. Use this option when @@ -527,7 +527,7 @@ pub enum UserAction { /// to the merchant page without processing the payment. Continue, /// After you redirect the customer to the PayPal payment page, a Pay Now button appears. - /// Use this option when the final amount is known when the checkout is initiated and you want to + /// Use this option when the final amount is known when the checkout is initiated and you want to /// process the payment immediately when the customer clicks Pay Now. PayNow, } @@ -538,13 +538,13 @@ impl Default for UserAction { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum PayeePreferred { /// Accepts any type of payment from the customer. Unrestricted, /// Accepts only immediate payment from the customer. - /// For example, credit card, PayPal balance, or instant ACH. + /// For example, credit card, PayPal balance, or instant ACH. /// Ensures that at the time of capture, the payment does not have the `pending` status. ImmediatePaymentRequired, } @@ -565,12 +565,12 @@ pub struct PaymentMethod { #[derive(Debug, Default, Serialize, Deserialize)] pub struct ApplicationContext { - /// The label that overrides the business name in the PayPal account on the PayPal site. + /// The label that overrides the business name in the PayPal account on the PayPal site. #[serde(skip_serializing_if = "Option::is_none")] pub brand_name: Option, /// The BCP 47-formatted locale of pages that the PayPal payment experience shows. PayPal supports a five-character code. /// - /// For example, da-DK, he-IL, id-ID, ja-JP, no-NO, pt-BR, ru-RU, sv-SE, th-TH, zh-CN, zh-HK, or zh-TW. + /// For example, da-DK, he-IL, id-ID, ja-JP, no-NO, pt-BR, ru-RU, sv-SE, th-TH, zh-CN, zh-HK, or zh-TW. #[serde(skip_serializing_if = "Option::is_none")] pub locale: Option, /// The type of landing page to show on the PayPal site for customer checkout @@ -582,13 +582,13 @@ pub struct ApplicationContext { /// Configures a Continue or Pay Now checkout flow. #[serde(skip_serializing_if = "Option::is_none")] pub user_action: Option, - /// The customer and merchant payment preferences. + /// The customer and merchant payment preferences. #[serde(skip_serializing_if = "Option::is_none")] pub payment_method: Option, - /// The URL where the customer is redirected after the customer approves the payment. + /// The URL where the customer is redirected after the customer approves the payment. #[serde(skip_serializing_if = "Option::is_none")] pub return_url: Option, - /// The URL where the customer is redirected after the customer cancels the payment. + /// The URL where the customer is redirected after the customer cancels the payment. #[serde(skip_serializing_if = "Option::is_none")] pub cancel_url: Option, } @@ -597,13 +597,13 @@ pub struct ApplicationContext { pub struct OrderPayload { /// The intent to either capture payment immediately or authorize a payment for an order after order creation. pub intent: Intent, - /// The customer who approves and pays for the order. The customer is also known as the payer. + /// The customer who approves and pays for the order. The customer is also known as the payer. #[serde(skip_serializing_if = "Option::is_none")] pub payer: Option, /// An array of purchase units. Each purchase unit establishes a contract between a payer and the payee. - /// Each purchase unit represents either a full or partial order that the payer intends to purchase from the payee. + /// Each purchase unit represents either a full or partial order that the payer intends to purchase from the payee. pub purchase_units: Vec, - /// Customize the payer experience during the approval process for the payment with PayPal. + /// Customize the payer experience during the approval process for the payment with PayPal. #[serde(skip_serializing_if = "Option::is_none")] pub application_context: Option, } @@ -618,8 +618,8 @@ impl OrderPayload { } } -/// The card brand or network. -#[derive(Debug, Serialize, Deserialize)] +/// The card brand or network. +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CardBrand { /// Visa card. @@ -656,28 +656,28 @@ pub enum CardBrand { ChinaUnionPay, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CardType { Credit, Debit, Prepaid, - Unknown + Unknown, } #[derive(Debug, Serialize, Deserialize)] pub struct CardResponse { - /// The last digits of the payment card. + /// The last digits of the payment card. pub last_digits: String, /// The card brand or network. pub brand: CardBrand, - #[serde(rename = "type")] + #[serde(rename = "type")] pub card_type: CardType, } #[derive(Debug, Serialize, Deserialize)] pub struct WalletResponse { - pub apple_pay: CardResponse + pub apple_pay: CardResponse, } #[derive(Debug, Serialize, Deserialize)] @@ -686,7 +686,7 @@ pub struct PaymentSourceResponse { pub wallet: WalletResponse, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum OrderStatus { /// The order was created with the specified context. @@ -702,7 +702,7 @@ pub enum OrderStatus { Completed, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum LinkMethod { Get, @@ -712,17 +712,16 @@ pub enum LinkMethod { Head, Connect, Options, - Patch + Patch, } - #[derive(Debug, Default, Serialize, Deserialize)] pub struct LinkDescription { - /// The complete target URL. + /// The complete target URL. pub href: String, /// The link relation type, which serves as an ID for a link that unambiguously describes the semantics of the link. pub rel: String, - /// The HTTP method required to make the related call. + /// The HTTP method required to make the related call. #[serde(skip_serializing_if = "Option::is_none")] pub method: Option, } @@ -743,45 +742,189 @@ pub struct Order { /// The intent to either capture payment immediately or authorize a payment for an order after order creation. #[serde(skip_serializing_if = "Option::is_none")] pub intent: Option, - /// The customer who approves and pays for the order. The customer is also known as the payer. + /// The customer who approves and pays for the order. The customer is also known as the payer. #[serde(skip_serializing_if = "Option::is_none")] pub payer: Option, /// An array of purchase units. Each purchase unit establishes a contract between a customer and merchant. - /// Each purchase unit represents either a full or partial order that the customer intends to purchase from the merchant. + /// Each purchase unit represents either a full or partial order that the customer intends to purchase from the merchant. #[serde(skip_serializing_if = "Option::is_none")] pub purchase_units: Option>, /// The order status. pub status: OrderStatus, - /// An array of request-related HATEOAS links. To complete payer approval, use the approve link to redirect the payer. + /// An array of request-related HATEOAS links. To complete payer approval, use the approve link to redirect the payer. pub links: Vec, } impl super::Client { - /// Creates an order. Supports orders with only one purchase unit. - pub async fn create_order(&self, order: OrderPayload, header_params: crate::HeaderParams) -> Result> { - let builder = self.setup_headers(self.client.post(format!("{}/v2/checkout/orders", self.endpoint()).as_str()), header_params); + /// Creates an order. Supports orders with only one purchase unit. + pub async fn create_order( + &self, + order: OrderPayload, + header_params: crate::HeaderParams, + ) -> Result> { + let builder = { + self.setup_headers( + self.client + .post(format!("{}/v2/checkout/orders", self.endpoint()).as_str()), + header_params, + ) + }; let res = builder.json(&order).send().await?; if res.status().is_success() { - let order: Order = res.json::().await?; + let order = res.json::().await?; Ok(order) } else { - Err(Box::new(errors::Errors::ApiCallFailure)) + Err(Box::new(errors::Errors::ApiCallFailure(res.text().await?))) + } + } + + /// Used internally for order requests that have no body. + async fn build_endpoint_order( + &self, + order_id: S, + endpoint: A, + post: bool, + header_params: crate::HeaderParams, + ) -> Result> { + let format = format!("{}/v2/checkout/orders/{}/{}", self.endpoint(), order_id, endpoint); + + let builder = self.setup_headers( + match post { + true => self.client.post(format.as_str()), + false => self.client.get(format.as_str()), + }, + header_params, + ); + + let res = builder.send().await?; + + if res.status().is_success() { + let order = res.json::().await?; + Ok(order) + } else { + Err(Box::new(errors::Errors::ApiCallFailure(res.text().await?))) } } /// Updates an order with the CREATED or APPROVED status. - /// /// You cannot update an order with the COMPLETED status. /// - /// TODO: Use a enum for status. https://developer.paypal.com/docs/api/orders/v2/#orders_patch - pub async fn update_order>(&self, id: S, status: S) { - todo!() + /// Only replacing the existing purchase units and intent is supported right now. + /// + /// Note: You can only update the intent from Authorize to Capture + /// + /// More info on what you can change: https://developer.paypal.com/docs/api/orders/v2/#orders_patch + pub async fn update_order( + &self, + id: S, + intent: Option, + purchase_units: Option>, + ) -> Result<(), Box> { + let mut intent_json = String::new(); + let units_json = String::new(); + + if let Some(p_units) = purchase_units { + let mut units_json = String::new(); + + for (i, unit) in p_units.iter().enumerate() { + let unit_str = serde_json::to_string(&unit)?; + let mut unit_json = format!(r#" + {{ + "op": "replace", + "path": "/purchase_units/@reference_id='{reference_id}'", + "value": {unit} + }} + "#, + reference_id = unit.reference_id.clone().unwrap_or(String::from("default")), + unit = unit_str); + + if i < p_units.len() - 1 { + unit_json += ","; + } + + units_json += unit_json.as_str(); + } + } + + if let Some(x) = intent { + let intent_str = match x { + Intent::Authorize => String::from("AUTHORIZE"), + Intent::Capture => String::from("CAPTURE") + }; + + intent_json = format!( + r#" + {{ + "op": "replace", + "path": "/intent", + "value": "{intent}" + }} + "#, + intent = intent_str + ); + } + + let final_json = { + if intent_json != "" && units_json != "" { + format!("[{},{}]", intent_json, units_json) + } else { + format!("[{}{}]", intent_json, units_json) + } + }; + + let builder = { + self.setup_headers( + self.client + .patch(format!("{}/v2/checkout/orders/{}", self.endpoint(), id).as_str()), + crate::HeaderParams { + content_type: Some(String::from("application/json")), + ..Default::default() + }, + ) + }; + + let res = builder.body(final_json.clone()).send().await?; + + if res.status().is_success() { + Ok(()) + } else { + Err(Box::new(errors::Errors::ApiCallFailure(res.text().await?))) + } } - pub async fn get_order>(id: S) { - todo!() + /// Shows details for an order, by ID. + pub async fn show_order_details( + &self, + order_id: S, + ) -> Result> { + self.build_endpoint_order(order_id, "", false, crate::HeaderParams::default()) + .await + } + + /// Captures payment for an order. To successfully capture payment for an order, + /// the buyer must first approve the order or a valid payment_source must be provided in the request. + /// A buyer can approve the order upon being redirected to the rel:approve URL that was returned in the HATEOAS links in the create order response. + pub async fn capture_order( + &self, + order_id: S, + header_params: crate::HeaderParams, + ) -> Result> { + self.build_endpoint_order(order_id, "capture", true, header_params) + .await + } + + /// Authorizes payment for an order. To successfully authorize payment for an order, + /// the buyer must first approve the order or a valid payment_source must be provided in the request. + /// A buyer can approve the order upon being redirected to the rel:approve URL that was returned in the HATEOAS links in the create order response. + pub async fn authorize_order( + &self, + order_id: S, + header_params: crate::HeaderParams, + ) -> Result> { + self.build_endpoint_order(order_id, "authorize", true, header_params) + .await } } -// TODO: Finish order https://developer.paypal.com/docs/api/orders/v2/ +// TODO: Add strong typed support for order errors in body: https://developer.paypal.com/docs/api/orders/v2/#errors diff --git a/src/tests.rs b/src/tests.rs index cbf0ce4..0edbeb1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -14,16 +14,25 @@ async fn it_works() { false, "should not error" ); - println!("{:#?}", client); let order = orders::OrderPayload::new( - orders::Intent::Capture, + orders::Intent::Authorize, vec![orders::PurchaseUnit::new(orders::Amount::new( "EUR", "10.0", ))], ); - let order_created = client.create_order(order, HeaderParams::default()).await.unwrap(); + let ref_id = format!("TEST-{:?}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()); - println!("{:#?}", order_created); + let order_created = client.create_order(order, HeaderParams { + prefer: Some(Prefer::Representation), + request_id: Some(ref_id.clone()), + ..Default::default() + }).await.unwrap(); + + assert!(order_created.id != "", "order id is not empty"); + assert_eq!(order_created.status, orders::OrderStatus::Created, "order status is created"); + assert_eq!(order_created.links.len(), 4, "order links exist"); + + client.update_order(order_created.id, Some(orders::Intent::Capture), Some(order_created.purchase_units.expect("to exist"))).await.unwrap(); }