""" Notes: ----- - `%d` Day of the month as a zero-padded decimal number. Ex: 01, 02, …, 31 - `%m` Month as a zero-padded decimal number. Ex: 01, 02, …, 12 - `%Y` Year with century as a decimal number. Ex: 0001, 0002, …, 2013, 2014 - https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes Documentation: -------------- - https://dev.iugu.com/reference/criar-fatura - https://dev.iugu.com/reference/criar-token - https://dev.iugu.com/reference/cobranca-direta - https://support.iugu.com/hc/pt-br/articles/212456346-Usar-cart%C3%B5es-de-teste-em-modo-de-teste """ from dataclasses import dataclass from datetime import datetime from decimal import Decimal from enum import Enum from urllib.parse import ParseResult, urlparse import requests from aws_lambda_powertools import Logger from layercake.extra_types import CreditCard from pydantic import BaseModel, ConfigDict, HttpUrl logger = Logger(__name__) class Status(str, Enum): PAID = 'PAID' DECLINED = 'DECLINED' class PaymentMethod(str, Enum): PIX = 'PIX' BANK_SLIP = 'BANK_SLIP' CREDIT_CARD = 'CREDIT_CARD' @dataclass class Address(BaseModel): postcode: str neighborhood: str city: str state: str address1: str address2: str | None = None class Item(BaseModel): id: str name: str quantity: int = 1 unit_price: Decimal class Order(BaseModel): model_config = ConfigDict(use_enum_values=True) id: str email: str name: str due_date: datetime address: Address items: tuple[Item, ...] payment_method: PaymentMethod cpf: str | None = None cnpj: str | None = None @dataclass class Invoice: ... @dataclass class BankSlip(BaseModel): digitable_line: str bank_slip_url: HttpUrl @dataclass class Credentials: account_id: str api_token: str test_mode: bool = True @dataclass class Token(str): id: str @dataclass class Transaction: status: Status response: dict class Iugu: base_url: ParseResult = urlparse('https://api.iugu.com') def __init__(self, credentials: Credentials) -> None: self.credentials = credentials def url(self, **kwargs) -> str: return self.base_url._replace( query=f'api_token={self.credentials.api_token}', **kwargs ).geturl() def create_invoice( self, order: Order, postback_url: ParseResult | str, ) -> Invoice: """ O que é uma fatura? ------------------- A fatura é uma forma de cobrança da iugu que possibilitar efetuar cobranças com os métodos cartão de crédito, boleto e PIX, fora isso nela é possível definir como será realizado o split de pagamentos. Por que usar a chamada de fatura? --------------------------------- No response dessa chamada é retornado uma url na propriedade secure_url, onde o cliente final pode acessar e efetuar o pagamento em um checkout da IUGU. """ url = self.url(path='/v1/invoices') if isinstance(postback_url, str): postback_url = urlparse(postback_url) items = [ { 'description': item.name, 'price_cents': int(item.unit_price * 100), 'quantity': item.quantity, } for item in order.items ] payload = { 'order_id': order.id, 'external_reference': order.id, 'due_date': order.due_date.strftime('%Y-%m-%d'), 'items': items, 'email': order.email, 'payable_with': order.payment_method.lower(), 'notification_url': postback_url.geturl(), 'payer': { 'name': order.name, 'email': order.email, 'cpf_cnpj': order.cnpj if order.cnpj else order.cpf, 'address': { 'zip_code': order.address.postcode, 'street': order.address.address1, 'number': '', 'district': order.address.neighborhood, 'city': order.address.city, 'state': order.address.state, 'complement': order.address.address2, 'country=': 'Brasil', }, }, } try: r = requests.post(url, json=payload, timeout=15) r.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: return r.json() # pix = ( # Pix(**response['pix']) # if order.payment_method == PaymentMethod.PIX # else None # ) # bank_slip = ( # BankSlip(**response['bank_slip']) # if order.payment_method == PaymentMethod.BANK_SLIP # else None # ) # return Invoice( # id=response['secure_id'], # pdf=bank_slip.bank_slip_url # if bank_slip # else '%s.pdf' % response['secure_url'], # pix=pix, # ) def payment_token(self, credit_card: CreditCard) -> Token: url = self.url(path='/v1/payment_token') try: r = requests.post( url, json={ 'test': self.credentials.test_mode, 'account_id': self.credentials.account_id, 'method': 'credit_card', 'data': { 'number': credit_card.number, 'verification_value': credit_card.cvv, 'first_name': credit_card.first_name, 'last_name': credit_card.last_name, 'month': credit_card.exp_month, 'year': credit_card.exp_year, }, }, timeout=15, ) r.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: return Token(r.json()['id']) def charge( self, invoice_id: str, token: Token, installments: int = 1, ) -> Transaction: """ O que é Cobrança Direta ----------------------- Requisição utilizada para realizar uma cobrança simples de forma pontual utilizando os métodos de pagamento cartão de crédito e boleto. Por que usar uma cobrança direta? --------------------------------- Com ela é possível realizar uma cobrança já inserindo os dados do cliente e o token gerado para o cartão de crédito (iugu js). Se o seu intuito é gerar apenas um boleto, essa chamada também retorna o PDF do boleto e o link de pagamento para efetuar o pagamento. """ url = self.url(path='/v1/charge') payload = { 'invoice_id': format_id(invoice_id), 'token': token, 'months': installments, } try: response = requests.post(url, json=payload, timeout=15) response.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: success = response.json()['success'] return Transaction( status=Status.PAID if success else Status.DECLINED, response=response.json(), ) def get_invoice(self, invoice_id: str) -> dict: url = self.url(path=f'/v1/invoices/{format_id(invoice_id)}') try: r = requests.get(url, timeout=15) r.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: return r.json() def format_id(invoice_id: str) -> str: return invoice_id.upper().replace('-', '')[:-4]