import axios from "axios"; import crypto from "crypto"; import { PaymentProvider } from "@prisma/client"; import { prisma } from "../lib/prisma"; import { HttpError } from "../lib/http-error"; import { decryptJson } from "../lib/security"; import { markInvoicePaid } from "./billing.service"; type PaymentSettings = { default_provider?: "paystack" | "flutterwave" | "manual"; paystack_public?: string; paystack_secret?: string; paystack_secret_previous?: string; flutterwave_public?: string; flutterwave_secret?: string; flutterwave_secret_previous?: string; flutterwave_webhook_hash?: string; flutterwave_webhook_hash_previous?: string; callback_url?: string; }; async function getPaymentSettings(): Promise { const setting = await prisma.setting.findUnique({ where: { key: "payment" } }); return decryptJson(setting?.value) ?? {}; } function normalizeProvider(provider: string | undefined, fallback: string): PaymentProvider { const value = (provider ?? fallback).toLowerCase(); if (value === "paystack") return PaymentProvider.PAYSTACK; if (value === "flutterwave") return PaymentProvider.FLUTTERWAVE; return PaymentProvider.MANUAL; } export async function createInvoicePaymentLink(invoiceId: string, requestedProvider?: string) { const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId }, include: { tenant: true } }); if (!invoice) { throw new HttpError(404, "Invoice not found", "INVOICE_NOT_FOUND"); } const settings = await getPaymentSettings(); const provider = normalizeProvider(requestedProvider, settings.default_provider ?? "manual"); if (provider === PaymentProvider.MANUAL) { throw new HttpError(400, "Manual payment provider cannot generate online links", "MANUAL_PROVIDER"); } const reference = invoice.payment_reference ?? `PAY-${invoice.invoice_number}-${Date.now()}`; if (provider === PaymentProvider.PAYSTACK) { if (!settings.paystack_secret) { throw new HttpError(400, "Paystack secret key is missing", "PAYSTACK_CONFIG_MISSING"); } const response = await axios.post( "https://api.paystack.co/transaction/initialize", { email: invoice.tenant.owner_email, amount: Math.round(Number(invoice.amount) * 100), reference, currency: invoice.currency, callback_url: settings.callback_url, metadata: { invoice_id: invoice.id, tenant_id: invoice.tenant_id } }, { headers: { Authorization: `Bearer ${settings.paystack_secret}`, "Content-Type": "application/json" } } ); const paymentUrl = response.data?.data?.authorization_url as string | undefined; await prisma.invoice.update({ where: { id: invoice.id }, data: { status: "PENDING", payment_provider: provider, payment_reference: reference, payment_url: paymentUrl } }); return { provider: "paystack", payment_url: paymentUrl, reference }; } if (!settings.flutterwave_secret) { throw new HttpError(400, "Flutterwave secret key is missing", "FLUTTERWAVE_CONFIG_MISSING"); } const response = await axios.post( "https://api.flutterwave.com/v3/payments", { tx_ref: reference, amount: Number(invoice.amount), currency: invoice.currency, redirect_url: settings.callback_url, customer: { email: invoice.tenant.owner_email, name: invoice.tenant.name }, customizations: { title: "ProxPanel Invoice Payment", description: `Invoice ${invoice.invoice_number}` }, meta: { invoice_id: invoice.id, tenant_id: invoice.tenant_id } }, { headers: { Authorization: `Bearer ${settings.flutterwave_secret}`, "Content-Type": "application/json" } } ); const paymentUrl = response.data?.data?.link as string | undefined; await prisma.invoice.update({ where: { id: invoice.id }, data: { status: "PENDING", payment_provider: provider, payment_reference: reference, payment_url: paymentUrl } }); return { provider: "flutterwave", payment_url: paymentUrl, reference }; } export async function handleManualInvoicePayment(invoiceId: string, reference: string, actorEmail: string) { return markInvoicePaid(invoiceId, PaymentProvider.MANUAL, reference, actorEmail); } export async function verifyPaystackSignature(signature: string | undefined, rawBody: string | undefined) { if (!signature || !rawBody) return false; const settings = await getPaymentSettings(); const secrets = [settings.paystack_secret, settings.paystack_secret_previous].filter( (value): value is string => typeof value === "string" && value.trim().length > 0 ); if (secrets.length === 0) return false; return secrets.some((secret) => { const expected = crypto.createHmac("sha512", secret).update(rawBody).digest("hex"); return expected === signature; }); } export async function verifyFlutterwaveSignature(signature: string | undefined) { const settings = await getPaymentSettings(); const validHashes = [settings.flutterwave_webhook_hash, settings.flutterwave_webhook_hash_previous].filter( (value): value is string => typeof value === "string" && value.trim().length > 0 ); if (validHashes.length === 0 || !signature) return false; return validHashes.includes(signature); } export async function processPaystackWebhook(payload: any) { if (payload?.event !== "charge.success") return { handled: false }; const reference = payload?.data?.reference as string | undefined; if (!reference) return { handled: false }; const invoice = await prisma.invoice.findFirst({ where: { payment_reference: reference } }); if (!invoice) return { handled: false }; if (invoice.status !== "PAID") { await markInvoicePaid(invoice.id, PaymentProvider.PAYSTACK, reference, "webhook@paystack"); } return { handled: true, invoice_id: invoice.id }; } export async function processFlutterwaveWebhook(payload: any) { const status = payload?.status?.toLowerCase(); if (status !== "successful") return { handled: false }; const reference = (payload?.txRef ?? payload?.tx_ref) as string | undefined; if (!reference) return { handled: false }; const invoice = await prisma.invoice.findFirst({ where: { payment_reference: reference } }); if (!invoice) return { handled: false }; if (invoice.status !== "PAID") { await markInvoicePaid(invoice.id, PaymentProvider.FLUTTERWAVE, reference, "webhook@flutterwave"); } return { handled: true, invoice_id: invoice.id }; }