Ga naar inhoud

Billing (Backend)

Technisch Ontwerp

De billing-backend beheert de commerciele relatie van een apotheek (tenant) met Remedice. Het ontwerp scheidt het Platform-abonnement (vast bedrag per maand) van de pay-as-you-go-componenten (extra patienten, SMS, brieven, Atlas Enterprise) en synct alle financiele state via Stripe-webhooks.

  • Platform-abonnement: vast 100 EUR per maand met 20 inbegrepen patienten per factuurperiode. Anniversary-billing geanchored op het activatiemoment (create_stripe_subscription); de eerste periode start direct, niet per de 1e van de maand.

  • Pay-as-you-go reviews: individuele patient kost 5,00 EUR boven de pool, afdelingpatient kost 2,50 EUR boven de pool (alle prijzen excl. btw, in spending_eur.py als INDIVIDUAL_PRICE_EUR en WARD_PRICE_EUR). Boekt via Stripe Meter Events (stripe.billing.MeterEvent.create) per BillingLog-rij.

  • Atlas Enterprise: optionele add-on van 50 EUR per maand (ATLAS_ENTERPRISE_PRICE_EUR). Toegevoegd als extra SubscriptionItem op de bestaande Stripe Subscription (attach_atlas_enterprise_item).

  • Trial: levenslange pool van 10 patienten plus een 14-daags venster (TRIAL_INCLUDED_PATIENTS, TRIAL_DAYS). Trial-pool wordt gestopt op 10 maar afdelingreviews boven de 10 patienten worden niet geblokkeerd; de extra patienten lopen door zonder als billed te tellen tijdens trial. Trial start eenmalig per tenant en wordt niet opnieuw geopend bij cancel/reactivate.

  • Spending caps: SpendingLimit per tenant. EUR-cap dekt alle pay-as-you-go-features samen; review-caps gelden per type (individueel/afdeling). De tenant beheert alleen de cap-velden en notificatiedrempels; bundel, prijs en overage-tarieven liggen in code/Stripe-prices.

  • Webhook-pipeline: StripeWebhookView valideert de Stripe-signature, slaat het event idempotent op in StripeWebhookEvent en dispatcht naar webhooks.dispatch_webhook_event. Concurrent retries worden via select_for_update geserialiseerd.

  • Asynchrone taken: Celery-taken in tasks.py voor het sturen van Meter Events naar Stripe (sync_billing_to_stripe), het verlopen van trials (expire_lapsed_trials_task), het sturen van overage-mails (notify_overflow_task), het checken van EUR-drempels (check_eur_thresholds_task) en het opruimen van oude webhook-events (cleanup_stripe_webhook_events).

  • Notificatie-idempotency: per BillingPeriod plus kind ligt er hooguit 1 rij in PeriodOverflowNotification. Per gebruiker plus ack_type plus periode ligt er hooguit 1 rij in BillingNoticeAcknowledgement. Samen beperken deze de overage-mails en in-app popups tot maximaal 1 per soort per factuurperiode.

flowchart TD
    A[Tenant start review] --> B[services.check_review_gate]
    B -->|trial of pool| C[register_review_usage]
    B -->|plan_inactive / trial_expired| Z[BillingGateError 402]
    C --> D[BillingLog + PeriodReviewUsage]
    D --> E[tasks.sync_billing_to_stripe]
    E --> F[stripe.billing.MeterEvent.create]
    G[Tenant verstuurt SMS/brief] --> H[ChannelBillingEvent in tenant schema]
    D --> I[spending_eur.compute_extra_spend_eur]
    H --> I
    I --> J[tasks.check_eur_thresholds_task]
    J --> K[overflow_notifications.maybe_notify_overflow]
    K --> L[PeriodOverflowNotification idempotency]
    M[Stripe Webhook] --> N[StripeWebhookView]
    N --> O[StripeWebhookEvent idempotent]
    O --> P[handle_subscription_updated / deleted / invoice.created / charge.refunded]
    P --> Q[upsert BillingPeriod + plan_status sync]

Datamodel (ERD)

Public-schema modellen in backend/billing/models.py. Tenant-schema model ChannelBillingEvent leeft in features.medicatiebeoordeling.models en wordt door de billing-laag aangevuld voor SMS- en briefoverage. Het ERD is opgesplitst in abonnement, gebruik en administratieve correcties.

