Skip to main content

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.billing.finanfut.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",
)

Pagament simple per Flutter PaymentSheet amb fallback web

from finanfut_billing_sdk.models import (
    ExternalSimplePaymentCreateRequest,
    ExternalSimplePaymentPayer,
)

payment = client.payments.create_simple_payment(
    ExternalSimplePaymentCreateRequest(
        request_id="event_123_payment_1",
        amount=25.99,
        currency="EUR",
        reference_type="EVENT",
        reference_id="event_123",
        description="Inscripció esdeveniment",
        preferred_checkout_surface="auto",
        payer=ExternalSimplePaymentPayer(email="payer@example.com", name="Payer One"),
        success_url="https://sports.example.com/payments/ok",
        cancel_url="https://sports.example.com/payments/cancel",
        payment_sheet_return_url="finanfut://stripe-redirect",
    )
)

if payment.payment_sheet:
    payment_sheet_config = payment.payment_sheet
else:
    print(payment.checkout_url)

Refund de pagament puntual

from finanfut_billing_sdk.models import ExternalPaymentRefundCreateRequest

refund = client.payments.create_refund(
    ExternalPaymentRefundCreateRequest(
        payment_session_id=payment.payment_session_id,
        amount=10.00,
        currency="EUR",
        reference_type="reservation_refund",
        reference_id="refund_request_456",
        reason="requested_by_customer",
        metadata={"origin": "finanfut-sports"},
    ),
    idempotency_key="refund_request_456",
)

assert refund.status in {"pending", "succeeded", "requires_action"}

El SDK envia Idempotency-Key amb l'argument explícit, o amb reference_id si no se'n passa cap. Billing rebutja refunds de quotes/subscripcions, valida import i moneda contra el pagament original, i retorna refs Stripe només informatives.

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.

Per app nativa amb flutter_stripe, demaneu PaymentSheet sense canviar la font d'integració Stripe:

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",
        preferred_checkout_surface="payment_sheet",
        payment_sheet_return_url="finanfut://stripe-redirect",
        metadata={"origin": "finanfut-sports"},
    ),
    idempotency_key="membership-enrollment-123-card-setup",
)

if setup.payment_sheet:
    # Passeu aquests valors directament a initPaymentSheet; no els logueu.
    payment_sheet_config = setup.payment_sheet
else:
    print(setup.checkout_url)

Per oferir un botó de "refrescar estat" si el webhook no ha arribat a Sports:

setups = client.payment_method_setups.list(
    subject_type="membership_enrollment",
    subject_id="enrollment_123",
)
latest = setups[0] if setups else None

if latest and latest.payment_method_status == "ready":
    replay = client.payment_method_setups.replay_webhook(latest.payment_method_setup_id)
    print(replay.event_id, replay.delivery_status)
elif latest and latest.payment_method_status in {"failed", "canceled", "cancelled"}:
    mark_card_setup_failed(latest.payment_method_setup_id)
else:
    keep_card_setup_pending()

replay_webhook(...) requereix payments:write i només reemet payment_method.saved quan el setup ja és ready; si encara és pendent, Billing retorna 409 i Sports ha de mantenir "pendent de targeta". Per consultar un setup concret, useu client.payment_method_setups.get(payment_method_setup_id).

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",
    provider_profile_id="club_42",
    organization_id="org_9",
    club_id="club_42",
    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",
    metadata={
        "name": "Quota temporada 2026 - Juvenil A",
        "plan_name": "Temporada 2026",
        "organizer_name": "Club Esportiu Exemple",
        "organizer_id": "club_42",
        "category_name": "Juvenil A",
    },
    sports_original_snapshot={"payment_type": "installments", "schedule_mode": "relative"},
    resolved_contract_snapshot={
        "plan_name": "Temporada 2026",
        "organizer_name": "Club Esportiu Exemple",
        "category_name": "Juvenil A",
    },
    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.

Recuperar una quota o factura amb confirmació (2.18.1)

get_recovery_preview només consulta l’estat. Mostra description, due_at, amount i currency abans de demanar confirmació. Conserva request_id durant les repeticions de la mateixa operació, incloses les respostes perdudes.

from finanfut_billing_sdk.models import ExternalContractPaymentRetryRequest

preview = client.contracts.get_recovery_preview(contract_id)
# Després de la confirmació de l’usuari i només si preview.can_retry:
result = client.contracts.retry_failed_payment(
    contract_id,
    ExternalContractPaymentRetryRequest(
        preferred_checkout_surface="auto",
        expected_target_type=preview.target_type,
        expected_target_id=preview.target_id,
        expected_amount=preview.amount,
        expected_currency=preview.currency,
    ),
    idempotency_key=preview.request_id,
)

Un conflicte recovery.preview_changed requereix consultar i confirmar de nou. Prioritza paid i processing sobre qualsevol URL de la resposta. Només obre checkout_url o hosted_invoice_url quan calgui intervenció. Per preparar o enviar un enllaç sense intentar cobrar la targeta, utilitza checkout.

Les operacions incertes conserven la identitat i no creen una via de pagament alternativa. Després de 23 hores, una operació incerta no inicia més crides de cobrament o Checkout: cal reconciliar-la amb Stripe.

Metadata

Release files for finanfut-billing-sdk 2.19.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for finanfut-billing-sdk 2.19.0
File Size Uploaded Provenance
finanfut_billing_sdk-2.19.0.tar.gz 45.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for finanfut-billing-sdk 2.19.0
File Interpreter ABI Platform
finanfut_billing_sdk-2.19.0-py3-none-any.whl Python 3 none any Details

Total release size: 87.2 kB

Release files / finanfut_billing_sdk-2.19.0.tar.gz

Download URL finanfut_billing_sdk-2.19.0.tar.gz
Size 45.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5ed1dbe38f783fcee16814ec7490303335d988eeee0fc4f42e42f626d9770be2
BLAKE2b-256 checksum
How to use checksums
b90d39e70df8a4e4fd2796face1320dc2abe891c8126dc19fb6c7c0e420263a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

Release files / finanfut_billing_sdk-2.19.0-py3-none-any.whl

Download URL finanfut_billing_sdk-2.19.0-py3-none-any.whl
Size 42.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f76445000781ea3f30b1c4bd95fd1f04df603d24296ebc39a81bf3c6e10cacb7
BLAKE2b-256 checksum
How to use checksums
fb09d68a3a0eb62118768cb04da97b33c972e0b4ad68c2bd0d3b97e3c6363cc0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

Security for finanfut-billing-sdk 2.19.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

2.19.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page