622 lines
20 KiB
TypeScript
622 lines
20 KiB
TypeScript
import { db } from '../db/client.js';
|
|
import type { BillingInterval, BillingPlan, RescueVerificationStatus, SubscriptionStatus, 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<WorkspaceRow>(
|
|
`SELECT id, name, workspace_type, billing_email, billing_plan, billing_interval, subscription_status, stripe_customer_id, stripe_subscription_id, 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<WorkspaceMemberRow>(
|
|
`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;
|
|
billing_interval: BillingInterval;
|
|
subscription_status: WorkspaceRow['subscription_status'];
|
|
stripe_customer_id: string | null;
|
|
stripe_subscription_id: string | null;
|
|
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.billing_interval,
|
|
workspaces.subscription_status,
|
|
workspaces.stripe_customer_id,
|
|
workspaces.stripe_subscription_id,
|
|
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 workspaceId = await getNextWorkspaceId();
|
|
|
|
await db.query(
|
|
`INSERT INTO workspaces (id, name, workspace_type, billing_plan, billing_interval, billing_email, subscription_status, rescue_verification_status)
|
|
VALUES ($1, $2, 'standard', 'household_basic', 'monthly', $3, 'none', 'not_required')`,
|
|
[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 ensureDefaultWorkspaceForUser = 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
|
|
ORDER BY workspaces.created_at ASC
|
|
LIMIT 1`,
|
|
[user.id],
|
|
);
|
|
|
|
if (existing.rowCount) {
|
|
return Number(existing.rows[0].workspace_id);
|
|
}
|
|
|
|
return ensurePersonalWorkspaceForUser(user);
|
|
};
|
|
|
|
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,
|
|
billingInterval,
|
|
owner,
|
|
}: {
|
|
id: number;
|
|
name: string;
|
|
workspaceType: WorkspaceType;
|
|
billingEmail: string | null;
|
|
billingPlan: BillingPlan;
|
|
billingInterval: BillingInterval;
|
|
owner: UserRow;
|
|
}) => {
|
|
await db.query(
|
|
`INSERT INTO workspaces (id, name, workspace_type, billing_email, billing_plan, billing_interval, subscription_status, rescue_verification_status)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
[
|
|
id,
|
|
name,
|
|
workspaceType,
|
|
billingEmail,
|
|
billingPlan,
|
|
billingInterval,
|
|
workspaceType === 'rescue' ? 'active' : 'none',
|
|
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,
|
|
billingInterval,
|
|
}: {
|
|
workspaceId: number;
|
|
name: string;
|
|
workspaceType: WorkspaceType;
|
|
billingEmail: string | null;
|
|
billingPlan: BillingPlan;
|
|
billingInterval: BillingInterval;
|
|
}) => {
|
|
const result = await db.query<WorkspaceRow>(
|
|
`WITH input AS (
|
|
SELECT
|
|
$1::integer AS workspace_id,
|
|
$2::varchar AS name,
|
|
$3::varchar AS workspace_type,
|
|
$4::varchar AS billing_email,
|
|
$5::varchar AS billing_plan,
|
|
$6::varchar AS billing_interval
|
|
)
|
|
UPDATE workspaces
|
|
SET name = input.name,
|
|
workspace_type = input.workspace_type,
|
|
billing_email = input.billing_email,
|
|
billing_plan = input.billing_plan,
|
|
billing_interval = input.billing_interval,
|
|
rescue_verification_status = CASE
|
|
WHEN input.workspace_type = 'rescue' AND workspaces.rescue_verification_status = 'not_required' THEN 'pending'
|
|
WHEN input.workspace_type = 'standard' THEN 'not_required'
|
|
ELSE workspaces.rescue_verification_status
|
|
END,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
FROM input
|
|
WHERE workspaces.id = input.workspace_id
|
|
RETURNING workspaces.id, workspaces.name, workspaces.workspace_type, workspaces.billing_email, workspaces.billing_plan, workspaces.billing_interval, workspaces.subscription_status, workspaces.stripe_customer_id, workspaces.stripe_subscription_id, workspaces.rescue_verification_status, workspaces.created_at, workspaces.updated_at`,
|
|
[workspaceId, name, workspaceType, billingEmail, billingPlan, billingInterval],
|
|
);
|
|
|
|
return result.rows[0] ?? null;
|
|
};
|
|
|
|
export const listWorkspaceMembers = async (workspaceId: number) => {
|
|
const result = await db.query<WorkspaceMemberRow>(
|
|
`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 listWorkspaceNotificationEmails = async (workspaceId: number) => {
|
|
const result = await db.query<{ email: string }>(
|
|
`SELECT DISTINCT LOWER(TRIM(email)) AS email
|
|
FROM (
|
|
SELECT COALESCE(invite_email, email) AS email
|
|
FROM workspace_members
|
|
WHERE workspace_id = $1
|
|
UNION
|
|
SELECT billing_email AS email
|
|
FROM workspaces
|
|
WHERE id = $1
|
|
) contact_emails
|
|
WHERE email IS NOT NULL
|
|
AND TRIM(email) <> ''
|
|
ORDER BY email ASC`,
|
|
[workspaceId],
|
|
);
|
|
|
|
return result.rows.map((row) => row.email);
|
|
};
|
|
|
|
export const findAlternateWorkspaceForUser = async (userId: string, excludeWorkspaceId: number) => {
|
|
const result = await db.query<{ workspace_id: number }>(
|
|
`SELECT workspace_id
|
|
FROM workspace_members
|
|
WHERE user_id = $1
|
|
AND workspace_id <> $2
|
|
ORDER BY created_at ASC
|
|
LIMIT 1`,
|
|
[userId, excludeWorkspaceId],
|
|
);
|
|
|
|
return result.rows[0] ? Number(result.rows[0].workspace_id) : null;
|
|
};
|
|
|
|
export const listOwnedWorkspacesByOwnerEmail = async (ownerEmail: string, excludeWorkspaceId: number) => {
|
|
const result = await db.query<WorkspaceRow>(
|
|
`SELECT workspaces.id, workspaces.name, workspaces.workspace_type, workspaces.billing_email, workspaces.billing_plan, workspaces.billing_interval, workspaces.subscription_status, workspaces.stripe_customer_id, workspaces.stripe_subscription_id, workspaces.rescue_verification_status, workspaces.created_at, workspaces.updated_at
|
|
FROM workspace_members
|
|
INNER JOIN workspaces ON workspaces.id = workspace_members.workspace_id
|
|
WHERE LOWER(COALESCE(workspace_members.invite_email, workspace_members.email)) = LOWER($1)
|
|
AND workspace_members.role = 'owner'
|
|
AND workspace_members.accepted_at IS NOT NULL
|
|
AND workspaces.id <> $2
|
|
ORDER BY workspaces.created_at ASC`,
|
|
[ownerEmail, excludeWorkspaceId],
|
|
);
|
|
|
|
return result.rows;
|
|
};
|
|
|
|
export const getWorkspaceBirdCount = async (workspaceId: number) => {
|
|
const birdCount = await db.query<{ count: string }>(
|
|
`SELECT COUNT(*)::text AS count
|
|
FROM birds
|
|
WHERE workspace_id = $1
|
|
AND memorialized_at IS NULL`,
|
|
[workspaceId],
|
|
);
|
|
|
|
return Number(birdCount.rows[0]?.count ?? 0);
|
|
};
|
|
|
|
export const getWorkspaceTotalBirdCount = async (workspaceId: number) => {
|
|
const birdCount = await db.query<{ count: string }>(
|
|
`SELECT COUNT(*)::text AS count
|
|
FROM birds
|
|
WHERE workspace_id = $1`,
|
|
[workspaceId],
|
|
);
|
|
|
|
return Number(birdCount.rows[0]?.count ?? 0);
|
|
};
|
|
|
|
export const deleteWorkspaceIfEmpty = async (workspaceId: number) => {
|
|
if ((await getWorkspaceTotalBirdCount(workspaceId)) > 0) {
|
|
return { deleted: false as const, reason: 'birds_present' as const };
|
|
}
|
|
|
|
const deleted = await db.query<{ id: number }>(
|
|
`DELETE FROM workspaces
|
|
WHERE id = $1
|
|
RETURNING id`,
|
|
[workspaceId],
|
|
);
|
|
|
|
if (!deleted.rowCount) {
|
|
return { deleted: false as const, reason: 'not_found' as const };
|
|
}
|
|
|
|
return { deleted: true as const };
|
|
};
|
|
|
|
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<WorkspaceMemberRow>(
|
|
`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,
|
|
workspaceId,
|
|
requesterMemberId,
|
|
requesterIsBillingOwner,
|
|
}: {
|
|
memberId: string;
|
|
workspaceId: number;
|
|
requesterMemberId: string;
|
|
requesterIsBillingOwner: boolean;
|
|
}) => {
|
|
const result = await db.query<{ id: string }>(
|
|
`DELETE FROM workspace_members
|
|
WHERE id = $1
|
|
AND workspace_id = $2
|
|
AND (
|
|
role <> 'owner'
|
|
OR (
|
|
$3 = TRUE
|
|
AND id <> $4
|
|
)
|
|
)
|
|
RETURNING id`,
|
|
[memberId, workspaceId, requesterIsBillingOwner, requesterMemberId],
|
|
);
|
|
|
|
return Boolean(result.rowCount);
|
|
};
|
|
|
|
export const updateWorkspaceMemberRole = async ({
|
|
memberId,
|
|
workspaceId,
|
|
role,
|
|
requesterMemberId,
|
|
requesterIsBillingOwner,
|
|
requesterRole,
|
|
billingEmail,
|
|
}: {
|
|
memberId: string;
|
|
workspaceId: number;
|
|
role: WorkspaceMemberRow['role'];
|
|
requesterMemberId: string;
|
|
requesterIsBillingOwner: boolean;
|
|
requesterRole: WorkspaceMemberRow['role'];
|
|
billingEmail: string;
|
|
}) => {
|
|
const result = await db.query<WorkspaceMemberRow>(
|
|
`UPDATE workspace_members
|
|
SET role = $3
|
|
WHERE id = $1
|
|
AND workspace_id = $2
|
|
AND (
|
|
$3 <> 'owner'
|
|
OR $4 = TRUE
|
|
)
|
|
AND (
|
|
role <> 'owner'
|
|
OR (
|
|
id <> $5
|
|
AND (
|
|
$4 = TRUE
|
|
OR (
|
|
$7 = 'owner'
|
|
AND LOWER(BTRIM(COALESCE(invite_email, email))) <> LOWER(BTRIM($6))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
RETURNING id, workspace_id, user_id, COALESCE(invite_email, email) AS invite_email, name, role, accepted_at::text, created_at`,
|
|
[memberId, workspaceId, role, requesterIsBillingOwner, requesterMemberId, billingEmail, requesterRole],
|
|
);
|
|
|
|
return result.rows[0] ?? null;
|
|
};
|
|
|
|
export const listRescueWorkspacesForAdmin = async () => {
|
|
const result = await db.query<
|
|
WorkspaceRow & {
|
|
bird_count: number;
|
|
member_count: number;
|
|
}
|
|
>(
|
|
`SELECT
|
|
workspaces.id,
|
|
workspaces.name,
|
|
workspaces.workspace_type,
|
|
workspaces.billing_email,
|
|
workspaces.billing_plan,
|
|
workspaces.billing_interval,
|
|
workspaces.subscription_status,
|
|
workspaces.stripe_customer_id,
|
|
workspaces.stripe_subscription_id,
|
|
workspaces.rescue_verification_status,
|
|
workspaces.created_at,
|
|
workspaces.updated_at,
|
|
COUNT(DISTINCT birds.id)::int AS bird_count,
|
|
COUNT(DISTINCT workspace_members.id)::int AS member_count
|
|
FROM workspaces
|
|
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
|
|
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<WorkspaceRow>(
|
|
`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,
|
|
billing_interval = CASE
|
|
WHEN $2 = 'rejected' THEN 'monthly'
|
|
ELSE billing_interval
|
|
END,
|
|
subscription_status = CASE
|
|
WHEN $2 = 'rejected' THEN 'none'
|
|
ELSE subscription_status
|
|
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, billing_interval, subscription_status, stripe_customer_id, stripe_subscription_id, 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<WorkspaceRow>(
|
|
`UPDATE workspaces
|
|
SET workspace_type = 'standard',
|
|
billing_plan = 'household_basic',
|
|
billing_interval = 'monthly',
|
|
subscription_status = 'none',
|
|
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, billing_interval, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at`,
|
|
[workspaceId],
|
|
);
|
|
|
|
return result.rows[0] ?? null;
|
|
};
|
|
|
|
export const setWorkspaceStripeCustomerId = async (workspaceId: number, stripeCustomerId: string) => {
|
|
const result = await db.query<WorkspaceRow>(
|
|
`UPDATE workspaces
|
|
SET stripe_customer_id = $2,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
RETURNING id, name, workspace_type, billing_email, billing_plan, billing_interval, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at`,
|
|
[workspaceId, stripeCustomerId],
|
|
);
|
|
|
|
return result.rows[0] ?? null;
|
|
};
|
|
|
|
export const setWorkspaceStripeSubscription = async ({
|
|
workspaceId,
|
|
stripeCustomerId,
|
|
stripeSubscriptionId,
|
|
subscriptionStatus,
|
|
billingPlan,
|
|
billingInterval,
|
|
}: {
|
|
workspaceId: number;
|
|
stripeCustomerId: string | null;
|
|
stripeSubscriptionId: string;
|
|
subscriptionStatus: SubscriptionStatus;
|
|
billingPlan?: BillingPlan | null;
|
|
billingInterval?: BillingInterval | null;
|
|
}) => {
|
|
const result = await db.query<WorkspaceRow>(
|
|
`UPDATE workspaces
|
|
SET stripe_customer_id = COALESCE($2, stripe_customer_id),
|
|
stripe_subscription_id = $3,
|
|
subscription_status = $4,
|
|
billing_plan = COALESCE($5, billing_plan),
|
|
billing_interval = COALESCE($6, billing_interval),
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
RETURNING id, name, workspace_type, billing_email, billing_plan, billing_interval, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at`,
|
|
[workspaceId, stripeCustomerId, stripeSubscriptionId, subscriptionStatus, billingPlan ?? null, billingInterval ?? null],
|
|
);
|
|
|
|
return result.rows[0] ?? null;
|
|
};
|
|
|
|
export const setWorkspaceSubscriptionStatusByStripeSubscriptionId = async (
|
|
stripeSubscriptionId: string,
|
|
subscriptionStatus: SubscriptionStatus,
|
|
billingPlan?: BillingPlan | null,
|
|
billingInterval?: BillingInterval | null,
|
|
) => {
|
|
const result = await db.query<WorkspaceRow>(
|
|
`UPDATE workspaces
|
|
SET subscription_status = $2,
|
|
billing_plan = COALESCE($3, billing_plan),
|
|
billing_interval = COALESCE($4, billing_interval),
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE stripe_subscription_id = $1
|
|
RETURNING id, name, workspace_type, billing_email, billing_plan, billing_interval, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at`,
|
|
[stripeSubscriptionId, subscriptionStatus, billingPlan ?? null, billingInterval ?? null],
|
|
);
|
|
|
|
return result.rows[0] ?? null;
|
|
};
|
|
|
|
export const getPlatformAdminSummary = async () => {
|
|
const result = await db.query<{
|
|
total_birds: number;
|
|
memorialized_birds: number;
|
|
total_users: number;
|
|
total_workspaces: number;
|
|
rescue_workspaces: number;
|
|
rescue_birds: number;
|
|
pending_rescues: number;
|
|
daily_users: number;
|
|
}>(
|
|
`SELECT
|
|
(SELECT COUNT(*)::int FROM birds) AS total_birds,
|
|
(SELECT COUNT(*)::int FROM birds WHERE memorialized_at IS NOT NULL) AS memorialized_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 birds INNER JOIN workspaces ON workspaces.id = birds.workspace_id WHERE workspaces.workspace_type = 'rescue') AS rescue_birds,
|
|
(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];
|
|
};
|