fixing platform admin
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user