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
8 changes: 7 additions & 1 deletion fintoc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@
from fintoc.managers.v2 import CustomersManager, EntitiesManager
from fintoc.managers.v2 import InvoicesManager as InvoicesManagerV2
from fintoc.managers.v2 import PaymentIntentsManager as PaymentIntentsManagerV2
from fintoc.managers.v2 import PaymentMethodsManager, ProductsManager, SimulateManager
from fintoc.managers.v2 import (
PaymentMethodsManager,
ProductsManager,
RecipientsManager,
SimulateManager,
)
from fintoc.managers.v2 import SubscriptionsManager as SubscriptionsManagerV2
from fintoc.managers.v2 import TransferBatchesManager, TransfersManager
from fintoc.version import __version__
Expand Down Expand Up @@ -85,6 +90,7 @@ def __init__(self, client):
)
self.customers = CustomersManager("/v2/customers", client)
self.entities = EntitiesManager("/v2/entities", client)
self.recipients = RecipientsManager("/v2/recipients", client)
self.simulate = SimulateManager("/v2/simulate", client)
self.checkout_sessions = CheckoutSessionsManagerV2(
"/v2/checkout_sessions", client
Expand Down
1 change: 1 addition & 0 deletions fintoc/managers/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .payment_intents_manager import PaymentIntentsManager
from .payment_methods_manager import PaymentMethodsManager
from .products_manager import ProductsManager
from .recipients_manager import RecipientsManager
from .simulate_manager import SimulateManager
from .subscription_items_manager import SubscriptionItemsManager
from .subscriptions_manager import SubscriptionsManager
Expand Down
10 changes: 10 additions & 0 deletions fintoc/managers/v2/recipients_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Module to hold the recipients manager."""

from fintoc.mixins import ManagerMixin


class RecipientsManager(ManagerMixin):
"""Represents a recipients manager."""

resource = "recipient"
methods = ["list", "get", "create", "update", "delete"]
1 change: 1 addition & 0 deletions fintoc/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .v2.onboarding_legal_representative import OnboardingLegalRepresentative
from .v2.onboarding_shareholder import OnboardingShareholder
from .v2.payment_method import PaymentMethod
from .v2.recipient import Recipient
from .v2.subscription import Subscription as SubscriptionV2
from .v2.subscription_item import SubscriptionItem
from .v2.transfer import Transfer
Expand Down
1 change: 1 addition & 0 deletions fintoc/resources/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .onboarding_shareholder import OnboardingShareholder
from .payment_method import PaymentMethod
from .product import Product
from .recipient import Recipient
from .subscription import Subscription
from .subscription_item import SubscriptionItem
from .transfer import Transfer
Expand Down
7 changes: 7 additions & 0 deletions fintoc/resources/v2/recipient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Module to hold the Recipient resource."""

from fintoc.mixins import ResourceMixin


class Recipient(ResourceMixin):
"""Represents a Fintoc Recipient."""
18 changes: 18 additions & 0 deletions tests/fixtures/recipient_cl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"id": "rcp_2hQ9vBmKpR7xLtZ3sWn1fJ4dGcA",
"account_number": "000000001",
"account_type": "checking_account",
"alias": "Test recipient",
"created_at": "2026-03-01T12:00:00Z",
"email": "recipient@example.com",
"holder_id": "111111111",
"holder_name": "Test Customer 1",
"holder_type": "person",
"updated_at": "2026-03-01T12:00:00Z",
"object": "recipient",
"institution": {
"id": "cl_banco_santander",
"name": "Banco Santander",
"country": "cl"
}
}
18 changes: 18 additions & 0 deletions tests/fixtures/recipient_mx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"id": "rcp_2hQ9vBmKpR7xLtZ3sWn1fJ4dGcA",
"account_number": "000000000000000000",
"account_type": "clabe",
"alias": "Test recipient",
"created_at": "2026-03-01T12:00:00Z",
"email": "recipient@example.com",
"holder_id": "XAXX010101000",
"holder_name": "Test Customer 1",
"holder_type": "person",
"updated_at": "2026-03-01T12:00:00Z",
"object": "recipient",
"institution": {
"id": "40002",
"name": "BANAMEX",
"country": "mx"
}
}
132 changes: 132 additions & 0 deletions tests/test_recipients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Recipients requests through the public v2 client."""

import json
from pathlib import Path

import httpx
import pytest

from fintoc.client import Client
from fintoc.core import Fintoc
from fintoc.resources.v2 import Recipient


@pytest.fixture(params=["mx", "cl"])
def payload(request):
path = Path(__file__).parent / "fixtures" / f"recipient_{request.param}.json"
return json.loads(path.read_text())


@pytest.fixture
def api(monkeypatch, payload):
requests = []

def respond(request):
requests.append(request)
if request.method == "DELETE":
return httpx.Response(204)
if request.method == "GET" and request.url.path == "/v2/recipients":
headers = {}
if "starting_after" not in request.url.params:
headers["link"] = (
"<https://api.fintoc.com/v2/recipients?starting_after="
+ payload["id"]
+ '>; rel="next"'
)
return httpx.Response(200, json=[payload], headers=headers)
updates = json.loads(request.content) if request.method == "PATCH" else {}
return httpx.Response(200, json={**payload, **updates})

with httpx.Client(transport=httpx.MockTransport(respond)) as transport:
monkeypatch.setattr(Client, "_client", transport)
yield Fintoc("sk_test_recipients").v2.recipients, requests


def test_create(api, payload):
recipients, requests = api
params = {
key: payload[key]
for key in ("account_number", "holder_name", "holder_type", "holder_id")
}
params["entity_id"] = "ent_test"
if payload["institution"]["country"] == "cl":
params.update(
institution_id=payload["institution"]["id"],
account_type=payload["account_type"],
)
recipient = recipients.create(idempotency_key="recipient-create", **params)

assert isinstance(recipient, Recipient)
assert recipient.serialize() == payload
assert requests[0].method == "POST"
assert requests[0].url.path == "/v2/recipients"
assert requests[0].headers["Idempotency-Key"] == "recipient-create"
assert json.loads(requests[0].content) == params


def test_get(api, payload):
recipients, requests = api
recipient = recipients.get(payload["id"])

assert isinstance(recipient, Recipient)
assert recipient.serialize() == payload
assert requests[0].url.path == f'/v2/recipients/{payload["id"]}'


@pytest.mark.parametrize("lazy", [True, False])
def test_list_filters_and_pagination(api, payload, lazy):
recipients, requests = api
filters = {
"entity_id": "ent_test",
"holder_type": "person",
"institution_id": payload["institution"]["id"],
"holder_id": payload["holder_id"],
"account_number": payload["account_number"],
"ending_before": "rcp_previous",
"limit": 1,
}
result = list(recipients.list(lazy=lazy, **filters))

assert len(result) == 2
assert all(isinstance(recipient, Recipient) for recipient in result)
assert dict(requests[0].url.params) == {k: str(v) for k, v in filters.items()}
assert requests[1].url.params["starting_after"] == payload["id"]


@pytest.mark.parametrize(
"updates",
[
{"alias": None, "email": None},
{"alias": "", "email": ""},
{"holder_name": "New name"},
],
)
@pytest.mark.parametrize("on_resource", [False, True])
def test_update(api, payload, updates, on_resource):
recipients, requests = api
if on_resource:
recipient = recipients.get(payload["id"])
result = recipient.update(**updates)
assert result is recipient
else:
result = recipients.update(payload["id"], **updates)

assert isinstance(result, Recipient)
assert result.serialize() == {**payload, **updates}
assert requests[-1].method == "PATCH"
assert requests[-1].url.path == f'/v2/recipients/{payload["id"]}'
assert json.loads(requests[-1].content) == updates


@pytest.mark.parametrize("on_resource", [False, True])
def test_delete_empty_response(api, payload, on_resource):
recipients, requests = api
result = (
recipients.get(payload["id"]).delete()
if on_resource
else recipients.delete(payload["id"])
)

assert result == payload["id"]
assert requests[-1].method == "DELETE"
assert requests[-1].url.path == f'/v2/recipients/{payload["id"]}'
Loading