Zarządzanie subskrypcjami i okresami próbnymi

1. Model subskrypcji

class Subscription(models.Model):
    company = models.OneToOneField(Company)
    plan = models.CharField(choices=[
        ('free', 'Free'),
        ('starter', 'Starter'),
        ('professional', 'Professional'),
        ('enterprise', 'Enterprise'),
    ])
    status = models.CharField(choices=[
        ('trial', 'Okres próbny'),
        ('pilot', 'Pilot'),
        ('af_trial', 'Trial biura rachunkowego'),
        ('af_client_trial', 'Trial klienta BR'),
        ('active', 'Aktywna'),
        ('expired', 'Wygasła'),
        ('cancelled', 'Anulowana'),
    ])
    trial_type = models.CharField(null=True, choices=[
        ('reverse_trial', 'Reverse trial'),
        ('pilot', 'Pilot'),
        ('af_trial', 'Trial BR'),
        ('af_client', 'Trial klienta BR'),
    ])
    valid_until = models.DateTimeField()
    features = models.JSONField(default=dict)

2. Typy okresów próbnych

TypCzas trwaniaPlanCel
Reverse trial14 dniProfessionalNowa firma — pełny dostęp, potem downgrade do Free
Pilot6 miesięcyProfessionalPierwsi klienci — bezpłatnie w zamian za feedback
AF trial3 miesiąceEnterpriseBiuro rachunkowe — wymagane min. 3 klientów
AF client trial3 miesiąceProfessionalKlient zaproszony przez biuro rachunkowe

3. Cykl życia triala

Rejestracja firmy
     │
     ▼
Trial aktywny (Professional / Enterprise)
     │  ← Banner: "Pozostało X dni trialu"
     │  ← Email: 7, 3, 1 dzień przed końcem
     │
     ▼
Trial wygasa
     │
     ├─→ Użytkownik wykupił plan → status: 'active'
     │
     └─→ Nie wykupił → downgrade do Free
          ├── Dane zachowane (nie usuwane)
          ├── Funkcje premium zablokowane
          └── Banner: "Uaktualnij plan"

4. Zachowanie przy downgrade

ZasóbZachowanie
Pojazdy ponad limitZachowane, ale oznaczone jako nieaktywne. Brak nowych tras.
Kierowcy ponad limitZachowani, ale brak możliwości logowania (oprócz 1).
RaportyIstniejące PDF zachowane. Generowanie nowych zablokowane.
Eksporty FKZablokowane. Istniejące pliki dostępne 30 dni.
Mapa real-timeZablokowana.
Dane GPSZachowane. Śledzenie GPS nadal aktywne (1 pojazd).

5. Feature gating

class FeatureGatingMiddleware:
    """Sprawdza plan subskrypcji przed dostępem do chronionych endpointów."""

    PLAN_REQUIREMENTS = {
        '/api/v1/reports/': 'starter',
        '/api/v1/exports/': 'professional',
        '/api/v1/accounting-firms/': 'enterprise',
        '/api/v1/map/live/': 'professional',
    }

    def __call__(self, request):
        required_plan = self._get_required_plan(request.path)
        if required_plan and not self._has_plan(request.user, required_plan):
            return JsonResponse({
                'error': 'plan_required',
                'required_plan': required_plan,
                'message': f'Ta funkcja wymaga planu {required_plan}'
            }, status=403)

6. Powiadomienia email

KiedyTreść
RejestracjaWitaj! Twój 14-dniowy trial Professional się rozpoczął.
7 dni przed końcemZostało 7 dni trialu. Poznaj plany.
3 dni przed końcemZostały 3 dni. Wybierz plan, aby nie stracić funkcji.
1 dzień przed końcemOstatni dzień trialu! Uaktualnij teraz.
Trial wygasłTrial wygasł. Twoje dane są bezpieczne. Uaktualnij plan.

7. Zadania Celery

TaskHarmonogramOpis
check_expiring_trialsCodziennie 8:00Wysyłka emaili o wygasających trialach
downgrade_expired_trialsCodziennie 0:05Downgrade wygasłych triali do Free
track_usage_snapshotsCodziennie 2:00Zapis snapshotów użycia per firma
cleanup_expired_exportsCo tydzieńUsunięcie plików eksportów starszych niż 30 dni

8. Architektura billing-ready

System jest przygotowany na integrację ze Stripe Billing (planowane Post-Sprint 7):

  • Model Subscription ma pola na stripe_customer_id i stripe_subscription_id
  • Webhook endpoint dla zdarzeń Stripe (payment_succeeded, subscription_cancelled)
  • Przejście z manual billing na automatyczny bez zmiany modelu danych