diff --git a/veza-backend-api/internal/handlers/admin_transfer_handler.go b/veza-backend-api/internal/handlers/admin_transfer_handler.go new file mode 100644 index 000000000..3049613b4 --- /dev/null +++ b/veza-backend-api/internal/handlers/admin_transfer_handler.go @@ -0,0 +1,145 @@ +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" +) + +// 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 { + c.JSON(http.StatusBadRequest, gin.H{"error": "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)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count transfers"}) + 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)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch transfers"}) + 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 { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Stripe Connect is not enabled"}) + return + } + + idStr := c.Param("id") + if idStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "transfer id required"}) + return + } + transferID, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid transfer id"}) + return + } + + var t marketplace.SellerTransfer + if err := h.db.First(&t, transferID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "transfer not found"}) + return + } + h.logger.Error("RetryTransfer find failed", zap.Error(err), zap.String("transfer_id", idStr)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch transfer"}) + return + } + + if t.Status != "failed" { + c.JSON(http.StatusBadRequest, gin.H{"error": "only failed transfers can be retried"}) + return + } + + err = h.ts.CreateTransfer(c.Request.Context(), t.SellerID, t.AmountCents, t.Currency, t.OrderID.String()) + if err != nil { + t.RetryCount++ + t.ErrorMessage = err.Error() + // Could set next_retry_at for worker to pick up later, but for manual retry we just record the failure + if saveErr := h.db.Save(&t).Error; 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)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Transfer failed: " + err.Error()}) + return + } + + t.Status = "completed" + t.ErrorMessage = "" + if err := h.db.Save(&t).Error; err != nil { + h.logger.Error("RetryTransfer save failed", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update transfer"}) + return + } + + RespondSuccess(c, http.StatusOK, t) +}