chore: initialize repository with deployment baseline
This commit is contained in:
182
backend/src/services/payment.service.ts
Normal file
182
backend/src/services/payment.service.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user