chore: initialize repository with deployment baseline

This commit is contained in:
Austin A
2026-04-17 23:03:00 +01:00
parent f02ddf42aa
commit 5def26e0df
166 changed files with 43065 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
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 { markInvoicePaid } from "./billing.service";
type PaymentSettings = {
default_provider?: "paystack" | "flutterwave" | "manual";
paystack_public?: string;
paystack_secret?: string;
flutterwave_public?: string;
flutterwave_secret?: string;
flutterwave_webhook_hash?: string;
callback_url?: string;
};
async function getPaymentSettings(): Promise<PaymentSettings> {
const setting = await prisma.setting.findUnique({
where: { key: "payment" }
});
return (setting?.value as PaymentSettings) ?? {};
}
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();
if (!settings.paystack_secret) return false;
const expected = crypto
.createHmac("sha512", settings.paystack_secret)
.update(rawBody)
.digest("hex");
return expected === signature;
}
export async function verifyFlutterwaveSignature(signature: string | undefined) {
const settings = await getPaymentSettings();
if (!settings.flutterwave_webhook_hash) return false;
return settings.flutterwave_webhook_hash === 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 };
}