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.pyalsINDIVIDUAL_PRICE_EURenWARD_PRICE_EUR). Boekt via Stripe Meter Events (stripe.billing.MeterEvent.create) perBillingLog-rij. -
Atlas Enterprise: optionele add-on van 50 EUR per maand (
ATLAS_ENTERPRISE_PRICE_EUR). Toegevoegd als extraSubscriptionItemop 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:
SpendingLimitper 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:
StripeWebhookViewvalideert de Stripe-signature, slaat het event idempotent op inStripeWebhookEventen dispatcht naarwebhooks.dispatch_webhook_event. Concurrent retries worden viaselect_for_updategeserialiseerd. -
Asynchrone taken: Celery-taken in
tasks.pyvoor 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
BillingPeriodpluskindligt er hooguit 1 rij inPeriodOverflowNotification. Per gebruiker plusack_typeplus periode ligt er hooguit 1 rij inBillingNoticeAcknowledgement. 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 uitBillingLog,BillingOverrideenChannelBillingEvent. -
GET/POST /acknowledgements/: huidige acknowledgements voor de lopendeBillingPerioden het registreren van nieuwe acks (review_overage,invite_letter_overage). -
POST /activate/: activeert het Platform-abonnement. Verwachtterms_acceptedpluspayment_method_idofsetup_intent_id. Reactiveer-flow herkent een lopende cancel-binnen-periode; na readonly geldtREACTIVATE_MIN_GAPvan 24 uur. -
POST /cancel/: zetcancel_at_period_end=trueop de Stripe Subscription enplan_status=cancelled. Echte einde komt viacustomer.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 viacommon.kvk.serviceen getoetst op duplicaten inBillingProfile. -
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 voorcard,idealensepa_debitmetusage=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 mutateertSpendingLimit. Mutatie controleert dat caps niet onder de huidige verbruiken worden gezet en dat drempels uitALLOWED_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 zetatlas_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.createdtriggert 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_statusin response). Pay-as-you-go gates raisenBillingGateError/SpendingCapReachedError/ReviewCapReachedErrordie door medicatiebeoordeling- en uitnodigingsviews als 402 worden teruggegeven met payload (code,current,max_countofcurrent_eur/max_eur). -
403 Forbidden: gebruiker zondersettings.manage_billing. Spending-limit mutatie op exempt apotheek. -
429 Too Many Requests: heractivatie binnen 24 uur na een definitieve cancel (REACTIVATE_MIN_GAP); response bevatretry_after_seconds. -
502 Bad Gateway: Stripe-call faalt (subscription-create, payment method attach, invoice list, etc.). -
500 Internal Server Error: webhook-handler error of ontbrekendSTRIPE_WEBHOOK_SECRET.
Autorisatie & Beveiliging
-
Tenant-isolatie: alle billing-models leven in het public schema en worden via
apotheekgefilterd. Reads/writes gebeuren binnenschema_context("public").ChannelBillingEventleeft in het tenant-schema en wordt cross-schema geaggregeerd incompute_extra_spend_eurenBillingUsageView. -
RBAC:
HasPermissionCodemetrequired_permission = "settings.manage_billing"op alle mutating views en de meeste read-views.acknowledgements/enatlas/status/staan opIsAuthenticatedzodat elke ingelogde gebruiker overage-popups kan dichtklikken. -
Webhook-validatie:
stripe.Webhook.construct_eventmetSTRIPE_WEBHOOK_SECRET. Ontbreekt de secret, dan retourneert de view 500 in plaats van events ongezien te accepteren. Idempotency viaStripeWebhookEvent.stripe_event_idplusselect_for_update. -
Anti-cycling:
REACTIVATE_MIN_GAP = 24hblokkeert het direct opnieuw aanmaken van een Stripe Subscription na een definitieve cancel.last_period_closed_atwordt gestamp opsubscription.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 naarBILLING_ADMIN_EMAILS. -
Dev-bypass:
BILLING_DEV_BYPASS=Truemockt 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 enunregister_apotheek. -
backend/billing/spending_eur.py: EUR-cap-berekening over reviews + SMS + brief, drempel-notificaties (check_eur_thresholds_and_notify) enenforce_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 endispatch_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 viaPeriodOverflowNotification. -
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 inservices.pyenspending_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.pybackend/billing/services.pybackend/billing/spending_eur.pybackend/billing/spending_limits.pybackend/billing/webhooks.pybackend/billing/tasks.pybackend/billing/overflow_notifications.pybackend/billing/views.py