veza/veza-backend-api/internal/handlers/admin_transfer_handler.go
senke a1000ce7fb style(backend): gofmt -w on 85 files (whitespace only)
backend-ci.yml's `test -z "$(gofmt -l .)"` strict gate (added in
13c21ac11) failed on a backlog of unformatted files. None of the
85 files in this commit had been edited since the gate was added
because no push touched veza-backend-api/** in between, so the
gate never fired until today's CI fixes triggered it.

The diff is exclusively whitespace alignment in struct literals
and trailing-space comments. `go build ./...` and the full test
suite (with VEZA_SKIP_INTEGRATION=1 -short) pass identically.
2026-04-14 12:22:14 +02:00

146 lines
4.4 KiB
Go

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
}
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))
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Transfer failed", err))
return
}
t.Status = "completed"
t.ErrorMessage = ""
if err := h.db.Save(&t).Error; 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)
}