package handlers import ( "Yimaru-Backend/internal/domain" "strconv" "github.com/gofiber/fiber/v2" ) // ===================== // Payment Types // ===================== type initiatePaymentReq struct { PlanID int64 `json:"plan_id" validate:"required"` Phone string `json:"phone" validate:"required"` Email string `json:"email" validate:"required,email"` } type paymentRes struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` PlanID *int64 `json:"plan_id,omitempty"` SubscriptionID *int64 `json:"subscription_id,omitempty"` SessionID *string `json:"session_id,omitempty"` Amount float64 `json:"amount"` Currency string `json:"currency"` PaymentMethod *string `json:"payment_method,omitempty"` Status string `json:"status"` PaymentURL *string `json:"payment_url,omitempty"` PlanName *string `json:"plan_name,omitempty"` PaidAt *string `json:"paid_at,omitempty"` ExpiresAt *string `json:"expires_at,omitempty"` CreatedAt string `json:"created_at"` } // ===================== // Subscription Payment Handlers // ===================== // InitiateSubscriptionPayment godoc // @Summary Initiate subscription payment // @Description Creates a payment session for a subscription plan // @Tags payments // @Accept json // @Produce json // @Param body body initiatePaymentReq true "Payment request" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/payments/subscribe [post] func (h *Handler) InitiateSubscriptionPayment(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Unauthorized", }) } var req initiatePaymentReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } result, err := h.arifpaySvc.InitiateSubscriptionPayment(c.Context(), userID, domain.InitiateSubscriptionPaymentRequest{ PlanID: req.PlanID, Phone: req.Phone, Email: req.Email, }) if err != nil { status := fiber.StatusInternalServerError if err.Error() == "user already has an active subscription" { status = fiber.StatusConflict } return c.Status(status).JSON(domain.ErrorResponse{ Message: "Failed to initiate payment", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Payment initiated successfully", Data: result, }) } // VerifyPayment godoc // @Summary Verify payment status // @Description Checks the payment status with the payment provider // @Tags payments // @Produce json // @Param session_id path string true "Session ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Router /api/v1/payments/verify/{session_id} [get] func (h *Handler) VerifyPayment(c *fiber.Ctx) error { sessionID := c.Params("session_id") if sessionID == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Session ID is required", }) } payment, err := h.arifpaySvc.VerifyPayment(c.Context(), sessionID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Payment not found or verification failed", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Payment status retrieved", Data: paymentToRes(payment), }) } // GetMyPayments godoc // @Summary Get payment history // @Description Returns the authenticated user's payment history // @Tags payments // @Produce json // @Param limit query int false "Limit" default(20) // @Param offset query int false "Offset" default(0) // @Success 200 {object} domain.Response // @Router /api/v1/payments [get] func (h *Handler) GetMyPayments(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Unauthorized", }) } limit, _ := strconv.Atoi(c.Query("limit", "20")) offset, _ := strconv.Atoi(c.Query("offset", "0")) payments, err := h.arifpaySvc.GetPaymentsByUser(c.Context(), userID, int32(limit), int32(offset)) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to get payment history", Error: err.Error(), }) } result := make([]paymentRes, len(payments)) for i, p := range payments { result[i] = *paymentToRes(&p) } return c.JSON(domain.Response{ Message: "Payment history retrieved successfully", Data: result, }) } // GetPaymentByID godoc // @Summary Get payment details // @Description Returns details of a specific payment // @Tags payments // @Produce json // @Param id path int true "Payment ID" // @Success 200 {object} domain.Response // @Failure 404 {object} domain.ErrorResponse // @Router /api/v1/payments/{id} [get] func (h *Handler) GetPaymentByID(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Unauthorized", }) } id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid payment ID", }) } payment, err := h.arifpaySvc.GetPaymentByID(c.Context(), id) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Payment not found", }) } if payment.UserID != userID { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ Message: "Access denied", }) } return c.JSON(domain.Response{ Message: "Payment retrieved successfully", Data: paymentToRes(payment), }) } // CancelPayment godoc // @Summary Cancel a pending payment // @Description Cancels a payment that is still pending // @Tags payments // @Produce json // @Param id path int true "Payment ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Router /api/v1/payments/{id}/cancel [post] func (h *Handler) CancelPayment(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Unauthorized", }) } id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid payment ID", }) } if err := h.arifpaySvc.CancelPayment(c.Context(), id, userID); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to cancel payment", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Payment cancelled successfully", }) } // HandleArifpayWebhook godoc // @Summary Handle ArifPay webhook // @Description Processes payment notifications from ArifPay // @Tags payments // @Accept json // @Produce json // @Param body body domain.WebhookRequest true "Webhook payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Router /api/v1/payments/webhook [post] func (h *Handler) HandleArifpayWebhook(c *fiber.Ctx) error { var req domain.WebhookRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid webhook payload", Error: err.Error(), }) } if err := h.arifpaySvc.ProcessPaymentWebhook(c.Context(), req); err != nil { h.logger.Error("Failed to process webhook", "error", err, "nonce", req.Nonce) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to process webhook", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Webhook processed successfully", }) } // GetArifpayPaymentMethods godoc // @Summary Get available payment methods // @Description Returns list of supported ArifPay payment methods // @Tags payments // @Produce json // @Success 200 {object} domain.Response // @Router /api/v1/payments/methods [get] func (h *Handler) GetArifpayPaymentMethods(c *fiber.Ctx) error { methods := h.arifpaySvc.GetPaymentMethodsMapping() return c.JSON(domain.Response{ Message: "Payment methods retrieved successfully", Data: methods, }) } // ===================== // Direct Payment Handlers // ===================== type initiateDirectPaymentReq struct { PlanID int64 `json:"plan_id" validate:"required"` Phone string `json:"phone" validate:"required"` Email string `json:"email" validate:"required,email"` PaymentMethod string `json:"payment_method" validate:"required"` } type verifyOTPReq struct { SessionID string `json:"session_id" validate:"required"` OTP string `json:"otp" validate:"required"` } // InitiateDirectPayment godoc // @Summary Initiate direct payment // @Description Creates a payment session and initiates direct payment (OTP-based) // @Tags payments // @Accept json // @Produce json // @Param body body initiateDirectPaymentReq true "Direct payment request" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/payments/direct [post] func (h *Handler) InitiateDirectPayment(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Unauthorized", }) } var req initiateDirectPaymentReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } result, err := h.arifpaySvc.InitiateDirectPayment(c.Context(), userID, domain.InitiateDirectPaymentRequest{ PlanID: req.PlanID, Phone: req.Phone, Email: req.Email, PaymentMethod: domain.DirectPaymentMethod(req.PaymentMethod), }) if err != nil { status := fiber.StatusInternalServerError if err.Error() == "user already has an active subscription" { status = fiber.StatusConflict } return c.Status(status).JSON(domain.ErrorResponse{ Message: "Failed to initiate direct payment", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: result.Message, Data: result, }) } // VerifyDirectPaymentOTP godoc // @Summary Verify OTP for direct payment // @Description Verifies the OTP sent for direct payment methods (Amole, HelloCash, etc.) // @Tags payments // @Accept json // @Produce json // @Param body body verifyOTPReq true "OTP verification request" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Router /api/v1/payments/direct/verify-otp [post] func (h *Handler) VerifyDirectPaymentOTP(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Unauthorized", }) } var req verifyOTPReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } result, err := h.arifpaySvc.VerifyDirectPaymentOTP(c.Context(), userID, domain.VerifyOTPRequest{ SessionID: req.SessionID, OTP: req.OTP, }) if err != nil { status := fiber.StatusInternalServerError if err.Error() == "payment not found" { status = fiber.StatusNotFound } return c.Status(status).JSON(domain.ErrorResponse{ Message: "Failed to verify OTP", Error: err.Error(), }) } if !result.Success { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: result.Message, }) } return c.JSON(domain.Response{ Message: result.Message, Data: result, }) } // GetDirectPaymentMethods godoc // @Summary Get direct payment methods // @Description Returns list of payment methods that support direct payment (OTP-based) // @Tags payments // @Produce json // @Success 200 {object} domain.Response // @Router /api/v1/payments/direct/methods [get] func (h *Handler) GetDirectPaymentMethods(c *fiber.Ctx) error { methods := h.arifpaySvc.GetDirectPaymentMethods() return c.JSON(domain.Response{ Message: "Direct payment methods retrieved successfully", Data: methods, }) } // Helper functions func paymentToRes(p *domain.Payment) *paymentRes { res := &paymentRes{ ID: p.ID, UserID: p.UserID, PlanID: p.PlanID, SubscriptionID: p.SubscriptionID, SessionID: p.SessionID, Amount: p.Amount, Currency: p.Currency, PaymentMethod: p.PaymentMethod, Status: p.Status, PaymentURL: p.PaymentURL, PlanName: p.PlanName, CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), } if p.PaidAt != nil { t := p.PaidAt.Format("2006-01-02T15:04:05Z07:00") res.PaidAt = &t } if p.ExpiresAt != nil { t := p.ExpiresAt.Format("2006-01-02T15:04:05Z07:00") res.ExpiresAt = &t } return res }