""" 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 """ import os 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 HTTP_CONNECT_TIMEOUT = int(os.environ.get('HTTP_CONNECT_TIMEOUT', 1)) HTTP_READ_TIMEOUT = int(os.environ.get('HTTP_READ_TIMEOUT', 3)) 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 discount: Decimal = Decimal('0') cpf: str | None = None cnpj: str | None = None @dataclass class Credentials: account_id: str api_token: str test_mode: bool = True 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, ) -> dict: """ 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. - https://dev.iugu.com/reference/criar-fatura """ 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(), 'discount_cents': int(order.discount * -100), '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=(HTTP_CONNECT_TIMEOUT, HTTP_READ_TIMEOUT) ) r.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: return r.json() def payment_token(self, credit_card: CreditCard) -> dict: """When creating a invoice, it can't make the charge immediately the invoice. It's necessary make have a token (payment token) to charge. Payment token doesn't depends an invoice, just a credit card to charge later. - https://dev.iugu.com/reference/criar-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=(HTTP_CONNECT_TIMEOUT, HTTP_READ_TIMEOUT), ) r.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: return r.json() def charge( self, invoice_id: str, token: str, installments: int = 1, ) -> dict: """ 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. - https://dev.iugu.com/reference/cobranca-direta """ url = self.url(path='/v1/charge') payload = { 'invoice_id': format_id(invoice_id), 'token': token, 'months': installments, } try: r = requests.post( url, json=payload, timeout=(HTTP_CONNECT_TIMEOUT, HTTP_READ_TIMEOUT) ) r.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: return r.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=(HTTP_CONNECT_TIMEOUT, HTTP_READ_TIMEOUT)) 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]