Files
flatrender/services/payment/internal/web/pages.go
T
soroush.asadi ec51e87d2d feat(payment): standalone ZarinPal broker on pay.flatrender.ir
A generic multi-client payment gateway so FlatRender, meezi.ir and
bargevasat.ir can all pay through ZarinPal's single verified callback
domain (pay.flatrender.ir).

New Go service services/payment (clones the notification skeleton +
vendored deps):
- migration 31_payment_broker.sql — `payment` schema: client_apps,
  transactions, webhook_deliveries.
- ZarinPal v4 client ported from the proven identity PaymentService
  (request.json -> StartPay -> verify.json; codes 100/101).
- client API: POST /v1/pay/request + /v1/pay/inquiry, authed by
  X-Api-Key + HMAC body signature; GET /callback/zarinpal (the single
  verified endpoint) verifies, then 302s the user back to the site's
  return_url (signed) and fires a signed, retried webhook.
- per-client ZarinPal merchant override (default = shared merchant);
  amount stored canonically in Rial, unit to ZarinPal env-configurable.
- admin API /v1/admin/* (FlatRender admin JWT): client-app CRUD +
  key issue/rotate + transactions list.

Deploy wiring: payment-svc in docker-compose.v2.yml (host port 1607),
pay.flatrender.ir server block in mirror-nginx conf, ENV_FILE +
README updates (cert SAN + manual migration note).

Admin UI: src/components/admin/PaymentsAdmin.tsx (client apps with
one-time key reveal + rotate, transactions table) + /admin/payments
page + nav link + fa/en strings; pay-admin proxy route to payment-svc.

Docs/SDK: deploy/PAYMENTS.md (integration contract) + deploy/sdk/flatpay.js
(zero-dep Node client + webhook verifier) for meezi/any site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:59:54 +03:30

64 lines
2.6 KiB
Go

// Package web serves the broker's minimal hosted pages (RTL Persian). Clients
// normally redirect users back to their OWN return_url; these pages are a
// branded landing + a fallback result screen for direct visits.
package web
import (
"fmt"
"html"
"net/http"
"github.com/gin-gonic/gin"
)
const shell = `<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s</title>
<style>
:root{color-scheme:dark}
body{margin:0;font-family:Tahoma,Vazirmatn,system-ui,sans-serif;background:#0b0d17;color:#e7e9f3;
display:flex;min-height:100vh;align-items:center;justify-content:center;text-align:center}
.card{background:#141726;border:1px solid #232742;border-radius:20px;padding:40px 32px;max-width:420px;width:90%%}
.logo{font-weight:800;font-size:22px;letter-spacing:.5px;color:#7c8cff}
h1{font-size:20px;margin:18px 0 8px}
p{color:#9aa0bd;line-height:1.9;font-size:14px;margin:6px 0}
.badge{display:inline-block;padding:6px 16px;border-radius:999px;font-size:13px;margin-top:14px}
.ok{background:rgba(34,197,94,.15);color:#4ade80}
.fail{background:rgba(239,68,68,.15);color:#f87171}
.muted{font-size:12px;color:#6a708f;margin-top:18px}
</style></head><body><div class="card">%s</div></body></html>`
func page(title, inner string) string {
return "<!--FlatPay-->" + fmt.Sprintf(shell, html.EscapeString(title), inner)
}
func Landing(c *gin.Context) {
inner := `<div class="logo">FlatRender Pay</div>
<h1>درگاه پرداخت امن</h1>
<p>این سرویس پرداخت‌های آنلاین را از طریق زرین‌پال پردازش می‌کند.</p>
<p class="muted">برای پرداخت، از طریق وب‌سایت مربوطه اقدام کنید.</p>`
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, page("FlatRender Pay", inner))
}
// Result is an optional fallback screen a client may point its return_url at.
func Result(c *gin.Context) {
status := c.Query("status")
ref := c.Query("ref_id")
var inner string
if status == "Paid" {
inner = `<div class="logo">FlatRender Pay</div>
<h1>پرداخت موفق</h1>
<span class="badge ok">پرداخت با موفقیت انجام شد</span>`
if ref != "" {
inner += `<p class="muted">کد پیگیری: ` + html.EscapeString(ref) + `</p>`
}
} else {
inner = `<div class="logo">FlatRender Pay</div>
<h1>پرداخت ناموفق</h1>
<span class="badge fail">پرداخت انجام نشد یا لغو شد</span>`
}
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, page("نتیجه پرداخت", inner))
}