yapay_sdk_rust/
common_types.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::fmt::Display;
4use std::num::NonZeroU8;
5
6use lazy_static::lazy_static;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use strum::{EnumIter, IntoEnumIterator};
10use time::macros::format_description;
11use time::{Date, OffsetDateTime};
12use validator::Validate;
13
14use crate::errors::InvalidError;
15use crate::helpers::format_available_payment_method;
16use crate::{CanValidate, SDKError};
17
18lazy_static! {
19    static ref REGEX_BIRTH_DATE: Regex = Regex::new(r"\d{2}/\d{2}/\d{4}$").unwrap();
20    pub static ref CLEAN_WEBHOOK_REGEX: Regex =
21        Regex::new(r"\&transaction\[products\]\[\].*?\&").unwrap();
22}
23
24/// Enum containing the current transactions status.
25///
26/// Generally you will match this when receiving webhooks from Yapay.
27#[derive(Copy, Clone, Deserialize, Serialize, PartialEq, Debug, strum::Display)]
28pub enum YapayTransactionStatus {
29    #[serde(rename = "4")]
30    AguardandoPagamento,
31    #[serde(rename = "6")]
32    Aprovada,
33    #[serde(rename = "7")]
34    Cancelada,
35    #[serde(rename = "89")]
36    Reprovada,
37
38    /// The code 24 is for both Chargeback and Contestacao.
39    /// Come on Yapay!
40    #[serde(rename = "24")]
41    Contestacao,
42    #[serde(rename = "89")]
43    Monitoring,
44    // #[serde(rename = "W")]
45    // Chargeback,
46}
47
48#[derive(Validate, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct YapayCustomer {
50    #[validate]
51    pub contacts: Vec<CustomerPhoneContact>,
52    #[validate]
53    pub addresses: Vec<CustomerAddress>,
54    #[validate(length(min = 1))]
55    pub name: String,
56    /// Format of DD/MM/YYYY
57    #[validate(regex = "REGEX_BIRTH_DATE")]
58    pub birth_date: String,
59    /// Only numbers.
60    #[validate(length(equal = 11), custom = "crate::helpers::validate_cpf")]
61    pub cpf: String,
62    pub cnpj: Option<String>,
63    #[validate(email)]
64    pub email: String,
65}
66
67impl YapayCustomer {
68    pub fn new(
69        name: String,
70        cpf: String,
71        email: String,
72        birth_date: String,
73        phones: Vec<CustomerPhoneContact>,
74        address: Vec<CustomerAddress>,
75    ) -> Result<Self, InvalidError> {
76        let customer = Self {
77            contacts: phones,
78            addresses: address,
79            name,
80            birth_date,
81            cpf,
82            cnpj: None,
83            email,
84        };
85
86        match customer.validate() {
87            Ok(_) => Ok(customer),
88            Err(e) => Err(InvalidError::ValidatorLibError(e)),
89        }
90    }
91}
92
93#[derive(Debug, Clone, PartialEq, Deserialize)]
94pub struct CustomerResponse {
95    pub name: String,
96    pub company_name: String,
97    pub trade_name: String,
98    pub cnpj: String,
99}
100
101#[derive(Validate, Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct CustomerPhoneContact {
103    pub type_contact: PhoneContactType,
104    #[validate(length(min = 8, max = 15))]
105    pub number_contact: String,
106}
107
108#[derive(Validate, Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct CustomerAddress {
110    pub type_address: AddressType,
111    /// CEP somente números.
112    #[validate(length(max = 8))]
113    pub postal_code: String,
114    pub street: String,
115    pub number: String,
116    pub completion: String,
117    pub neighborhood: String,
118    pub city: String,
119    /// Somente a sigla do estado. Exemplo: SP
120    #[validate(length(equal = 2))]
121    pub state: String,
122}
123
124#[derive(Validate, Debug, Clone, PartialEq, Serialize, Deserialize)]
125pub struct YapayProduct {
126    pub code: String,
127    #[validate(length(max = 50))]
128    pub sku_code: String,
129    pub description: String,
130    pub quantity: String,
131    pub price_unit: String,
132    pub extra: Option<String>,
133}
134
135impl YapayProduct {
136    #[must_use]
137    pub fn new(
138        sku_or_code: String,
139        description: String,
140        quantity: NonZeroU8,
141        price_unit: f64,
142    ) -> Self {
143        Self {
144            code: sku_or_code.clone(),
145            sku_code: sku_or_code,
146            description,
147            quantity: quantity.get().to_string(),
148            price_unit: price_unit.to_string(),
149            extra: None,
150        }
151    }
152}
153
154#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct TransactionResponseCommon {}
156
157/// A payment transaction used on requests.
158///
159/// Use the available builder methods:
160///
161/// [`YapayTransaction::online_goods`] if there are no shipping address;
162///
163/// [`YapayTransaction::physical_goods`] if there IS a shipping address;
164#[derive(Validate, Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct YapayTransaction {
166    pub available_payment_methods: String,
167    /// When [`Option::none`], this param will be the same as the [`transaction_id`] on
168    /// `TransactionResponse`. You must ensure that his field is not repeated ever.
169    #[validate(length(max = 20))]
170    pub order_number: Option<String>,
171    pub customer_ip: String,
172    pub shipping_type: Option<String>,
173    pub shipping_price: Option<String>,
174    pub price_discount: String,
175    /// URL in your server to receive IPN (Instant Payment Notification).
176    pub url_notification: String,
177    pub free: String,
178}
179
180impl YapayTransaction {
181    /// An online transaction does not include a shipping address.
182    ///
183    /// `notification_url` should be an URL in your server to receive IPN (Instant Payment
184    /// Notification).
185    pub fn online_goods(
186        order_number: String,
187        customer_ip: String,
188        available_payment_methods: Option<String>,
189        notification_url: Option<&str>,
190    ) -> Result<Self, SDKError> {
191        let methods = available_payment_methods
192            .unwrap_or_else(|| "2,3,4,5,6,7,14,15,16,18,19,21,22,23".to_string());
193
194        let transaction = Self {
195            available_payment_methods: methods,
196            order_number: Some(order_number),
197            customer_ip,
198            shipping_type: None,
199            shipping_price: None,
200            price_discount: "".to_string(),
201            url_notification: notification_url.unwrap_or("").to_string(),
202            free: "".to_string(),
203        };
204
205        if let Err(err) = transaction.validate() {
206            return Err(InvalidError::ValidatorLibError(err).into());
207        }
208        Ok(transaction)
209    }
210
211    /// A physical product include a shipping address.
212    pub fn physical_goods() {}
213}
214
215#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
216pub struct TransactionTrace {
217    pub estimated_date: String,
218}
219
220/// Represents a card that was previously used to create a payment, and it was saved.
221#[derive(Validate, Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub struct YaypaySavedCardData {
223    /// Parte do sistema anti-fraude. Obrigatório nos cartões.
224    ///
225    /// Veja mais sobre em:
226    /// [Yapay Fingerprint](https://intermediador.dev.yapay.com.br/#/transacao-fingerprint)
227    pub finger_print: String,
228
229    /// The card token UUID that was return after a payment request.
230    ///
231    /// Example: a66cf237-3541-45d1-ab9c-a6b6e3f795f5
232    pub card_token: String,
233
234    #[validate(length(max = 4))]
235    pub card_cvv: String,
236    #[validate(length(min = 1, max = 2))]
237    pub split: String,
238}
239
240#[derive(Validate, Debug, Clone, PartialEq, Serialize, Deserialize)]
241#[validate(schema(function = "validate_card_exp"))]
242pub struct YapayCardData {
243    /// Parte do sistema anti-fraude. Obrigatório nos cartões.
244    ///
245    /// Veja mais sobre em:
246    /// [Yapay Fingerprint](https://intermediador.dev.yapay.com.br/#/transacao-fingerprint)
247    pub finger_print: String,
248    pub payment_method_id: PaymentCreditCard,
249    pub card_name: String,
250    pub card_number: String,
251
252    /// Month in format of MM.
253    #[validate(length(equal = 2))]
254    pub card_expdate_month: String,
255
256    /// Year in format of YYYY.
257    #[validate(length(equal = 4))]
258    pub card_expdate_year: String,
259
260    #[validate(length(max = 4))]
261    pub card_cvv: String,
262    #[validate(length(min = 1, max = 2))]
263    pub split: String,
264}
265
266impl YapayCardData {
267    pub fn new(
268        cc: PaymentCreditCard,
269        cc_owner_name: String,
270        cc_number: String,
271        cc_exp_mm: String,
272        cc_exp_yyyy: String,
273        cc_cvv: String,
274        installments: i8,
275    ) -> Result<Self, SDKError> {
276        let payment = Self {
277            finger_print: "".to_string(),
278            payment_method_id: cc,
279            card_name: cc_owner_name,
280            card_number: cc_number,
281            card_expdate_month: cc_exp_mm,
282            card_expdate_year: cc_exp_yyyy,
283            card_cvv: cc_cvv,
284            split: installments.to_string(),
285        };
286
287        if let Err(err) = payment.validate() {
288            Err(InvalidError::ValidatorLibError(err).into())
289        } else {
290            Ok(payment)
291        }
292    }
293}
294
295impl CanValidate for YapayCardData {}
296
297pub fn validate_card_exp(card_data: &YapayCardData) -> Result<(), validator::ValidationError> {
298    let now = OffsetDateTime::now_utc().date();
299    let res = validate_card_expiration(
300        now,
301        &*card_data.card_expdate_month,
302        &*card_data.card_expdate_year,
303    );
304
305    match res {
306        Ok(_) => Ok(()),
307        Err(err) => Err(validator::ValidationError {
308            code: Cow::from("exp_month and exp_year"),
309            message: Some(Cow::from(err.to_string())),
310            params: HashMap::default(),
311        }),
312    }
313}
314
315/// Month: 2 char
316/// Year: 4 char
317pub fn validate_card_expiration(
318    time_cmp: Date,
319    exp_month: &str,
320    exp_year: &str,
321) -> Result<(), SDKError> {
322    let card_expiration = Date::parse(
323        &*format!("01-{}-{}", exp_month, exp_year),
324        format_description!("[day]-[month]-[year]"),
325    )
326    .unwrap();
327
328    if card_expiration >= time_cmp {
329        Ok(())
330    } else {
331        Err(InvalidError::CreditCardExpired.into())
332    }
333}
334
335#[derive(Copy, Clone, Deserialize, Serialize, PartialEq, Debug)]
336pub enum AddressType {
337    #[serde(rename = "B")]
338    Cobranca,
339    #[serde(rename = "D")]
340    Entrega,
341}
342
343/// Tabela de Contact
344#[derive(Copy, Clone, Deserialize, Serialize, PartialEq, Debug, strum::Display)]
345pub enum PhoneContactType {
346    #[serde(rename = "H")]
347    Residencial,
348    #[serde(rename = "M")]
349    Celular,
350    #[serde(rename = "W")]
351    Comercial,
352}
353
354/// Tabela de Contact
355#[derive(Copy, Clone, Deserialize, Serialize, PartialEq, Debug)]
356pub enum PaymentType {
357    Card(PaymentCreditCard),
358    BankTransfer(PaymentOtherMethods),
359}
360
361#[derive(strum::Display, EnumIter, Copy, Clone, Deserialize, Serialize, PartialEq, Debug)]
362pub enum PaymentOtherMethods {
363    #[serde(rename = "6")]
364    #[strum(serialize = "6")]
365    Boleto,
366    #[serde(rename = "27")]
367    #[strum(serialize = "27")]
368    PIX,
369    /// Transferência online Itaú Shopline
370    #[serde(rename = "7")]
371    #[strum(serialize = "7")]
372    BankTransferItauShopline,
373    /// Transferência online Banco do Brasil
374    #[serde(rename = "23")]
375    #[strum(serialize = "23")]
376    BankTransferBB,
377}
378
379#[derive(strum::Display, EnumIter, Copy, Clone, Deserialize, Serialize, PartialEq, Debug)]
380pub enum PaymentCreditCard {
381    #[serde(rename = "3")]
382    #[strum(serialize = "3")]
383    Visa,
384    #[serde(rename = "4")]
385    #[strum(serialize = "4")]
386    MasterCard,
387    #[serde(rename = "5")]
388    #[strum(serialize = "5")]
389    Amex,
390    #[serde(rename = "16")]
391    #[strum(serialize = "16")]
392    Elo,
393    #[serde(rename = "20")]
394    #[strum(serialize = "20")]
395    HiperCard,
396    #[serde(rename = "25")]
397    #[strum(serialize = "25")]
398    HiperItau,
399}
400
401/// Methods that takes this trait, you should pass either `OtherMethods` or `CreditCard`.
402pub trait AsPaymentMethod: Display + IntoEnumIterator {
403    fn payment_methods_all() -> String {
404        format_available_payment_method(&<Self as IntoEnumIterator>::iter().collect::<Vec<_>>())
405    }
406}
407impl AsPaymentMethod for PaymentOtherMethods {}
408impl AsPaymentMethod for PaymentCreditCard {}
409
410#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
411pub struct Card {
412    pub first_six_digits: String,
413    pub last_four_digits: String,
414    pub expiration_month: i64,
415    pub expiration_year: i64,
416
417    pub card_number_length: i64,
418    pub security_code_length: i64,
419
420    #[serde(with = "time::serde::rfc3339")]
421    pub date_created: OffsetDateTime,
422    #[serde(with = "time::serde::rfc3339")]
423    pub date_last_updated: OffsetDateTime,
424    #[serde(with = "time::serde::rfc3339")]
425    pub date_due: OffsetDateTime,
426}
427
428#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
429pub struct ResponseRoot<T> {
430    pub message_response: ResponseMessage,
431    pub data_response: T,
432}
433
434#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
435pub struct ResponseMessage {
436    pub message: String,
437}
438
439#[cfg(test)]
440mod tests {
441    use time::macros::format_description;
442    use time::Date;
443
444    use crate::common_types::{
445        validate_card_expiration, AsPaymentMethod, PaymentCreditCard, PaymentOtherMethods,
446    };
447    use crate::helpers::format_available_payment_method;
448
449    #[test]
450    fn cc_valid_date() {
451        let fmt = format_description!("[year]/[month padding:zero]/[day]");
452        let datetime = Date::parse("2022/05/01", &fmt).unwrap();
453
454        let res = validate_card_expiration(datetime, "05", "2022");
455        assert!(res.is_ok());
456    }
457
458    #[test]
459    fn cc_invalid_date() {
460        let fmt = format_description!("[year]/[month padding:zero]/[day]");
461        let datetime = Date::parse("2022/05/01", &fmt).unwrap();
462
463        let res = validate_card_expiration(datetime, "06", "2021");
464        assert!(res.is_err());
465    }
466
467    #[test]
468    fn t_cc_methods() {
469        let res = PaymentCreditCard::payment_methods_all();
470        assert_eq!(res, "3,4,5,16,20,25".to_string());
471    }
472
473    #[test]
474    fn t_specific_methods() {
475        let res = format_available_payment_method(&[
476            PaymentOtherMethods::Boleto,
477            PaymentOtherMethods::BankTransferBB,
478        ]);
479        assert_eq!(res, "6,23".to_string());
480    }
481}