pingql/apps/pay/src/receipt.ts

130 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sql from "./db";
import { COINS } from "./plans";
export async function generateReceipt(paymentId: number): Promise<string> {
const [payment] = await sql`SELECT * FROM payments WHERE id = ${paymentId}`;
if (!payment) throw new Error("Payment not found");
// Already locked — return as-is
if (payment.receipt_html) return payment.receipt_html;
const coinInfo = COINS[payment.coin];
const txs = await sql`
SELECT txid, amount, confirmed, detected_at
FROM payment_txs WHERE payment_id = ${paymentId}
ORDER BY detected_at ASC
`;
const paidDate = payment.paid_at
? new Date(payment.paid_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })
: "—";
const createdDate = new Date(payment.created_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
const planLabel = payment.plan === "lifetime"
? "Lifetime"
: `Pro × ${payment.months} month${payment.months > 1 ? "s" : ""}`;
const txRows = txs.map((tx: any) => {
const date = new Date(tx.detected_at).toLocaleDateString("en-US", {
year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
});
return `<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e5e7eb;font-family:monospace;font-size:12px;word-break:break-all">${tx.txid}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e5e7eb;text-align:right">${tx.amount} ${coinInfo?.ticker || payment.coin.toUpperCase()}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e5e7eb;text-align:right">${date}</td>
</tr>`;
}).join("");
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PingQL Receipt #${payment.id}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #1a1a1a; max-width: 640px; margin: 0 auto; padding: 40px 24px; }
.print-btn { background: #2563eb; color: white; border: none; padding: 10px 24px; border-radius: 6px; font-size: 14px; cursor: pointer; margin-bottom: 32px; display: inline-block; }
.print-btn:hover { background: #3b82f6; }
@media print { .no-print { display: none !important; } body { padding: 0; } }
.header { border-bottom: 2px solid #1a1a1a; padding-bottom: 16px; margin-bottom: 24px; }
.logo { font-size: 24px; font-weight: 700; }
.logo span { color: #2563eb; }
.meta { color: #6b7280; font-size: 13px; margin-top: 4px; }
.section { margin-bottom: 24px; }
.section-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; margin-bottom: 8px; }
.row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 14px; }
.row .label { color: #6b7280; }
.row .value { font-weight: 500; }
.total { border-top: 2px solid #1a1a1a; padding-top: 12px; margin-top: 12px; font-size: 16px; font-weight: 600; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { text-align: left; padding: 8px 12px; border-bottom: 2px solid #e5e7eb; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; }
.address { font-family: monospace; font-size: 12px; color: #6b7280; word-break: break-all; }
.footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #9ca3af; }
</style>
</head>
<body>
<div class="no-print">
<button class="print-btn" onclick="window.print()">Print Receipt</button>
</div>
<div class="header">
<div class="logo">Ping<span>QL</span></div>
<div class="meta">Receipt #${payment.id} · Issued ${paidDate}</div>
</div>
<div class="section">
<div class="section-title">Plan</div>
<div class="row">
<span class="label">Product</span>
<span class="value">PingQL ${planLabel}</span>
</div>
<div class="row">
<span class="label">Invoice Date</span>
<span class="value">${createdDate}</span>
</div>
<div class="row">
<span class="label">Payment Date</span>
<span class="value">${paidDate}</span>
</div>
</div>
<div class="section">
<div class="section-title">Payment</div>
<div class="row">
<span class="label">Currency</span>
<span class="value">${coinInfo?.label || payment.coin} (${coinInfo?.ticker || payment.coin.toUpperCase()})</span>
</div>
<div class="row">
<span class="label">Amount Paid</span>
<span class="value">${payment.amount_received || payment.amount_crypto} ${coinInfo?.ticker || payment.coin.toUpperCase()}</span>
</div>
<div class="row">
<span class="label">Payment Address</span>
<span class="value address">${payment.address}</span>
</div>
<div class="row total">
<span>Total (USD)</span>
<span>$${Number(payment.amount_usd).toFixed(2)}</span>
</div>
</div>
${txs.length > 0 ? `<div class="section">
<div class="section-title">Transactions</div>
<table>
<thead><tr><th>Transaction ID</th><th style="text-align:right">Amount</th><th style="text-align:right">Date</th></tr></thead>
<tbody>${txRows}</tbody>
</table>
</div>` : ""}
<div class="footer">
PingQL · pingql.com<br>
This receipt was generated on ${paidDate} and cannot be modified.
</div>
</body>
</html>`;
// Lock it
await sql`UPDATE payments SET receipt_html = ${html} WHERE id = ${paymentId}`;
return html;
}