"Gültig bis" ist nur die Frist zum Annehmen. Jetzt steht auch das Rabattende da: - Formular "Neues Angebot", Kunde ohne Abo: Schätzung "Rabatt läuft N Monate ab Abschluss und endet voraussichtlich zwischen <heute + N> und <gültig bis + N>", mit Hinweis auf den späteren Start nach der Widerrufsfrist (gut 2 Wochen) - Formular, laufendes Abo: genaues Ende aus den nächsten Abbuchungen (Anker = Leistungsbeginn, Monatsende wie bei Stripe), auch in der Erfolgsmeldung - Angebotsliste: offen -> Schätzung, aktiv -> "Rabatt endet am ..." aus dem Abo-Dokument (bzw. "endete", "endet mit dem Abo am ...", "unbekannt (Abo nicht gefunden)") Rechnung in server/services/offerDiscount.js: Der Coupon deckt die N Abbuchungen ab dem Anwenden. Beim Checkout ist das der Abo-Start, bei laufendem Abo und Testkonto (Coupon per Webhook) der Zeitpunkt des Anhängens; Testkonten laufen deshalb einen Monat länger. Tests: npm test (37 Fälle, davon 8 neu). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
2.4 KiB
JavaScript
70 lines
2.4 KiB
JavaScript
import { Router } from 'express'
|
|
import { config } from '../config.js'
|
|
import { listDocuments, Query } from '../services/appwriteAdmin.js'
|
|
import { adminOfferView, createOffer, listOffers, offerContext, 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')
|
|
}
|
|
})
|