ec51e87d2d
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>
85 lines
2.6 KiB
Go
85 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/flatrender/payment-svc/internal/config"
|
|
"github.com/flatrender/payment-svc/internal/db"
|
|
"github.com/flatrender/payment-svc/internal/handlers"
|
|
"github.com/flatrender/payment-svc/internal/middleware"
|
|
"github.com/flatrender/payment-svc/internal/web"
|
|
"github.com/flatrender/payment-svc/internal/zarinpal"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
func main() {
|
|
cfg := config.Load()
|
|
|
|
pool, err := pgxpool.New(context.Background(), cfg.DatabaseURL)
|
|
if err != nil {
|
|
log.Fatalf("connect db: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
if err := pool.Ping(context.Background()); err != nil {
|
|
log.Fatalf("ping db: %v", err)
|
|
}
|
|
|
|
store := db.NewStore(pool)
|
|
zp := zarinpal.New()
|
|
disp := handlers.NewDispatcher(store)
|
|
|
|
// Background webhook delivery loop.
|
|
go disp.Run(context.Background())
|
|
|
|
payH := handlers.NewPayHandler(store, zp, disp, cfg)
|
|
adminH := handlers.NewAdminHandler(store)
|
|
|
|
r := gin.Default()
|
|
r.SetTrustedProxies(nil) //nolint — behind mirror-nginx; we read X-Forwarded-* only informally
|
|
|
|
health := func(c *gin.Context) {
|
|
if err := store.Ping(c.Request.Context()); err != nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"status": "down", "error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "service": "payment"})
|
|
}
|
|
r.GET("/health", health)
|
|
r.GET("/healthz", health)
|
|
|
|
// Public pages + ZarinPal callback (the single verified domain).
|
|
r.GET("/", web.Landing)
|
|
r.GET("/result", web.Result)
|
|
r.GET("/callback/zarinpal", payH.Callback)
|
|
|
|
v1 := r.Group("/v1")
|
|
|
|
// ── Client API (API key + HMAC signature) ────────────────────────────────
|
|
pay := v1.Group("/pay", middleware.ClientAuth(store))
|
|
{
|
|
pay.POST("/request", payH.Request)
|
|
pay.POST("/inquiry", payH.Inquiry)
|
|
}
|
|
|
|
// ── Admin API (FlatRender admin JWT) ─────────────────────────────────────
|
|
admin := v1.Group("/admin", middleware.JWTAuth(cfg.JWTSecret), middleware.RequireAdmin())
|
|
{
|
|
admin.GET("/clients", adminH.List)
|
|
admin.POST("/clients", adminH.Create)
|
|
admin.GET("/clients/:id", adminH.Get)
|
|
admin.PUT("/clients/:id", adminH.Update)
|
|
admin.DELETE("/clients/:id", adminH.Delete)
|
|
admin.POST("/clients/:id/rotate-secret", adminH.RotateSecret)
|
|
admin.GET("/transactions", adminH.ListTransactions)
|
|
}
|
|
|
|
log.Printf("payment-svc listening on :%s (sandbox=%v, unit=%s, base=%s)",
|
|
cfg.Port, cfg.ZarinPalSandbox, cfg.ZarinPalAmountUnit, cfg.PublicBaseURL)
|
|
if err := r.Run(":" + cfg.Port); err != nil {
|
|
log.Fatalf("server: %v", err)
|
|
}
|
|
}
|