Abonnement en factuurperiode

erDiagram
    Apotheek ||--|| BillingProfile : "heeft"
    Apotheek ||--o{ BillingPeriod : "heeft"
    Apotheek ||--|| SpendingLimit : "configureert"
    Apotheek ||--o{ TermsConsent : "accepteert"

    BillingProfile {
        bigint id PK
        bigint apotheek_id FK
        string kvk_number
        string stripe_customer_id
        string stripe_subscription_id
        string stripe_atlas_item_id
        boolean billing_active
        boolean billing_exempt
        boolean onboarding_completed
        string plan_status
        string atlas_plan
        datetime trial_started_at
        datetime trial_expires_at
        int platform_included_patients
        int trial_patients_used
        date platform_active_since
        date platform_deactivated_at
        datetime last_period_closed_at
    }
    BillingPeriod {
        bigint id PK
        bigint apotheek_id FK
        string stripe_subscription_id
        datetime period_start
        datetime period_end
        string status
        boolean cancel_at_period_end
        datetime atlas_warned_80_at
        datetime atlas_warned_100_at
        datetime closed_at
    }
    SpendingLimit {
        bigint id PK
        bigint apotheek_id FK
        int max_extra_individual
        int max_extra_ward
        decimal max_extra_euros
        json notification_thresholds
    }
    TermsConsent {
        bigint id PK
        bigint apotheek_id FK
        string terms_type
        string terms_version
        bigint accepted_by_id FK
        string ip_address
        datetime accepted_at
    }

Gebruik, meters en notificaties

erDiagram
    Apotheek ||--o{ BillingLog : "registreert"
    BillingPeriod ||--o| PeriodReviewUsage : "telt"
    BillingPeriod ||--o{ BillingLog : "groepeert"
    BillingPeriod ||--o{ PeriodOverflowNotification : "idempotent per kind"
    BillingPeriod ||--o{ BillingNoticeAcknowledgement : "idempotent per user"

    BillingPeriod {
        bigint id PK
        bigint apotheek_id FK
        datetime period_start
        datetime period_end
        string status
    }
    PeriodReviewUsage {
        bigint id PK
        bigint period_id FK
        bigint apotheek_id FK
        int included_used
        int billed_individual
        int billed_ward
        datetime created_at
        datetime updated_at
    }
    BillingLog {
        bigint id PK
        bigint apotheek_id FK
        bigint period_id FK
        string schema_name
        uuid review_id
        string review_type
        int patient_count
        string billing_key
        boolean is_trial
        int included_free_count
        int billed_count
        boolean stripe_synced
        string stripe_meter_event_id
        text stripe_sync_error
        datetime created_at
    }
    PeriodOverflowNotification {
        bigint id PK
        bigint apotheek_id FK
        bigint period_id FK
        string kind
        int billed_individual
        int billed_ward
        int billed_units
        int users_notified
        datetime notified_at
    }
    BillingNoticeAcknowledgement {
        bigint id PK
        bigint apotheek_id FK
        bigint period_id FK
        bigint user_id FK
        string ack_type
        datetime acknowledged_at
    }

Webhooks en correcties

erDiagram
    Apotheek ||--o{ BillingOverride : "admin-correctie"
    Apotheek ||--o{ BillingExemption : "vrijstelling"

    StripeWebhookEvent {
        bigint id PK
        string stripe_event_id
        string event_type
        boolean processed
        datetime processed_at
        json payload
    }
    BillingOverride {
        bigint id PK
        bigint apotheek_id FK
        int year
        int month
        string review_type
        int override_count
        text note
        boolean stripe_synced
        text stripe_sync_error
        bigint created_by_id FK
        datetime created_at
        datetime updated_at
    }
    BillingExemption {
        bigint id PK
        bigint apotheek_id FK
        date start_date
        date end_date
        text reason
        bigint created_by_id FK
        datetime created_at
    }

API & Communicatie

Alle endpoints leven onder /api/billing/ en eisen settings.manage_billing, behalve atlas/status/ (alleen IsAuthenticated), acknowledgements/ (alleen IsAuthenticated) en webhook/ (publiek, Stripe-signature gevalideerd).

  • GET /summary/: complete status van plan, trial, pool, factuurperiode, factuurgegevens, channel-usage en jaartabbladen.

  • GET /status/: lichtere consolidatie (plan, trial, period, pool, atlas) voor banners en gating-checks.

  • GET /usage/?year=YYYY: maandaggregaat van patientaantallen, billed-counters, platformfee en SMS-/briefoverage uit BillingLog, BillingOverride en ChannelBillingEvent.

  • GET/POST /acknowledgements/: huidige acknowledgements voor de lopende BillingPeriod en het registreren van nieuwe acks (review_overage, invite_letter_overage).

  • POST /activate/: activeert het Platform-abonnement. Verwacht terms_accepted plus payment_method_id of setup_intent_id. Reactiveer-flow herkent een lopende cancel-binnen-periode; na readonly geldt REACTIVATE_MIN_GAP van 24 uur.

  • POST /cancel/: zet cancel_at_period_end=true op de Stripe Subscription en plan_status=cancelled. Echte einde komt via customer.subscription.deleted.

  • POST /accept-general-terms/: registreert acceptatie van de algemene voorwaarden (TermsConsent).

  • PATCH /billing-email/: werkt de factuur-e-mail bij en synct naar Stripe.

  • PATCH /billing-info/: werkt naam, factuur-e-mail, adres en KVK-nummer bij. KVK wordt gevalideerd via common.kvk.service en getoetst op duplicaten in BillingProfile.

  • GET /invoices/?limit=N: lijst Stripe-facturen voor de tenant (max 100).

  • GET /payment-methods/: lijst betaalmethoden (kaart, iDEAL, SEPA) met de huidige default.

  • POST /payment-methods/setup-intent/: maakt een Stripe SetupIntent voor card, ideal en sepa_debit met usage=off_session.

  • POST /payment-methods/set-default/: stelt de default betaalmethode in op de Stripe-customer.

  • POST /payment-methods/detach/: ontkoppelt een betaalmethode (kan niet als het de enige actieve methode is op een actief plan).

  • GET/PUT /spending-limit/: leest en mutateert SpendingLimit. Mutatie controleert dat caps niet onder de huidige verbruiken worden gezet en dat drempels uit ALLOWED_THRESHOLDS = [10, 25, 50, 75, 90, 100] komen.

  • GET /atlas/: status van Atlas-plan en token-usage tegen de fair-use cap.

  • POST /atlas/upgrade/: voegt het Stripe Atlas Enterprise SubscriptionItem toe (vereist actief Platform-abonnement).

  • POST /atlas/downgrade/: verwijdert het Atlas-item op Stripe en zet atlas_plan=free.

  • GET /atlas/status/: lichte endpoint voor authenticated users om het Atlas-plan op te halen.

  • POST /webhook/: Stripe webhook receiver. Behandelde events: invoice.created, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, setup_intent.succeeded, charge.refunded, credit_note.created. invoice.created triggert reconcile-meters; refund en credit_note loggen audit en sturen admin-alert.

Foutafhandeling & Statuscodes

  • 400 Bad Request: ontbrekende betaalmethode bij activatie, ongeldige KVK, KVK al in gebruik, dubbele Atlas-upgrade/downgrade zonder context, exempt apotheek probeert te activeren.

  • 402 Payment Required: Stripe-subscription kon niet active worden tijdens activatie (stripe_status in response). Pay-as-you-go gates raisen BillingGateError/SpendingCapReachedError/ReviewCapReachedError die door medicatiebeoordeling- en uitnodigingsviews als 402 worden teruggegeven met payload (code, current, max_count of current_eur/max_eur).

  • 403 Forbidden: gebruiker zonder settings.manage_billing. Spending-limit mutatie op exempt apotheek.

  • 429 Too Many Requests: heractivatie binnen 24 uur na een definitieve cancel (REACTIVATE_MIN_GAP); response bevat retry_after_seconds.

  • 502 Bad Gateway: Stripe-call faalt (subscription-create, payment method attach, invoice list, etc.).

  • 500 Internal Server Error: webhook-handler error of ontbrekend STRIPE_WEBHOOK_SECRET.

Autorisatie & Beveiliging

  • Tenant-isolatie: alle billing-models leven in het public schema en worden via apotheek gefilterd. Reads/writes gebeuren binnen schema_context("public"). ChannelBillingEvent leeft in het tenant-schema en wordt cross-schema geaggregeerd in compute_extra_spend_eur en BillingUsageView.

  • RBAC: HasPermissionCode met required_permission = "settings.manage_billing" op alle mutating views en de meeste read-views. acknowledgements/ en atlas/status/ staan op IsAuthenticated zodat elke ingelogde gebruiker overage-popups kan dichtklikken.

  • Webhook-validatie: stripe.Webhook.construct_event met STRIPE_WEBHOOK_SECRET. Ontbreekt de secret, dan retourneert de view 500 in plaats van events ongezien te accepteren. Idempotency via StripeWebhookEvent.stripe_event_id plus select_for_update.

  • Anti-cycling: REACTIVATE_MIN_GAP = 24h blokkeert het direct opnieuw aanmaken van een Stripe Subscription na een definitieve cancel. last_period_closed_at wordt gestamp op subscription.deleted.

  • Audit: refund- en credit-note-events loggen via common.audit.service.audit_logger (STRIPE_REFUND_RECEIVED, STRIPE_CREDIT_NOTE_CREATED) en sturen een admin-alert naar BILLING_ADMIN_EMAILS.

  • Dev-bypass: BILLING_DEV_BYPASS=True mockt Stripe-calls (subscription, attach, detach, cancel) zodat developers zonder Stripe-keys lokaal kunnen draaien. Tenant-caps (count + EUR) blijven gewoon werken zodat ze testbaar blijven.

Bestandsstructuur & Verantwoordelijkheden

  • backend/billing/models.py: BillingProfile, BillingPeriod, PeriodReviewUsage, BillingLog, SpendingLimit, PeriodOverflowNotification, BillingNoticeAcknowledgement, BillingOverride, BillingExemption, TermsConsent, StripeWebhookEvent.

  • backend/billing/views.py: alle DRF-views voor /api/billing/. Verwerkt summary, status, activate, cancel, payment methods, invoices, spending-limit, atlas en webhook-receiver.

  • backend/billing/urls.py: registratie van de routes.

  • backend/billing/serializers.py: serializers voor summary, usage, activate, atlas, payment methods, invoice, spending-limit en acknowledgements.

  • backend/billing/services.py: kern van de business logic. Beheer van trial, billing-periods, gates (check_review_gate, check_invitation_gate), Stripe-integratie (customer, subscription, setup-intent, payment methods, invoice list), meter-events, atlas fair-use, cancel/reactivate, exemption-checks en unregister_apotheek.

  • backend/billing/spending_eur.py: EUR-cap-berekening over reviews + SMS + brief, drempel-notificaties (check_eur_thresholds_and_notify) en enforce_spend_cap.

  • backend/billing/spending_limits.py: per-type review-caps (check_review_cap, maybe_notify_review_cap_reached).

  • backend/billing/webhooks.py: Stripe-event handlers en dispatch_webhook_event. Update-flow voor subscription, deleted, invoice-events, charge-refunded en credit-note.

  • backend/billing/tasks.py: Celery-taken voor Stripe-meter-sync, trial-expiry, overflow-mails, EUR-drempels en webhook-event cleanup.

  • backend/billing/overflow_notifications.py: bouw van de overage- en cap-mails plus de idempotency via PeriodOverflowNotification.

  • backend/billing/trial_notifications.py: trial-pool-uitputting en trial-expired e-mails.

  • backend/billing/cost_rates.py: kostenmodel-constanten voor het admin-monitoring-dashboard (LLM-token-kosten, STT, SMS, brief, Stripe-fee). Niet de tenant-prijzen; dat zijn andere constanten in services.py en spending_eur.py.

  • backend/billing/admin.py: Django admin-registraties voor billing-modellen.

  • backend/billing/management/: management commands voor billing-onderhoud.

  • backend/billing/tests/: testsuite voor gates, services, webhooks en tasks.

Belangrijke bestanden

  • backend/billing/models.py
  • backend/billing/services.py
  • backend/billing/spending_eur.py
  • backend/billing/spending_limits.py
  • backend/billing/webhooks.py
  • backend/billing/tasks.py
  • backend/billing/overflow_notifications.py
  • backend/billing/views.py

API & Communicatie (Swagger)