Files
Emailsorter/server/routes/stripe.mjs
ANDJ abf761db07 Email Sorter Beta
Ich habe soweit automatisiert the Emails sortieren aber ich muss noch schauen was es fur bugs es gibt wenn die app online  ist deswegen wurde ich mit diesen Commit die website veroffentlichen obwohjl es sein konnte  das es noch nicht fertig ist und verkaufs bereit
2026-01-22 19:32:12 +01:00

352 lines
9.0 KiB
JavaScript

/**
* Stripe Routes
* Payment and subscription management
*/
import express from 'express'
import Stripe from 'stripe'
import { asyncHandler, ValidationError, NotFoundError } from '../middleware/errorHandler.mjs'
import { validate, rules } from '../middleware/validate.mjs'
import { limiters } from '../middleware/rateLimit.mjs'
import { respond } from '../utils/response.mjs'
import { subscriptions, submissions } from '../services/database.mjs'
import { config } from '../config/index.mjs'
import { log } from '../middleware/logger.mjs'
const router = express.Router()
const stripe = new Stripe(config.stripe.secretKey)
/**
* Plan configuration
*/
const PLANS = {
basic: {
name: 'Basic',
priceId: config.stripe.prices.basic,
features: {
emailAccounts: 1,
emailsPerDay: 500,
historicalSync: false,
customRules: false,
prioritySupport: false,
},
},
pro: {
name: 'Pro',
priceId: config.stripe.prices.pro,
features: {
emailAccounts: 3,
emailsPerDay: -1,
historicalSync: true,
customRules: true,
prioritySupport: false,
},
},
business: {
name: 'Business',
priceId: config.stripe.prices.business,
features: {
emailAccounts: 10,
emailsPerDay: -1,
historicalSync: true,
customRules: true,
prioritySupport: true,
},
},
}
/**
* POST /api/subscription/checkout
* Create subscription checkout session
*/
router.post('/checkout',
limiters.auth,
validate({
body: {
userId: [rules.required('userId')],
plan: [rules.required('plan'), rules.isIn('plan', ['basic', 'pro', 'business'])],
},
}),
asyncHandler(async (req, res) => {
const { userId, plan, email } = req.body
const planConfig = PLANS[plan]
if (!planConfig) {
throw new ValidationError('Ungültiger Plan', { plan: ['Ungültig'] })
}
// Check for existing subscription
const existing = await subscriptions.getByUser(userId)
let customerId = existing?.stripeCustomerId
// Create checkout session
const sessionConfig = {
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: planConfig.priceId,
quantity: 1,
},
],
success_url: `${config.frontendUrl}/setup?subscription=success&setup=auto`,
cancel_url: `${config.frontendUrl}/pricing?subscription=cancelled`,
metadata: {
userId,
plan,
},
subscription_data: {
trial_period_days: 14,
metadata: { userId, plan },
},
allow_promotion_codes: true,
}
if (customerId) {
sessionConfig.customer = customerId
} else if (email) {
sessionConfig.customer_email = email
}
const session = await stripe.checkout.sessions.create(sessionConfig)
log.info(`Checkout Session erstellt für User ${userId}`, { plan })
respond.success(res, {
url: session.url,
sessionId: session.id,
})
})
)
/**
* GET /api/subscription/status
* Get user's subscription status
*/
router.get('/status', asyncHandler(async (req, res) => {
const { userId } = req.query
if (!userId) {
throw new ValidationError('userId ist erforderlich')
}
const sub = await subscriptions.getByUser(userId)
if (!sub) {
// No subscription - return trial info
return respond.success(res, {
status: 'trial',
plan: 'pro',
features: PLANS.pro.features,
trialEndsAt: null, // Would calculate from user creation date
cancelAtPeriodEnd: false,
})
}
respond.success(res, {
status: sub.status,
plan: sub.plan,
features: PLANS[sub.plan]?.features || PLANS.basic.features,
currentPeriodEnd: sub.currentPeriodEnd,
cancelAtPeriodEnd: sub.cancelAtPeriodEnd || false,
})
}))
/**
* POST /api/subscription/portal
* Create Stripe Customer Portal session
*/
router.post('/portal',
validate({
body: {
userId: [rules.required('userId')],
},
}),
asyncHandler(async (req, res) => {
const { userId } = req.body
const sub = await subscriptions.getByUser(userId)
if (!sub?.stripeCustomerId) {
throw new NotFoundError('Subscription')
}
const session = await stripe.billingPortal.sessions.create({
customer: sub.stripeCustomerId,
return_url: `${config.frontendUrl}/settings`,
})
respond.success(res, { url: session.url })
})
)
/**
* POST /api/subscription/cancel
* Cancel subscription at period end
*/
router.post('/cancel',
validate({
body: {
userId: [rules.required('userId')],
},
}),
asyncHandler(async (req, res) => {
const { userId } = req.body
const sub = await subscriptions.getByUser(userId)
if (!sub?.stripeSubscriptionId) {
throw new NotFoundError('Subscription')
}
await stripe.subscriptions.update(sub.stripeSubscriptionId, {
cancel_at_period_end: true,
})
await subscriptions.update(sub.$id, { cancelAtPeriodEnd: true })
log.info(`Subscription gekündigt für User ${userId}`)
respond.success(res, null, 'Subscription wird zum Ende der Periode gekündigt')
})
)
/**
* POST /api/subscription/reactivate
* Reactivate cancelled subscription
*/
router.post('/reactivate',
validate({
body: {
userId: [rules.required('userId')],
},
}),
asyncHandler(async (req, res) => {
const { userId } = req.body
const sub = await subscriptions.getByUser(userId)
if (!sub?.stripeSubscriptionId) {
throw new NotFoundError('Subscription')
}
await stripe.subscriptions.update(sub.stripeSubscriptionId, {
cancel_at_period_end: false,
})
await subscriptions.update(sub.$id, { cancelAtPeriodEnd: false })
log.info(`Subscription reaktiviert für User ${userId}`)
respond.success(res, null, 'Subscription wurde reaktiviert')
})
)
/**
* POST /stripe/webhook & POST /api/subscription/webhook
* Stripe webhook handler
*/
router.post('/webhook', express.raw({ type: 'application/json' }), asyncHandler(async (req, res) => {
const sig = req.headers['stripe-signature']
let event
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
config.stripe.webhookSecret
)
} catch (err) {
log.error('Webhook Signatur Fehler', { error: err.message })
return res.status(400).send(`Webhook Error: ${err.message}`)
}
log.info(`Stripe Webhook: ${event.type}`)
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object
const { userId, plan } = session.metadata || {}
if (userId && session.subscription) {
const subscription = await stripe.subscriptions.retrieve(session.subscription)
await subscriptions.upsertByUser(userId, {
stripeCustomerId: session.customer,
stripeSubscriptionId: session.subscription,
plan: plan || 'basic',
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
})
log.success(`Subscription erstellt für User ${userId}`, { plan })
}
// Handle one-time payment (legacy)
if (session.metadata?.submissionId) {
await submissions.updateStatus(session.metadata.submissionId, 'paid')
log.success(`Zahlung abgeschlossen: ${session.metadata.submissionId}`)
}
break
}
case 'customer.subscription.updated': {
const subscription = event.data.object
const sub = await subscriptions.getByStripeId(subscription.id)
if (sub) {
await subscriptions.update(sub.$id, {
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
})
log.info(`Subscription aktualisiert: ${subscription.id}`)
}
break
}
case 'customer.subscription.deleted': {
const subscription = event.data.object
const sub = await subscriptions.getByStripeId(subscription.id)
if (sub) {
await subscriptions.update(sub.$id, {
status: 'cancelled',
})
log.info(`Subscription gelöscht: ${subscription.id}`)
}
break
}
case 'invoice.payment_failed': {
const invoice = event.data.object
log.warn(`Zahlung fehlgeschlagen: ${invoice.id}`, {
customer: invoice.customer,
})
// TODO: Send notification email
break
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object
log.success(`Zahlung erfolgreich: ${invoice.id}`)
break
}
default:
log.debug(`Unbehandelter Webhook: ${event.type}`)
}
res.json({ received: true })
} catch (err) {
log.error('Webhook Handler Fehler', { error: err.message, event: event.type })
res.status(500).json({ error: 'Webhook handler failed' })
}
}))
export default router