From 42e5396ad0e0b17e791e0747b325fc36386fc15b Mon Sep 17 00:00:00 2001 From: Edgar Date: Tue, 9 Jun 2020 18:23:46 +0200 Subject: [PATCH] create_order --- Cargo.toml | 5 +- README.md | 1 + src/errors.rs | 20 +-- src/lib.rs | 79 ++++++--- src/orders.rs | 455 ++++++++++++++++++++++++++++++++++++++++++++++---- src/tests.rs | 26 ++- 6 files changed, 514 insertions(+), 72 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4e5f6a7..dda66e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "paypal-rs" -version = "0.0.3" +version = "0.0.4" authors = ["Edgar "] description = "A library that wraps the paypal api asynchronously." repository = "https://github.com/edg-l/paypal-rs/" @@ -20,4 +20,5 @@ jsonwebtoken = "7" base64 = "0.12" log = "0.4" dotenv = "0.15.0" -chrono = { version = "0.4", features = ["serde"] } \ No newline at end of file +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1.0" \ No newline at end of file diff --git a/README.md b/README.md index 026eb0c..b58a401 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # paypal-rs ![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. diff --git a/src/errors.rs b/src/errors.rs index 9bd659e..7582f0b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,13 +1,9 @@ -use std::fmt; -use std::error::Error; +use thiserror::Error; -#[derive(Debug, Clone)] -pub struct GetAccessTokenError; - -impl fmt::Display for GetAccessTokenError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "error getting access token") - } -} - -impl Error for GetAccessTokenError {} \ No newline at end of file +#[derive(Debug, Error)] +pub enum Errors { + #[error("failed to get access token")] + GetAccessTokenFailure, + #[error("failure when calling the paypal api")] + ApiCallFailure, +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index e660637..2636d2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,32 +14,46 @@ use reqwest::header; pub const LIVE_ENDPOINT: &str = "https://api.paypal.com"; pub const SANDBOX_ENDPOINT: &str = "https://api.sandbox.paypal.com"; -/// Represents the access token returned by the oauth2 authentication. +/// Represents the access token returned by the OAuth2 authentication. /// /// https://developer.paypal.com/docs/api/get-an-access-token-postman/ #[derive(Debug, Deserialize)] pub struct AccessToken { + /// The OAuth2 scopes. pub scope: String, + /// The access token. pub access_token: String, + /// The token type. pub token_type: String, + /// The app id. pub app_id: String, + /// Seconds until it expires. pub expires_in: u64, + /// The nonce. pub nonce: String, } +/// Stores OAuth2 information. #[derive(Debug)] pub struct Auth { + /// Your client id. pub client_id: String, + /// The secret. pub secret: String, + /// The access token returned by oauth2 authentication. pub access_token: Option, + /// Used to check when the token expires. pub expires: Option<(Instant, Duration)>, } /// Represents a client used to interact with the paypal api. #[derive(Debug)] pub struct Client { + /// Internal http client pub client: reqwest::Client, + /// Whether you are or not in a sandbox enviroment. pub sandbox: bool, + /// Api Auth information pub auth: Auth, } @@ -51,26 +65,60 @@ pub struct Client { /// ``` /// let query = Query { count: Some(40), ..Default::default() }; /// ``` -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize)] pub struct Query { + /// The number of items to list in the response. + #[serde(skip_serializing_if = "Option::is_none")] pub count: Option, + /// The end date and time for the range to show in the response. + #[serde(skip_serializing_if = "Option::is_none")] pub end_time: Option>, + /// The page number indicating which set of items will be returned in the response. + /// So, the combination of page=1 and page_size=20 returns the first 20 items. + /// The combination of page=2 and page_size=20 returns items 21 through 40. + #[serde(skip_serializing_if = "Option::is_none")] pub page: Option, + /// The number of items to return in the response. + #[serde(skip_serializing_if = "Option::is_none")] pub page_size: Option, + /// Indicates whether to show the total count in the response. + #[serde(skip_serializing_if = "Option::is_none")] pub total_count_required: Option, + /// Sorts the payments in the response by a specified value, such as the create time or update time. + #[serde(skip_serializing_if = "Option::is_none")] pub sort_by: Option, + /// Sorts the items in the response in ascending or descending order. + #[serde(skip_serializing_if = "Option::is_none")] pub sort_order: Option, + /// The ID of the starting resource in the response. + /// 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. + /// 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, + /// The start date and time for the range to show in the response. + #[serde(skip_serializing_if = "Option::is_none")] pub start_time: Option>, + // TODO: Use https://github.com/samscott89/serde_qs } #[derive(Debug)] 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. Minimal, + /// The server returns a complete resource representation, including the current state of the resource. Representation, } +impl Default for Prefer { + fn default() -> Self { + Prefer::Minimal + } +} + /// Represents the optional header values used on paypal requests. /// /// https://developer.paypal.com/docs/api/reference/api-requests/#paypal-auth-assertion @@ -96,21 +144,15 @@ impl Client { /// Example: /// /// ``` - /// #[tokio::main] - /// async fn main() { - /// dotenv().ok(); - /// let clientid = env::var("PAYPAL_CLIENTID").unwrap(); - /// let secret = env::var("PAYPAL_SECRET").unwrap(); + /// let clientid = env::var("PAYPAL_CLIENTID").unwrap(); + /// let secret = env::var("PAYPAL_SECRET").unwrap(); /// - /// let mut client = Client::new( - /// clientid.as_str(), - /// secret.as_str(), - /// true, - /// ); - /// - /// client.get_access_token().await.unwrap(); - /// println!("{:#?}", client); - /// } + /// let mut client = Client::new( + /// clientid.as_str(), + /// secret.as_str(), + /// true, + /// ); + /// client.get_access_token().await.unwrap(); /// ``` pub fn new>(client_id: S, secret: S, sandbox: bool) -> Client { Client { @@ -199,11 +241,8 @@ impl Client { let token = res.json::().await?; self.auth.expires = Some((Instant::now(), Duration::new(token.expires_in, 0))); self.auth.access_token = Some(token); - println!("{:#?}", self.auth); } else { - println!("status = {:#?}", res.status()); - println!("res = {:#?}", res); - return Err(Box::new(errors::GetAccessTokenError)); + return Err(Box::new(errors::Errors::GetAccessTokenFailure)); } Ok(()) diff --git a/src/orders.rs b/src/orders.rs index a3c9252..ada32fd 100644 --- a/src/orders.rs +++ b/src/orders.rs @@ -1,5 +1,9 @@ +use serde::{Serialize, Deserialize}; +use crate::errors; + /// The intent to either capture payment immediately or authorize a payment for an order after order creation. -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum Intent { /// The merchant intends to capture payment immediately after the customer makes a payment. Capture, @@ -20,7 +24,7 @@ impl Default for Intent { /// Represents a payer name. /// /// https://developer.paypal.com/docs/api/orders/v2/#definition-payer.name -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct PayerName { /// When the party is a person, the party's given, or first, name. pub given_name: String, @@ -32,7 +36,8 @@ pub struct PayerName { /// The phone type. /// /// https://developer.paypal.com/docs/api/orders/v2/#definition-phone_with_type -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum PhoneType { Fax, Home, @@ -41,7 +46,7 @@ pub enum PhoneType { Pager, } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] 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. @@ -51,13 +56,15 @@ pub struct PhoneNumber { /// The phone number of the customer. Available only when you enable the /// Contact Telephone Number option in the Profile & Settings for the merchant's PayPal account. -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct Phone { + #[serde(skip_serializing_if = "Option::is_none")] pub phone_type: Option, pub phone_number: PhoneNumber, } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[allow(non_camel_case_types)] pub enum TaxIdType { /// The individual tax ID type. @@ -66,7 +73,7 @@ pub enum TaxIdType { BR_CNPJ, } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct TaxInfo { /// The customer's tax ID. Supported for the PayPal payment method only. /// Typically, the tax ID is 11 characters long for individuals and 14 characters long for businesses. @@ -75,19 +82,24 @@ pub struct TaxInfo { pub tax_id_type: TaxIdType, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct Address { /// The first line of the address. For example, number or street. For example, 173 Drury Lane. /// Required for data entry and compliance and risk checks. Must contain the full address. + #[serde(skip_serializing_if = "Option::is_none")] pub address_line_1: Option, /// The second line of the address. For example, suite or apartment number. + #[serde(skip_serializing_if = "Option::is_none")] pub address_line_2: Option, /// A city, town, or village. Smaller than admin_area_level_1. + #[serde(skip_serializing_if = "Option::is_none")] pub admin_area_2: Option, /// The highest level sub-division in a country, which is usually a province, state, or ISO-3166-2 subdivision. /// Format for postal delivery. For example, CA and not California. + #[serde(skip_serializing_if = "Option::is_none")] pub admin_area_1: Option, /// The postal code, which is the zip code or equivalent. Typically required for countries with a postal code or an equivalent. + #[serde(skip_serializing_if = "Option::is_none")] pub postal_code: Option, /// The two-character [ISO 3166-1](https://developer.paypal.com/docs/api/reference/country-codes/) code that identifies the country or region. pub country_code: String, @@ -96,26 +108,33 @@ pub struct Address { /// The customer who approves and pays for the order. The customer is also known as the payer. /// /// https://developer.paypal.com/docs/api/orders/v2/#definition-payer -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct Payer { /// The name of the payer. + #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, /// The email address of the payer. + #[serde(skip_serializing_if = "Option::is_none")] pub email_address: Option, /// The PayPal-assigned ID for the payer. + #[serde(skip_serializing_if = "Option::is_none")] pub payer_id: Option, /// The phone number of the customer. Available only when you enable the Contact /// Telephone Number option in the Profile & Settings for the merchant's PayPal account. + #[serde(skip_serializing_if = "Option::is_none")] pub phone: Option, /// The birth date of the payer in YYYY-MM-DD format. + #[serde(skip_serializing_if = "Option::is_none")] pub birth_date: Option, /// The tax information of the payer. Required only for Brazilian payer's. + #[serde(skip_serializing_if = "Option::is_none")] pub tax_info: Option, /// The address of the payer. + #[serde(skip_serializing_if = "Option::is_none")] pub address: Option
, } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] 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, @@ -128,26 +147,33 @@ pub struct Money { } /// Breakdown provides details such as total item amount, total tax amount, shipping, handling, insurance, and discounts, if any. -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct Breakdown { /// The subtotal for all items. Required if the request includes purchase_units[].items[].unit_amount. /// Must equal the sum of (items[].unit_amount * items[].quantity) for all items. + #[serde(skip_serializing_if = "Option::is_none")] pub item_total: Option, /// The shipping fee for all items within a given purchase_unit. + #[serde(skip_serializing_if = "Option::is_none")] pub shipping: Option, /// The handling fee for all items within a given purchase_unit. + #[serde(skip_serializing_if = "Option::is_none")] pub handling: Option, /// The total tax for all items. Required if the request includes purchase_units.items.tax. Must equal the sum of (items[].tax * items[].quantity) for all items. + #[serde(skip_serializing_if = "Option::is_none")] pub tax_total: Option, /// The insurance fee for all items within a given purchase_unit. + #[serde(skip_serializing_if = "Option::is_none")] pub insurance: Option, /// The shipping discount for all items within a given purchase_unit. + #[serde(skip_serializing_if = "Option::is_none")] pub shipping_discount: Option, /// The discount for all items within a given purchase_unit. + #[serde(skip_serializing_if = "Option::is_none")] pub discount: Option, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct Amount { /// 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, @@ -158,24 +184,39 @@ pub struct Amount { /// For the required number of decimal places for a currency code, see [Currency Codes](https://developer.paypal.com/docs/api/reference/currency-codes/). pub value: String, /// The breakdown of the amount. + #[serde(skip_serializing_if = "Option::is_none")] pub breakdown: Option, } -#[derive(Debug, Default)] +impl Amount { + /// Creates a new amount with the required values. + pub fn new>(currency_code: S, value: S) -> Self { + Amount { + currency_code: currency_code.into(), + value: value.into(), + breakdown: None, + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] pub struct Payee { /// The email address of merchant. + #[serde(skip_serializing_if = "Option::is_none")] pub email_address: Option, /// The encrypted PayPal account ID of the merchant. + #[serde(skip_serializing_if = "Option::is_none")] pub merchant_id: Option, } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct PlatformFee { pub amount: Money, + #[serde(skip_serializing_if = "Option::is_none")] pub payee: Option, } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub enum DisbursementMode { /// The funds are released to the merchant immediately. Instant, @@ -191,15 +232,18 @@ impl Default for DisbursementMode { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct PaymentInstruction { /// 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 } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] +#[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 @@ -215,13 +259,15 @@ impl Default for ItemCategoryType { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct ShippingDetail { + #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub address: Option
, } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct Item { /// The item name or title. pub name: String, @@ -229,21 +275,153 @@ pub struct Item { /// 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. + #[serde(skip_serializing_if = "Option::is_none")] pub tax: Option, /// The item quantity. Must be a whole number. pub quantity: String, /// The detailed item description. + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// The stock keeping unit (SKU) for the item. + #[serde(skip_serializing_if = "Option::is_none")] pub sku: Option, /// The item category type + #[serde(skip_serializing_if = "Option::is_none")] pub category: Option, } -#[derive(Debug, Default)] -pub struct PurchaseUnitRequest { +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AuthorizationStatus { + /// The authorized payment is created. No captured payments have been made for this authorized payment. + Created, + /// The authorized payment has one or more captures against it. The sum of these captured payments is greater than the amount of the original authorized payment. + Captured, + /// PayPal cannot authorize funds for this authorized payment. + Denied, + /// The authorized payment has expired. + Expired, + /// A captured payment was made for the authorized payment for an amount that is less than the amount of the original authorized payment. + PartiallyExpired, + /// The payment which was authorized for an amount that is less than the originally requested amount. + PartiallyCaptured, + /// The authorized payment was voided. No more captured payments can be made against this authorized payment. + Voided, + /// The created authorization is in pending state. For more information, see status.details. + Pending, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AuthorizationStatusDetails { + /// Authorization is pending manual review. + PendingReview, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuthorizationWithData { + /// The status for the authorized payment. + status: AuthorizationStatus, + /// The details of the authorized order pending status. + status_details: AuthorizationStatusDetails, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CaptureStatus { + /// The funds for this captured payment were credited to the payee's PayPal account. + Completed, + /// The funds could not be captured. + Declined, + /// An amount less than this captured payment's amount was partially refunded to the payer. + PartiallyRefunded, + /// The funds for this captured payment was not yet credited to the payee's PayPal account. For more information, see status.details. + Pending, + /// An amount greater than or equal to this captured payment's amount was refunded to the payer. + Refunded, +} + +#[derive(Debug, Serialize, Deserialize)] +#[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 issuer of the financial instrument used to pay for this captured payment. + Chargeback, + /// The payer paid by an eCheck that has not yet cleared. + Echeck, + /// Visit your online account. In your **Account Overview**, accept and deny this payment. + InternationalWithdrawal, + /// No additional specific reason can be provided. For more information about this captured payment, visit your account online or contact PayPal. + Other, + /// The captured payment is pending manual review. + 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 + /// payment is different from the primary holding currency of the payee. + ReceivingPreferenceMandatesManualAction, + /// The captured funds were refunded. + Refunded, + /// The payer must send the funds for this captured payment. This code generally appears for manual EFTs. + TransactionApprovedAwaitingFunding, + /// The payee does not have a PayPal account. + Unilateral, + /// The payee's PayPal account is not verified. + VerificationRequired, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Capture { + /// The status of the captured payment. + status: CaptureStatus, + /// The details of the captured payment status. + status_details: CaptureStatusDetails, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RefundStatus { + /// The refund was cancelled. + Cancelled, + /// The refund is pending. For more information, see status_details.reason. + Pending, + /// The funds for this transaction were debited to the customer's account. + Completed, +} + +/// The reason why the refund has the PENDING or FAILED status. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RefundStatusDetails { + /// The customer's account is funded through an eCheck, which has not yet cleared. + Echeck, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Refund { + /// The status of the refund. + status: RefundStatus, + /// The details of the refund status. + 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. + authorizations: Vec, + /// An array of captured payments for a purchase unit. A purchase unit can have zero or more captured payments. + captures: Vec, + /// An array of refunds for a purchase unit. A purchase unit can have zero or more refunds. + refuns: Vec, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct PurchaseUnit { /// The API caller-provided external ID for the purchase unit. Required for multiple purchase units when you must update the order through PATCH. /// If you omit this value and the order contains only one purchase unit, PayPal sets this value to default. + #[serde(skip_serializing_if = "Option::is_none")] pub reference_id: Option, /// The total order amount with an optional breakdown that provides details, such as the total item amount, /// total tax amount, shipping, handling, insurance, and discounts, if any. @@ -254,31 +432,57 @@ pub struct PurchaseUnitRequest { /// see the PayPal REST APIs [Currency Codes](https://developer.paypal.com/docs/integration/direct/rest/currency-codes/). pub amount: Amount, /// The merchant who receives payment for this transaction. + #[serde(skip_serializing_if = "Option::is_none")] 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. - pub payment_instruction: Option, + #[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. + #[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. + #[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. + #[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. /// /// 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. + #[serde(skip_serializing_if = "Option::is_none")] pub items: Option>, /// 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")] + pub payments: Option, +} + +impl PurchaseUnit { + pub fn new(amount: Amount) -> Self { + Self { + amount, + ..Default::default() + } + } } /// The type of landing page to show on the PayPal site for customer checkout. -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] +#[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, @@ -298,7 +502,8 @@ impl Default for LandingPage { } /// The shipping preference -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ShippingPreference { /// Use the customer-provided shipping address on the PayPal site. GetFromFile, @@ -314,7 +519,8 @@ impl Default for ShippingPreference { } } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] +#[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 /// the final amount is not known when the checkout flow is initiated and you want to redirect the customer @@ -332,7 +538,8 @@ impl Default for UserAction { } } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum PayeePreferred { /// Accepts any type of payment from the customer. Unrestricted, @@ -348,45 +555,233 @@ impl Default for PayeePreferred { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct PaymentMethod { + #[serde(skip_serializing_if = "Option::is_none")] pub payer_selected: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub payer_prefered: Option, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct ApplicationContext { /// 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. + #[serde(skip_serializing_if = "Option::is_none")] pub locale: Option, /// The type of landing page to show on the PayPal site for customer checkout + #[serde(skip_serializing_if = "Option::is_none")] pub landing_page: Option, /// The shipping preference + #[serde(skip_serializing_if = "Option::is_none")] pub shipping_preference: Option, /// 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. + #[serde(skip_serializing_if = "Option::is_none")] pub payment_method: Option, /// 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. + #[serde(skip_serializing_if = "Option::is_none")] pub cancel_url: Option, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] 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. + #[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. - pub purchase_units: Vec, + pub purchase_units: Vec, /// Customize the payer experience during the approval process for the payment with PayPal. + #[serde(skip_serializing_if = "Option::is_none")] pub application_context: Option, } +impl OrderPayload { + pub fn new>>(intent: Intent, purchase_units: S) -> Self { + Self { + intent, + purchase_units: purchase_units.into(), + ..Default::default() + } + } +} + +/// The card brand or network. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CardBrand { + /// Visa card. + Visa, + /// Mastecard card. + Mastercard, + /// Discover card. + Discover, + /// American Express card. + Amex, + /// Solo debit card. + Solo, + /// Japan Credit Bureau card. + JCB, + /// Military Star card. + Star, + /// Delta Airlines card. + Delta, + /// Switch credit card. + Switch, + /// Maestro credit card. + Maestro, + /// Carte Bancaire (CB) credit card. + CbNationale, + /// Configoga credit card. + Configoga, + /// Confidis credit card. + Confidis, + /// Visa Electron credit card. + Electron, + /// Cetelem credit card. + Cetelem, + /// China union pay credit card. + ChinaUnionPay, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CardType { + Credit, + Debit, + Prepaid, + Unknown +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CardResponse { + /// The last digits of the payment card. + pub last_digits: String, + /// The card brand or network. + pub brand: CardBrand, + #[serde(rename = "type")] + pub card_type: CardType, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WalletResponse { + pub apple_pay: CardResponse +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaymentSourceResponse { + pub card: CardResponse, + pub wallet: WalletResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OrderStatus { + /// The order was created with the specified context. + Created, + /// The order was saved and persisted. The order status continues to be in progress until a capture + /// is made with final_capture = true for all purchase units within the order. + Saved, + /// The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on. + Approved, + /// All purchase units in the order are voided. + Voided, + /// The payment was authorized or the authorized payment was captured for the order. + Completed, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum LinkMethod { + Get, + Post, + Put, + Delete, + Head, + Connect, + Options, + Patch +} + + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct LinkDescription { + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub method: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Order { + /// The date and time when the transaction occurred. + #[serde(skip_serializing_if = "Option::is_none")] + pub create_time: Option>, + /// The date and time when the transaction was last updated. + #[serde(skip_serializing_if = "Option::is_none")] + pub update_time: Option>, + /// The ID of the order. + pub id: String, + /// The payment source used to fund the payment. + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_source: Option, + /// 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. + #[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. + #[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. + 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); + let res = builder.json(&order).send().await?; + + if res.status().is_success() { + let order: Order = res.json::().await?; + Ok(order) + } else { + Err(Box::new(errors::Errors::ApiCallFailure)) + } + } + + /// 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!() + } + + pub async fn get_order>(id: S) { + todo!() + } +} + // TODO: Finish order https://developer.paypal.com/docs/api/orders/v2/ diff --git a/src/tests.rs b/src/tests.rs index f21b75e..cbf0ce4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,19 +1,29 @@ use crate::*; -use dotenv::dotenv; use std::env; #[tokio::test] async fn it_works() { - dotenv().ok(); + dotenv::dotenv().ok(); let clientid = env::var("PAYPAL_CLIENTID").unwrap(); let secret = env::var("PAYPAL_SECRET").unwrap(); - let mut client = Client::new( - clientid.as_str(), - secret.as_str(), - true, + let mut client = Client::new(clientid.as_str(), secret.as_str(), true); + + assert_eq!( + client.get_access_token().await.is_err(), + false, + "should not error" + ); + println!("{:#?}", client); + + let order = orders::OrderPayload::new( + orders::Intent::Capture, + vec![orders::PurchaseUnit::new(orders::Amount::new( + "EUR", "10.0", + ))], ); - assert_eq!(client.get_access_token().await.is_err(), false, "should not error"); - println!("{:#?}", client); + let order_created = client.create_order(order, HeaderParams::default()).await.unwrap(); + + println!("{:#?}", order_created); }