/** * Multi-tenant teams (orgs). An org has an owner and members; the org's * subscription tier is inherited by all members (so a team owner pays * once and seats share the tier). Minimal v1: add existing users by * email; token-based email invites are a later refinement. */ import { getDb } from '../db/pool.js'; import { newId } from '../lib/crypto.js'; import { getUserByEmail, getSubscription, type Tier } from './store.js'; export interface Org { id: string; name: string; owner_user_id: string; created_at: string; } export interface OrgMember { id: string; org_id: string; user_id: string; role: string; created_at: string; email?: string; } export function createOrg(name: string, ownerUserId: string): Org { const id = newId(); const db = getDb(); db.prepare('INSERT INTO orgs (id, name, owner_user_id) VALUES (?,?,?)').run(id, name, ownerUserId); db.prepare('INSERT INTO org_members (id, org_id, user_id, role) VALUES (?,?,?,?)') .run(newId(), id, ownerUserId, 'owner'); return getOrgById(id)!; } export function getOrgById(id: string): Org | null { return (getDb().prepare('SELECT * FROM orgs WHERE id = ?').get(id) as Org | undefined) ?? null; } export function getOrgsForUser(userId: string): (Org & { role: string })[] { return getDb().prepare(` SELECT o.*, m.role FROM orgs o JOIN org_members m ON m.org_id = o.id WHERE m.user_id = ? ORDER BY o.created_at `).all(userId) as (Org & { role: string })[]; } export function isOwner(orgId: string, userId: string): boolean { const row = getDb().prepare('SELECT role FROM org_members WHERE org_id = ? AND user_id = ?') .get(orgId, userId) as { role: string } | undefined; return row?.role === 'owner'; } export function isMember(orgId: string, userId: string): boolean { return !!getDb().prepare('SELECT 1 FROM org_members WHERE org_id = ? AND user_id = ?').get(orgId, userId); } export function getMembers(orgId: string): OrgMember[] { return getDb().prepare(` SELECT m.*, u.email FROM org_members m JOIN users u ON u.id = m.user_id WHERE m.org_id = ? ORDER BY m.created_at `).all(orgId) as OrgMember[]; } export function addMemberByEmail(orgId: string, email: string, role = 'member'): { ok: boolean; error?: string } { const user = getUserByEmail(email.trim().toLowerCase()); if (!user) return { ok: false, error: 'user_not_found' }; // must have an account (invites: later) if (isMember(orgId, user.id)) return { ok: false, error: 'already_member' }; getDb().prepare('INSERT INTO org_members (id, org_id, user_id, role) VALUES (?,?,?,?)') .run(newId(), orgId, user.id, role); return { ok: true }; } export function removeMember(orgId: string, userId: string): void { // Never remove the owner via this path. getDb().prepare("DELETE FROM org_members WHERE org_id = ? AND user_id = ? AND role != 'owner'") .run(orgId, userId); } /** * The best active tier a user inherits from any org they belong to * (via that org's owner's subscription). Returns null if none active. */ export function inheritedTier(userId: string): Tier | null { const orgs = getOrgsForUser(userId); let best: Tier | null = null; const rank: Record = { basic: 1, pro: 2, max: 3 }; for (const org of orgs) { const sub = getSubscription(org.owner_user_id); if (sub && (sub.status === 'active' || sub.status === 'trialing')) { if (!best || rank[sub.tier] > rank[best]) best = sub.tier; } } return best; }