import { db } from '../db/client.js'; import type { BillingPlan, RescueVerificationStatus, UserRow, WorkspaceMemberRow, WorkspaceRow, WorkspaceType } from '../types.js'; export const getNextWorkspaceId = async () => { const result = await db.query<{ next_id: number }>('SELECT COALESCE(MAX(id), 0) + 1 AS next_id FROM workspaces'); return Number(result.rows[0]?.next_id ?? 1); }; export const getWorkspaceById = async (workspaceId: number) => { const result = await db.query( `SELECT id, name, workspace_type, billing_email, billing_plan, subscription_status, rescue_verification_status, created_at, updated_at FROM workspaces WHERE id = $1`, [workspaceId], ); return result.rows[0] ?? null; }; export const getMembershipForUser = async (userId: string, workspaceId: number) => { const result = await db.query( `SELECT id, workspace_id, user_id, COALESCE(invite_email, email) AS invite_email, name, role, accepted_at::text, created_at FROM workspace_members WHERE workspace_id = $1 AND user_id = $2`, [workspaceId, userId], ); return result.rows[0] ?? null; }; export const listMembershipsForUser = async (userId: string) => { const result = await db.query< WorkspaceMemberRow & { workspace_name: string; workspace_type: WorkspaceType; billing_email: string | null; billing_plan: BillingPlan; subscription_status: WorkspaceRow['subscription_status']; rescue_verification_status: RescueVerificationStatus; workspace_created_at: string; workspace_updated_at: string; } >( `SELECT workspace_members.id, workspace_members.workspace_id, workspace_members.user_id, COALESCE(workspace_members.invite_email, workspace_members.email) AS invite_email, workspace_members.name, workspace_members.role, workspace_members.accepted_at::text, workspace_members.created_at, workspaces.name AS workspace_name, workspaces.workspace_type, workspaces.billing_email, workspaces.billing_plan, workspaces.subscription_status, workspaces.rescue_verification_status, workspaces.created_at AS workspace_created_at, workspaces.updated_at AS workspace_updated_at FROM workspace_members INNER JOIN workspaces ON workspaces.id = workspace_members.workspace_id WHERE workspace_members.user_id = $1 ORDER BY workspaces.created_at ASC`, [userId], ); return result.rows; }; export const ensurePersonalWorkspaceForUser = async (user: UserRow) => { const existing = await db.query<{ workspace_id: number }>( `SELECT workspace_id FROM workspace_members INNER JOIN workspaces ON workspaces.id = workspace_members.workspace_id WHERE workspace_members.user_id = $1 AND workspaces.workspace_type = 'standard' ORDER BY workspaces.created_at ASC LIMIT 1`, [user.id], ); if (existing.rowCount) { return Number(existing.rows[0].workspace_id); } const unclaimed = await db.query<{ workspace_id: number }>( `SELECT workspaces.id AS workspace_id FROM workspaces LEFT JOIN workspace_members ON workspace_members.workspace_id = workspaces.id WHERE workspaces.id = 1 GROUP BY workspaces.id HAVING COUNT(workspace_members.id) = 0 LIMIT 1`, ); const workspaceId = unclaimed.rowCount ? Number(unclaimed.rows[0].workspace_id) : await getNextWorkspaceId(); if (!unclaimed.rowCount) { await db.query( `INSERT INTO workspaces (id, name, workspace_type, billing_plan, billing_email, subscription_status, rescue_verification_status) VALUES ($1, $2, 'standard', 'household_basic', $3, 'active', 'not_required')`, [workspaceId, `${user.name}'s Flock`, user.email], ); } else { await db.query( `UPDATE workspaces SET name = $2, workspace_type = 'standard', billing_plan = 'household_basic', billing_email = $3, subscription_status = 'active', rescue_verification_status = 'not_required', updated_at = CURRENT_TIMESTAMP WHERE id = $1`, [workspaceId, `${user.name}'s Flock`, user.email], ); } await db.query( `INSERT INTO workspace_members (workspace_id, user_id, email, invite_email, name, role, accepted_at) VALUES ($1, $2, $3, $3, $4, 'owner', CURRENT_TIMESTAMP) ON CONFLICT (workspace_id, invite_email) DO UPDATE SET user_id = EXCLUDED.user_id, email = EXCLUDED.email, name = EXCLUDED.name, role = 'owner', accepted_at = CURRENT_TIMESTAMP`, [workspaceId, user.id, user.email, user.name], ); return workspaceId; }; export const claimWorkspaceInvites = async (user: UserRow) => { await db.query( `UPDATE workspace_members SET user_id = $1, accepted_at = CURRENT_TIMESTAMP WHERE LOWER(COALESCE(invite_email, email)) = LOWER($2) AND user_id IS NULL`, [user.id, user.email], ); }; export const createWorkspace = async ({ id, name, workspaceType, billingEmail, billingPlan, owner, }: { id: number; name: string; workspaceType: WorkspaceType; billingEmail: string | null; billingPlan: BillingPlan; owner: UserRow; }) => { await db.query( `INSERT INTO workspaces (id, name, workspace_type, billing_email, billing_plan, subscription_status, rescue_verification_status) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [ id, name, workspaceType, billingEmail, billingPlan, workspaceType === 'rescue' ? 'active' : 'active', workspaceType === 'rescue' ? 'pending' : 'not_required', ], ); await db.query( `INSERT INTO workspace_members (workspace_id, user_id, email, invite_email, name, role, accepted_at) VALUES ($1, $2, $3, $3, $4, 'owner', CURRENT_TIMESTAMP)`, [id, owner.id, owner.email, owner.name], ); return getWorkspaceById(id); }; export const updateWorkspace = async ({ workspaceId, name, workspaceType, billingEmail, billingPlan, }: { workspaceId: number; name: string; workspaceType: WorkspaceType; billingEmail: string | null; billingPlan: BillingPlan; }) => { const result = await db.query( `UPDATE workspaces SET name = $2, workspace_type = $3, billing_email = $4, billing_plan = $5, rescue_verification_status = CASE WHEN $3 = 'rescue' AND rescue_verification_status = 'not_required' THEN 'pending' WHEN $3 = 'standard' THEN 'not_required' ELSE rescue_verification_status END, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, rescue_verification_status, created_at, updated_at`, [workspaceId, name, workspaceType, billingEmail, billingPlan], ); return result.rows[0] ?? null; }; export const listWorkspaceMembers = async (workspaceId: number) => { const result = await db.query( `SELECT id, workspace_id, user_id, COALESCE(invite_email, email) AS invite_email, name, role, accepted_at::text, created_at FROM workspace_members WHERE workspace_id = $1 ORDER BY created_at ASC`, [workspaceId], ); return result.rows; }; export const upsertWorkspaceMember = async ({ workspaceId, inviteEmail, name, role, existingUser, }: { workspaceId: number; inviteEmail: string; name: string; role: WorkspaceMemberRow['role']; existingUser: UserRow | null; }) => { const result = await db.query( `INSERT INTO workspace_members (workspace_id, user_id, email, invite_email, name, role, accepted_at) VALUES ($1, $2, $3, $3, $4, $5, $6) ON CONFLICT (workspace_id, invite_email) DO UPDATE SET name = EXCLUDED.name, role = EXCLUDED.role, email = EXCLUDED.email, user_id = COALESCE(workspace_members.user_id, EXCLUDED.user_id), accepted_at = COALESCE(workspace_members.accepted_at, EXCLUDED.accepted_at) RETURNING id, workspace_id, user_id, COALESCE(invite_email, email) AS invite_email, name, role, accepted_at::text, created_at`, [workspaceId, existingUser?.id ?? null, inviteEmail, name, role, existingUser ? new Date().toISOString() : null], ); return result.rows[0] ?? null; }; export const deleteWorkspaceMember = async (memberId: string, workspaceId: number) => { const result = await db.query<{ id: string }>( `DELETE FROM workspace_members WHERE id = $1 AND workspace_id = $2 AND role <> 'owner' RETURNING id`, [memberId, workspaceId], ); return Boolean(result.rowCount); }; export const listRescueWorkspacesForAdmin = async () => { const result = await db.query< WorkspaceRow & { owner_email: string | null; bird_count: number; member_count: number; } >( `SELECT workspaces.id, workspaces.name, workspaces.workspace_type, workspaces.billing_email, workspaces.billing_plan, workspaces.subscription_status, workspaces.rescue_verification_status, workspaces.created_at, workspaces.updated_at, owner.invite_email AS owner_email, COUNT(DISTINCT birds.id)::int AS bird_count, COUNT(DISTINCT workspace_members.id)::int AS member_count FROM workspaces LEFT JOIN workspace_members owner ON owner.workspace_id = workspaces.id AND owner.role = 'owner' LEFT JOIN birds ON birds.workspace_id = workspaces.id LEFT JOIN workspace_members ON workspace_members.workspace_id = workspaces.id WHERE workspaces.workspace_type = 'rescue' GROUP BY workspaces.id, owner.invite_email ORDER BY CASE workspaces.rescue_verification_status WHEN 'pending' THEN 0 WHEN 'approved' THEN 1 WHEN 'rejected' THEN 2 ELSE 3 END, workspaces.created_at DESC`, ); return result.rows; }; export const updateRescueVerificationStatus = async (workspaceId: number, status: RescueVerificationStatus) => { const result = await db.query( `UPDATE workspaces SET workspace_type = CASE WHEN $2 = 'rejected' THEN 'standard' ELSE workspace_type END, billing_plan = CASE WHEN $2 = 'rejected' THEN 'household_basic' ELSE billing_plan END, rescue_verification_status = CASE WHEN $2 = 'rejected' THEN 'not_required' ELSE $2 END, updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND workspace_type = 'rescue' RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, rescue_verification_status, created_at, updated_at`, [workspaceId, status], ); return result.rows[0] ?? null; }; export const cancelRescueVerificationRequest = async (workspaceId: number) => { const result = await db.query( `UPDATE workspaces SET workspace_type = 'standard', billing_plan = 'household_basic', rescue_verification_status = 'not_required', updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND workspace_type = 'rescue' AND rescue_verification_status = 'pending' RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, rescue_verification_status, created_at, updated_at`, [workspaceId], ); return result.rows[0] ?? null; }; export const getPlatformAdminSummary = async () => { const result = await db.query<{ total_birds: number; total_users: number; total_workspaces: number; rescue_workspaces: number; pending_rescues: number; daily_users: number; }>( `SELECT (SELECT COUNT(*)::int FROM birds) AS total_birds, (SELECT COUNT(*)::int FROM users) AS total_users, (SELECT COUNT(*)::int FROM workspaces) AS total_workspaces, (SELECT COUNT(*)::int FROM workspaces WHERE workspace_type = 'rescue') AS rescue_workspaces, (SELECT COUNT(*)::int FROM workspaces WHERE workspace_type = 'rescue' AND rescue_verification_status = 'pending') AS pending_rescues, (SELECT COUNT(DISTINCT user_id)::int FROM auth_sessions WHERE created_at >= CURRENT_DATE) AS daily_users`, ); return result.rows[0]; };