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']}