Files
proxpanel/backend/src/services/payment.service.ts

193 lines
6.6 KiB
TypeScript

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<PaymentSettings> {
const setting = await prisma.setting.findUnique({
where: { key: "payment" }
});
return decryptJson<PaymentSettings>(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 };
}