app.bettersight.io/backend/services/licence_service.py

60 lines
2.4 KiB
Python

from repositories.tenant_repository import tenant_repository
from repositories.seat_repository import seat_repository
# Both trial and active tenants may use the dashboard and the Apps Script
# export plugin — only 'inactive' (cancelled, or trial expired with no
# card on file) blocks access.
VALID_STATUSES = ['active', 'trial']
def validate_email(email):
"""
Validates a PM's access to Bettersight via two checks: domain
registration and seat count.
Note: CLAUDE.md's earlier §9 draft described a third, sheet_id-binding
layer, but §15 explicitly removes it for the standalone Apps Script
model ("validation is email-only... remove the sheet_id binding layer
— it is redundant and would break the standalone model"). That later,
more specific instruction is what's implemented here.
Data flow:
email →
domain extraction → tenant lookup by domain (Layer 1) →
status check (must be trial or active) →
existing active seat → refresh last_accessed, allow →
no existing seat → seat count vs tenant.max_seats →
under limit: register new seat, allow / at limit: reject →
{ valid: bool, tier: str|None, reason: str|None }
"""
if not email or '@' not in email:
return {'valid': False, 'reason': 'Invalid email address'}
domain = email.split('@', 1)[1].lower()
# Layer 1: domain must belong to a known tenant in good standing
tenant = tenant_repository.get_by_domain(domain)
if not tenant:
return {'valid': False, 'reason': 'Domain not registered'}
if tenant.get('status') not in VALID_STATUSES:
return {'valid': False, 'reason': 'Subscription inactive'}
# Layer 2: existing seat is a fast-path allow with a freshness bump;
# a new seat must fit within the tenant's purchased seat count
existing_seat = seat_repository.get_active(tenant['id'], email)
if existing_seat:
seat_repository.update_last_accessed(existing_seat['id'])
return {'valid': True, 'tier': tenant['tier']}
active_count = seat_repository.count_active(tenant['id'])
if active_count >= tenant.get('max_seats', 0):
return {'valid': False, 'reason': 'Seat limit reached'}
seat_repository.create({
'tenant_id': tenant['id'],
'email': email,
'active': True,
'added_by': 'self',
})
return {'valid': True, 'tier': tenant['tier']}