Files
saladeaula.digital/orders-events/app/iugu.py

260 lines
7.7 KiB
Python

"""
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]