fixing platform admin

This commit is contained in:
root
2026-05-06 22:58:23 -04:00
parent 695a7f7cc7
commit 750ae56a29
175 changed files with 31249 additions and 328 deletions
+288
View File
@@ -0,0 +1,288 @@
import { Router } from 'express'
import { z } from 'zod'
import { PLAN_PRICES } from '@rentaldrivego/types'
import { prisma } from '../lib/prisma'
import { requireCompanyAuth } from '../middleware/requireCompanyAuth'
import { requireTenant } from '../middleware/requireTenant'
import * as amanpay from '../services/amanpayService'
import * as paypal from '../services/paypalService'
const router = Router()
router.get('/plans', (_req, res) => {
res.json({ data: PLAN_PRICES })
})
// ─── AmanPay subscription webhook (no auth) ──────────────────────────────────
router.post('/webhooks/amanpay', async (req, res, next) => {
try {
const rawBody = JSON.stringify(req.body)
const signature = req.headers['x-amanpay-signature'] as string ?? ''
if (amanpay.isConfigured() && !amanpay.verifyWebhookSignature(rawBody, signature)) {
return res.status(401).json({ error: 'invalid_signature' })
}
const event = req.body
const transactionId = event.transaction_id ?? event.id
const status = event.status?.toUpperCase()
if (status === 'PAID' || status === 'SUCCEEDED') {
const invoice = await prisma.subscriptionInvoice.findFirst({
where: { amanpayTransactionId: transactionId },
include: { subscription: true },
})
if (invoice) {
await prisma.subscriptionInvoice.update({
where: { id: invoice.id },
data: { status: 'PAID', paidAt: new Date() },
})
await prisma.subscription.update({
where: { id: invoice.subscriptionId },
data: {
status: 'ACTIVE',
currentPeriodStart: new Date(),
currentPeriodEnd: addPeriod(new Date(), invoice.subscription.billingPeriod),
},
})
}
}
res.json({ received: true })
} catch (err) { next(err) }
})
// ─── PayPal subscription webhook (no auth) ───────────────────────────────────
router.post('/webhooks/paypal', async (req, res, next) => {
try {
const rawBody = JSON.stringify(req.body)
const isValid = await paypal.verifyWebhookEvent(req.headers as Record<string, string>, rawBody)
if (paypal.isConfigured() && !isValid) {
return res.status(401).json({ error: 'invalid_signature' })
}
const event = req.body
if (event.event_type === 'PAYMENT.CAPTURE.COMPLETED') {
const captureId = event.resource?.id as string
const invoice = await prisma.subscriptionInvoice.findFirst({
where: { paypalCaptureId: captureId },
include: { subscription: true },
})
if (invoice) {
await prisma.subscriptionInvoice.update({
where: { id: invoice.id },
data: { status: 'PAID', paidAt: new Date() },
})
await prisma.subscription.update({
where: { id: invoice.subscriptionId },
data: {
status: 'ACTIVE',
currentPeriodStart: new Date(),
currentPeriodEnd: addPeriod(new Date(), invoice.subscription.billingPeriod),
},
})
}
}
res.json({ received: true })
} catch (err) { next(err) }
})
// ─── PayPal capture redirect (no full auth needed — just valid session) ───────
router.post('/capture-paypal', requireCompanyAuth, requireTenant, async (req, res, next) => {
try {
const { paypalOrderId } = z.object({ paypalOrderId: z.string() }).parse(req.body)
const invoice = await prisma.subscriptionInvoice.findFirstOrThrow({
where: { paypalCaptureId: paypalOrderId, companyId: req.companyId },
include: { subscription: true },
})
const capture = await paypal.captureOrder(paypalOrderId)
const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId
await prisma.subscriptionInvoice.update({
where: { id: invoice.id },
data: { status: 'PAID', paidAt: new Date(), paypalCaptureId: captureId },
})
await prisma.subscription.update({
where: { id: invoice.subscriptionId },
data: {
status: 'ACTIVE',
currentPeriodStart: new Date(),
currentPeriodEnd: addPeriod(new Date(), invoice.subscription.billingPeriod),
},
})
res.json({ data: { success: true } })
} catch (err) { next(err) }
})
// ─── Authenticated subscription routes ───────────────────────────────────────
router.use(requireCompanyAuth, requireTenant)
router.get('/me', async (req, res, next) => {
try {
const subscription = await prisma.subscription.findUnique({
where: { companyId: req.companyId },
include: { invoices: { orderBy: { createdAt: 'desc' }, take: 12 } },
})
res.json({ data: subscription })
} catch (err) { next(err) }
})
router.get('/invoices', async (req, res, next) => {
try {
const invoices = await prisma.subscriptionInvoice.findMany({
where: { companyId: req.companyId },
orderBy: { createdAt: 'desc' },
take: 50,
})
res.json({ data: invoices })
} catch (err) { next(err) }
})
const checkoutSchema = z.object({
plan: z.enum(['STARTER', 'GROWTH', 'PRO']),
billingPeriod: z.enum(['MONTHLY', 'ANNUAL']),
currency: z.enum(['MAD', 'USD', 'EUR']),
provider: z.enum(['AMANPAY', 'PAYPAL']),
successUrl: z.string().url(),
failureUrl: z.string().url(),
})
router.post('/checkout', async (req, res, next) => {
try {
const body = checkoutSchema.parse(req.body)
const prices = PLAN_PRICES[body.plan]?.[body.billingPeriod]
if (!prices) return res.status(400).json({ error: 'invalid_plan', message: 'Invalid plan or billing period', statusCode: 400 })
const amount = prices[body.currency]
if (!amount) return res.status(400).json({ error: 'invalid_currency', message: 'Currency not supported for this plan', statusCode: 400 })
const company = await prisma.company.findUniqueOrThrow({ where: { id: req.companyId } })
let subscription = await prisma.subscription.findUnique({ where: { companyId: req.companyId } })
if (!subscription) {
subscription = await prisma.subscription.create({
data: {
companyId: req.companyId,
plan: body.plan,
billingPeriod: body.billingPeriod,
currency: body.currency,
status: 'PENDING' as any,
},
})
}
const orderId = `sub-${req.companyId}-${Date.now()}`
const description = `${body.plan} plan — ${body.billingPeriod}`
const webhookBase = process.env.API_URL ?? 'http://localhost:4000'
let checkoutUrl: string
let amanpayTransactionId: string | null = null
let paypalCaptureId: string | null = null
if (body.provider === 'AMANPAY') {
if (!amanpay.isConfigured()) {
return res.status(503).json({ error: 'provider_not_configured', message: 'AmanPay is not configured on this platform', statusCode: 503 })
}
const result = await amanpay.createCheckout({
amount,
currency: body.currency,
orderId,
description,
customerEmail: company.email,
customerName: company.name,
successUrl: body.successUrl,
failureUrl: body.failureUrl,
webhookUrl: `${webhookBase}/api/v1/subscriptions/webhooks/amanpay`,
})
checkoutUrl = result.checkoutUrl
amanpayTransactionId = result.transactionId
} else {
if (!paypal.isConfigured()) {
return res.status(503).json({ error: 'provider_not_configured', message: 'PayPal is not configured on this platform', statusCode: 503 })
}
const result = await paypal.createOrder({
amount,
currency: body.currency,
orderId,
description,
returnUrl: body.successUrl,
cancelUrl: body.failureUrl,
})
checkoutUrl = result.approveUrl
paypalCaptureId = result.orderId
}
const invoice = await prisma.subscriptionInvoice.create({
data: {
companyId: req.companyId,
subscriptionId: subscription.id,
amount,
currency: body.currency,
status: 'PENDING',
paymentProvider: body.provider,
amanpayTransactionId,
paypalCaptureId,
},
})
res.json({ data: { invoice, checkoutUrl } })
} catch (err) { next(err) }
})
router.post('/change-plan', async (req, res, next) => {
try {
const { plan, billingPeriod, currency } = z.object({
plan: z.enum(['STARTER', 'GROWTH', 'PRO']),
billingPeriod: z.enum(['MONTHLY', 'ANNUAL']),
currency: z.enum(['MAD', 'USD', 'EUR']),
}).parse(req.body)
const updated = await prisma.subscription.update({
where: { companyId: req.companyId },
data: { plan, billingPeriod, currency },
})
res.json({ data: updated })
} catch (err) { next(err) }
})
router.post('/cancel', async (req, res, next) => {
try {
const updated = await prisma.subscription.update({
where: { companyId: req.companyId },
data: { cancelAtPeriodEnd: true },
})
res.json({ data: updated })
} catch (err) { next(err) }
})
router.post('/resume', async (req, res, next) => {
try {
const updated = await prisma.subscription.update({
where: { companyId: req.companyId },
data: { cancelAtPeriodEnd: false },
})
res.json({ data: updated })
} catch (err) { next(err) }
})
// ─── Helpers ─────────────────────────────────────────────────────────────────
function addPeriod(date: Date, period: string): Date {
const d = new Date(date)
if (period === 'ANNUAL') {
d.setFullYear(d.getFullYear() + 1)
} else {
d.setMonth(d.getMonth() + 1)
}
return d
}
export default router