Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 49 additions & 24 deletions barte/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@
import requests
from dacite import from_dict
from .models import (
Charge, CardToken, Refund, InstallmentOptions,
PixCharge, PixQRCode, DACITE_CONFIG, Config,
InstallmentSimulation, Buyer, BuyerList
Charge,
CardToken,
Refund,
InstallmentOptions,
PixCharge,
PixQRCode,
DACITE_CONFIG,
Config,
InstallmentSimulation,
Buyer,
BuyerList,
)


class BarteClient:
VALID_ENVIRONMENTS = ["production", "sandbox"]
_instance = None
Expand All @@ -23,20 +32,25 @@ def __init__(self, api_key: str, environment: str = "production"):
ValueError: If the environment is not "production" or "sandbox"
"""
if environment not in self.VALID_ENVIRONMENTS:
raise ValueError(f"Invalid environment. Must be one of: {', '.join(self.VALID_ENVIRONMENTS)}")
raise ValueError(
f"Invalid environment. Must be one of: {', '.join(self.VALID_ENVIRONMENTS)}"
)

self.api_key = api_key
self.base_url = "https://api.barte.com" if environment == "production" else "https://sandbox-api.barte.com"
self.headers = {
"X-Token-Api": api_key,
"Content-Type": "application/json"
}
self.base_url = (
"https://api.barte.com"
if environment == "production"
else "https://sandbox-api.barte.com"
)
self.headers = {"X-Token-Api": api_key, "Content-Type": "application/json"}
BarteClient._instance = self

@classmethod
def get_instance(cls) -> "BarteClient":
if cls._instance is None:
raise RuntimeError("BarteClient not initialized. Call BarteClient(api_key) first.")
raise RuntimeError(
"BarteClient not initialized. Call BarteClient(api_key) first."
)
return cls._instance

def create_charge(self, data: Dict[str, Any]) -> Charge:
Expand All @@ -58,7 +72,10 @@ def list_charges(self, params: Optional[Dict[str, Any]] = None) -> List[Charge]:
endpoint = f"{self.base_url}/v1/charges"
response = requests.get(endpoint, headers=self.headers, params=params)
response.raise_for_status()
return [from_dict(data_class=Charge, data=item, config=DACITE_CONFIG) for item in response.json()["data"]]
return [
from_dict(data_class=Charge, data=item, config=DACITE_CONFIG)
for item in response.json()["data"]
]

def cancel_charge(self, charge_id: str) -> Charge:
"""Cancel a specific charge"""
Expand All @@ -77,14 +94,18 @@ def get_buyer(self, filters: Dict[str, any]) -> BuyerList:
endpoint = f"{self.base_url}/v2/buyers"
response = requests.get(endpoint, params=filters, headers=self.headers)
response.raise_for_status()
return from_dict(data_class=BuyerList, data=response.json(), config=DACITE_CONFIG)
return from_dict(
data_class=BuyerList, data=response.json(), config=DACITE_CONFIG
)

def create_card_token(self, card_data: Dict[str, Any]) -> CardToken:
"""Create a token for a credit card"""
endpoint = f"{self.base_url}/v1/tokens"
endpoint = f"{self.base_url}/v2/cards"
response = requests.post(endpoint, headers=self.headers, json=card_data)
response.raise_for_status()
return from_dict(data_class=CardToken, data=response.json(), config=DACITE_CONFIG)
return from_dict(
data_class=CardToken, data=response.json(), config=DACITE_CONFIG
)

def charge_with_card_token(self, token_id: str, data: Dict[str, Any]) -> Charge:
"""Create a charge using an existing card token"""
Expand All @@ -93,7 +114,7 @@ def charge_with_card_token(self, token_id: str, data: Dict[str, Any]) -> Charge:
transaction_data = {
**data,
"payment_method": "credit_card",
"card_token": token_id
"card_token": token_id,
}

response = requests.post(endpoint, headers=self.headers, json=transaction_data)
Expand All @@ -104,14 +125,13 @@ def create_pix_charge(self, data: Dict[str, Any]) -> PixCharge:
"""Create a PIX charge"""
endpoint = f"{self.base_url}/v1/charges"

pix_data = {
**data,
"payment_method": "pix"
}
pix_data = {**data, "payment_method": "pix"}

response = requests.post(endpoint, headers=self.headers, json=pix_data)
response.raise_for_status()
return from_dict(data_class=PixCharge, data=response.json(), config=DACITE_CONFIG)
return from_dict(
data_class=PixCharge, data=response.json(), config=DACITE_CONFIG
)

def get_pix_qrcode(self, charge_id: str) -> PixQRCode:
"""Get PIX QR Code data for a charge"""
Expand All @@ -129,17 +149,22 @@ def simulate_installments(self, amount: int, brand: str) -> InstallmentOptions:
return from_dict(
data_class=InstallmentOptions,
data=response.json(),
config=Config(cast=[List[InstallmentSimulation]])
config=Config(cast=[List[InstallmentSimulation]]),
)

def get_charge_refunds(self, charge_id: str) -> List[Refund]:
"""Get all refunds for a charge"""
endpoint = f"{self.base_url}/v1/charges/{charge_id}/refunds"
response = requests.get(endpoint, headers=self.headers)
response.raise_for_status()
return [from_dict(data_class=Refund, data=item, config=DACITE_CONFIG) for item in response.json()["data"]]

def refund_charge(self, charge_id: str, data: Optional[Dict[str, Any]] = None) -> Refund:
return [
from_dict(data_class=Refund, data=item, config=DACITE_CONFIG)
for item in response.json()["data"]
]

def refund_charge(
self, charge_id: str, data: Optional[Dict[str, Any]] = None
) -> Refund:
"""Refund a charge"""
endpoint = f"{self.base_url}/v1/charges/{charge_id}/refund"
response = requests.post(endpoint, headers=self.headers, json=data or {})
Expand Down
19 changes: 12 additions & 7 deletions barte/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ class Customer:

@dataclass
class CardToken:
id: str
type: str
created_at: datetime
last_digits: str
holder_name: str
expiration_month: int
expiration_year: int
uuid: str
status: str
createdAt: datetime
brand: str
cardHolderName: str
cvvChecked: bool
fingerprint: str
first6digits: str
last4digits: str
buyerId: str
expirationMonth: str
expirationYear: str
cardId: str


@dataclass
Expand Down
33 changes: 19 additions & 14 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,25 @@ def test_create_pix_charge(self, mock_post, barte_client, mock_charge_response):
def test_create_card_token(self, mock_post, barte_client):
"""Test creating a card token"""
mock_response = {
"id": "tok_123456",
"type": "card",
"created_at": "2024-03-20T10:00:00Z",
"last_digits": "1111",
"holder_name": "John Doe",
"expiration_month": 12,
"expiration_year": 2025,
"brand": "visa",
"uuid": "790e8637-c16b-4ed5-a9bf-faec76dbc5aa",
"status": "ACTIVE",
"createdAt": "2025-02-07",
"brand": "mastercard",
"cardHolderName": "John Doe",
"cvvChecked": True,
"fingerprint": "MLvWOfRXBcGIvK9cWSj9vLy0yhmBMzbxldLSJHYvEEw=",
"first6digits": "538363",
"last4digits": "0891",
"buyerId": "5929a30b-e68f-4c81-9481-d25adbabafeb",
"expirationMonth": "12",
"expirationYear": "2025",
"cardId": "9dc2ffe0-d588-44b7-b74d-d5ad88a31143",
}
mock_post.return_value.json.return_value = mock_response
mock_post.return_value.raise_for_status = Mock()

card_data = {
"number": "4111111111111111",
"number": "5383630891",
"holder_name": "John Doe",
"expiration_month": 12,
"expiration_year": 2025,
Expand All @@ -138,13 +143,13 @@ def test_create_card_token(self, mock_post, barte_client):
token = barte_client.create_card_token(card_data)

assert isinstance(token, CardToken)
assert token.id == "tok_123456"
assert token.last_digits == "1111"
assert token.holder_name == "John Doe"
assert isinstance(token.created_at, datetime)
assert token.uuid == "790e8637-c16b-4ed5-a9bf-faec76dbc5aa"
assert token.last4digits == "0891"
assert token.cardHolderName == "John Doe"
assert isinstance(token.createdAt, datetime)

mock_post.assert_called_once_with(
f"{barte_client.base_url}/v1/tokens",
f"{barte_client.base_url}/v2/cards",
headers=barte_client.headers,
json=card_data,
)
Expand Down