115 lines
5.4 KiB
TypeScript
115 lines
5.4 KiB
TypeScript
import { z } from 'zod';
|
|
import { db } from './db';
|
|
import { canManageMembers, canManageBilling, slugifyOrg, withinSeats, type OrgRole } from '#shared/org';
|
|
|
|
export const createOrgSchema = z.object({ name: z.string().min(2).max(80) });
|
|
export const addMemberSchema = z.object({ email: z.string().email(), role: z.enum(['admin', 'member']) });
|
|
|
|
export async function getActiveOrg(userId: string) {
|
|
const membership = await db.membership.findFirst({
|
|
where: { userId },
|
|
orderBy: { createdAt: 'asc' },
|
|
include: { org: { include: { subscription: true } } },
|
|
});
|
|
if (!membership) return null;
|
|
return { org: membership.org, role: membership.role as OrgRole, membershipId: membership.id };
|
|
}
|
|
|
|
export function getUserOrgs(userId: string) {
|
|
return db.membership.findMany({ where: { userId }, include: { org: true }, orderBy: { createdAt: 'asc' } });
|
|
}
|
|
|
|
export async function createOrgForUser(userId: string, name: string) {
|
|
const base = slugifyOrg(name) || 'org';
|
|
let slug = base;
|
|
for (let i = 2; await db.organization.findUnique({ where: { slug } }); i++) slug = `${base}-${i}`;
|
|
|
|
return db.organization.create({
|
|
data: {
|
|
slug,
|
|
name,
|
|
ownerId: userId,
|
|
memberships: { create: { userId, role: 'owner' } },
|
|
subscription: { create: { plan: 'free', status: 'active', provider: 'mock' } },
|
|
},
|
|
include: { subscription: true },
|
|
});
|
|
}
|
|
|
|
export function listMembers(orgId: string) {
|
|
return db.membership.findMany({ where: { orgId }, include: { user: true }, orderBy: { createdAt: 'asc' } });
|
|
}
|
|
|
|
export async function getMembership(userId: string, orgId: string) {
|
|
const membership = await db.membership.findUnique({
|
|
where: { orgId_userId: { orgId, userId } },
|
|
include: { org: { include: { subscription: true } } },
|
|
});
|
|
if (!membership) return null;
|
|
return { role: membership.role as OrgRole, org: membership.org, membershipId: membership.id };
|
|
}
|
|
|
|
export async function addMember(orgId: string, actorRole: OrgRole, email: string, role: 'admin' | 'member') {
|
|
if (!canManageMembers(actorRole)) return { ok: false as const, error: 'You do not have permission to manage members.' };
|
|
const user = await db.user.findUnique({ where: { email: email.toLowerCase() } });
|
|
if (!user) return { ok: false as const, error: 'No user with that email. Ask them to sign up first.' };
|
|
if (await db.membership.findUnique({ where: { orgId_userId: { orgId, userId: user.id } } })) {
|
|
return { ok: false as const, error: 'That user is already a member.' };
|
|
}
|
|
const [sub, count] = await Promise.all([
|
|
db.orgSubscription.findUnique({ where: { orgId } }),
|
|
db.membership.count({ where: { orgId } }),
|
|
]);
|
|
if (!withinSeats(sub?.plan ?? 'free', count)) {
|
|
return { ok: false as const, error: `Your plan allows ${count} members. Upgrade to add more.` };
|
|
}
|
|
const membership = await db.membership.create({ data: { orgId, userId: user.id, role }, include: { user: true } });
|
|
return { ok: true as const, membership };
|
|
}
|
|
|
|
export async function updateMemberRole(orgId: string, actorRole: OrgRole, membershipId: string, role: 'admin' | 'member') {
|
|
if (!canManageMembers(actorRole)) return { ok: false as const, error: 'Forbidden' };
|
|
const target = await db.membership.findUnique({ where: { id: membershipId } });
|
|
if (!target || target.orgId !== orgId) return { ok: false as const, error: 'Not found' };
|
|
if (target.role === 'owner') return { ok: false as const, error: 'The owner role cannot be changed.' };
|
|
const membership = await db.membership.update({ where: { id: membershipId }, data: { role } });
|
|
return { ok: true as const, membership };
|
|
}
|
|
|
|
export async function removeMember(orgId: string, actorRole: OrgRole, membershipId: string) {
|
|
if (!canManageMembers(actorRole)) return { ok: false as const, error: 'Forbidden' };
|
|
const target = await db.membership.findUnique({ where: { id: membershipId } });
|
|
if (!target || target.orgId !== orgId) return { ok: false as const, error: 'Not found' };
|
|
if (target.role === 'owner') return { ok: false as const, error: 'The owner cannot be removed.' };
|
|
await db.membership.delete({ where: { id: membershipId } });
|
|
return { ok: true as const };
|
|
}
|
|
|
|
export function getSubscription(orgId: string) {
|
|
return db.orgSubscription.findUnique({ where: { orgId } });
|
|
}
|
|
|
|
export function listInvoices(orgId: string) {
|
|
return db.invoice.findMany({ where: { orgId }, orderBy: { createdAt: 'desc' }, take: 24 });
|
|
}
|
|
|
|
export async function subscribeOrg(orgId: string, actorRole: OrgRole, plan: 'free' | 'pro', provider: { name: string }) {
|
|
if (!canManageBilling(actorRole)) return { ok: false as const, error: 'Only the owner can change the plan.' };
|
|
const currentPeriodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
const subscription = await db.orgSubscription.upsert({
|
|
where: { orgId },
|
|
update: { plan, status: 'active', provider: provider.name, currentPeriodEnd },
|
|
create: { orgId, plan, status: 'active', provider: provider.name, currentPeriodEnd },
|
|
});
|
|
if (plan === 'pro') {
|
|
await db.invoice.create({ data: { orgId, amount: 2900, status: provider.name === 'mock' ? 'paid' : 'open' } });
|
|
}
|
|
return { ok: true as const, subscription };
|
|
}
|
|
|
|
export async function cancelOrg(orgId: string, actorRole: OrgRole) {
|
|
if (!canManageBilling(actorRole)) return { ok: false as const, error: 'Only the owner can cancel.' };
|
|
const subscription = await db.orgSubscription.update({ where: { orgId }, data: { plan: 'free', status: 'canceled' } });
|
|
return { ok: true as const, subscription };
|
|
}
|