feat(payments): route coin purchases through FlatRender Pay broker
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 56s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m11s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 3m38s

ZarinPal only accepts callbacks on pay.flatrender.ir, so bargevasat
pays through the shared broker and is credited via a signed webhook.

- FlatPayService: broker client (HMAC-signed /v1/pay/request) + webhook
  signature verification + in-memory idempotency guard.
- Program.cs: /api/coins/pay/request prefers the broker when configured
  (FlatPay__ApiKey/Secret set), else the legacy direct ZarinPal path;
  new public POST /api/coins/pay/webhook verifies the HMAC and credits
  coins from the echoed metadata (idempotent).
- appsettings + docker-compose: FlatPay config (empty ⇒ legacy path).
- web: recognise the broker's ?status=Paid return + re-refresh profile
  (coins are credited server-side via webhook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-16 00:36:12 +03:30
parent 8262fa79b3
commit d05cce6550
5 changed files with 170 additions and 8 deletions
+7
View File
@@ -56,6 +56,13 @@ services:
Zarinpal__Sandbox: ${ZARINPAL_SANDBOX:-true}
Zarinpal__CallbackUrl: ${ZARINPAL_CALLBACK_URL:-http://localhost:1505/api/coins/pay/callback}
Zarinpal__ClientReturnUrl: ${ZARINPAL_CLIENT_RETURN_URL:-http://localhost:1500}
# FlatRender Pay broker (pay.flatrender.ir): shared ZarinPal via the single
# verified domain. Set FLATPAY_API_KEY + FLATPAY_SECRET to route through it
# (issued in FlatRender admin → پرداخت). Empty ⇒ legacy direct ZarinPal above.
FlatPay__BaseUrl: ${FLATPAY_BASE_URL:-https://pay.flatrender.ir}
FlatPay__ApiKey: ${FLATPAY_API_KEY:-}
FlatPay__Secret: ${FLATPAY_SECRET:-}
FlatPay__ReturnUrl: ${FLATPAY_RETURN_URL:-https://bargevasat.ir/?pay=done}
# Store in-app billing verification (Cafe Bazaar / Myket) — fill from panels.
Iab__PackageName: ${IAB_PACKAGE_NAME:-com.bargevasat.app}
Iab__BazaarClientId: ${IAB_BAZAAR_CLIENT_ID:-}