improved rescue workflow

This commit is contained in:
blaisadmin
2026-05-21 13:30:28 -04:00
parent 62afc94f2f
commit 4715306d14
3 changed files with 52 additions and 33 deletions
+39 -20
View File
@@ -468,13 +468,11 @@ const normalizeWorkspace = (row: WorkspaceRow) => ({
const normalizeAdminRescueWorkspace = ( const normalizeAdminRescueWorkspace = (
row: WorkspaceRow & { row: WorkspaceRow & {
owner_email: string | null;
bird_count: number; bird_count: number;
member_count: number; member_count: number;
}, },
) => ({ ) => ({
workspace: normalizeWorkspace(row), workspace: normalizeWorkspace(row),
ownerEmail: row.owner_email,
birdCount: Number(row.bird_count ?? 0), birdCount: Number(row.bird_count ?? 0),
memberCount: Number(row.member_count ?? 0), memberCount: Number(row.member_count ?? 0),
}); });
@@ -1267,10 +1265,12 @@ const sendRescueStatusNotification = async ({
workspace, workspace,
ownerEmail, ownerEmail,
event, event,
note,
}: { }: {
workspace: WorkspaceRow; workspace: WorkspaceRow;
ownerEmail: string | null; ownerEmail: string | null;
event: 'created' | 'converted' | 'status_changed' | 'canceled'; event: 'created' | 'converted' | 'status_changed' | 'canceled';
note?: string | null;
}) => { }) => {
const statusLabel = workspace.rescue_verification_status.replace(/_/g, ' '); const statusLabel = workspace.rescue_verification_status.replace(/_/g, ' ');
const eventLabel = const eventLabel =
@@ -1294,6 +1294,11 @@ const sendRescueStatusNotification = async ({
`Billing email: ${workspace.billing_email ?? 'not set'}`, `Billing email: ${workspace.billing_email ?? 'not set'}`,
`Flock ID: ${workspace.id}`, `Flock ID: ${workspace.id}`,
]; ];
const escapedNote = note ? escapeHtml(note) : null;
if (note) {
lines.push(`Note: ${note}`);
}
if (!mailTransport) { if (!mailTransport) {
console.log(`Rescue status notification for ${rescueStatusNotificationEmail}:\n${lines.join('\n')}`); console.log(`Rescue status notification for ${rescueStatusNotificationEmail}:\n${lines.join('\n')}`);
@@ -1314,6 +1319,7 @@ const sendRescueStatusNotification = async ({
<li><strong>Billing email:</strong> ${escapedBillingEmail}</li> <li><strong>Billing email:</strong> ${escapedBillingEmail}</li>
<li><strong>Flock ID:</strong> ${workspace.id}</li> <li><strong>Flock ID:</strong> ${workspace.id}</li>
</ul> </ul>
${escapedNote ? `<p><strong>Note:</strong> ${escapedNote}</p>` : ''}
`, `,
}); });
@@ -1368,6 +1374,17 @@ const sendRescueOnboardingWebhook = async ({
} }
}; };
const trySendRescueOnboardingWebhook = async (payload: Parameters<typeof sendRescueOnboardingWebhook>[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 ({ const issueMagicLinkInvite = async ({
email, email,
name, name,
@@ -2553,15 +2570,6 @@ app.post('/api/workspaces', requireAuth, requireSessionAuth, async (req: Request
res.status(400).json({ error: 'Rescue onboarding details are required.' }); res.status(400).json({ error: 'Rescue onboarding details are required.' });
return; 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({ const workspace = await createWorkspace({
@@ -2575,10 +2583,20 @@ app.post('/api/workspaces', requireAuth, requireSessionAuth, async (req: Request
}); });
if (workspace?.workspace_type === 'rescue') { 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({ await sendRescueStatusNotification({
workspace, workspace,
ownerEmail: req.auth!.user.email, ownerEmail: req.auth!.user.email,
event: 'created', 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.' }); res.status(400).json({ error: 'Rescue onboarding details are required.' });
return; 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({ const workspace = await updateWorkspace({
@@ -2647,10 +2656,20 @@ app.put('/api/workspace', requireAuth, requireSessionAuth, requireWorkspaceRole(
}); });
if (workspace?.workspace_type === 'rescue' && isConvertingToRescue) { 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({ await sendRescueStatusNotification({
workspace, workspace,
ownerEmail: req.auth!.user.email, ownerEmail: req.auth!.user.email,
event: 'converted', event: 'converted',
note: onboardingWebhookError,
}); });
} }
@@ -380,7 +380,6 @@ export const deleteWorkspaceMember = async (memberId: string, workspaceId: numbe
export const listRescueWorkspacesForAdmin = async () => { export const listRescueWorkspacesForAdmin = async () => {
const result = await db.query< const result = await db.query<
WorkspaceRow & { WorkspaceRow & {
owner_email: string | null;
bird_count: number; bird_count: number;
member_count: number; member_count: number;
} }
@@ -398,17 +397,13 @@ export const listRescueWorkspacesForAdmin = async () => {
workspaces.rescue_verification_status, workspaces.rescue_verification_status,
workspaces.created_at, workspaces.created_at,
workspaces.updated_at, workspaces.updated_at,
owner.invite_email AS owner_email,
COUNT(DISTINCT birds.id)::int AS bird_count, COUNT(DISTINCT birds.id)::int AS bird_count,
COUNT(DISTINCT workspace_members.id)::int AS member_count COUNT(DISTINCT workspace_members.id)::int AS member_count
FROM workspaces 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 birds ON birds.workspace_id = workspaces.id
LEFT JOIN workspace_members ON workspace_members.workspace_id = workspaces.id LEFT JOIN workspace_members ON workspace_members.workspace_id = workspaces.id
WHERE workspaces.workspace_type = 'rescue' WHERE workspaces.workspace_type = 'rescue'
GROUP BY workspaces.id, owner.invite_email GROUP BY workspaces.id
ORDER BY ORDER BY
CASE workspaces.rescue_verification_status CASE workspaces.rescue_verification_status
WHEN 'pending' THEN 0 WHEN 'pending' THEN 0
+12 -7
View File
@@ -158,7 +158,6 @@ type AdminSummary = {
type AdminRescueWorkspace = { type AdminRescueWorkspace = {
workspace: Workspace; workspace: Workspace;
ownerEmail: string | null;
birdCount: number; birdCount: number;
memberCount: number; memberCount: number;
}; };
@@ -1003,6 +1002,16 @@ const formatBillingPlanBirdLimit = (billingPlan: BillingPlan) => {
return null; 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) => { const formatSubscriptionStatus = (status: SubscriptionStatus) => {
if (status === 'trialing') { if (status === 'trialing') {
return 'Trialing'; return 'Trialing';
@@ -4554,7 +4563,7 @@ function App() {
{formatRescueVerificationStatus(entry.workspace.rescueVerificationStatus)} {entry.birdCount} birds {entry.memberCount} members {formatRescueVerificationStatus(entry.workspace.rescueVerificationStatus)} {entry.birdCount} birds {entry.memberCount} members
</span> </span>
<small> <small>
Owner {entry.ownerEmail ?? 'unknown'} Billing {entry.workspace.billingEmail ?? 'not set'} Billing {entry.workspace.billingEmail ?? 'not set'}
</small> </small>
<div className="button-row"> <div className="button-row">
<button <button
@@ -5572,11 +5581,7 @@ function App() {
<span>Billing contact for invoices, receipts, and account notices.</span> <span>Billing contact for invoices, receipts, and account notices.</span>
</article> </article>
<article className="summary-card"> <article className="summary-card">
<strong> <strong>{workspace ? formatBillingBirdUsage(workspace.billingPlan, birds.length) : `${birds.length} bird${birds.length === 1 ? '' : 's'}`}</strong>
{workspace && formatBillingPlanBirdLimit(workspace.billingPlan)
? `${birds.length} / ${formatBillingPlanBirdLimit(workspace.billingPlan)} birds`
: `${birds.length} bird${birds.length === 1 ? '' : 's'}`}
</strong>
<span> <span>
{workspace && formatBillingPlanBirdLimit(workspace.billingPlan) {workspace && formatBillingPlanBirdLimit(workspace.billingPlan)
? 'Current bird count against your paid plan allowance.' ? 'Current bird count against your paid plan allowance.'