Skip to main content

Python SDK for Finanfut Billing External API

Project description

Finanfut Billing Python SDK

Client oficial sincrònic per consumir la Finanfut Billing External API (/external/v1) amb models compatibles amb Pydantic v2, ara preparat per treballar amb Business Units.

Instal·lació

  • Pydantic 2.x: pip install finanfut-billing-sdk>=2.0
  • Pydantic 1.x: pip install finanfut-billing-sdk<2.0
  • Des del repositori local: pip install -e backend/sdk

Dependències principals:

  • pydantic>=2.0,<3.0
  • requests>=2.31

Configuració bàsica i Business Units

from finanfut_billing_sdk import FinanfutBillingClient

client = FinanfutBillingClient(
    base_url="https://api.finanfut-billing.com",
    api_key="sk_live_xxx",
    business_unit_id="bu_default",  # opcional: aplicada a serveis/factures/liquidacions per defecte
    timeout=10,
    max_retries=2,
)

Com funciona business_unit_id

  • Global al client: passa business_unit_id al constructor i s'aplicarà automàticament a les operacions compatibles.
  • Per operació: pots sobreescriure-la en cada mètode (business_unit_id="bu_alt").
  • Endpoints sense BU: tax rates i partner payment methods són d'abast de companyia i ignoren la BU (l'SDK emet un avís si n'hi ha una definida).
  • Liquidacions (settlements): la BU és opcional però s'envia quan està disponible per enrutar payouts.
  • External API: serveis i factures externes accepten BU però actualment poden ignorar-la; l'SDK ja no la tracta com a obligatòria.

Exemples d'ús

Crear producte/servei amb BU

from decimal import Decimal
from finanfut_billing_sdk.models import ExternalServiceUpsertRequest

payload = ExternalServiceUpsertRequest(
    external_reference="service_abc",
    type="service",
    name="Monthly subscription",
    description="Access to premium content",
    price=Decimal("29.90"),
    vat_rate_code="vat_21",
)
service = client.upsert_service(payload)  # usa la BU global

Crear factura amb BU (sobre-escrivint la BU global)

from finanfut_billing_sdk.models import ExternalInvoiceCreateRequest, ExternalInvoiceLine

invoice = client.create_invoice(
    ExternalInvoiceCreateRequest(
        client_external_reference="client_123",
        currency="EUR",
        lines=[
            ExternalInvoiceLine(
                service_external_reference="service_abc",
                description="Premium plan",
                qty=1,
                price=29.90,
                vat_rate_id="tax_rate_uuid",
            ),
        ],
    ),
    business_unit_id="bu_sales",  # prioritat respecte la BU global
)

Operacions sense BU (àmbit de companyia)

# Els tax rates i partner payment methods ignoren la BU.
client.list_tax_rates()
client.partner_payment_methods.list_partner_payment_methods()

Idempotència en liquidacions

settlement = client.settlements.create_settlement(
    payload,
    idempotency_key="settlement-create-2024-12-31",
)

Enviar factura i registrar pagament

from finanfut_billing_sdk.models import ExternalInvoiceEmailRequest, ExternalPaymentCreateRequest

email = client.send_invoice_email(
    invoice.invoice_id,
    ExternalInvoiceEmailRequest(subject="La teva factura", body="Adjunt trobaràs el PDF"),
)

payment = client.register_payment(
    invoice.invoice_id,
    ExternalPaymentCreateRequest(amount=29.90, method="stripe"),
)

Checkout i onboarding de Stripe Connect

from finanfut_billing_sdk.models import ExternalCheckoutCreateRequest, ExternalConnectOnboardRequest

checkout = client.payments.create_checkout(
    "stripe",
    ExternalCheckoutCreateRequest(
        amount=29.90,
        currency="EUR",
        business_unit_id="bu_sales",
        provider_payload={"payment_method_types": ["card"]},
    ),
)

connect = client.payments.connect_onboard(
    "stripe",
    ExternalConnectOnboardRequest(
        provider_id="provider_uuid",
        return_url="https://app.example.com/connect/return",
        refresh_url="https://app.example.com/connect/refresh",
    ),
)

Checkout sessions de Stripe (external)

from finanfut_billing_sdk.models import ExternalCheckoutSessionCreateRequest

session = client.payments.create_checkout_session(
    ExternalCheckoutSessionCreateRequest(
        amount=49.90,
        currency="EUR",
        success_url="https://app.example.com/ok",
        cancel_url="https://app.example.com/cancel",
        description="Pagament",
    ),
    idempotency_key="checkout-session-2024-12-01",
)

Validació de targeta sense cobrar

from finanfut_billing_sdk.models import PaymentMethodSetupStartRequest, SubscriptionPayer

