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
352 lines
9.0 KiB
JavaScript
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
|