diff --git a/apps/pay/src/monitor.ts b/apps/pay/src/monitor.ts index 171efec..d70a236 100644 --- a/apps/pay/src/monitor.ts +++ b/apps/pay/src/monitor.ts @@ -3,6 +3,7 @@ import sql from "./db"; import { getAddressInfo, getAddressInfoBulk } from "./freedom"; import { COINS } from "./plans"; +import { generateReceipt } from "./receipt"; const SOCK_API = process.env.FREEDOM_SOCK ?? "https://sock-v1.freedom.st"; const THRESHOLD = 0.95; @@ -83,6 +84,7 @@ async function evaluatePayment(paymentId: number) { if (newStatus === "paid") { await sql`UPDATE payments SET status = 'paid', amount_received = ${received.toFixed(8)}, paid_at = now() WHERE id = ${paymentId} AND status != 'paid'`; await applyPlan(payment); + await generateReceipt(paymentId).catch(e => console.error(`Receipt generation failed for ${paymentId}:`, e)); addressMap.delete(payment.address); console.log(`Payment ${paymentId} paid`); } else { diff --git a/apps/pay/src/receipt.ts b/apps/pay/src/receipt.ts new file mode 100644 index 0000000..0d7ac3f --- /dev/null +++ b/apps/pay/src/receipt.ts @@ -0,0 +1,129 @@ +import sql from "./db"; +import { COINS } from "./plans"; + +export async function generateReceipt(paymentId: number): Promise { + 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 ` + ${tx.txid} + ${tx.amount} ${coinInfo?.ticker || payment.coin.toUpperCase()} + ${date} + `; + }).join(""); + + const html = ` + + + + + PingQL Receipt #${payment.id} + + + +
+ +
+ +
+ +
Receipt #${payment.id} · Issued ${paidDate}
+
+ +
+
Plan
+
+ Product + PingQL ${planLabel} +
+
+ Invoice Date + ${createdDate} +
+
+ Payment Date + ${paidDate} +
+
+ +
+
Payment
+
+ Currency + ${coinInfo?.label || payment.coin} (${coinInfo?.ticker || payment.coin.toUpperCase()}) +
+
+ Amount Paid + ${payment.amount_received || payment.amount_crypto} ${coinInfo?.ticker || payment.coin.toUpperCase()} +
+
+ Payment Address + ${payment.address} +
+
+ Total (USD) + $${Number(payment.amount_usd).toFixed(2)} +
+
+ + ${txs.length > 0 ? `
+
Transactions
+ + + ${txRows} +
Transaction IDAmountDate
+
` : ""} + + + +`; + + // Lock it + await sql`UPDATE payments SET receipt_html = ${html} WHERE id = ${paymentId}`; + return html; +} diff --git a/apps/pay/src/routes.ts b/apps/pay/src/routes.ts index aec489a..f81eb6a 100644 --- a/apps/pay/src/routes.ts +++ b/apps/pay/src/routes.ts @@ -3,6 +3,7 @@ import sql from "./db"; import { derive } from "./address"; import { getExchangeRates, getAvailableCoins, fetchQrBase64 } from "./freedom"; import { PLANS, COINS } from "./plans"; +import { generateReceipt } from "./receipt"; import { watchPayment } from "./monitor"; // Resolve account from key (same logic as API/web apps) @@ -178,7 +179,7 @@ export const routes = new Elysia() }; }) - // Generate or serve a locked receipt for a paid invoice + // Serve locked receipt for a paid invoice .get("/checkout/:id/receipt", async ({ accountId, params, set }) => { const [payment] = await sql` SELECT * FROM payments WHERE id = ${params.id} AND account_id = ${accountId} @@ -186,129 +187,8 @@ export const routes = new Elysia() if (!payment) { set.status = 404; return { error: "Payment not found" }; } if (payment.status !== "paid") { set.status = 400; return { error: "Receipt is only available for paid invoices" }; } - // If receipt already generated, serve it as-is (locked) - if (payment.receipt_html) { - set.headers["content-type"] = "text/html; charset=utf-8"; - return payment.receipt_html; - } - - // Generate receipt - const coinInfo = COINS[payment.coin]; - const txs = await sql` - SELECT txid, amount, confirmed, detected_at - FROM payment_txs WHERE payment_id = ${payment.id} - 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 ` - ${tx.txid} - ${tx.amount} ${coinInfo?.ticker || payment.coin.toUpperCase()} - ${date} - `; - }).join(""); - - const receiptHtml = ` - - - - - PingQL Receipt #${payment.id} - - - -
- -
- -
- -
Receipt #${payment.id} · Issued ${paidDate}
-
- -
-
Plan
-
- Product - PingQL ${planLabel} -
-
- Invoice Date - ${createdDate} -
-
- Payment Date - ${paidDate} -
-
- -
-
Payment
-
- Currency - ${coinInfo?.label || payment.coin} (${coinInfo?.ticker || payment.coin.toUpperCase()}) -
-
- Amount Paid - ${payment.amount_received || payment.amount_crypto} ${coinInfo?.ticker || payment.coin.toUpperCase()} -
-
- Payment Address - ${payment.address} -
-
- Total (USD) - $${Number(payment.amount_usd).toFixed(2)} -
-
- - ${txs.length > 0 ? `
-
Transactions
- - - ${txRows} -
Transaction IDAmountDate
-
` : ""} - - - -`; - - // Lock the receipt by storing it - await sql`UPDATE payments SET receipt_html = ${receiptHtml} WHERE id = ${payment.id}`; - set.headers["content-type"] = "text/html; charset=utf-8"; - return receiptHtml; + return payment.receipt_html || await generateReceipt(payment.id); }) ; diff --git a/apps/web/src/views/landing.ejs b/apps/web/src/views/landing.ejs index 54cb4ca..58c6962 100644 --- a/apps/web/src/views/landing.ejs +++ b/apps/web/src/views/landing.ejs @@ -423,10 +423,6 @@ -
- - Hosted in EU (Prague) — GDPR compliant -
diff --git a/apps/web/src/views/privacy.ejs b/apps/web/src/views/privacy.ejs index 0f0a89d..302a894 100644 --- a/apps/web/src/views/privacy.ejs +++ b/apps/web/src/views/privacy.ejs @@ -118,11 +118,6 @@ Ping history is retained for 90 days by default. Account data is kept until you delete your account. If you want everything deleted, contact us and we'll do it.

-

Where data is stored

-

- Servers are in Prague, Czech Republic (EU). GDPR applies. -

-

Security

All data in transit is encrypted via TLS. All data at rest is on encrypted disks. Your account key is the only credential — there's no password database to breach, no hashed passwords to crack, no OAuth tokens to steal.