Add note to reactivation request
All checks were successful
Build and Push / build (push) Successful in 1m28s
All checks were successful
Build and Push / build (push) Successful in 1m28s
This commit is contained in:
@@ -272,6 +272,8 @@ export default async function TenantDetailPage({
|
||||
? {
|
||||
id: pendingResumeRequest.id,
|
||||
createdAt: pendingResumeRequest.createdAt,
|
||||
customerNotes:
|
||||
pendingResumeRequest.customerNotes ?? null,
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getSessionUser, canMutate } from "@/lib/session";
|
||||
import { getTenant, setTenantAnnotation } from "@/lib/k8s";
|
||||
import { canUserSeeTenant } from "@/lib/visibility";
|
||||
@@ -7,8 +8,26 @@ import {
|
||||
getPendingResumeRequestForTenant,
|
||||
getTenantRequestByTenantName,
|
||||
} from "@/lib/db";
|
||||
import { sendResumeRequestAdminNotificationEmail } from "@/lib/email";
|
||||
import { safeError } from "@/lib/errors";
|
||||
|
||||
/**
|
||||
* Body schema. Both fields optional; the customer can submit a
|
||||
* resume request with no body at all (the JS client sends `{}`),
|
||||
* or with a note explaining their reactivation rationale.
|
||||
*
|
||||
* Length cap mirrors `billing_notes` (2000 chars) — same lower
|
||||
* bound for "free-form text we don't want abused".
|
||||
*/
|
||||
const bodySchema = z.object({
|
||||
customerNotes: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(2000)
|
||||
.optional()
|
||||
.transform((v) => (v && v.length > 0 ? v : undefined)),
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/tenants/[name]/resume-request
|
||||
*
|
||||
@@ -82,6 +101,18 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
// Body is optional — the customer can submit a resume request
|
||||
// with no payload at all, or attach a free-form note.
|
||||
const rawBody = await req.json().catch(() => ({}));
|
||||
const parsed = bodySchema.safeParse(rawBody ?? {});
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid input", details: parsed.error.flatten() },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const customerNotes = parsed.data.customerNotes;
|
||||
|
||||
// Already a pending request? Don't duplicate.
|
||||
const existing = await getPendingResumeRequestForTenant(name);
|
||||
if (existing) {
|
||||
@@ -110,6 +141,7 @@ export async function POST(
|
||||
contactEmail: user.email,
|
||||
companyName: provision?.companyName ?? tenant.spec.displayName ?? name,
|
||||
agentName: provision?.agentName ?? "Assistant",
|
||||
customerNotes,
|
||||
});
|
||||
|
||||
// Stamp the annotation so the operator pauses its TTL. If this
|
||||
@@ -128,6 +160,20 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
// Notify admin distribution. Fire-and-log: failure to email
|
||||
// doesn't roll back the request creation. The customer's note
|
||||
// (if any) is included so admin can triage from the email
|
||||
// without opening the queue.
|
||||
sendResumeRequestAdminNotificationEmail({
|
||||
tenantName: name,
|
||||
companyName: resumeRequest.companyName,
|
||||
contactName: resumeRequest.contactName,
|
||||
contactEmail: resumeRequest.contactEmail,
|
||||
customerNotes,
|
||||
}).catch((e) =>
|
||||
console.error("resume admin notification email failed:", e)
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "Resume request submitted. An admin will review shortly.",
|
||||
|
||||
Reference in New Issue
Block a user