package handlers import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" "veza-backend-api/internal/core/marketplace" apperrors "veza-backend-api/internal/errors" ) // AdminTransferHandler handles admin transfer dashboard endpoints (v0.701). type AdminTransferHandler struct { db *gorm.DB ts marketplace.TransferService logger *zap.Logger feeRate float64 } // NewAdminTransferHandler creates a new AdminTransferHandler. func NewAdminTransferHandler(db *gorm.DB, ts marketplace.TransferService, feeRate float64, logger *zap.Logger) *AdminTransferHandler { return &AdminTransferHandler{ db: db, ts: ts, logger: logger, feeRate: feeRate, } } // GetTransfers returns a paginated list of all platform transfers with optional filters. // Query params: status, seller_id, from, to, limit (default 50), offset (default 0). func (h *AdminTransferHandler) GetTransfers(c *gin.Context) { query := h.db.Model(&marketplace.SellerTransfer{}) if status := c.Query("status"); status != "" { query = query.Where("status = ?", status) } if sellerIDStr := c.Query("seller_id"); sellerIDStr != "" { sellerID, err := uuid.Parse(sellerIDStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid seller_id")) return } query = query.Where("seller_id = ?", sellerID) } if from := c.Query("from"); from != "" { query = query.Where("created_at >= ?", from) } if to := c.Query("to"); to != "" { query = query.Where("created_at <= ?", to) } var total int64 if err := query.Count(&total).Error; err != nil { h.logger.Error("GetTransfers count failed", zap.Error(err)) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to count transfers", err)) return } limit := 50 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 { limit = parsed } } offset := 0 if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } var transfers []marketplace.SellerTransfer if err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&transfers).Error; err != nil { h.logger.Error("GetTransfers find failed", zap.Error(err)) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to fetch transfers", err)) return } RespondSuccess(c, http.StatusOK, gin.H{ "transfers": transfers, "total": total, }) } // RetryTransfer manually retries a failed transfer. func (h *AdminTransferHandler) RetryTransfer(c *gin.Context) { if h.ts == nil { RespondWithAppError(c, apperrors.NewServiceUnavailableError("Stripe Connect is not enabled")) return } idStr := c.Param("id") if idStr == "" { RespondWithAppError(c, apperrors.NewValidationError("transfer id required")) return } transferID, err := uuid.Parse(idStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid transfer id")) return } var t marketplace.SellerTransfer if err := h.db.First(&t, transferID).Error; err != nil { if err == gorm.ErrRecordNotFound { RespondWithAppError(c, apperrors.NewNotFoundError("transfer")) return } h.logger.Error("RetryTransfer find failed", zap.Error(err), zap.String("transfer_id", idStr)) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to fetch transfer", err)) return } if t.Status != "failed" { RespondWithAppError(c, apperrors.NewValidationError("only failed transfers can be retried")) return } stripeTransferID, err := h.ts.CreateTransfer(c.Request.Context(), t.SellerID, t.AmountCents, t.Currency, t.OrderID.String()) if err != nil { // Failure: stay in 'failed' (same-state) but bump retry_count and // record the error. For manual admin retry we don't set // next_retry_at — ops is driving this, not the worker. failExtras := map[string]interface{}{ "retry_count": t.RetryCount + 1, "error_message": err.Error(), } if saveErr := t.TransitionStatus(h.db.WithContext(c.Request.Context()), t.Status, failExtras); saveErr != nil { h.logger.Error("RetryTransfer save failed", zap.Error(saveErr)) } h.logger.Error("RetryTransfer CreateTransfer failed", zap.Error(err), zap.String("transfer_id", idStr)) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Transfer failed", err)) return } extras := map[string]interface{}{ "error_message": "", "stripe_transfer_id": stripeTransferID, } if err := t.TransitionStatus(h.db.WithContext(c.Request.Context()), marketplace.TransferStatusCompleted, extras); err != nil { h.logger.Error("RetryTransfer save failed", zap.Error(err)) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to update transfer", err)) return } RespondSuccess(c, http.StatusOK, t) }