193 lines
6.6 KiB
TypeScript
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 };
|
|
}
|