feat(admin): add admin transfer handler (GET list, POST retry)
This commit is contained in:
parent
06db7d6936
commit
9ee4b18c33
1 changed files with 145 additions and 0 deletions
145
veza-backend-api/internal/handlers/admin_transfer_handler.go
Normal file
145
veza-backend-api/internal/handlers/admin_transfer_handler.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in a new issue