Files
Webklar-Kundenbereich/server/routes/offers.js
T
ClaudeandClaude Opus 5.5 32e441c8bc Abo-Angebote: Mail-Status in der Liste und E-Mail erneut senden
Bisher sah man nirgends, ob die Angebots-Mail rausging, Fehler landeten
nicht im Log, und senden ließ sie sich nur beim Anlegen.
- Angebotsliste: Kunden-Adresse unter dem Namen, im Status "E-Mail
  gesendet <Zeitpunkt>" bzw. "E-Mail nicht verschickt" / "Keine
  E-Mail-Adresse hinterlegt"
- Knopf "E-Mail senden" / "Erneut senden" mit Rückfrage (Adresse, letzter
  Versand), solange das Angebot offen und gültig ist; bei laufendem Abo,
  solange die erste rabattierte Abbuchung noch bevorsteht (die Mail
  nennt sie). POST /api/portal-admin/offers/:id/notify setzt notifiedAt.
- Server-Log: "[offers] Angebots-Mail <Ref> vom Mailserver angenommen:
  <Message-ID> 250 … queued as …" bzw. "… fehlgeschlagen: <Grund>"
- Lehnt der Mailserver nur den Kunden ab (die BCC-Kopie ans Team geht
  trotzdem raus), gilt die Mail als nicht verschickt
- sendCustomerMail gibt die nodemailer-Info zurück

Tests: npm test (40), Server-Durchlauf gegen Appwrite-Mock, SMTP-Sink
und Stripe-Double (28 Prüfungen), Oberfläche (18 Prüfungen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:12:39 +00:00

79 lines
2.7 KiB
JavaScript

import { Router } from 'express'
import { config } from '../config.js'
import { listDocuments, Query } from '../services/appwriteAdmin.js'
import { adminOfferView, createOffer, listOffers, offerContext, resendOfferMail, withdrawOffer } from '../services/offers.js'
/**
* Individuelle Abo-Angebote im Admin-Panel (Tab "Abo-Angebote").
* Eingehaengt in routes/portalAdmin.js unter /api/portal-admin/offers, dort hinter requireAdminSession.
*/
export const offersAdminRouter = Router()
const adminOf = (req) => ({
name: req.session.adminName || req.session.name,
email: req.session.adminEmail || req.session.email,
})
function sendError(res, err, fallback) {
const status = err.status || 500
if (status >= 500) console.error('[offers]', fallback, err.message)
return res.status(status).json({ error: err.message || fallback })
}
offersAdminRouter.get('/', async (_req, res) => {
try {
const [docs, customers, subs] = await Promise.all([
listOffers(),
listDocuments(config.collections.customers, [Query.limit(1000)]),
// Abo-Dokumente (ID = Stripe-Subscription) fuer das Rabattende eingeloester Angebote
listDocuments(config.collections.subscriptions, [Query.limit(1000)]),
])
const byId = new Map(customers.map((c) => [c.$id, c]))
const subById = new Map(subs.map((s) => [s.$id, s]))
return res.json({
offers: docs.map((d) => adminOfferView(d, byId.get(d.customerId), subById.get(d.stripeSubscriptionId))),
})
} catch (err) {
return sendError(res, err, 'Angebote konnten nicht geladen werden')
}
})
offersAdminRouter.get('/context/:customerId', async (req, res) => {
try {
return res.json(await offerContext(req.params.customerId))
} catch (err) {
return sendError(res, err, 'Kunde konnte nicht geladen werden')
}
})
offersAdminRouter.post('/', async (req, res) => {
try {
const { doc, customer, mail, sub } = await createOffer(req.body || {}, adminOf(req))
return res.status(201).json({
offer: adminOfferView(doc, customer, sub),
mailSent: mail.sent,
mailError: mail.sent ? '' : mail.reason || '',
})
} catch (err) {
return sendError(res, err, 'Angebot konnte nicht angelegt werden')
}
})
offersAdminRouter.post('/:id/withdraw', async (req, res) => {
try {
const doc = await withdrawOffer(req.params.id, adminOf(req))
return res.json({ success: true, status: doc.status })
} catch (err) {
return sendError(res, err, 'Angebot konnte nicht zurückgezogen werden')
}
})
offersAdminRouter.post('/:id/notify', async (req, res) => {
try {
const doc = await resendOfferMail(req.params.id)
return res.json({ sent: true, notifiedAt: doc.notifiedAt })
} catch (err) {
return sendError(res, err, 'E-Mail konnte nicht verschickt werden')
}
})