Repository navigation
Mail Rechnungsberichtigungen and Stornorechnungen to the customer - #286
Draft
claude[bot] wants to merge 4 commits into
Draft
claude[bot] wants to merge 4 commits into
claude[bot] wants to merge 4 commits into
Conversation
Invoices, corrections and cancellations now print the conference date(s) as Leistungsdatum (§ 14 Abs. 4 Nr. 6 UStG) in the meta block. The dates are recorded in the invoice snapshot; cancellations of snapshots written before this change fall back to the conference's start_on/end_on. When no date is known the line is omitted and a warning is logged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
Each generated invoice document (original invoice from the paid hook, Rechnungsberichtigung and Stornorechnung from the invoice-lifecycle endpoint) is sent as a separate plain-text mail with exactly one PDF attachment to ACCOUNTING_INVOICE_EMAIL, for timely recording (GoBD). A pre-issuance regenerate sends the replacement version, marked "(ersetzt vorherige Fassung)", since accounting already holds the earlier version under the same number. Unset env var: skipped with an info log. Failures are retried three times, then alerted to Slack with the invoice number; they never fail order processing or the endpoint and never touch sent_at. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
The accounting upload mailbox rejects mail (550 at RCPT TO) unless the SMTP envelope sender equals its registered sender address. The Directus MailService goes through the transactional mail provider, which always uses its own return path, so it can never pass that check. Send the accounting copy through its own nodemailer transport configured by ACCOUNTING_SMTP_HOST/PORT/SECURE/USER/PASSWORD and ACCOUNTING_MAIL_FROM, created lazily once, with `from` and the envelope sender both set to ACCOUNTING_MAIL_FROM. No fallback to the MailService. If ACCOUNTING_INVOICE_EMAIL is set but the SMTP config is incomplete, skip with a warning per document and a Slack alert once per process. nodemailer is added as a bundle dependency (same version Directus ships) and kept external in the Rollup build. The sendRawEmail attachment extension is reverted, since nothing uses it anymore. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
Corrections and cancellations created through the invoice-lifecycle endpoint are now emailed to the customer like the original invoice: To the purchaser, CC billing_email, stored PDF attached, via the Directus MailService. Each has its own email_templates row (ticket_invoice_correction, ticket_invoice_cancellation). A successful send stamps sent_at (retried, Slack alert if it cannot be stored). A failed send or a missing template never fails the endpoint: sent_at stays null, Slack is alerted, and the response reports customer_mail, which the invoice-actions UI shows. Regenerate keeps never mailing the customer, for originals and corrections alike. The sent_at retry loop moves from ticket-order-processing into the shared helper, and email-service gains deliverTemplatedEmail, which tells a missing template apart from a send failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Richard Hartmann · Slack thread
Before: Only the original invoice reached the customer (with the order confirmation). A Rechnungsberichtigung or Stornorechnung created through
POST /invoice-lifecycle/:id/{correction|cancellation}was stored and, with #282, copied to the accounting mailbox, but never sent to the customer. Itssent_atstayed null, the admin had to send the PDF by hand, and the dialog said "no mail to the customer".After: A new correction or cancellation is emailed to the customer automatically, to the same recipients as the original invoice: To the purchaser, CC
billing_emailif set, stored PDF attached, through the Directus MailService /EMAIL_FROM(not the accounting SMTP). A successful send stampssent_at, so the document counts as issued. If the send fails, the endpoint still answers 200,sent_atstays null and Slack is alerted. The response carriescustomer_mail: 'sent' | 'failed' | 'template_missing'. The invoice-actions UI shows it (as a warning when the mail did not go out), and the dialogs now say the document "geht automatisch per E-Mail an Kunde (Käufer, CC Rechnungs-E-Mail) und Buchhaltung".How:
shared/invoice-customer-mail.ts:sendInvoiceDocumentToCustomersends the templated mail, then stampssent_at. It never throws, and every outcome that needs a human goes to Slack: send failure, missing template, missingpurchaser_email, or asent_atstamp that cannot be stored.recordInvoiceSentholds thesent_atretry logic (3 attempts with the same timestamp, then a Slack alert).ticket-order-processingnow uses it instead of its own copy of the loop; its behaviour is unchanged.email_templates), liketicket_order_confirmation. Each document type gets its own key:ticket_invoice_correctionandticket_invoice_cancellation. Variables:purchaser_name,conference_title,order_number,invoice_number,invoice_date,reference_invoice_number,reference_invoice_date.shared/email-service.tsgainsdeliverTemplatedEmail, which returns'sent' | 'template_missing' | 'failed'. A missing template is then an explicit failure (Slack alert naming the template, no mail sent) rather than a generic one. There is deliberately no fallback text in the code.sendTemplatedEmailis now a boolean wrapper around it, with the same behaviour as before.invoice-lifecycle: readsbilling_emailand calls the helper after the document row is stored and the accounting copy has been sent. The accounting copy is independent of the customer mail.Regenerate: Regenerate never mails the customer, for originals and corrections alike. That matches how regenerate already works for originals. It is only possible while
sent_atis null, i.e. when the customer mail did not go out. The flow is the same as for an original invoice whose confirmation mail failed: fix the data, regenerate, send the PDF by hand, setsent_at. Accounting still receives the regenerated version marked "ersetzt vorherige Fassung". A Stornorechnung is never the current document, so it cannot be regenerated at all. Side effect of this PR: a correction that was sent is now issued, so it can be corrected or cancelled in turn, and can no longer be regenerated.Internal orders have no invoice, so nothing is sent (the endpoint still rejects them with 400).
Tests:
sent_atstamped on the new row only;billing_emailis empty;sent_atnull, accounting copy still sent;template_missing, Slack alert and error log name the template, no other mail path used;deliverTemplatedEmail: sent with CC and attachments, template missing, template read error, transport error;recordInvoiceSent: retries with a stable timestamp, Slack alert;sent, German dates, missingpurchaser_email, a Slack failure is swallowed;prettier:check,lint,test(48 suites, 676 tests) andbuildall pass.invoice-zugferd(Issue ticket invoices as ZUGFeRD / Factur-X e-invoices (EN 16931) #284): no conflicts, and the same four steps pass on the merge (51 suites, 703 tests).Deployment:
customer_mailistemplate_missingand Slack is alerted:email_templates→ + Create Itemticket_invoice_correction, name e.g.Rechnungsberichtigung an KundeRechnungsberichtigung {{invoice_number}} zu Ihrer Rechnung {{reference_invoice_number}}<>) and paste:<p>Guten Tag {{purchaser_name}},</p><p>anbei erhalten Sie die Rechnungsberichtigung Nr. {{invoice_number}} vom {{invoice_date}} zur Rechnung Nr. {{reference_invoice_number}} vom {{reference_invoice_date}} (Bestellung {{order_number}}, {{conference_title}}). Sie ersetzt die ursprüngliche Rechnung.</p><p>Viele Grüße<br>Ihr programmier.bar-Team</p>ticket_invoice_cancellation, name e.g.Stornorechnung an Kunde, subjectStornorechnung {{invoice_number}} zu Ihrer Rechnung {{reference_invoice_number}}, body_html:<p>Guten Tag {{purchaser_name}},</p><p>anbei erhalten Sie die Stornorechnung Nr. {{invoice_number}} vom {{invoice_date}} zur Rechnung Nr. {{reference_invoice_number}} vom {{reference_invoice_date}} (Bestellung {{order_number}}, {{conference_title}}). Eine Erstattung bereits gezahlter Beträge erfolgt separat.</p><p>Viele Grüße<br>Ihr programmier.bar-Team</p>docs/tickets.md.sent_at= null, even if they were sent by hand. Setsent_aton those that were sent, so they count as issued and can no longer be regenerated.Builds on #282 (and #280); merge after them.
Generated by Claude Code