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

248 lines
7.3 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
"""
from dataclasses import dataclass
from enum import Enum
from urllib.parse import ParseResult, urlparse
import requests
from aws_lambda_powertools import Logger
from pydantic import BaseModel, HttpUrl
# from data_classes.invoice import Invoice, Pix
# from data_classes.order import CreditCard, Order, PaymentMethod, Status
logger = Logger(__name__)
class Status(Enum):
PAID = 'PAID'
DECLINED = 'DECLINED'
class PaymentMethod(Enum):
PIX = 'PIX'
BANK_SLIP = 'BANK_SLIP'
CREDIT_CARD = 'CREDIT_CARD'
class Order: ...
class CreditCard: ...
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'), # type: ignore
'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.street,
'number': order.address.street_number,
'district': order.address.neighborhood,
'city': order.address.city,
'state': order.address.state,
'complement': order.address.complement,
'country=': 'Brasil',
},
},
}
try:
response = requests.post(url, json=payload, timeout=15)
response.raise_for_status()
except requests.HTTPError as err:
logger.exception(err)
raise
else:
response = response.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')
payload = {
'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.strftime('%m'),
'year': credit_card.exp.strftime('%Y'),
},
}
try:
response = requests.post(url, json=payload, timeout=15)
response.raise_for_status()
except requests.HTTPError as err:
logger.exception(err)
raise
else:
return Token(response.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:
response = requests.get(url, timeout=15)
response.raise_for_status()
except requests.HTTPError as err:
logger.exception(err)
raise
else:
return response.json()
def format_id(invoice_id: str) -> str:
return invoice_id.upper().replace('-', '')[:-4]