improved rescue workflow
This commit is contained in:
+39
-20
@@ -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
@@ -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.'
|
||||||
|
|||||||
Reference in New Issue
Block a user