diff --git a/app/api/webhooks/stripe/route.ts b/app/api/webhooks/stripe/route.ts index 51740ba..fccb7a6 100644 --- a/app/api/webhooks/stripe/route.ts +++ b/app/api/webhooks/stripe/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import Stripe from "stripe"; import { verifyStripeWebhookSignature } from "../../../lib/payments/stripeProvider"; +import { sendConfirmedPaymentEmail, type ConfirmPaymentOrderSnapshot } from "../../../lib/payments/confirmPaymentEmail"; import { sendCriticalAlert } from "../../../lib/alertAdmin"; const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de"; @@ -67,5 +68,17 @@ export async function POST(request: Request) { return NextResponse.json({ ok: false }, { status: 502 }); } + const data: { ok: boolean; alreadyProcessed?: boolean; order?: ConfirmPaymentOrderSnapshot } = await res.json(); + + // Fire-and-forget, same reasoning as the checkout route's own send: a + // failed confirmation email must never turn an already-successful + // payment confirmation into a non-2xx response (that would make Stripe + // retry a webhook we've already fully processed). `alreadyProcessed`/ + // missing `order` means this is a repeat delivery — see confirmPayment.ts's + // own comment on why the email must not be sent twice. + if (data.order && !data.alreadyProcessed) { + void sendConfirmedPaymentEmail(data.order); + } + return NextResponse.json({ ok: true }); } diff --git a/app/api/webhooks/stripe/test-confirm/route.ts b/app/api/webhooks/stripe/test-confirm/route.ts index 26232a9..caf2a88 100644 --- a/app/api/webhooks/stripe/test-confirm/route.ts +++ b/app/api/webhooks/stripe/test-confirm/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { isPaymentTestMode } from "../../../../lib/payments"; +import { sendConfirmedPaymentEmail, type ConfirmPaymentOrderSnapshot } from "../../../../lib/payments/confirmPaymentEmail"; import { sendCriticalAlert } from "../../../../lib/alertAdmin"; const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de"; @@ -40,5 +41,15 @@ export async function POST(request: Request) { return NextResponse.json({ ok: false, reason: "Backend hat die Testzahlung nicht bestätigt." }, { status: 502 }); } + const data: { ok: boolean; alreadyProcessed?: boolean; order?: ConfirmPaymentOrderSnapshot } = await res.json(); + + // Same email-send as the real webhook route — see its own comment and + // confirmPaymentEmail.ts. Reproduces today's "immediate confirmation" + // behavior on a test click, exercising the real send path rather than a + // separate short-circuit. + if (data.order && !data.alreadyProcessed) { + void sendConfirmedPaymentEmail(data.order); + } + return NextResponse.json({ ok: true }); } diff --git a/app/lib/payments/confirmPaymentEmail.ts b/app/lib/payments/confirmPaymentEmail.ts new file mode 100644 index 0000000..4f99418 --- /dev/null +++ b/app/lib/payments/confirmPaymentEmail.ts @@ -0,0 +1,31 @@ +import { sendOrderConfirmationEmail, type OrderConfirmationEmailData } from "../orderEmail"; +import { sendCriticalAlert } from "../alertAdmin"; + +// The `order` snapshot returned by the backend's confirm-payment endpoint +// (see docker/payload's src/lib/endpoints/confirmPayment.ts) — matches +// OrderConfirmationEmailData minus `customerEmail`, which is passed +// separately to sendOrderConfirmationEmail. Backend has no SMTP-based +// order-confirmation sender of its own (only the 4 status-change +// templates), so it returns everything needed here instead of the +// frontend needing an authenticated order-read path it doesn't otherwise +// have (ORDER_SERVICE_SECRET only ever authorizes *creating* an order). +export type ConfirmPaymentOrderSnapshot = OrderConfirmationEmailData & { customerEmail: string }; + +// Called from both the real Stripe webhook route and its PAYMENT_TEST_MODE +// test-confirm sibling, right after confirm-payment reports success (and +// NOT `alreadyProcessed: true` — a repeat delivery must never resend +// this). Mirrors exactly what app/api/checkout/route.ts already does for +// a manual/Überweisung order today, just triggered from the payment +// webhook instead of the checkout request itself for gated methods. +export async function sendConfirmedPaymentEmail(order: ConfirmPaymentOrderSnapshot): Promise { + const { customerEmail, ...emailData } = order; + try { + await sendOrderConfirmationEmail(emailData, customerEmail); + } catch (err) { + sendCriticalAlert("Bestätigungs-Mail konnte nach Zahlungsbestätigung nicht gesendet werden", { + orderNumber: order.orderNumber, + customerEmail, + error: String(err), + }); + } +}