Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
247
api/src/routes/paypal.ts
Normal file
247
api/src/routes/paypal.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
/**
|
||||
* PayPal direct integration — capture, tracking, refund.
|
||||
*
|
||||
* POST /api/v1/paypal/capture — Capture approved order (called from success page)
|
||||
* POST /api/v1/paypal/tracking — Add tracking number to captured order
|
||||
* POST /api/v1/paypal/refund — Refund a captured payment
|
||||
* GET /api/v1/paypal/order/:id/status — Check PayPal order status
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { pool } from "../db.js";
|
||||
import { handlePaymentComplete } from "./checkout.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ─── PayPal API helpers ───────────────────────────────────────────────────────
|
||||
|
||||
const PAYPAL_API_URL = process.env.PAYPAL_API_URL || "https://api-m.paypal.com";
|
||||
const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || "";
|
||||
const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET || "";
|
||||
|
||||
async function getAccessToken(): Promise<string> {
|
||||
const res = await fetch(`${PAYPAL_API_URL}/v1/oauth2/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`).toString("base64")}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: "grant_type=client_credentials",
|
||||
});
|
||||
const data = await res.json() as { access_token?: string; error?: string };
|
||||
if (!data.access_token) throw new Error(`PayPal auth failed: ${data.error || "no access_token"}`);
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
async function paypalFetch(method: string, path: string, body?: object): Promise<{ status: number; data: any }> {
|
||||
const token = await getAccessToken();
|
||||
const res = await fetch(`${PAYPAL_API_URL}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return { status: res.status, data };
|
||||
}
|
||||
|
||||
// ─── Capture an approved PayPal order ─────────────────────────────────────────
|
||||
// Called when the buyer is redirected back after approving payment on PayPal.
|
||||
|
||||
router.post("/api/v1/paypal/capture", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { paypal_order_id, order_id, order_type } = req.body as {
|
||||
paypal_order_id?: string;
|
||||
order_id?: string;
|
||||
order_type?: string;
|
||||
};
|
||||
|
||||
if (!paypal_order_id) {
|
||||
res.status(400).json({ error: "paypal_order_id required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { status, data } = await paypalFetch("POST", `/v2/checkout/orders/${paypal_order_id}/capture`);
|
||||
|
||||
if (status >= 400) {
|
||||
console.error("[paypal] Capture failed:", data);
|
||||
// If already captured, treat as success
|
||||
if (data?.details?.[0]?.issue === "ORDER_ALREADY_CAPTURED") {
|
||||
const check = await paypalFetch("GET", `/v2/checkout/orders/${paypal_order_id}`);
|
||||
if (check.data?.status === "COMPLETED") {
|
||||
res.json({ success: true, status: "COMPLETED", already_captured: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.status(502).json({ error: "PayPal capture failed", details: data });
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === "COMPLETED") {
|
||||
console.log(`[paypal] Captured ${paypal_order_id} successfully`);
|
||||
|
||||
// Find the internal order and mark as paid
|
||||
const resolvedOrderId = order_id || data.purchase_units?.[0]?.custom_id || data.purchase_units?.[0]?.reference_id;
|
||||
const resolvedOrderType = order_type || "canada_crtc";
|
||||
|
||||
if (resolvedOrderId) {
|
||||
try {
|
||||
// Store PayPal capture details in the order before marking paid
|
||||
const captureId = data.purchase_units?.[0]?.payments?.captures?.[0]?.id || "";
|
||||
const payerEmail = data.payer?.email_address || "";
|
||||
const table = resolvedOrderType === "formation" ? "formation_orders" : "canada_crtc_orders";
|
||||
await pool.query(
|
||||
`UPDATE ${table} SET paypal_order_id = $1 WHERE order_number = $2`,
|
||||
[paypal_order_id, resolvedOrderId],
|
||||
).catch(() => {});
|
||||
|
||||
await handlePaymentComplete(resolvedOrderId, resolvedOrderType, `paypal-${captureId}`);
|
||||
} catch (err) {
|
||||
console.error("[paypal] handlePaymentComplete failed (non-blocking):", err);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
status: "COMPLETED",
|
||||
capture_id: data.purchase_units?.[0]?.payments?.captures?.[0]?.id,
|
||||
payer_email: data.payer?.email_address,
|
||||
});
|
||||
} else {
|
||||
res.json({ success: false, status: data.status, details: data });
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("[paypal] Capture error:", err);
|
||||
res.status(500).json({ error: err.message || "PayPal capture failed" });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Check PayPal order status ────────────────────────────────────────────────
|
||||
|
||||
router.get("/api/v1/paypal/order/:id/status", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, data } = await paypalFetch("GET", `/v2/checkout/orders/${id}`);
|
||||
if (status >= 400) {
|
||||
res.status(status).json({ error: "PayPal order lookup failed", details: data });
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
paypal_order_id: data.id,
|
||||
status: data.status,
|
||||
payer_email: data.payer?.email_address,
|
||||
amount: data.purchase_units?.[0]?.amount,
|
||||
custom_id: data.purchase_units?.[0]?.custom_id,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[paypal] Status check error:", err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Add tracking number to a captured order ──────────────────────────────────
|
||||
|
||||
router.post("/api/v1/paypal/tracking", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { capture_id, tracking_number, carrier, order_id } = req.body as {
|
||||
capture_id?: string;
|
||||
tracking_number?: string;
|
||||
carrier?: string;
|
||||
order_id?: string;
|
||||
};
|
||||
|
||||
if (!capture_id || !tracking_number) {
|
||||
res.status(400).json({ error: "capture_id and tracking_number required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// PayPal tracking API — POST /v1/shipping/trackers-batch
|
||||
const { status, data } = await paypalFetch("POST", "/v1/shipping/trackers-batch", {
|
||||
trackers: [{
|
||||
transaction_id: capture_id,
|
||||
tracking_number,
|
||||
status: "SHIPPED",
|
||||
carrier: carrier || "OTHER",
|
||||
}],
|
||||
});
|
||||
|
||||
if (status >= 400) {
|
||||
console.error("[paypal] Tracking update failed:", data);
|
||||
res.status(502).json({ error: "PayPal tracking update failed", details: data });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[paypal] Tracking added for capture ${capture_id}: ${tracking_number}`);
|
||||
res.json({ success: true, tracking_number, capture_id });
|
||||
} catch (err: any) {
|
||||
console.error("[paypal] Tracking error:", err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Refund a captured payment ────────────────────────────────────────────────
|
||||
|
||||
router.post("/api/v1/paypal/refund", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { capture_id, amount, currency, reason, order_id } = req.body as {
|
||||
capture_id?: string;
|
||||
amount?: string;
|
||||
currency?: string;
|
||||
reason?: string;
|
||||
order_id?: string;
|
||||
};
|
||||
|
||||
if (!capture_id) {
|
||||
res.status(400).json({ error: "capture_id required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const refundBody: Record<string, any> = {};
|
||||
if (amount) {
|
||||
refundBody.amount = { value: amount, currency_code: currency || "USD" };
|
||||
}
|
||||
if (reason) {
|
||||
refundBody.note_to_payer = reason;
|
||||
}
|
||||
|
||||
const { status, data } = await paypalFetch(
|
||||
"POST",
|
||||
`/v2/payments/captures/${capture_id}/refund`,
|
||||
Object.keys(refundBody).length > 0 ? refundBody : undefined,
|
||||
);
|
||||
|
||||
if (status >= 400) {
|
||||
console.error("[paypal] Refund failed:", data);
|
||||
res.status(502).json({ error: "PayPal refund failed", details: data });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[paypal] Refund ${data.id} for capture ${capture_id}: ${data.status}`);
|
||||
|
||||
// Update order status if order_id provided
|
||||
if (order_id) {
|
||||
await pool.query(
|
||||
`UPDATE canada_crtc_orders SET payment_status = 'refunded' WHERE order_number = $1`,
|
||||
[order_id],
|
||||
).catch(() => {});
|
||||
await pool.query(
|
||||
`UPDATE formation_orders SET payment_status = 'refunded' WHERE order_number = $1`,
|
||||
[order_id],
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
refund_id: data.id,
|
||||
status: data.status,
|
||||
amount: data.amount,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[paypal] Refund error:", err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Loading…
Add table
Add a link
Reference in a new issue