Rechnungskopie: unter Einstellungen > Firma > "Rechnungskopie an" laesst sich eine Adresse hinterlegen; jede Rechnung und Stornorechnung geht zusaetzlich als eigene Mail mit derselben E-Rechnung dorthin (lib/invoice-mail.ts). Stornorechnung: eine Rechnung wird nie geloescht. Bei Stornierung im Admin, erstatteter Retoure oder voller Erstattung bei Stripe entsteht genau eine Stornorechnung ST-JJJJ-NNNN (eigener lueckenloser Nummernkreis) mit allen Positionen und Bezug auf die Rechnung, als E-Rechnung (Gutschrift 381) an die Kundschaft und als Kopie. Gleichzeitige Ausloeser (Admin-Storno und Stripe-Webhook derselben Erstattung) enden bei einer Stornorechnung. Bestellungen mit Stornorechnung lassen sich nicht wieder oeffnen. ERP-Export: GET /api/erp/rechnungen?art=storno liefert Stornorechnungen mit Bezug (cancels), Rechnungen tragen cancelledBy; PDFs auch fuer ST-Nummern. Admin-Bestelldetail zeigt die Stornorechnung mit Download. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
import { getContentValues } from "@/lib/content";
|
|
import { normalizeContactEmail } from "@/lib/contact-email";
|
|
import { escapeHtml } from "@/lib/escape-html";
|
|
import { sendMail } from "@/lib/mail";
|
|
|
|
/**
|
|
* Kopie jeder Rechnung und Stornorechnung an eine im Admin hinterlegte
|
|
* Adresse (Einstellungen > Firma > "Rechnungskopie an"), etwa fuer die
|
|
* Buchhaltung. Die Kopie ist eine eigene Mail mit demselben PDF (E-Rechnung
|
|
* mit eingebettetem XML), nicht ein BCC der Kundenmail: so steht im Betreff,
|
|
* welcher Beleg es ist, und die Kundenmail bleibt unveraendert.
|
|
*/
|
|
|
|
export const INVOICE_COPY_KEY = "invoice.copyEmail";
|
|
|
|
/** Die hinterlegte Adresse, oder null, wenn keine (gueltige) eingetragen ist. */
|
|
export async function getInvoiceCopyAddress(): Promise<string | null> {
|
|
try {
|
|
const values = await getContentValues([INVOICE_COPY_KEY], "de");
|
|
const raw = values[INVOICE_COPY_KEY]?.trim() ?? "";
|
|
return raw ? normalizeContactEmail(raw) : null;
|
|
} catch (error: unknown) {
|
|
console.error("[invoice-mail] Adresse fuer die Rechnungskopie nicht lesbar:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export interface InvoiceCopy {
|
|
/** "Rechnung" oder "Stornorechnung". */
|
|
title: string;
|
|
number: string;
|
|
orderNumber: string;
|
|
customerName: string;
|
|
customerEmail: string;
|
|
pdf: Buffer;
|
|
}
|
|
|
|
/** Betreff und Text der Kopie (ohne Versand, testbar). */
|
|
export function invoiceCopyContent(copy: Omit<InvoiceCopy, "pdf">): { subject: string; text: string; html: string } {
|
|
const subject = `Kopie: ${copy.title} ${copy.number} (Bestellung ${copy.orderNumber})`;
|
|
const text =
|
|
`${copy.title} ${copy.number} zur Bestellung ${copy.orderNumber} an ${copy.customerName} <${copy.customerEmail}>.\n` +
|
|
"Anbei die E-Rechnung (PDF/A-3 mit eingebettetem XML nach EN 16931, ZUGFeRD/Factur-X).";
|
|
const html = text
|
|
.split("\n")
|
|
.map((line) => `<p style="margin:0 0 8px;line-height:1.5;">${escapeHtml(line)}</p>`)
|
|
.join("");
|
|
return { subject, text, html };
|
|
}
|
|
|
|
/**
|
|
* Schickt die Kopie. null: keine Adresse hinterlegt; sonst, ob der
|
|
* Mailserver die Mail angenommen hat. Wirft nie (wie sendMail): eine
|
|
* fehlende Kopie darf keine Bestellung oder Stornierung aufhalten.
|
|
*/
|
|
export async function sendInvoiceCopy(copy: InvoiceCopy): Promise<boolean | null> {
|
|
const to = await getInvoiceCopyAddress();
|
|
if (!to) return null;
|
|
const { subject, text, html } = invoiceCopyContent(copy);
|
|
return sendMail({
|
|
to,
|
|
subject,
|
|
text,
|
|
html,
|
|
attachments: [{ filename: `${copy.number}.pdf`, content: copy.pdf, contentType: "application/pdf" }],
|
|
});
|
|
}
|