Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions apps/web/app/(ee)/api/cron/check-paypal-balance/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { verifyVercelSignature } from "@/lib/cron/verify-vercel";
import { getPayPalBalance } from "@/lib/paypal/get-paypal-balance";
import { getPendingPaypalPayouts } from "@/lib/paypal/get-pending-payouts";
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";

// This route is used to check PayPal balance and notify us if it needs refilling
// Runs every 3 hours (0 */3 * * *)

export async function GET(req: Request) {
try {
await verifyVercelSignature(req);

const [pendingPaypalPayouts, currentPaypalBalance] = await Promise.all([
getPendingPaypalPayouts(),
getPayPalBalance(),
]);

const pendingPaypalPayoutsTotal = pendingPaypalPayouts.reduce(
(acc, payout) => acc + payout.amount,
0,
);

return NextResponse.json({
pendingPaypalPayoutsTotal,
currentPaypalBalance,
});
} catch (error) {
return handleAndReturnErrorResponse(error);
}
}
86 changes: 86 additions & 0 deletions apps/web/lib/paypal/get-paypal-balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { createPaypalToken } from "@/lib/paypal/create-paypal-token";
import { paypalEnv } from "@/lib/paypal/env";

interface PayPalBalance {
currency: string;
value: string;
}

interface PayPalBalanceResponse {
balances: PayPalBalance[];
}

// Get the current PayPal account balance by computing from transaction history
export async function getPayPalBalance(): Promise<PayPalBalanceResponse> {
const paypalAccessToken = await createPaypalToken();

// Get transactions for the last 30 days to compute current balance
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30); // Last 30 days

const url = new URL(`${paypalEnv.PAYPAL_API_HOST}/v1/reporting/transactions`);
url.searchParams.append(
"start_date",
startDate.toISOString().replace("Z", "-0700"),
);
url.searchParams.append(
"end_date",
endDate.toISOString().replace("Z", "-0700"),
);
url.searchParams.append("fields", "transaction_info"); // Only get transaction info to reduce payload

const response = await fetch(url.toString(), {
method: "GET",
headers: {
Authorization: `Bearer ${paypalAccessToken}`,
"Content-Type": "application/json",
},
});

console.log("[PayPal] Balance computation started", response);

const data = await response.json();

if (!response.ok) {
console.error("[PayPal] Balance computation failed", data);
throw new Error(
`[PayPal] Balance computation failed. Error: ${JSON.stringify(data)}`,
);
}

// Compute balance from transaction history
let balance = 0;
const currency = "USD"; // Default to USD, could be made dynamic

if (data.transaction_details) {
data.transaction_details.forEach(({ transaction_info }: any) => {
const amount = parseFloat(
transaction_info.transaction_amount?.value || "0",
);
const type = transaction_info.transaction_event_code;

// Credit transactions (money received)
if (["T0006", "T1107", "T1101", "T0004", "T0002"].includes(type)) {
balance += amount;
}
// Debit transactions (money sent, fees, etc.)
else if (["T0007", "T0400", "T1109", "T0003", "T0005"].includes(type)) {
balance -= amount;
}
});
}

const result: PayPalBalanceResponse = {
balances: [
{
currency,
value: balance.toFixed(2),
},
],
};

console.log("[PayPal] Balance computed", result);

return result;
}