From 4715306d14ba8379258444c7d0b1d6aa7dbbb13f Mon Sep 17 00:00:00 2001 From: blaisadmin Date: Thu, 21 May 2026 13:30:28 -0400 Subject: [PATCH] improved rescue workflow --- backend/src/app.ts | 59 ++++++++++++------- .../src/repositories/workspaceRepository.ts | 7 +-- frontend/src/App.tsx | 19 +++--- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index af55def..1ded93f 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -468,13 +468,11 @@ const normalizeWorkspace = (row: WorkspaceRow) => ({ const normalizeAdminRescueWorkspace = ( row: WorkspaceRow & { - owner_email: string | null; bird_count: number; member_count: number; }, ) => ({ workspace: normalizeWorkspace(row), - ownerEmail: row.owner_email, birdCount: Number(row.bird_count ?? 0), memberCount: Number(row.member_count ?? 0), }); @@ -1267,10 +1265,12 @@ const sendRescueStatusNotification = async ({ workspace, ownerEmail, event, + note, }: { workspace: WorkspaceRow; ownerEmail: string | null; event: 'created' | 'converted' | 'status_changed' | 'canceled'; + note?: string | null; }) => { const statusLabel = workspace.rescue_verification_status.replace(/_/g, ' '); const eventLabel = @@ -1294,6 +1294,11 @@ const sendRescueStatusNotification = async ({ `Billing email: ${workspace.billing_email ?? 'not set'}`, `Flock ID: ${workspace.id}`, ]; + const escapedNote = note ? escapeHtml(note) : null; + + if (note) { + lines.push(`Note: ${note}`); + } if (!mailTransport) { console.log(`Rescue status notification for ${rescueStatusNotificationEmail}:\n${lines.join('\n')}`); @@ -1314,6 +1319,7 @@ const sendRescueStatusNotification = async ({
  • Billing email: ${escapedBillingEmail}
  • Flock ID: ${workspace.id}
  • + ${escapedNote ? `

    Note: ${escapedNote}

    ` : ''} `, }); @@ -1368,6 +1374,17 @@ const sendRescueOnboardingWebhook = async ({ } }; +const trySendRescueOnboardingWebhook = async (payload: Parameters[0]) => { + try { + await sendRescueOnboardingWebhook(payload); + return null; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown rescue onboarding webhook error.'; + console.error(`Rescue onboarding webhook failed for workspace ${payload.workspaceId}:`, error); + return `The rescue onboarding webhook failed and this rescue requires manual review. ${errorMessage}`; + } +}; + const issueMagicLinkInvite = async ({ email, name, @@ -2553,15 +2570,6 @@ app.post('/api/workspaces', requireAuth, requireSessionAuth, async (req: Request res.status(400).json({ error: 'Rescue onboarding details are required.' }); return; } - - await sendRescueOnboardingWebhook({ - action: 'created', - workspaceId, - flockName: parsed.data.name, - ownerEmail: req.auth!.user.email, - requestedByUserId: req.auth!.user.id, - rescueOnboarding: parsed.data.rescueOnboarding, - }); } const workspace = await createWorkspace({ @@ -2575,10 +2583,20 @@ app.post('/api/workspaces', requireAuth, requireSessionAuth, async (req: Request }); if (workspace?.workspace_type === 'rescue') { + const onboardingWebhookError = await trySendRescueOnboardingWebhook({ + action: 'created', + workspaceId: workspace.id, + flockName: workspace.name, + ownerEmail: req.auth!.user.email, + requestedByUserId: req.auth!.user.id, + rescueOnboarding: parsed.data.rescueOnboarding!, + }); + await sendRescueStatusNotification({ workspace, ownerEmail: req.auth!.user.email, event: 'created', + note: onboardingWebhookError, }); } @@ -2626,15 +2644,6 @@ app.put('/api/workspace', requireAuth, requireSessionAuth, requireWorkspaceRole( res.status(400).json({ error: 'Rescue onboarding details are required.' }); return; } - - await sendRescueOnboardingWebhook({ - action: 'converted', - workspaceId: currentWorkspace.id, - flockName: parsed.data.name, - ownerEmail: req.auth!.user.email, - requestedByUserId: req.auth!.user.id, - rescueOnboarding: parsed.data.rescueOnboarding, - }); } const workspace = await updateWorkspace({ @@ -2647,10 +2656,20 @@ app.put('/api/workspace', requireAuth, requireSessionAuth, requireWorkspaceRole( }); if (workspace?.workspace_type === 'rescue' && isConvertingToRescue) { + const onboardingWebhookError = await trySendRescueOnboardingWebhook({ + action: 'converted', + workspaceId: workspace.id, + flockName: workspace.name, + ownerEmail: req.auth!.user.email, + requestedByUserId: req.auth!.user.id, + rescueOnboarding: parsed.data.rescueOnboarding!, + }); + await sendRescueStatusNotification({ workspace, ownerEmail: req.auth!.user.email, event: 'converted', + note: onboardingWebhookError, }); } diff --git a/backend/src/repositories/workspaceRepository.ts b/backend/src/repositories/workspaceRepository.ts index c3d9ff1..44d9171 100644 --- a/backend/src/repositories/workspaceRepository.ts +++ b/backend/src/repositories/workspaceRepository.ts @@ -380,7 +380,6 @@ export const deleteWorkspaceMember = async (memberId: string, workspaceId: numbe export const listRescueWorkspacesForAdmin = async () => { const result = await db.query< WorkspaceRow & { - owner_email: string | null; bird_count: number; member_count: number; } @@ -398,17 +397,13 @@ export const listRescueWorkspacesForAdmin = async () => { 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 + GROUP BY workspaces.id ORDER BY CASE workspaces.rescue_verification_status WHEN 'pending' THEN 0 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2275a36..b58ff34 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -158,7 +158,6 @@ type AdminSummary = { type AdminRescueWorkspace = { workspace: Workspace; - ownerEmail: string | null; birdCount: number; memberCount: number; }; @@ -1003,6 +1002,16 @@ const formatBillingPlanBirdLimit = (billingPlan: BillingPlan) => { return null; }; +const formatBillingBirdUsage = (billingPlan: BillingPlan, birdCount: number) => { + const birdLimit = formatBillingPlanBirdLimit(billingPlan); + + if (!birdLimit) { + return `${birdCount} bird${birdCount === 1 ? '' : 's'}`; + } + + return `${birdCount} of ${birdLimit} birds used`; +}; + const formatSubscriptionStatus = (status: SubscriptionStatus) => { if (status === 'trialing') { return 'Trialing'; @@ -4554,7 +4563,7 @@ function App() { {formatRescueVerificationStatus(entry.workspace.rescueVerificationStatus)} • {entry.birdCount} birds • {entry.memberCount} members - Owner {entry.ownerEmail ?? 'unknown'} • Billing {entry.workspace.billingEmail ?? 'not set'} + Billing {entry.workspace.billingEmail ?? 'not set'}