Metadata-Version: 2.4
Name: finanfut-billing-sdk
Version: 2.1.23
Summary: Python SDK for Finanfut Billing External API
Author: Finanfut
License: MIT License
        
        Copyright (c) 2025 erovirafinanfut
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://finanfut.com
Project-URL: Source, https://github.com/finanfut/finanfut-billing
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pydantic<3.0,>=2.0
Requires-Dist: requests>=2.31
Dynamic: license-file

# 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

```python
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
```python
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)
```python
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)
```python
# 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
```python
settlement = client.settlements.create_settlement(
    payload,
    idempotency_key="settlement-create-2024-12-31",
)
```

### Enviar factura i registrar pagament
```python
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
```python
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)
```python
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
```python
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
```python
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`](../../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í.

```python
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"`:

```python
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`:

```python
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ó:

```python
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

```python
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.