setup = client.payment_method_setups.start(
    PaymentMethodSetupStartRequest(
        request_id="membership-enrollment-123-card-setup",
        business_unit_id="bu_sports",
        subject_type="membership_enrollment",
        subject_id="enrollment_123",
        billing_client_id="client_uuid",
        payer=SubscriptionPayer(email="payer@example.com", name="Payer One"),
        success_url="https://sports.example.com/memberships/card-ok",
        cancel_url="https://sports.example.com/memberships/card-cancel",
        metadata={"origin": "finanfut-sports"},
    ),
    idempotency_key="membership-enrollment-123-card-setup",
)
print(setup.checkout_url, setup.payment_method_status)

Aquest flux crea un Stripe Checkout mode=setup i no cobra cap import. Per altes pendents d'aprovació, no useu subscriptions.start_subscription() només per validar targeta; espereu el webhook payment_method.saved i creeu/activeu els cobraments posteriors amb el customer i payment method guardats.

Subscripcions BU

from finanfut_billing_sdk.models import SubscriptionPricingSnapshot, SubscriptionStartRequest

payload = SubscriptionStartRequest(
    request_id="sports-pro-2025-01",
    business_unit_id="bu_sales",
    subject_type="team",
    subject_id="team_123",
    billing_client_id="client_uuid",
    bu_plan_ref="pro_v3",
    pricing_snapshot=SubscriptionPricingSnapshot(
        amount=29.9,
        currency="EUR",
        interval="month",
    ),
    success_url="https://app.example.com/billing/success",
    cancel_url="https://app.example.com/billing/cancel",
)

response = client.subscriptions.start_subscription(payload)

Contractes canònics Sports ↔ Billing

Sports ha d'integrar-se amb Billing a través de finanfut-billing-sdk>=2.1.22. L'endpoint HTTP és el transport intern i la font OpenAPI, però el contracte d'aplicació és client.contracts. Sports no ha de construir JSON manual.

La guia canònica completa és docs/integrations/sports_billing_contracts_v1.md. Aquest README només en resumeix la superfície pública.

Per quotes finites noves, Sports ha d'enviar scheduled_charges ja resolts. No s'ha d'enviar billing_mode, installments, fixed_installments ni custom_schedule a través d'aquest camí.

from datetime import datetime
from zoneinfo import ZoneInfo

from finanfut_billing_sdk.models import ExternalContractCharge, ExternalContractStartRequest

payload = ExternalContractStartRequest(
    request_id="sports_membership_enrollment_123_v1",
    source_system="sports",
    contract_type="scheduled_charges",
    business_unit_id="bu_sales",
    source_entity_type="membership_enrollment",
    source_entity_id="membership_enrollment_123",
    billing_client_id="client_uuid",
    bu_plan_ref="membership_plan:plan_123",
    bu_price_ref="membership_plan_price:price_123",
    currency="EUR",
    total_amount_minor=12000,
    charges=[
        ExternalContractCharge(
            sequence=1,
            amount_minor=4000,
            due_at=datetime(2026, 7, 15, tzinfo=ZoneInfo("Europe/Madrid")),
            external_ref="membership_enrollment_123_charge_1",
        )
    ],
    payment_method_required=True,
    success_url="https://sports.example.test/billing/success",
    cancel_url="https://sports.example.test/billing/cancel",
    sports_original_snapshot={"payment_type": "installments", "schedule_mode": "relative"},
    economic_classification={
        "economic_flow_type": "club_membership",
        "recognition_role": "THIRD_PARTY_FUNDS",
        "seller_role": "MARKETPLACE_OPERATOR",
        "funds_ownership": "THIRD_PARTY_FUNDS",
        "tax_ownership": "MERCHANT_TAX",
        "ownership_confidence": "explicit",
    },
    economic_owner="club",
    merchant="billing",
    seller="club",
)

response = client.contracts.start_contract(payload, idempotency_key=payload.request_id)

Billing és propietari de targetes, setup checkout, payment checkout, retries, webhooks, invoices, ledger i settlements. Sports pot enviar un payment_method_setup_id creat per Billing, però no ha d'enviar cap stripe_payment_method_id cru. Billing només accepta setups ready; un setup pendent retorna un 409 funcional perquè Sports pugui reintentar després de payment_method.saved.

Per quotes recurrents, Sports ha d'usar el mateix client amb contract_type="recurring":

from datetime import date

from finanfut_billing_sdk.models import ExternalContractStartRequest

payload = ExternalContractStartRequest(
    request_id="sports_membership_enrollment_123_recurring_v1",
    source_system="sports",
    contract_type="recurring",
    business_unit_id="bu_sales",
    source_entity_type="membership_enrollment",
    source_entity_id="membership_enrollment_123",
    billing_client_id="client_uuid",
    bu_plan_ref="membership_plan:plan_123",
    bu_price_ref="membership_plan_price:monthly",
    currency="EUR",
    amount_minor=3000,
    frequency="monthly",
    start_date=date(2026, 9, 1),
    end_date=date(2027, 6, 30),
    billing_day_of_month=1,
    billing_months=[9, 10, 11, 12, 1, 2, 3, 4, 5, 6],
    success_url="https://sports.example.test/success",
    cancel_url="https://sports.example.test/cancel",
    sports_original_snapshot={"payment_type": "recurring"},
    economic_classification={
        "economic_flow_type": "club_membership",
        "recognition_role": "THIRD_PARTY_FUNDS",
        "seller_role": "MARKETPLACE_OPERATOR",
        "funds_ownership": "THIRD_PARTY_FUNDS",
        "tax_ownership": "MERCHANT_TAX",
        "ownership_confidence": "explicit",
    },
    economic_owner="club",
    merchant="billing",
    seller="club",
)

