This document provides the technical implementation overview for billing, refund, and promotions behavior in the hosted reference deployment.
Scope
This overview covers:
- Promotion data model and precedence resolution.
- Billing integration points where promotions affect price/tier behavior.
- Refund lifecycle processing, adjustment reconciliation, and audit coverage.
- Security controls and policy guardrails applied to billing/refund actions.
This is an implementation overview, not a product roadmap. Future enhancements should be tracked as separate technical proposals.
Architecture Overview
Core implementation areas:
- Domain and resolver
checktick_app/core/models.pychecktick_app/core/services/promotion_resolver.py- Billing/provider integration
checktick_app/core/billing.pychecktick_app/core/views_billing.py- Platform admin billing/refund operations
checktick_app/core/views_platform_admin.pychecktick_app/core/templates/core/platform_admin/billing.html- Public pricing/signup surfacing
checktick_app/core/views.pychecktick_app/core/templates/core/home.htmlchecktick_app/core/templates/core/pricing.html- Lifecycle operations and notifications
checktick_app/core/management/commands/process_promotion_lifecycle.pychecktick_app/core/email_utils.py
Promotions: Model and Resolution
Promotion model
Promotions are represented as first-class records with:
- Scope (
platform,tier,account) and target metadata. - Effect type/value for discounting or tier override behavior.
- Activation window (
starts_at,ends_at) and active flag. - Priority and audit metadata for deterministic selection and traceability.
Deterministic precedence
Effective promotion resolution is deterministic:
- Account-scoped promotions (most specific).
- Tier-scoped promotions.
- Platform-scoped promotions.
- Baseline pricing/tier behavior when no promotion applies.
Within a scope, priority and recency determine the winner.
Public pricing/signup integration
Public pages surface resolved active offers for eligible tiers while preserving baseline pricing as canonical fallback.
Billing Integration
Price and tier evaluation
Billing flows integrate promotion resolution before provider-side amounts are prepared.
Key rules:
- Applied promotion metadata is carried with billing records for traceability.
- Promotion outputs are bounded by business constraints (for example, no negative charge amounts).
- Effective tier/price decisions are reproducible from persisted metadata.
VAT on discounted checkouts
When a promotion reduces the charged amount, VAT is computed on the discounted ex-VAT amount, not the base tier price. The flow is:
- At checkout,
billing.create_subscription_for_userresolves the effective pricing viapromotion_resolver.resolve_effective_pricing_for_user(or_for_team), which returnseffective_amount_ex_vat_penceandeffective_amount_penceafter applying any promotion. - The resolved ex-VAT amount, applied promotion, and effective tier are cached on
UserProfile(last_checkout_amount_ex_vat,last_checkout_applied_promotion,last_checkout_effective_tier) so the webhook handler can retrieve them when the payment confirms. - When the
payments.confirmedwebhook fires,handle_gocardless_payment_confirmedpasses the cached pricing intoPayment.create_from_subscriptionvia theresolved_amount_ex_vat/applied_promotion/effective_tierkeyword arguments. Payment.create_from_subscriptionuses the resolved ex-VAT amount as canonical and computes VAT from it viasettings.VAT_RATE(seechecktick_app/core/pricing.py). ThePaymentrecord stores the discounted ex-VAT, VAT, and inc-VAT amounts, plus a FK to the appliedPromotion.
This ensures VAT returns generated from the Platform Admin billing view and the Django admin CSV export reflect the actually-charged amounts, with promotion attribution for audit.
Organization checkout
Organization checkout applies effective pricing with the same guardrails used across other billing entry points. Organisations support both monthly and annual billing cycles (set by the platform admin per-org). For annual org billing, the compute_annual_amount() helper in pricing.py applies the configured ANNUAL_DISCOUNT_PERCENT to the monthly per-seat or flat-rate amount ร 12.
Billing cycle switching
Existing subscribers can switch between monthly and annual billing from the subscription portal. Because GoCardless doesn't support changing interval_unit on an existing subscription, the switch_billing_cycle view cancels the old subscription and creates a new one with the new billing cycle, reusing the existing Direct Debit mandate. The new subscription's amount and interval are computed via the same pricing helpers used at checkout.
Refund Lifecycle and Reconciliation
Admin-initiated refunds
Platform admin billing supports operator-initiated refund actions with policy constraints:
- Hosted reference flow supports full refunds for all billing cycles.
- Partial (pro-rata) refunds are supported for annual subscriptions. The refund amount is calculated by
compute_pro_rata_refund()inpricing.pyasamount ร (days_remaining / total_days). Monthly subscriptions always refund the full amount. - Reason code is mandatory.
- Additional free-text reason is required when reason code is
other.
Webhook-driven lifecycle states
Refund lifecycle transitions are processed from signed provider webhooks (for example: created, paid, failed, funds_returned, refund_settled), with idempotent reconciliation.
Webhook replay protection (F14)
The GoCardless webhook signature (HMAC-SHA256 over the request body in the Webhook-Signature header) proves authenticity but not freshness โ a captured body+signature can be replayed. Each GoCardless event carries a unique id (EV...). The payment_webhook handler records every processed event id in the WebhookEvent model (unique constraint on event_id) inside a transaction.atomic() block via WebhookEvent.objects.get_or_create(event_id=...) before dispatching the event to its handler. If the row already exists the event is skipped (logged at INFO) and no handler runs, so a replayed webhook cannot duplicate Payment rows, re-send welcome emails, or re-trigger refund side effects. Event routing is centralised in _dispatch_gocardless_event so the idempotency guard is the single chokepoint for every resource type (subscriptions, payments, refunds, mandates). Per-handler idempotency (Payment.objects.filter(payment_id=...).exists() in handle_gocardless_payment_confirmed; _refund_event_already_logged for refund handlers) is retained as a defence-in-depth second layer, backed by a partial unique constraint payment_provider_payment_id_unique on Payment.payment_id (condition payment_id != '') that enforces at the database level that a replayed payments.confirmed cannot create a duplicate Payment row even if the WebhookEvent idempotency record is missing. The constraint is partial so manual/offline payments with a blank payment_id remain allowed. A read-only WebhookEventAdmin provides an audit view of processed events. See docs/compliance/security-review-august-2026.md (F14) and tests/test_billing.py::TestWebhookReplayProtection.
Adjustment reporting
Promotion-linked adjustments are summarized in platform admin billing reporting with bounded query windows and structured metadata for finance and audit workflows.
Audit and Notifications
Audit coverage includes:
- Promotion create/update/toggle/revoke lifecycle events.
- Refund request and reconciliation metadata.
- Operator attribution and provider reference fields.
Notification coverage includes customer-facing refund processed messaging and promotion lifecycle notifications where configured.
Security Controls
Security controls in this implementation include:
- Superuser-only access for platform admin billing/promotion actions.
- Strict HTTP method controls for sensitive endpoints.
- Rate limiting on billing and admin operations.
- Webhook signature verification with required webhook secret.
- Webhook replay protection via
WebhookEventevent-id idempotency (F14). - CSRF protection on administrative form actions.
Self-Hosted Behavior
For SELF_HOSTED=true deployments, billing is disabled by default. Promotion and refund integrations are only applicable where operators explicitly implement and configure an external billing provider.