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:
@@ -93,6 +93,14 @@ const MIGRATION_SQL = `
|
||||
-- is only meaningful for rejected and cancelled rows.
|
||||
ALTER TABLE tenant_requests ADD COLUMN IF NOT EXISTS dismissed_at TIMESTAMPTZ;
|
||||
|
||||
-- Feature 6: free-form customer note attached to the request.
|
||||
-- Currently surfaced only by resume requests (where the customer
|
||||
-- explains why they want reactivation), but the column is generic
|
||||
-- so future flows could reuse it. Distinct from billing_notes
|
||||
-- (provision-only, accounting-related) and admin_notes (admin's
|
||||
-- reason on reject/approve). Optional — nullable.
|
||||
ALTER TABLE tenant_requests ADD COLUMN IF NOT EXISTS customer_notes TEXT;
|
||||
|
||||
-- Bug 37a: resume requests use the same table as provision requests so
|
||||
-- the customer dashboard and admin queue share rendering. Discriminator
|
||||
-- is request_type. Default 'provision' on backfill keeps existing rows
|
||||
@@ -558,14 +566,21 @@ export async function createResumeRequest(params: {
|
||||
// tenant request for traceability rather than storing dummy values.
|
||||
companyName: string;
|
||||
agentName: string;
|
||||
/**
|
||||
* Feature 6: optional free-form note from the customer explaining
|
||||
* why they want reactivation. Surfaced to admin in the queue and
|
||||
* forwarded to the platform notification email so the admin can
|
||||
* decide before opening the request.
|
||||
*/
|
||||
customerNotes?: string | null;
|
||||
}): Promise<TenantRequest> {
|
||||
await ensureSchema();
|
||||
const result = await getPool().query(
|
||||
`INSERT INTO tenant_requests (
|
||||
zitadel_org_id, zitadel_user_id, company_name,
|
||||
contact_name, contact_email, agent_name,
|
||||
tenant_name, request_type, status
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, 'resume', 'pending')
|
||||
tenant_name, request_type, status, customer_notes
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, 'resume', 'pending', $8)
|
||||
RETURNING *`,
|
||||
[
|
||||
params.zitadelOrgId,
|
||||
@@ -575,6 +590,7 @@ export async function createResumeRequest(params: {
|
||||
params.contactEmail,
|
||||
params.agentName,
|
||||
params.tenantName,
|
||||
params.customerNotes ?? null,
|
||||
]
|
||||
);
|
||||
return mapRow(result.rows[0]);
|
||||
@@ -876,6 +892,7 @@ function mapRow(row: any): TenantRequest {
|
||||
packages: row.packages ?? [],
|
||||
billingAddress: row.billing_address ?? {},
|
||||
billingNotes: row.billing_notes,
|
||||
customerNotes: row.customer_notes ?? null,
|
||||
status: row.status as TenantRequestStatus,
|
||||
adminNotes: row.admin_notes,
|
||||
tenantName: row.tenant_name,
|
||||
|
||||
@@ -319,6 +319,89 @@ export async function sendAdminNotificationEmail(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Feature 6: resume-request admin notification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Notify the admin distribution list that a customer has requested
|
||||
* reactivation of a suspended tenant. Distinct from the onboarding
|
||||
* notification because the action consequences differ (admin
|
||||
* approving a resume just unsuspends an existing tenant; no
|
||||
* provisioning runs), and because the customer's note — explaining
|
||||
* why they want reactivation — is meaningful context for the admin
|
||||
* triaging the queue.
|
||||
*
|
||||
* Skipped silently if ADMIN_NOTIFICATION_EMAIL isn't set, matching
|
||||
* the pattern of the other admin notification functions.
|
||||
*/
|
||||
export async function sendResumeRequestAdminNotificationEmail(params: {
|
||||
tenantName: string;
|
||||
companyName: string;
|
||||
contactName: string;
|
||||
contactEmail: string;
|
||||
customerNotes?: string | null;
|
||||
}): Promise<void> {
|
||||
const adminEmail = process.env.ADMIN_NOTIFICATION_EMAIL;
|
||||
if (!adminEmail) return;
|
||||
|
||||
const safeCompany = escapeHtml(params.companyName);
|
||||
const safeName = escapeHtml(params.contactName);
|
||||
const safeEmail = escapeHtml(params.contactEmail);
|
||||
const safeTenant = escapeHtml(params.tenantName);
|
||||
const safeNotes = params.customerNotes ? escapeHtml(params.customerNotes) : "";
|
||||
|
||||
const noteText = params.customerNotes
|
||||
? `\nCustomer's note:\n${params.customerNotes}\n`
|
||||
: "";
|
||||
const noteHtml = safeNotes
|
||||
? `<div style="background: #2a2a2a; border-left: 3px solid #3b82f6; padding: 12px 16px; border-radius: 6px; margin: 16px 0; white-space: pre-wrap;">
|
||||
<p style="color: #ccc; font-size: 13px; margin: 0 0 8px 0;"><strong>Customer's note:</strong></p>
|
||||
<p style="color: #e0e0e0; font-size: 13px; margin: 0;">${safeNotes}</p>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
try {
|
||||
await getTransporter().sendMail({
|
||||
from: getFrom(),
|
||||
to: adminEmail,
|
||||
subject: `Reactivation request: ${params.companyName}`,
|
||||
text: [
|
||||
`A customer has requested reactivation of a suspended tenant.`,
|
||||
"",
|
||||
`Company: ${params.companyName}`,
|
||||
`Tenant: ${params.tenantName}`,
|
||||
`Contact: ${params.contactName} (${params.contactEmail})`,
|
||||
noteText,
|
||||
`Review at https://app.pieced.ch/admin`,
|
||||
]
|
||||
.filter((s) => s !== "")
|
||||
.join("\n"),
|
||||
html: `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 560px; margin: 0 auto; color: #e0e0e0; background: #1a1a1a; padding: 32px; border-radius: 12px;">
|
||||
<h2 style="color: #ffffff; margin-top: 0;">Reactivation request</h2>
|
||||
<p>A customer has requested reactivation of a suspended tenant.</p>
|
||||
<table style="color: #ccc; font-size: 14px; margin: 16px 0;">
|
||||
<tr><td style="padding: 4px 12px 4px 0; color: #888;">Company:</td><td>${safeCompany}</td></tr>
|
||||
<tr><td style="padding: 4px 12px 4px 0; color: #888;">Tenant:</td><td style="font-family: monospace;">${safeTenant}</td></tr>
|
||||
<tr><td style="padding: 4px 12px 4px 0; color: #888;">Contact:</td><td>${safeName} (${safeEmail})</td></tr>
|
||||
</table>
|
||||
${noteHtml}
|
||||
<p>
|
||||
<a href="https://app.pieced.ch/admin" style="display: inline-block; padding: 10px 24px; background: #3b82f6; color: #ffffff; text-decoration: none; border-radius: 8px; font-weight: 500;">
|
||||
Review Request
|
||||
</a>
|
||||
</p>
|
||||
<hr style="border: none; border-top: 1px solid #333; margin: 24px 0;" />
|
||||
<p style="color: #666; font-size: 12px;">PieCed IT — Admin notification</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to send resume request admin notification:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Feature 5: support ticket emails
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user