response = client.contracts.start_contract(payload, idempotency_key=payload.request_id)

Sports ha d'enviar start_date com a data efectiva d'alta/activació. Billing no ha de cobrar períodes anteriors a aquesta data; si start_date és futura, el checkout/setup pot passar ara i el primer període facturable comença a start_date.

La resposta és un ExternalContractStartResponse:

assert response.contract_id
assert response.ledger_id

if response.contract_type == "scheduled_charges":
    assert response.schedule_id
    assert response.next_charge_at
    assert response.payment_method_status

Si response.checkout_url és present, Sports ha de redirigir l'usuari. Si no hi ha checkout i payment_method_status=="ready", Billing ja té una targeta operativa.

Operacions de consulta i cancel·lació:

contract = client.contracts.get_contract(response.contract_id)

canceled = client.contracts.cancel_contract(
    response.contract_id,
    reason="sports_contract_replaced",
    idempotency_key=f"{response.contract_id}:cancel:v1",
)

La cancel·lació conserva ítems pagats i només cancel·la ítems pendents. Sports ha de tractar subscription.canceled com la confirmació canònica.

Webhooks que Sports ha de consumir:

  • subscription.invoice_paid
  • subscription.payment_failed
  • subscription.completed
  • subscription.canceled

Els payloads de contracte inclouen external_contract_id, contract_type, ledger_id, schedule_id, schedule_sequence, external_ref, billing_transaction_id, amount_total, amount_fee i amount_net. settlement_lines_suggested només apareix quan Billing té commission_breakdown. Sports no ha de calcular nets ni fees a partir del contracte inicial.

Errors habituals:

  • pydantic.ValidationError: el payload no compleix els models del SDK.
  • FinanfutBillingValidationError: Billing rebutja valors o forma del payload.
  • FinanfutBillingServiceError amb status_code == 409: mateix (source_system, request_id) amb payload diferent.
  • FinanfutBillingAuthError: API key absent, incorrecta o sense scope.

Errors

from finanfut_billing_sdk.errors import (
    FinanfutBillingAuthError,
    FinanfutBillingServiceError,
    FinanfutBillingValidationError,
)

try:
    client.list_tax_rates()
except FinanfutBillingAuthError:
    print("API key incorrecta o sense permisos")
except FinanfutBillingValidationError as e:
    print("Error de validació:", e.payload)
except FinanfutBillingServiceError as e:
    print(f"Error de servei ({e.request_id}): {e.error}")
    if e.retry_after is not None:
        print(f"Reintenta després de {e.retry_after:.0f}s")

Els errors del backend inclouen sempre error, message i request_id. El client reintenta automàticament errors transitoris (429, 500, 502, 503, 504) en lectures i en mutacions només quan la petició porta Idempotency-Key.

Publicació a PyPI

El paquet està preparat per publicar-se a PyPI quan es creen tags v* al repositori. El workflow publish-sdk.yml valida la versió (__version__) i fa l'upload amb Twine.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

finanfut_billing_sdk-2.1.23.tar.gz (24.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

finanfut_billing_sdk-2.1.23-py3-none-any.whl (21.6 kB view details)

Uploaded Python 3

File details

Details for the file finanfut_billing_sdk-2.1.23.tar.gz.

File metadata

  • Download URL: finanfut_billing_sdk-2.1.23.tar.gz
  • Upload date:
  • Size: 24.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for finanfut_billing_sdk-2.1.23.tar.gz
Algorithm Hash digest
SHA256 64d8e2f1e9469ac07afb989e8113d83edf18069b2e6ba469de11bb5bce26debe
MD5 2fa4e0b853836bcb9af7b6532d8d9288
BLAKE2b-256 12012ceb884d54a153b58c8f92d16662bbf404160d798be9763539b222002592

See more details on using hashes here.

File details

Details for the file finanfut_billing_sdk-2.1.23-py3-none-any.whl.

File metadata

File hashes

Hashes for finanfut_billing_sdk-2.1.23-py3-none-any.whl
Algorithm Hash digest
SHA256 91ff0611bb21e514b8aadb926056b8cc180ed7248dbac278d865db7b7457fc46
MD5 2b9111ea27c63c0363eb05142b2d8dc9
BLAKE2b-256 f753cb2ab91978c9540adbc8b3ec2c2e6b00c48835fff6908e2f04473afb7476

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page