Subscription Model & License Management
Overviewβ
Visla GPS utilizza un modello di subscription basato su licenze per dispositivo. Ogni dispositivo attivo richiede una licenza valida. Il sistema gestisce automaticamente upgrade, downgrade e sospensione dispositivi per garantire che l'utente rimanga sempre nei limiti del proprio piano.
Concetti Chiaveβ
Licenze vs Dispositiviβ
- Licenza: Permesso di tracciare un dispositivo attivamente
- Dispositivo Attivo: Dispositivo che invia posizioni e viene tracciato
- Dispositivo Sospeso: Dispositivo associato all'account ma non tracciato (non consuma licenza)
Stati Dispositivoβ
| Stato | Descrizione | Consuma Licenza | Visibile in App |
|---|---|---|---|
| Attivo | Tracciato normalmente | β SΓ¬ | β SΓ¬ |
| Sospeso | Associato ma in pausa | β No | β SΓ¬ (badge arancione) |
| Rimosso | Dissociato dall'account | β No | β No |
Scenari Utente (iOS)β
1. Utente Nuovo (0 Licenze, 0 Dispositivi)β
Situazione Iniziale:
- Licenze: 0
- Dispositivi: 0
Comportamento:
- β PuΓ² navigare l'app liberamente
- β Nessun modal bloccante
- β NON puΓ² aggiungere dispositivi
Flusso Aggiunta Dispositivo:
- Utente clicca "Aggiungi Dispositivo"
- Inserisce token Visla
- PRIMA del claim, app controlla licenze
- Alert: "Nessuna Licenza Attiva"
- Messaggio: "Per aggiungere dispositivi devi prima acquistare almeno una licenza"
- Bottoni: "Acquista Licenza" (blu) | "Annulla" (grigio)
Codice iOS:
// In AddDeviceView.claimDevice()
let status = try await ApiClient.shared.getLicenseStatus()
if status.allowed == 0 {
showNoLicenseModal = true
return
}
2. Utente con Licenze Sufficienti (2 Licenze, 1 Dispositivo)β
Situazione Iniziale:
- Licenze: 2
- Dispositivi Attivi: 1
- Disponibili: 1
Comportamento:
- β PuΓ² aggiungere dispositivi normalmente
- β Nessun modal bloccante
- β PuΓ² aggiungere fino a 1 dispositivo aggiuntivo
Flusso Aggiunta Dispositivo:
- Utente clicca "Aggiungi Dispositivo"
- Inserisce token Visla
- Check licenze:
1 < 2β OK - Device aggiunto con successo
- Nuovo stato: 2 licenze, 2 dispositivi attivi
3. Utente al Limite (2 Licenze, 2 Dispositivi, Prova ad Aggiungere 3Β°)β
Situazione Iniziale:
- Licenze: 2
- Dispositivi Attivi: 2
- Disponibili: 0
Comportamento:
- β NON puΓ² aggiungere dispositivi
- Alert preventivo prima del claim
Flusso Aggiunta Dispositivo:
- Utente clicca "Aggiungi Dispositivo"
- Inserisce token Visla
- PRIMA del claim, app controlla licenze
- Alert: "Limite Dispositivi Raggiunto"
- Messaggio: "Hai 2 dispositivi attivi e 2 licenze. Per aggiungere altri dispositivi devi acquistare piΓΉ licenze o sospendere un dispositivo esistente"
- Bottoni: "Acquista Licenze" (blu) | "Annulla" (grigio)
Codice iOS:
if status.active >= status.allowed {
showLimitReachedModal = true
return
}
4. Downgrade: PiΓΉ Dispositivi che Licenze (3 Dispositivi, 2 Licenze)β
Situazione Iniziale:
- Licenze: 2 (dopo downgrade da 3)
- Dispositivi Attivi: 3
- Squilibrio: +1 dispositivo
Comportamento:
- β οΈ Modal bloccante all'apertura app
- Utente DEVE risolvere lo squilibrio
Flusso:
- App si apre
checkLicenseStatus()rileva:3 > 2- Modal: "Dispositivi Oltre il Limite"
- Messaggio: "Hai 3 dispositivi attivi ma solo 2 licenze. Scegli cosa fare:"
- Opzioni:
- "Acquista Licenze" (blu) β Vai a piano superiore
- "Sospendi Dispositivi" (arancione) β Apre DeviceSelectionModal
DeviceSelectionModal:
- Mostra lista di tutti i dispositivi
- Utente seleziona quali 2 mantenere attivi
- Gli altri vengono sospesi automaticamente
- Badge arancione "π Sospeso" sui dispositivi sospesi
Codice iOS:
// In MainTabView.checkLicenseStatus()
if status.active > status.allowed {
showSubscriptionRequired = true
}
Backend:
# In devices/license_utils.py
def suspend_excess_devices(user_id: int, device_ids_to_keep: List[int]):
# Sospende tutti i device NON in device_ids_to_keep
# Imposta suspended=True in user_device_attributes
5. Upgrade: Da 2 a 5 Licenzeβ
Situazione Iniziale:
- Licenze: 2
- Dispositivi Attivi: 2
- Dispositivi Sospesi: 1
Flusso:
- Utente acquista upgrade (2 β 5 licenze)
- Backend aggiorna
allowed_licensesvia webhook - Nuovo stato: 5 licenze, 2 attivi, 3 disponibili
- β PuΓ² riattivare dispositivi sospesi
- β PuΓ² aggiungere fino a 3 nuovi dispositivi
Riattivazione Dispositivo Sospeso:
- Utente va in Device Settings di un dispositivo sospeso
- Sezione "Gestione Licenza" mostra stato "π Dispositivo Sospeso"
- Bottone verde "Riattiva Dispositivo" (se licenze disponibili)
- Conferma: "Il dispositivo verrΓ riattivato e riprenderΓ il tracciamento"
- Backend imposta
suspended=False - Badge cambia da "π Sospeso" a "π’ Online/π΄ Offline"
Sospensione Manuale:
- Utente va in Device Settings di un dispositivo attivo
- Sezione "Gestione Licenza" mostra stato "β Dispositivo Attivo"
- Bottone arancione "Sospendi Dispositivo"
- Conferma: "Il dispositivo verrΓ sospeso e non verrΓ piΓΉ tracciato"
- Backend imposta
suspended=True - Libera una licenza per altri dispositivi
Restrizioni Providerβ
Regola: Un Provider per Utenteβ
Importante: Un utente che inizia una subscription con un provider (Apple, Google, Stripe) DEVE continuare con quello stesso provider. Non Γ¨ possibile mescolare provider.
Esempiβ
β Scenario Validoβ
- Utente si registra con Apple
- Acquista piano Basic (1 licenza) via Apple
- Fa upgrade a Pro (5 licenze) via Apple
- Fa downgrade a Basic via Apple
- Tutto tramite Apple β
β Scenario NON Validoβ
- Utente si registra con Apple
- Acquista piano Basic via Apple
- Prova a fare upgrade via Google Play
- Backend rifiuta: "Hai giΓ una subscription attiva con Apple"
Cambio Providerβ
Per cambiare provider, l'utente DEVE:
- Disdire completamente la subscription con il provider corrente
- Aspettare la scadenza naturale del periodo pagato
- Dopo la scadenza, puΓ² acquistare con un nuovo provider
Esempio:
1. Utente ha subscription Apple (scade 15 Gennaio)
2. Disdice da Apple il 5 Gennaio
3. Subscription rimane attiva fino al 15 Gennaio
4. Dal 16 Gennaio puΓ² acquistare con Google Play
Messaggi Backendβ
Il backend Γ¨ configurato per gestire questi casi con messaggi chiari:
# In billing/routes/subscriptions.py
if existing_provider and existing_provider != new_provider:
raise ConflictError(
f"Hai giΓ una subscription attiva con {existing_provider}. "
f"Per cambiare provider devi prima disdire la subscription corrente."
)
Logica Backendβ
Endpoint Chiaveβ
GET /api/billing/license-statusβ
Restituisce lo stato corrente delle licenze:
{
"allowed": 5, // Licenze totali del piano
"active": 3, // Dispositivi attivi
"suspended": 1, // Dispositivi sospesi
"total": 4 // Totale dispositivi (attivi + sospesi)
}
POST /api/devices/{device_id}/suspendβ
Sospende un dispositivo:
# Imposta suspended=True in user_device_attributes
# Il device rimane visibile ma non tracciato
# Non consuma licenza
POST /api/devices/{device_id}/reactivateβ
Riattiva un dispositivo sospeso (se licenze disponibili):
if active_count >= allowed_licenses:
raise BadRequestError("Nessuna licenza disponibile")
# Imposta suspended=False
POST /api/devices/select-activeβ
Seleziona quali dispositivi mantenere attivi durante downgrade:
# Riceve: device_ids_to_keep
# Sospende tutti gli altri
# Usato da DeviceSelectionModal
Webhook Subscriptionβ
Quando un utente cambia piano (upgrade/downgrade):
# In billing/webhooks.py
@router.post("/stripe/webhook")
async def stripe_webhook():
# 1. Riceve evento da Stripe/Apple/Google
# 2. Aggiorna allowed_licenses nel database
# 3. Se downgrade e active > allowed:
# - Invia notifica push all'utente
# - Utente deve aprire app e risolvere
Copertura Scenariβ
β Scenari Copertiβ
| Scenario | iOS | Android | Web | Backend |
|---|---|---|---|---|
| Nuovo utente (0 lic, 0 dev) | β | π TODO | π TODO | β |
| Aggiunta device senza licenze | β | π TODO | π TODO | β |
| Aggiunta device con licenze | β | β | β | β |
| Aggiunta oltre limite | β | π TODO | π TODO | β |
| Downgrade con squilibrio | β | β | β | β |
| Upgrade | β | β | β | β |
| Sospensione dispositivo | β | β | β | β |
| Riattivazione dispositivo | β | β | β | β |
| Cambio provider (bloccato) | β | β | β | β |
π Prossimi Passiβ
Android:
- Implementare controllo licenze pre-claim in
AddDeviceActivity - Aggiungere alert "No License" e "Limit Reached"
Web:
- Implementare controllo licenze pre-claim in
AddDeviceModal - Aggiungere modal differenziati per scenari
Diagramma Flusso Decisionaleβ
Note Implementativeβ
iOSβ
File Chiave:
MainTabView.swift- Check licenze all'apertura appAddDeviceView.swift- Validazione pre-claimSubscriptionRequiredModal.swift- Modal squilibrioDeviceSelectionModal.swift- Selezione dispositivi da mantenere
Logging:
Logger.shared.info("=== checkLicenseStatus START ===")
Logger.shared.info("License status", context: [
"allowed": status.allowed,
"active": status.active,
"suspended": status.suspended
])
Backendβ
File Chiave:
billing/routes/subscriptions.py- Gestione subscriptiondevices/license_utils.py- Logica sospensione/riattivazionedevices/routes/devices.py- Endpoint suspend/reactivatebilling/webhooks.py- Webhook provider
Database:
-- user_device_attributes
ALTER TABLE user_device_attributes
ADD COLUMN suspended BOOLEAN DEFAULT FALSE;
Supportoβ
Per domande o problemi relativi al modello di subscription:
- Backend: Vedi
billing/edevices/license_utils.py - iOS: Vedi
MainTabView.swifteAddDeviceView.swift - Android: Vedi
SubscriptionRequiredDialog.kteDeviceSelectionDialog.kt - Web: Vedi
subscription-required-modal.tsxedevice-selection-modal.tsx