Phase2.5: Skill SetUp Process
All checks were successful
Build and Push / build (push) Successful in 1m39s

This commit is contained in:
2026-05-24 17:25:08 +02:00
parent cd15b391ac
commit 49b085e59e
22 changed files with 1666 additions and 14 deletions

View File

@@ -723,3 +723,180 @@ export async function sendSupportAdminNotificationEmail(params: {
console.error("Failed to send admin support notification:", err);
}
}
// ---------------------------------------------------------------------------
// Skill activation requests — Phase 2.5
// ---------------------------------------------------------------------------
//
// Three notifications:
//
// sendSkillActivationAdminNotification — to ADMIN_NOTIFICATION_EMAIL
// when a customer requests a
// flagged skill.
//
// sendSkillActivationApprovalEmail — to the customer, on approve.
//
// sendSkillActivationRejectionEmail — to the customer, on reject,
// including the admin's reason.
//
// All three follow the existing patterns in this file (HTML + plaintext,
// escaped vars, best-effort with errors logged not thrown).
/**
* Notify admin (ADMIN_NOTIFICATION_EMAIL) that a customer has
* requested activation of a manual-setup skill. The skill name +
* tenant + requester are all included so admin can act without
* loading the portal.
*/
export async function sendSkillActivationAdminNotification(params: {
tenantName: string;
skillId: string;
skillName: string;
requesterEmail: string;
requesterName: string;
companyName: string | null;
}): Promise<void> {
const adminEmail = process.env.ADMIN_NOTIFICATION_EMAIL;
if (!adminEmail) return;
const safeTenant = escapeHtml(params.tenantName);
const safeSkillId = escapeHtml(params.skillId);
const safeSkillName = escapeHtml(params.skillName);
const safeRequester = escapeHtml(params.requesterName);
const safeRequesterEmail = escapeHtml(params.requesterEmail);
const safeCompany = params.companyName
? escapeHtml(params.companyName)
: "—";
try {
await getTransporter().sendMail({
from: getFrom(),
to: adminEmail,
subject: `[PieCed] Skill activation requested — ${params.skillName} on ${params.tenantName}`,
text: [
"A customer has requested activation of a manual-setup skill.",
"",
`Skill: ${params.skillName} (${params.skillId})`,
`Tenant: ${params.tenantName}`,
`Organization:${params.companyName ?? "—"}`,
`Requested by:${params.requesterName} <${params.requesterEmail}>`,
"",
"Review and act in the admin queue:",
"https://app.pieced.ch/admin/skills/pending",
].join("\n"),
html: `
<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 560px; padding: 24px; background: #1a1a1a; color: #e5e5e5;">
<h2 style="margin: 0 0 16px; color: #10B981;">Skill activation requested</h2>
<p>A customer has requested activation of a manual-setup skill.</p>
<table style="width:100%; border-collapse: collapse; margin: 12px 0;">
<tr><td style="color:#888; padding:4px 0;">Skill</td><td>${safeSkillName} (<code>${safeSkillId}</code>)</td></tr>
<tr><td style="color:#888; padding:4px 0;">Tenant</td><td><code>${safeTenant}</code></td></tr>
<tr><td style="color:#888; padding:4px 0;">Organization</td><td>${safeCompany}</td></tr>
<tr><td style="color:#888; padding:4px 0;">Requested by</td><td>${safeRequester} &lt;${safeRequesterEmail}&gt;</td></tr>
</table>
<p>
<a href="https://app.pieced.ch/admin/skills/pending" style="display:inline-block; padding:10px 24px; background:#10B981; color:#fff; text-decoration:none; border-radius:8px; font-weight:500;">
Open admin queue
</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 skill activation admin notification:", err);
}
}
export async function sendSkillActivationApprovalEmail(params: {
to: string;
contactName: string;
skillName: string;
tenantName: string;
}): Promise<void> {
const safeName = escapeHtml(params.contactName);
const safeSkill = escapeHtml(params.skillName);
const safeTenant = escapeHtml(params.tenantName);
try {
await getTransporter().sendMail({
from: getFrom(),
to: params.to,
subject: `Your skill activation has been approved — ${params.skillName}`,
text: [
`Hello ${params.contactName},`,
"",
`Good news — your request to activate "${params.skillName}" on tenant ${params.tenantName} has been approved and the skill is now live.`,
"",
"You can manage it from your tenant settings.",
"",
"Best regards,",
"PieCed IT",
].join("\n"),
html: `
<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width:560px; padding:24px; background:#1a1a1a; color:#e5e5e5;">
<h2 style="margin:0 0 16px; color:#10B981;">Skill approved & activated</h2>
<p>Hello ${safeName},</p>
<p>Your request to activate <strong>${safeSkill}</strong> on tenant <code>${safeTenant}</code> has been approved and the skill is now live.</p>
<p>You can manage it from your tenant settings.</p>
<p>
<a href="https://app.pieced.ch/tenants/${encodeURIComponent(params.tenantName)}" style="display:inline-block; padding:10px 24px; background:#10B981; color:#fff; text-decoration:none; border-radius:8px; font-weight:500;">
Open tenant
</a>
</p>
<hr style="border:none; border-top:1px solid #333; margin:24px 0;" />
<p style="color:#666; font-size:12px;">PieCed IT</p>
</div>
`,
});
} catch (err) {
console.error("Failed to send skill activation approval email:", err);
}
}
export async function sendSkillActivationRejectionEmail(params: {
to: string;
contactName: string;
skillName: string;
tenantName: string;
reason: string;
}): Promise<void> {
const safeName = escapeHtml(params.contactName);
const safeSkill = escapeHtml(params.skillName);
const safeTenant = escapeHtml(params.tenantName);
const safeReason = escapeHtml(params.reason);
try {
await getTransporter().sendMail({
from: getFrom(),
to: params.to,
subject: `Update on your skill activation request — ${params.skillName}`,
text: [
`Hello ${params.contactName},`,
"",
`We were unable to approve your request to activate "${params.skillName}" on tenant ${params.tenantName}.`,
"",
"Reason from our team:",
params.reason,
"",
"You can try again from your tenant settings once the matter is resolved.",
"",
"Best regards,",
"PieCed IT",
].join("\n"),
html: `
<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width:560px; padding:24px; background:#1a1a1a; color:#e5e5e5;">
<h2 style="margin:0 0 16px; color:#ef4444;">Activation request not approved</h2>
<p>Hello ${safeName},</p>
<p>We were unable to approve your request to activate <strong>${safeSkill}</strong> on tenant <code>${safeTenant}</code>.</p>
<div style="background:#2a2a2a; border-left:3px solid #ef4444; padding:12px 16px; border-radius:6px; margin:16px 0;">
<p style="color:#ccc; font-size:13px; margin:0;"><strong>Reason from our team:</strong></p>
<p style="color:#aaa; font-size:13px; margin:8px 0 0 0; white-space:pre-wrap;">${safeReason}</p>
</div>
<p>You can try again from your tenant settings once the matter is resolved.</p>
<hr style="border:none; border-top:1px solid #333; margin:24px 0;" />
<p style="color:#666; font-size:12px;">PieCed IT</p>
</div>
`,
});
} catch (err) {
console.error("Failed to send skill activation rejection email:", err);
}
}