yapay_sdk_rust/
lib.rs

1//! An open source, strongly-typed SDK for the Yapay API.
2//!
3//! It will try to hold your hand and reduce the possibility of errors, providing the correct API
4//! surface.
5//!
6//! ### Note
7//!
8//! The library is still under development and its public API is subject to change.
9//!
10//! # Installation
11//!
12//! Added the following into your Cargo.toml:
13//!
14//! ```toml
15//! yapay_sdk_rust = "0.1"
16//! ```
17//!
18//! # Usage
19//!
20//! The client is built using the
21//! [`YapaySDKBuilder::with_token`](crate::YapaySDKBuilder) `with_token`
22//! method.
23//!
24//! ```rust
25//! # fn main() {
26//! use yapay_sdk_rust::{YapaySDK, YapaySDKBuilder};
27//!
28//! let yapay_sdk: YapaySDK = YapaySDKBuilder::with_token(env!("YAPAY_ACCOUNT_TOKEN"));
29//!
30//! # }
31//! ```
32//!
33//! Once the token is inserted, you can call methods on [`crate::YapaySDK`]
34//!
35//!
36//!
37//! # Creating a Checkout link
38//!
39//! You can easily retrieve a checkout link with the method below.
40//!
41//! ```no_run
42//! use std::num::NonZeroU8;
43//!
44//! use uuid::Uuid;
45//! use yapay_sdk_rust::{
46//!     CheckoutPreferences, PaymentCreditCard, YapayEnv, YapayProduct, YapaySDKBuilder,
47//! };
48//!
49//! #[tokio::main]
50//! async fn async_main() {
51//!     // your token, can come from environment or else
52//!     let yapay_token = "YAPAY_ACCOUNT_TOKEN";
53//!     let yapay_sdk = YapaySDKBuilder::with_token(yapay_token);
54//!
55//!     let product = YapayProduct::new(
56//!         "note-100sk".to_string(),
57//!         "Notebook Cinza".to_string(),
58//!         NonZeroU8::new(1).unwrap(),
59//!         2453.50,
60//!     );
61//!
62//!     let order_number = Uuid::new_v4().to_string();
63//!     let checkout_preferences = CheckoutPreferences::new(order_number, vec![product])
64//!         .expect("Validation failed.")
65//!         .set_notification_url("https://your-notifications-url.com")
66//!         .expect("Notifications URL failed to validate.")
67//!         .set_available_payment_methods(&PaymentCreditCard::payment_methods_all());
68//!
69//!     let checkout_url = yapay_sdk
70//!         .create_checkout_page(YapayEnv::PRODUCTION, checkout_preferences)
71//!         .await
72//!         .expect("Something went wrong creating the checkout.");
73//! }
74//! ```
75//!
76//! # Other Examples
77//!
78//! Check out the `tests` folder inside our repository to check for more examples.
79//!
80//! # License
81//! Project is licensed under the permissive MIT license.
82
83#![allow(
84    clippy::missing_const_for_fn,
85    clippy::missing_errors_doc,
86    clippy::missing_panics_doc,
87    clippy::must_use_candidate,
88    clippy::non_ascii_literal,
89    clippy::redundant_closure,
90    clippy::use_self,
91    clippy::used_underscore_binding
92)]
93#![warn(missing_debug_implementations, missing_copy_implementations)]
94#![deny(
95    trivial_casts,
96    trivial_numeric_casts,
97    unused_import_braces,
98    unused_qualifications
99)]
100
101mod checkout;
102mod common_types;
103pub mod errors;
104mod helpers;
105mod simulation;
106mod transaction;
107mod webhooks;
108
109use std::marker::PhantomData;
110
111pub use checkout::CheckoutPreferences;
112use common_types::ResponseRoot;
113pub use common_types::{
114    AddressType, AsPaymentMethod, CustomerAddress, CustomerPhoneContact, PaymentCreditCard,
115    PaymentOtherMethods, PhoneContactType, YapayCardData, YapayCustomer, YapayProduct,
116    YapayTransaction, YapayTransactionStatus,
117};
118use futures::TryFutureExt;
119use reqwest::header::{CONTENT_TYPE, LOCATION};
120use reqwest::redirect::Policy;
121use reqwest::{Client, Method};
122use serde::de::DeserializeOwned;
123use serde::Serialize;
124use validator::Validate;
125pub use webhooks::YapayWebhook;
126
127use crate::errors::{ApiError, InvalidError, SDKError};
128use crate::simulation::{PaymentTaxResponse, SimulatePayload, SimulationResponseWrapper};
129use crate::transaction::creditcard::TransactionResponse;
130use crate::transaction::{PaymentRequestRoot, TransactionResponseWrapper};
131
132const API_PROD_BASE: &str = "https://api.intermediador.yapay.com.br/api";
133const API_TEST_BASE: &str = "https://api.intermediador.sandbox.yapay.com.br/api";
134
135const CHECKOUT_PROD_BASE: &str = "https://tc.intermediador.yapay.com.br/payment/transaction";
136const CHECKOUT_TEST_BASE: &str =
137    "https://tc-intermediador-sandbox.yapay.com.br/payment/transaction";
138
139pub trait CanValidate: Serialize + Validate {}
140
141#[derive(Debug, Copy, Clone, Eq, PartialEq)]
142pub enum YapayEnv {
143    PRODUCTION,
144    SANDBOX,
145}
146
147impl YapayEnv {
148    pub const fn checkout_link(self) -> &'static str {
149        match self {
150            Self::PRODUCTION => CHECKOUT_PROD_BASE,
151            Self::SANDBOX => CHECKOUT_TEST_BASE,
152        }
153    }
154
155    pub const fn api_link(self) -> &'static str {
156        match self {
157            Self::PRODUCTION => API_PROD_BASE,
158            Self::SANDBOX => API_TEST_BASE,
159        }
160    }
161}
162
163///
164#[derive(Copy, Clone, Debug)]
165pub struct YapaySDKBuilder {}
166
167impl YapaySDKBuilder {
168    /// Creates an [`YapaySDK`] ready to request the API.
169    pub fn with_token<T>(account_token: &T) -> YapaySDK
170    where
171        T: ToString,
172    {
173        let http_client = Client::builder()
174            .cookie_store(true)
175            .redirect(Policy::none())
176            .build()
177            .expect("Failed to create client.");
178
179        YapaySDK {
180            http_client,
181            account_token: account_token.to_string(),
182        }
183    }
184}
185
186#[derive(Debug)]
187pub struct YapaySDK {
188    pub(crate) http_client: Client,
189    pub(crate) account_token: String,
190}
191
192#[derive(Debug)]
193pub struct SDKJsonRequest<'a, RP> {
194    http_client: &'a Client,
195    method: Method,
196    endpoint: &'a str,
197    payload: String,
198    response_type: PhantomData<RP>,
199}
200
201impl<'a, RP> SDKJsonRequest<'a, RP> {
202    #[must_use]
203    pub fn from_sdk(sdk: &'a YapaySDK, method: Method, endpoint: &'a str, payload: String) -> Self {
204        Self {
205            http_client: &sdk.http_client,
206            method,
207            endpoint,
208            response_type: Default::default(),
209            payload,
210        }
211    }
212}
213
214impl<'a, RP> SDKJsonRequest<'a, RP> {
215    /// Injects bearer token, and return response
216    pub async fn execute(self, yapay_env: YapayEnv) -> Result<RP, SDKError>
217    where
218        RP: DeserializeOwned + Send,
219    {
220        let api_endpoint = format!("{}{}", yapay_env.api_link(), self.endpoint);
221        tracing::trace!("api endpoint: {:?}", api_endpoint);
222
223        let request = self
224            .http_client
225            .request(self.method, api_endpoint)
226            .body(self.payload)
227            .header(CONTENT_TYPE, "application/json")
228            .build()
229            .unwrap();
230        tracing::trace!("request = {:#?}", request);
231
232        let response = self
233            .http_client
234            .execute(request)
235            .and_then(reqwest::Response::text)
236            .await?;
237        tracing::trace!("response = {}", response);
238
239        // matches errors due to wrong payloads etc
240        let error_jd = serde_json::from_str::<ApiError>(&*response);
241        if let Ok(err) = error_jd {
242            tracing::error!("err = {:#?}", err);
243            return Err(SDKError::PayloadError(err));
244        }
245
246        let jd = &mut serde_json::Deserializer::from_str(&*response);
247        let res: Result<RP, _> = serde_path_to_error::deserialize(jd);
248
249        match res {
250            Ok(deserialized_resp) => Ok(deserialized_resp),
251            Err(err) => {
252                tracing::error!("{:?}", err.path());
253                tracing::error!("Error = {:#?}", err);
254                Err(SDKError::GenericError)
255            }
256        }
257    }
258}
259
260pub type CardTransactionResponse = ResponseRoot<TransactionResponseWrapper<TransactionResponse>>;
261pub type SimulationResponse = ResponseRoot<SimulationResponseWrapper<PaymentTaxResponse>>;
262
263impl YapaySDK {
264    pub async fn create_checkout_page(
265        &self,
266        yapay_env: YapayEnv,
267        checkout_preferences: CheckoutPreferences,
268    ) -> Result<String, SDKError> {
269        let querystring = checkout_preferences.to_form(&*self.account_token);
270        let request = self
271            .http_client
272            .request(Method::POST, yapay_env.checkout_link())
273            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
274            .body(querystring)
275            .build()
276            .unwrap();
277
278        let response = self.http_client.execute(request).await.unwrap();
279
280        response
281            .headers()
282            .get(LOCATION)
283            .and_then(|hdr| hdr.to_str().ok())
284            .map(ToString::to_string)
285            .ok_or(SDKError::GenericError)
286    }
287
288    /// Returns an error if it fails to validate any of its arguments.
289    pub fn create_credit_card_payment(
290        &self,
291        customer: YapayCustomer,
292        transaction: YapayTransaction,
293        products: Vec<YapayProduct>,
294        cc_payment_data: YapayCardData,
295    ) -> Result<SDKJsonRequest<CardTransactionResponse>, SDKError> {
296        let request_payload = PaymentRequestRoot::new(
297            self.account_token.clone(),
298            customer,
299            products,
300            transaction,
301            cc_payment_data,
302        );
303
304        if let Err(errs) = request_payload.validate() {
305            return Err(InvalidError::ValidatorLibError(errs).into());
306        }
307
308        let payload = serde_json::to_string(&request_payload).expect("Safe to unwrap.");
309
310        Ok(SDKJsonRequest::from_sdk(
311            self,
312            Method::POST,
313            "/v3/transactions/payment",
314            payload,
315        ))
316    }
317
318    #[must_use]
319    pub fn simulate_payment(&self, total_amount: f64) -> SDKJsonRequest<SimulationResponse> {
320        let request_payload = SimulatePayload::new(self.account_token.clone(), total_amount);
321        let payload = serde_json::to_string(&request_payload).unwrap();
322
323        SDKJsonRequest::from_sdk(
324            self,
325            Method::POST,
326            "/v1/transactions/simulate_splitting",
327            payload,
328        )
329    }
330}