Lizenz-Vertrieb: Chargen-Vergabe, Storno, Verlaengerung, Mail, Einloeselink
Erweitert das Lizenzmodul um eine Vertriebs-/Vergabe-Schicht ueber dem bestehenden Einloese-Flow (non-breaking, alle Alt-Codes + generate/redeem /assign unveraendert). Schema (Migration 2026-08-26-lizenz-vertrieb.sql, additiv): - license_customers (Besteller: Kundennummer/Email/Name) - license_batches (Bestellung/Charge: externe Bestellnummer, Typ, Menge, Gueltigkeit, Sammel-Einloesetoken, Storno) - licenses += batch_id, issued_at, valid_from/until (pro Code -> Verlaengerung), canceled_at/reason; code auf VARCHAR(32) (laengere Codes) - license_mails (Mail-Archiv, unabh. vom IMAP), license_redeem_attempts (Brute-Force-Schutz), license_events (Audit-Log) Backend (php/api/licenses.php): - issue_batch: N frische 100-Bit-Codes in Transaktion, sofort vergeben, an Kunde/Bestellung/Gueltigkeit gebunden; sticky Default Gueltig-bis - Uebersicht (view=batches mit Aggregat ausgegeben/storniert/netto/eingeloest /abgelaufen), batch_detail, Kunden-Autocomplete, sales_default - cancel_batch/cancel_license, extend_batch/extend_license - send_batch_mail (Server-Treiber via Mailer, archiviert, Anhang bei >40) - redeem_charge (Sammel-Einloeselink) - Brute-Force-Bremse + Audit + Storno-Ablehnung im redeem/redeem_batch UI (admin-licenses.html): Ausgabe-Formular (Bestaetigung bei Grossmengen), Ergebnis mit Copy/Download(txt+csv)/Mail-Composer/Sammellink, Bestellungs- Uebersicht mit Filtern + Detail-Modal (Storno/Verlaengern/Verlauf/Mails). Einloese-Landingpage (einloesen.html + pages/einloesen.php): ?charge=Token (alle auf einmal) und ?code=CODE, mit Lehrer-Login-Gate. Lib: Database Transaktionen; Mailer sendWithAttachments + renderPlainBody. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,31 @@ class Mailer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Versand mit optionalen Datei-Anhaengen (z. B. Lizenz-Codes als .txt/.csv
|
||||
* bei grossen Chargen). $attachments: [ ['content'=>string,'name'=>string,'type'=>string], ... ]
|
||||
* Wirft bei Fehler eine Exception (Aufrufer archiviert Status/Fehler selbst).
|
||||
*/
|
||||
public static function sendWithAttachments(string $to, string $subject, string $htmlBody, string $textBody = '', array $attachments = []): void {
|
||||
$mail = self::create();
|
||||
$mail->addAddress($to);
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = self::wrapHtml($htmlBody);
|
||||
$mail->AltBody = $textBody ?: strip_tags(str_replace(['<br>','<br/>','</p>','</li>'], "\n", $htmlBody));
|
||||
foreach ($attachments as $a) {
|
||||
if (empty($a['content']) || empty($a['name'])) continue;
|
||||
$mail->addStringAttachment($a['content'], $a['name'], \PHPMailer\PHPMailer\PHPMailer::ENCODING_BASE64, $a['type'] ?? 'text/plain');
|
||||
}
|
||||
$mail->send();
|
||||
}
|
||||
|
||||
/** Freitext-Body (aus Admin-Textbaustein) in die Card-Optik giessen. */
|
||||
public static function renderPlainBody(string $text): string {
|
||||
$safe = nl2br(htmlspecialchars($text, ENT_QUOTES, 'UTF-8'));
|
||||
return '<div style="font-size:14px;color:#333;line-height:1.6">' . $safe . '</div>';
|
||||
}
|
||||
|
||||
// ─── Willkommen (Lehrperson registriert sich) ───
|
||||
|
||||
public static function sendWelcomeTeacher(string $email, string $name): bool {
|
||||
|
||||
Reference in New Issue
Block a user