using FlatRender.IdentitySvc.Application.Services; using FlatRender.IdentitySvc.Application.Services.Interfaces; using FlatRender.IdentitySvc.Models.Requests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace FlatRender.IdentitySvc.Controllers; [ApiController] [Route("v1")] public class PaymentsController(IPaymentService paymentService) : ControllerBase { // ── Helpers ─────────────────────────────────────────────────────────────────── private Guid GetUserId() => Guid.Parse( User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? User.FindFirst("sub")?.Value ?? throw new UnauthorizedAccessException()); private bool IsAdmin => User.FindFirst("is_admin")?.Value == "true"; // ── Listing ─────────────────────────────────────────────────────────────────── /// GET /v1/payments — list the caller's payment history [Authorize] [HttpGet("payments")] public async Task List( [FromQuery] int page = 1, [FromQuery] int page_size = 20) => Ok(await paymentService.GetUserPaymentsAsync(GetUserId(), page, page_size)); /// GET /v1/payments/{id} — get a single payment [Authorize] [HttpGet("payments/{id:guid}")] public async Task GetById(Guid id) => Ok(await paymentService.GetByIdAsync(id, GetUserId())); // ── ZarinPal flow ───────────────────────────────────────────────────────────── /// /// GET /v1/payments/gateway/zarinpal?payment_id={id} /// Initiates a ZarinPal payment and redirects the browser to zarinpal.com. /// Called from the frontend after PurchasePlan returns the redirect URL. /// [Authorize] [HttpGet("payments/gateway/zarinpal")] public async Task InitiateZarinPal([FromQuery] Guid payment_id) { var redirectUrl = await paymentService.InitiateZarinPalAsync(payment_id, GetUserId()); return Redirect(redirectUrl); } /// /// GET /v1/payments/callback/zarinpal?Authority={a}&Status={s} /// ZarinPal calls this URL after the user completes (or cancels) payment. /// Verifies with ZarinPal, activates the plan, then redirects to the frontend. /// [AllowAnonymous] [HttpGet("payments/callback/zarinpal")] public async Task ZarinPalCallback( [FromQuery] string Authority, [FromQuery] string Status) { var frontendUrl = await paymentService.HandleZarinPalCallbackAsync(Authority, Status); return Redirect(frontendUrl); } // ── FlatRender Pay broker flow ──────────────────────────────────────────────── /// /// GET /v1/payments/callback/broker?payment_id={p}&id={brokerTxn}&status=&sign= /// The broker redirects the user's browser here after payment. We confirm the /// transaction authoritatively via the broker inquiry API, activate the plan, /// then redirect to the frontend result page. Public (no JWT). /// [AllowAnonymous] [HttpGet("payments/callback/broker")] public async Task BrokerCallback( [FromQuery] Guid payment_id, [FromQuery] string? id) { var frontendUrl = await paymentService.HandleBrokerCallbackAsync(payment_id, id ?? ""); return Redirect(frontendUrl); } // ── SnapPay flow ────────────────────────────────────────────────────────────── /// /// GET /v1/payments/gateway/snappay?payment_id={id} /// Initiates a SnapPay payment and redirects the browser to snappay.ir. /// [Authorize] [HttpGet("payments/gateway/snappay")] public async Task InitiateSnapPay([FromQuery] Guid payment_id) { var redirectUrl = await paymentService.InitiateSnapPayAsync(payment_id, GetUserId()); return Redirect(redirectUrl); } /// /// GET /v1/payments/callback/snappay?paymentToken={t}&shapSnapStatus={s} /// SnapPay calls this URL after the user completes (or cancels) payment. /// Verifies with SnapPay, activates the plan, then redirects to the frontend. /// [AllowAnonymous] [HttpGet("payments/callback/snappay")] public async Task SnapPayCallback( [FromQuery] string paymentToken, [FromQuery] string shapSnapStatus) { var frontendUrl = await paymentService.HandleSnapPayCallbackAsync(paymentToken, shapSnapStatus); return Redirect(frontendUrl); } // ── Tara flow ───────────────────────────────────────────────────────────────── /// /// GET /v1/payments/gateway/tara?payment_id={id} /// Initiates a Tara payment and redirects the browser to tara.ir. /// [Authorize] [HttpGet("payments/gateway/tara")] public async Task InitiateTara([FromQuery] Guid payment_id) { var redirectUrl = await paymentService.InitiateTaraAsync(payment_id, GetUserId()); return Redirect(redirectUrl); } /// /// GET /v1/payments/callback/tara?token={t}&status={s} /// Tara calls this URL after the user completes (or cancels) payment. /// Verifies with Tara, activates the plan, then redirects to the frontend. /// [AllowAnonymous] [HttpGet("payments/callback/tara")] public async Task TaraCallback( [FromQuery] string token, [FromQuery] string status) { var frontendUrl = await paymentService.HandleTaraCallbackAsync(token, status); return Redirect(frontendUrl); } // ── Stripe webhook ──────────────────────────────────────────────────────────── /// /// POST /v1/payments/webhook/stripe /// Receives Stripe webhook events. Must be reachable from the public internet. /// Register this URL in your Stripe dashboard under Developers → Webhooks. /// [AllowAnonymous] [HttpPost("payments/webhook/stripe")] public async Task StripeWebhook() { using var reader = new StreamReader(Request.Body); var payload = await reader.ReadToEndAsync(); var signature = Request.Headers["Stripe-Signature"].ToString(); await paymentService.HandleStripeWebhookAsync(payload, signature); return Ok(); } // ── Admin ───────────────────────────────────────────────────────────────────── /// /// POST /v1/admin/payments/{id}/refund /// Issues a refund for a payment. Admin-only. /// [Authorize] [HttpPost("admin/payments/{id:guid}/refund")] public async Task Refund(Guid id, [FromBody] IssueRefundRequest request) { if (!IsAdmin) return Forbid(); var result = await paymentService.IssueRefundAsync( id, request.AmountMinor, request.Reason, request.RefundTo); return Ok(result); } }