// POST /api/registration/confirm { token } // // Marks a pending registration as confirmed, then hands it to the (feature-flagged) // ERP onboarding module. The ERP step can never break confirmation: it is fail-soft and // no-ops entirely while ERP_ONBOARDING_URL / ERP_SERVICE_TOKEN are unset. // // Registrations flagged `needs_manual_approval` (couriers, and clinics whose Standort has // no e-mail domain on record) are NOT auto-onboarded — see the gate below. They stay in // the DB with erp_status 'manual-approval' until an operator provisions them. import { registrationTypeLabelDe } from '../../utils/registrationTypes' import { asLang, serverMessages, typeLabelFor } from '../../utils/i18n' const MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000 export default defineEventHandler(async (event) => { const body = await readBody(event) const config = useRuntimeConfig() const token = String(body?.token ?? '').trim() // Two languages are in play and they are NOT the same thing: // pageLang — the language on screen right now, used for this HTTP response; // mailLang — the language stored on the registration row, used for the welcome mail. // Someone who registered in English must not be written to in German just because the // confirmation link was opened in a German browser (or by a mail client). const pageLang = asLang(body?.lang) if (!token) { return { status: 400, message: serverMessages(pageLang).api.noToken } } const registration = findByToken(token) if (!registration) { return { status: 404, message: serverMessages(pageLang).api.invalidToken } } const mailLang = asLang(registration.lang) // READ PATH. `registration.type` may be a retired value ('abgebende_klinik' / // 'beziehende_klinik') from a link that has been sitting in an inbox since before the // types were collapsed. Confirmation itself never looks at the type — it is keyed on the // token alone — and both label helpers normalise, so such a link confirms and reads // correctly instead of showing the raw key. const typeLabel = typeLabelFor(registration.type, pageLang) const typeLabelDe = registrationTypeLabelDe(registration.type) if (registration.status === 'confirmed') { // Whether credentials went out is recorded in the stored ERP payload (see // setErpResult below). Rows confirmed before that field existed read false, // which degrades to the generic confirmed text rather than a wrong promise. let credentialsSent = false try { credentialsSent = !!JSON.parse((registration as any).erp_payload || '{}')?.credentialsSent } catch { /* ignore */ } return { status: 200, alreadyConfirmed: true, message: serverMessages(pageLang).api.alreadyConfirmed, typeLabel, orgName: registration.org_name, needsManualApproval: !!registration.needs_manual_approval, credentialsSent, } } if (registration.status !== 'pending') { return { status: 410, message: serverMessages(pageLang).api.notValid } } const age = Date.now() - Date.parse(registration.created_at || '') if (Number.isFinite(age) && age > MAX_AGE_MS) { return { status: 410, message: serverMessages(pageLang).api.expired } } markConfirmed(registration.id) const confirmed = { ...registration, status: 'confirmed' as const } // ---- ERP onboarding (feature-flagged, fail-soft) ------------------------------------- // GATED on needs_manual_approval. That flag marks exactly the registrations nothing // could verify — every courier, and clinics whose Standort has no e-mail domain on // record. Auto-onboarding them would mail live ERP credentials to an address nobody // checked, while the form, the confirmation mail and the welcome mail all promise a // manual review ("we will get back to you within one working day"). This gate is what // actually keeps that promise. Verified and test registrations stay fully automatic. let erp = { enabled: false, ok: false, status: 'disabled', message: '', credentialsSent: false } as any if (confirmed.needs_manual_approval) { erp = { enabled: false, ok: false, status: 'manual-approval', message: 'ERP-Onboarding zurückgestellt — Registrierung wartet auf manuelle Freigabe.', credentialsSent: false, } } else { try { erp = await runErpOnboarding(confirmed) } catch (err: any) { // runErpOnboarding already swallows its own errors; this is belt and braces. erp = { enabled: true, ok: false, status: 'error', message: err?.message || String(err), credentialsSent: false } } } setErpResult(registration.id, erp.status, erp.message, { ctx: erp.ctx ?? null, report: erp.report ?? null, credentialsSent: !!erp.credentialsSent, }) // ---- welcome mail --------------------------------------------------------------------- const mail = buildWelcomeMail({ to: confirmed.contact_email, contactName: confirmed.contact_name, orgName: confirmed.org_name, typeLabel: typeLabelFor(confirmed.type, mailLang), needsManualApproval: !!confirmed.needs_manual_approval, credentialsSent: !!erp.credentialsSent, lang: mailLang, }) await sendMail({ to: confirmed.contact_email, subject: mail.subject, html: mail.html, text: mail.text }) const notify = String(config.notifyEmail || '') if (notify) { const internal = buildInternalMail('Blut24: Registrierung bestätigt', [ ['Einrichtung', confirmed.org_name], ['Typ', typeLabelDe], ['STOID', confirmed.stoid || '–'], ['E-Mail', confirmed.contact_email], ['Domain geprüft', confirmed.domain_verified ? 'ja' : 'nein'], ['Manuelle Freigabe nötig', confirmed.needs_manual_approval ? 'ja' : 'nein'], ['ERP-Onboarding', erp.status === 'manual-approval' ? 'zurückgestellt — manuelle Freigabe erforderlich' : erp.enabled ? `${erp.status}${erp.message ? ': ' + erp.message : ''}` : 'deaktiviert'], ['Zugangsdaten versendet', erp.credentialsSent ? 'ja' : 'nein'], ['Sprache', mailLang], ]) await sendMail({ to: notify, subject: internal.subject, html: internal.html, text: internal.text }) } return { status: 200, alreadyConfirmed: false, message: serverMessages(pageLang).api.confirmed, typeLabel, orgName: confirmed.org_name, needsManualApproval: !!confirmed.needs_manual_approval, credentialsSent: !!erp.credentialsSent, } })