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.
This commit is contained in:
senke 2026-04-14 12:22:14 +02:00
parent eb97cad991
commit a1000ce7fb
86 changed files with 790 additions and 769 deletions

View file

@ -5,30 +5,30 @@ import "flag"
// Config holds all seed volume parameters.
type Config struct {
// Users
TotalUsers int
NormalUsers int
Artists int
Labels int
Moderators int
Admins int
TotalUsers int
NormalUsers int
Artists int
Labels int
Moderators int
Admins int
// Content
Tracks int
Albums int
Playlists int
Tracks int
Albums int
Playlists int
PlaylistMinTracks int
PlaylistMaxTracks int
// Social
Follows int
TrackLikes int
TrackReposts int
Comments int
CommentLikes int
Follows int
TrackLikes int
TrackReposts int
Comments int
CommentLikes int
// Chat
Conversations int
Messages int
Conversations int
Messages int
// Live
PastLiveStreams int
@ -40,9 +40,9 @@ type Config struct {
ProductReviews int
// Analytics
PlayEvents int
ProfileViews int
AnalyticsMonths int
PlayEvents int
ProfileViews int
AnalyticsMonths int
// Notifications
Notifications int
@ -64,12 +64,12 @@ type Config struct {
// FullConfig returns the full-scale configuration (~1200 users, ~5000 tracks).
func FullConfig() Config {
return Config{
TotalUsers: 1200,
NormalUsers: 1000,
Artists: 150,
Labels: 30,
Moderators: 15,
Admins: 5,
TotalUsers: 1200,
NormalUsers: 1000,
Artists: 150,
Labels: 30,
Moderators: 15,
Admins: 5,
Tracks: 5000,
Albums: 300,
@ -115,12 +115,12 @@ func FullConfig() Config {
// MinimalConfig returns a reduced configuration for fast dev iteration.
func MinimalConfig() Config {
return Config{
TotalUsers: 50,
NormalUsers: 30,
Artists: 12,
Labels: 3,
Moderators: 2,
Admins: 3,
TotalUsers: 50,
NormalUsers: 30,
Artists: 12,
Labels: 3,
Moderators: 2,
Admins: 3,
Tracks: 200,
Albums: 25,

View file

@ -275,18 +275,18 @@ var trackNouns = []string{
}
var trackTemplates = []string{
"%s %s", // "Neon Dreams"
"%s %s", // "Midnight Waves"
"The %s", // "The Storm"
"%s", // "Pulse"
"%s & %s", // "Lights & Shadows"
"After %s", // "After Rain"
"Last %s", // "Last Signal"
"%s at Dawn", // "Waves at Dawn"
"%s Protocol", // "Midnight Protocol"
"%s Sessions", // "Deep Sessions"
"Into the %s", // "Into the Storm"
"%s Mode", // "Neon Mode"
"%s %s", // "Neon Dreams"
"%s %s", // "Midnight Waves"
"The %s", // "The Storm"
"%s", // "Pulse"
"%s & %s", // "Lights & Shadows"
"After %s", // "After Rain"
"Last %s", // "Last Signal"
"%s at Dawn", // "Waves at Dawn"
"%s Protocol", // "Midnight Protocol"
"%s Sessions", // "Deep Sessions"
"Into the %s", // "Into the Storm"
"%s Mode", // "Neon Mode"
}
// GenTrackTitle generates a realistic track title.

View file

@ -22,7 +22,7 @@ func SeedChat(db *sql.DB, cfg Config, users []SeededUser) ([]SeededRoom, error)
// ── 1. Create rooms ──────────────────────────────────────────────────────
// Mix of group rooms and DM rooms
groupCount := cfg.Conversations / 5 // 20% group rooms
groupCount := cfg.Conversations / 5 // 20% group rooms
dmCount := cfg.Conversations - groupCount // 80% DMs
p := NewProgress("rooms", cfg.Conversations)

View file

@ -156,7 +156,7 @@ func SeedContent(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrac
newUUID(), c.id, li, title,
fmt.Sprintf("Lesson %d: %s", li+1, title),
randInt(300, 1800), // duration 5-30min
li < 2, // first 2 lessons free preview
li < 2, // first 2 lessons free preview
"completed",
})
}

View file

@ -178,12 +178,12 @@ func SeedMarketplace(db *sql.DB, cfg Config, users []SeededUser, tracks []Seeded
sellerRows = append(sellerRows, []interface{}{
newUUID(), artist.ID,
fmt.Sprintf("acct_%s", newUUID()[:16]),
true, // is_onboarded
true, // payouts_enabled
true, // charges_enabled
true, // is_onboarded
true, // payouts_enabled
true, // charges_enabled
"verified", // kyc_status
nil, nil, // kyc_verification_session_id, kyc_verified_at
nil, // kyc_last_error
nil, // kyc_last_error
time.Now(), time.Now(),
})
}

View file

@ -104,10 +104,10 @@ func SeedUsers(db *sql.DB, cfg Config) ([]SeededUser, error) {
}
// Distribute roles
artistCount := cfg.Artists - 1 // -1 for test artist
artistCount := cfg.Artists - 1 // -1 for test artist
labelCount := cfg.Labels
modCount := cfg.Moderators - 1 // -1 for test mod
adminCount := cfg.Admins - 1 // -1 for test admin
modCount := cfg.Moderators - 1 // -1 for test mod
adminCount := cfg.Admins - 1 // -1 for test admin
normalCount := remaining - artistCount - labelCount - modCount - adminCount
type roleAssignment struct {

View file

@ -196,12 +196,12 @@ func (r *APIRouter) Setup(router *gin.Engine) error {
// Middlewares globaux (after CORS)
router.Use(middleware.CacheHeaders(middleware.DefaultCacheHeadersConfig())) // v0.12.4: CDN cache headers
router.Use(middleware.MaintenanceGin()) // v0.803 ADM1-03: Maintenance mode (503 except /health, /admin)
router.Use(middleware.RequestLogger(r.logger)) // Utilisation du structured logger
router.Use(middleware.Metrics()) // Prometheus Metrics
router.Use(middleware.SentryRecover(r.logger)) // Sentry error tracking
router.Use(middleware.SecurityHeaders()) // MOD-P2-005: Security headers (HSTS, CSP, etc.)
router.Use(middleware.CCPA()) // v0.803 SEC2-06: CCPA Do Not Sell (Sec-GPC)
router.Use(middleware.MaintenanceGin()) // v0.803 ADM1-03: Maintenance mode (503 except /health, /admin)
router.Use(middleware.RequestLogger(r.logger)) // Utilisation du structured logger
router.Use(middleware.Metrics()) // Prometheus Metrics
router.Use(middleware.SentryRecover(r.logger)) // Sentry error tracking
router.Use(middleware.SecurityHeaders()) // MOD-P2-005: Security headers (HSTS, CSP, etc.)
router.Use(middleware.CCPA()) // v0.803 SEC2-06: CCPA Do Not Sell (Sec-GPC)
// v0.803 SEC2-03: HTTP audit middleware for auto-logging POST/PUT/DELETE
if r.config != nil && r.config.AuditService != nil {

View file

@ -46,9 +46,9 @@ func (r *APIRouter) setupAnalyticsRoutes(router *gin.RouterGroup) {
r.applyCSRFProtection(creatorGroup)
}
{
creatorGroup.GET("/dashboard", creatorHandler.GetDashboard) // F381
creatorGroup.GET("/plays", creatorHandler.GetPlayEvolution) // F382
creatorGroup.GET("/sales", creatorHandler.GetSales) // F383
creatorGroup.GET("/dashboard", creatorHandler.GetDashboard) // F381
creatorGroup.GET("/plays", creatorHandler.GetPlayEvolution) // F382
creatorGroup.GET("/sales", creatorHandler.GetSales) // F383
creatorGroup.GET("/discovery", creatorHandler.GetDiscoverySources) // F381
creatorGroup.GET("/geographic", creatorHandler.GetGeographic) // F381
creatorGroup.GET("/audience", creatorHandler.GetAudience) // F384
@ -60,13 +60,13 @@ func (r *APIRouter) setupAnalyticsRoutes(router *gin.RouterGroup) {
advancedService := services.NewAdvancedAnalyticsService(r.db.GormDB, r.logger)
advancedHandler := analytics.NewAdvancedAnalyticsHandler(advancedService, r.logger)
creatorGroup.GET("/heatmap/:trackId", advancedHandler.GetTrackHeatmap) // F396
creatorGroup.GET("/compare", advancedHandler.ComparePeriods) // F397
creatorGroup.GET("/marketplace", advancedHandler.GetMarketplaceAnalytics) // F398
creatorGroup.GET("/alerts", advancedHandler.GetMetricAlerts) // F399
creatorGroup.POST("/alerts", advancedHandler.CreateAlert) // F399
creatorGroup.PUT("/alerts/preferences", advancedHandler.UpdateAlertPreference) // F399
creatorGroup.DELETE("/alerts/:alertId", advancedHandler.DeleteAlert) // F399
creatorGroup.POST("/alerts/check", advancedHandler.CheckAlerts) // F399
creatorGroup.GET("/heatmap/:trackId", advancedHandler.GetTrackHeatmap) // F396
creatorGroup.GET("/compare", advancedHandler.ComparePeriods) // F397
creatorGroup.GET("/marketplace", advancedHandler.GetMarketplaceAnalytics) // F398
creatorGroup.GET("/alerts", advancedHandler.GetMetricAlerts) // F399
creatorGroup.POST("/alerts", advancedHandler.CreateAlert) // F399
creatorGroup.PUT("/alerts/preferences", advancedHandler.UpdateAlertPreference) // F399
creatorGroup.DELETE("/alerts/:alertId", advancedHandler.DeleteAlert) // F399
creatorGroup.POST("/alerts/check", advancedHandler.CheckAlerts) // F399
}
}

View file

@ -368,7 +368,7 @@ func NewConfig() (*Config, error) {
// Configuration RabbitMQ
// BE-SEC-014: In production, require RABBITMQ_URL to be set (no default with credentials)
RabbitMQURL: rabbitMQURL,
RabbitMQMaxRetries: getEnvInt("RABBITMQ_MAX_RETRIES", 10), // 10 tentatives par défaut (RabbitMQ peut prendre ~30s au démarrage)
RabbitMQMaxRetries: getEnvInt("RABBITMQ_MAX_RETRIES", 10), // 10 tentatives par défaut (RabbitMQ peut prendre ~30s au démarrage)
RabbitMQRetryInterval: getEnvDuration("RABBITMQ_RETRY_INTERVAL", 5*time.Second), // 5 secondes par défaut
RabbitMQEnable: getEnvBool("RABBITMQ_ENABLE", true), // Activé par défaut

View file

@ -432,7 +432,7 @@ func (s *Service) GetTracksFromFollowedGenres(ctx context.Context, userID uuid.U
}
var trackIDs []uuid.UUID
if err := sub.Order("tracks.created_at DESC, tracks.id DESC").Limit(limit + 1).Pluck("tracks.id", &trackIDs).Error; err != nil {
if err := sub.Order("tracks.created_at DESC, tracks.id DESC").Limit(limit+1).Pluck("tracks.id", &trackIDs).Error; err != nil {
return nil, "", fmt.Errorf("get track ids: %w", err)
}
if len(trackIDs) == 0 {

View file

@ -78,11 +78,11 @@ type TrackDistribution struct {
OverallStatus DistributionStatus `gorm:"not null;size:50;default:'submitted'" json:"overall_status"`
PlatformStatuses json.RawMessage `gorm:"type:jsonb;not null;default:'{}'" json:"platform_statuses"`
SubmittedAt time.Time `gorm:"not null" json:"submitted_at"`
FirstLiveAt *time.Time `json:"first_live_at,omitempty"`
LastStatusCheckAt *time.Time `json:"last_status_check_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
SubmittedAt time.Time `gorm:"not null" json:"submitted_at"`
FirstLiveAt *time.Time `json:"first_live_at,omitempty"`
LastStatusCheckAt *time.Time `json:"last_status_check_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (TrackDistribution) TableName() string {
@ -150,23 +150,23 @@ func (e *StatusHistoryEntry) BeforeCreate(tx *gorm.DB) error {
// ExternalStreamingRoyalty represents monthly royalty data from an external platform
type ExternalStreamingRoyalty struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
TrackID uuid.UUID `gorm:"type:uuid;not null" json:"track_id"`
CreatorID uuid.UUID `gorm:"type:uuid;not null" json:"creator_id"`
ReportingPeriodStart time.Time `gorm:"type:date;not null" json:"reporting_period_start"`
ReportingPeriodEnd time.Time `gorm:"type:date;not null" json:"reporting_period_end"`
Platform string `gorm:"not null;size:50" json:"platform"`
TotalStreams int64 `gorm:"not null;default:0" json:"total_streams"`
TotalRevenueCents int64 `gorm:"not null;default:0" json:"total_revenue_cents"`
Currency string `gorm:"size:3;default:'USD'" json:"currency"`
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
TrackID uuid.UUID `gorm:"type:uuid;not null" json:"track_id"`
CreatorID uuid.UUID `gorm:"type:uuid;not null" json:"creator_id"`
ReportingPeriodStart time.Time `gorm:"type:date;not null" json:"reporting_period_start"`
ReportingPeriodEnd time.Time `gorm:"type:date;not null" json:"reporting_period_end"`
Platform string `gorm:"not null;size:50" json:"platform"`
TotalStreams int64 `gorm:"not null;default:0" json:"total_streams"`
TotalRevenueCents int64 `gorm:"not null;default:0" json:"total_revenue_cents"`
Currency string `gorm:"size:3;default:'USD'" json:"currency"`
StreamsBreakdown json.RawMessage `gorm:"type:jsonb" json:"streams_breakdown,omitempty"`
Distributor string `gorm:"not null;size:50;default:'distrokid'" json:"distributor"`
DistributorReportID string `gorm:"size:255" json:"distributor_report_id,omitempty"`
ImportStatus ImportStatus `gorm:"not null;size:50;default:'pending'" json:"import_status"`
ImportError string `gorm:"type:text" json:"import_error,omitempty"`
ImportedAt *time.Time `json:"imported_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Distributor string `gorm:"not null;size:50;default:'distrokid'" json:"distributor"`
DistributorReportID string `gorm:"size:255" json:"distributor_report_id,omitempty"`
ImportStatus ImportStatus `gorm:"not null;size:50;default:'pending'" json:"import_status"`
ImportError string `gorm:"type:text" json:"import_error,omitempty"`
ImportedAt *time.Time `json:"imported_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (ExternalStreamingRoyalty) TableName() string {

View file

@ -16,13 +16,13 @@ import (
// Service errors
var (
ErrDistributionNotFound = errors.New("distribution not found")
ErrTrackNotFound = errors.New("track not found")
ErrNotEligible = errors.New("distribution requires Creator or Premium plan")
ErrTrackNotPublic = errors.New("track must be public to distribute")
ErrAlreadyDistributed = errors.New("track already has a pending or live distribution")
ErrInvalidPlatform = errors.New("unsupported platform")
ErrNoPlatformsSelected = errors.New("at least one platform must be selected")
ErrDistributionNotFound = errors.New("distribution not found")
ErrTrackNotFound = errors.New("track not found")
ErrNotEligible = errors.New("distribution requires Creator or Premium plan")
ErrTrackNotPublic = errors.New("track must be public to distribute")
ErrAlreadyDistributed = errors.New("track already has a pending or live distribution")
ErrInvalidPlatform = errors.New("unsupported platform")
ErrNoPlatformsSelected = errors.New("at least one platform must be selected")
ErrDistributionNotRemovable = errors.New("distribution cannot be removed in current status")
)
@ -42,16 +42,16 @@ type DistributorSubmitRequest struct {
// DistributorSubmitResponse is the response from the distributor API
type DistributorSubmitResponse struct {
SubmissionID string `json:"submission_id"`
UPC string `json:"upc,omitempty"`
ISRC string `json:"isrc,omitempty"`
EstimatedLiveDays int `json:"estimated_live_days"`
SubmissionID string `json:"submission_id"`
UPC string `json:"upc,omitempty"`
ISRC string `json:"isrc,omitempty"`
EstimatedLiveDays int `json:"estimated_live_days"`
}
// DistributorStatusResponse is the status response from the distributor API
type DistributorStatusResponse struct {
SubmissionID string `json:"submission_id"`
OverallStatus DistributionStatus `json:"overall_status"`
SubmissionID string `json:"submission_id"`
OverallStatus DistributionStatus `json:"overall_status"`
PlatformStatuses map[Platform]PlatformStatus `json:"platform_statuses"`
}
@ -95,8 +95,8 @@ type SubmitRequest struct {
// SubmitResponse holds the result of a distribution submission
type SubmitResponse struct {
Distribution *TrackDistribution `json:"distribution"`
EstimatedLiveDays int `json:"estimated_live_days"`
Distribution *TrackDistribution `json:"distribution"`
EstimatedLiveDays int `json:"estimated_live_days"`
}
// Submit submits a track for distribution to selected platforms
@ -471,7 +471,7 @@ func (s *Service) GetExternalRoyalties(ctx context.Context, creatorID uuid.UUID,
var summaryRows []struct {
Platform string `gorm:"column:platform"`
TotalStreams int64 `gorm:"column:total_streams"`
TotalStreams int64 `gorm:"column:total_streams"`
TotalRevenueCents int64 `gorm:"column:total_revenue_cents"`
}
summaryQuery.Select("platform, SUM(total_streams) as total_streams, SUM(total_revenue_cents) as total_revenue_cents").
@ -491,8 +491,8 @@ func (s *Service) GetExternalRoyalties(ctx context.Context, creatorID uuid.UUID,
// RoyaltySummary provides aggregated royalty data
type RoyaltySummary struct {
TotalStreams int64 `json:"total_streams"`
TotalRevenueCents int64 `json:"total_revenue_cents"`
TotalStreams int64 `json:"total_streams"`
TotalRevenueCents int64 `json:"total_revenue_cents"`
ByPlatform map[string]PlatformRoyaltySummary `json:"by_platform"`
}

View file

@ -197,7 +197,7 @@ func TestExternalStreamingRoyaltyFields(t *testing.T) {
ReportingPeriodStart: now,
ReportingPeriodEnd: now.AddDate(0, 1, 0),
Platform: string(PlatformSpotify),
TotalStreams: 50000,
TotalStreams: 50000,
TotalRevenueCents: 17500,
Currency: "USD",
Distributor: "distrokid",
@ -217,7 +217,7 @@ func TestExternalStreamingRoyaltyFields(t *testing.T) {
func TestRoyaltySummary(t *testing.T) {
summary := RoyaltySummary{
TotalStreams: 150000,
TotalStreams: 150000,
TotalRevenueCents: 52500,
ByPlatform: map[string]PlatformRoyaltySummary{
"spotify": {Streams: 50000, RevenueCents: 17500},

View file

@ -140,17 +140,17 @@ func (l *Lesson) BeforeCreate(tx *gorm.DB) error {
// CourseEnrollment represents a user's enrollment (purchase) in a course
type CourseEnrollment struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
CourseID uuid.UUID `gorm:"type:uuid;not null" json:"course_id"`
PurchasedPriceCents int `gorm:"not null;default:0" json:"purchased_price_cents"`
Currency string `gorm:"size:3;not null;default:'USD'" json:"currency"`
Status EnrollmentStatus `gorm:"size:50;not null;default:'active'" json:"status"`
AccessExpiresAt *time.Time `json:"access_expires_at,omitempty"`
PurchasedAt time.Time `gorm:"not null" json:"purchased_at"`
RefundedAt *time.Time `json:"refunded_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
CourseID uuid.UUID `gorm:"type:uuid;not null" json:"course_id"`
PurchasedPriceCents int `gorm:"not null;default:0" json:"purchased_price_cents"`
Currency string `gorm:"size:3;not null;default:'USD'" json:"currency"`
Status EnrollmentStatus `gorm:"size:50;not null;default:'active'" json:"status"`
AccessExpiresAt *time.Time `json:"access_expires_at,omitempty"`
PurchasedAt time.Time `gorm:"not null" json:"purchased_at"`
RefundedAt *time.Time `json:"refunded_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (CourseEnrollment) TableName() string {
@ -196,7 +196,7 @@ type Certificate struct {
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
CourseID uuid.UUID `gorm:"type:uuid;not null" json:"course_id"`
EnrollmentID uuid.UUID `gorm:"type:uuid;not null" json:"enrollment_id"`
CertificateCode string `gorm:"size:255;not null;uniqueIndex" json:"certificate_code"`
CertificateCode string `gorm:"size:255;not null;uniqueIndex" json:"certificate_code"`
IssueDate time.Time `gorm:"not null" json:"issue_date"`
Status CertificateStatus `gorm:"size:50;not null;default:'active'" json:"status"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`

View file

@ -14,20 +14,20 @@ import (
// Service errors
var (
ErrCourseNotFound = errors.New("course not found")
ErrLessonNotFound = errors.New("lesson not found")
ErrEnrollmentNotFound = errors.New("enrollment not found")
ErrAlreadyEnrolled = errors.New("already enrolled in this course")
ErrNotEnrolled = errors.New("not enrolled in this course")
ErrCourseNotPublished = errors.New("course is not published")
ErrNotCourseOwner = errors.New("you are not the owner of this course")
ErrReviewAlreadyExists = errors.New("you have already reviewed this course")
ErrInvalidRating = errors.New("rating must be between 1 and 5")
ErrCertificateNotFound = errors.New("certificate not found")
ErrCourseNotCompleted = errors.New("course is not fully completed")
ErrCertificateExists = errors.New("certificate already issued for this course")
ErrCourseNotFound = errors.New("course not found")
ErrLessonNotFound = errors.New("lesson not found")
ErrEnrollmentNotFound = errors.New("enrollment not found")
ErrAlreadyEnrolled = errors.New("already enrolled in this course")
ErrNotEnrolled = errors.New("not enrolled in this course")
ErrCourseNotPublished = errors.New("course is not published")
ErrNotCourseOwner = errors.New("you are not the owner of this course")
ErrReviewAlreadyExists = errors.New("you have already reviewed this course")
ErrInvalidRating = errors.New("rating must be between 1 and 5")
ErrCertificateNotFound = errors.New("certificate not found")
ErrCourseNotCompleted = errors.New("course is not fully completed")
ErrCertificateExists = errors.New("certificate already issued for this course")
ErrCannotEnrollOwnCourse = errors.New("cannot enroll in your own course")
ErrSlugAlreadyTaken = errors.New("slug is already taken")
ErrSlugAlreadyTaken = errors.New("slug is already taken")
)
// ServiceOption is a functional option for configuring the Service
@ -327,11 +327,11 @@ func (s *Service) DeleteCourse(ctx context.Context, creatorID, courseID uuid.UUI
// CreateLessonRequest holds the fields for creating a lesson
type CreateLessonRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
VideoFilePath string `json:"video_file_path"`
DurationSeconds int `json:"duration_seconds"`
IsPreviewFree bool `json:"is_preview_free"`
Title string `json:"title" binding:"required"`
Description string `json:"description"`
VideoFilePath string `json:"video_file_path"`
DurationSeconds int `json:"duration_seconds"`
IsPreviewFree bool `json:"is_preview_free"`
}
// CreateLesson adds a lesson to a course
@ -346,13 +346,13 @@ func (s *Service) CreateLesson(ctx context.Context, creatorID, courseID uuid.UUI
Select("COALESCE(MAX(order_index), 0)").Scan(&maxOrder)
lesson := &Lesson{
CourseID: courseID,
OrderIndex: maxOrder + 1,
Title: req.Title,
Description: req.Description,
VideoFilePath: req.VideoFilePath,
CourseID: courseID,
OrderIndex: maxOrder + 1,
Title: req.Title,
Description: req.Description,
VideoFilePath: req.VideoFilePath,
DurationSeconds: req.DurationSeconds,
IsPreviewFree: req.IsPreviewFree,
IsPreviewFree: req.IsPreviewFree,
}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@ -362,7 +362,7 @@ func (s *Service) CreateLesson(ctx context.Context, creatorID, courseID uuid.UUI
// Update course counters
return tx.Model(&Course{}).Where("id = ?", courseID).
UpdateColumns(map[string]interface{}{
"lesson_count": gorm.Expr("lesson_count + 1"),
"lesson_count": gorm.Expr("lesson_count + 1"),
"total_duration_seconds": gorm.Expr("total_duration_seconds + ?", req.DurationSeconds),
}).Error
})
@ -458,7 +458,7 @@ func (s *Service) DeleteLesson(ctx context.Context, creatorID, courseID, lessonI
}
return tx.Model(&Course{}).Where("id = ?", courseID).
UpdateColumns(map[string]interface{}{
"lesson_count": gorm.Expr("lesson_count - 1"),
"lesson_count": gorm.Expr("lesson_count - 1"),
"total_duration_seconds": gorm.Expr("total_duration_seconds - ?", lesson.DurationSeconds),
}).Error
})
@ -648,8 +648,8 @@ func (s *Service) UpdateProgress(ctx context.Context, userID, lessonID uuid.UUID
if errors.Is(err, gorm.ErrRecordNotFound) {
progress = LessonProgress{
UserID: userID,
LessonID: lessonID,
EnrollmentID: enrollment.ID,
LessonID: lessonID,
EnrollmentID: enrollment.ID,
WatchedPercentage: percentage,
WatchedDurationSeconds: req.WatchedDurationSeconds,
PlaybackPositionSeconds: req.PlaybackPositionSeconds,

View file

@ -184,10 +184,10 @@ func TestNewService(t *testing.T) {
func TestCourseFields(t *testing.T) {
course := Course{
ID: uuid.New(),
CreatorID: uuid.New(),
Title: "Test Course",
Slug: "test-course",
ID: uuid.New(),
CreatorID: uuid.New(),
Title: "Test Course",
Slug: "test-course",
PriceCents: 1999,
Currency: "USD",
Status: CourseStatusDraft,
@ -263,11 +263,11 @@ func TestCertificateFields(t *testing.T) {
func TestReviewFields(t *testing.T) {
review := CourseReview{
ID: uuid.New(),
Rating: 4,
Title: "Great course",
Content: "Loved it!",
Status: ReviewApproved,
ID: uuid.New(),
Rating: 4,
Title: "Great course",
Content: "Loved it!",
Status: ReviewApproved,
}
if review.Rating != 4 {

View file

@ -16,14 +16,14 @@ import (
// SellerBalance tracks pending and available balance for a seller
type SellerBalance struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
SellerID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:idx_seller_balance_currency" json:"seller_id"`
AvailableCents int64 `gorm:"default:0" json:"available_cents"`
PendingCents int64 `gorm:"default:0" json:"pending_cents"`
TotalEarnedCents int64 `gorm:"default:0" json:"total_earned_cents"`
TotalPaidOutCents int64 `gorm:"default:0" json:"total_paid_out_cents"`
Currency string `gorm:"size:3;default:'EUR';uniqueIndex:idx_seller_balance_currency" json:"currency"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
SellerID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:idx_seller_balance_currency" json:"seller_id"`
AvailableCents int64 `gorm:"default:0" json:"available_cents"`
PendingCents int64 `gorm:"default:0" json:"pending_cents"`
TotalEarnedCents int64 `gorm:"default:0" json:"total_earned_cents"`
TotalPaidOutCents int64 `gorm:"default:0" json:"total_paid_out_cents"`
Currency string `gorm:"size:3;default:'EUR';uniqueIndex:idx_seller_balance_currency" json:"currency"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (SellerBalance) TableName() string { return "seller_balances" }
@ -311,7 +311,7 @@ func (s *Service) processOnePayout(ctx context.Context, bal *SellerBalance) erro
tx.WithContext(ctx).Model(&SellerBalance{}).
Where("id = ?", lockedBal.ID).
Updates(map[string]interface{}{
"pending_cents": gorm.Expr("pending_cents - ?", amount),
"pending_cents": gorm.Expr("pending_cents - ?", amount),
"total_paid_out_cents": gorm.Expr("total_paid_out_cents + ?", amount),
})
}

View file

@ -68,12 +68,12 @@ func TestSellerPayoutFields(t *testing.T) {
func TestSellerBalanceFields(t *testing.T) {
balance := SellerBalance{
SellerID: uuid.New(),
AvailableCents: 10000,
PendingCents: 2000,
TotalEarnedCents: 50000,
SellerID: uuid.New(),
AvailableCents: 10000,
PendingCents: 2000,
TotalEarnedCents: 50000,
TotalPaidOutCents: 38000,
Currency: "EUR",
Currency: "EUR",
}
if balance.AvailableCents != 10000 {

View file

@ -47,25 +47,25 @@ const (
// Plan represents a subscription plan definition
type Plan struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
Name PlanName `gorm:"not null;uniqueIndex;size:50" json:"name"`
DisplayName string `gorm:"not null;size:100" json:"display_name"`
Description string `gorm:"type:text" json:"description"`
PriceMonthly int `gorm:"column:price_monthly_cents;not null;default:0" json:"price_monthly_cents"`
PriceYearly int `gorm:"column:price_yearly_cents;not null;default:0" json:"price_yearly_cents"`
Currency string `gorm:"size:3;default:'USD'" json:"currency"`
UploadLimitMonthly *int `gorm:"column:upload_limit_monthly" json:"upload_limit_monthly"`
StorageLimitBytes int64 `gorm:"not null;default:0" json:"storage_limit_bytes"`
MarketplaceCommission float64 `gorm:"column:marketplace_commission_rate;type:numeric(5,4);default:0" json:"marketplace_commission_rate"`
CanSellOnMarketplace bool `gorm:"not null;default:false" json:"can_sell_on_marketplace"`
HasPrioritySupport bool `gorm:"not null;default:false" json:"has_priority_support"`
HasCollaborationTools bool `gorm:"not null;default:false" json:"has_collaboration_tools"`
HasDistribution bool `gorm:"not null;default:false" json:"has_distribution"`
TrialDays int `gorm:"not null;default:0" json:"trial_days"`
IsActive bool `gorm:"not null;default:true" json:"is_active"`
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
Name PlanName `gorm:"not null;uniqueIndex;size:50" json:"name"`
DisplayName string `gorm:"not null;size:100" json:"display_name"`
Description string `gorm:"type:text" json:"description"`
PriceMonthly int `gorm:"column:price_monthly_cents;not null;default:0" json:"price_monthly_cents"`
PriceYearly int `gorm:"column:price_yearly_cents;not null;default:0" json:"price_yearly_cents"`
Currency string `gorm:"size:3;default:'USD'" json:"currency"`
UploadLimitMonthly *int `gorm:"column:upload_limit_monthly" json:"upload_limit_monthly"`
StorageLimitBytes int64 `gorm:"not null;default:0" json:"storage_limit_bytes"`
MarketplaceCommission float64 `gorm:"column:marketplace_commission_rate;type:numeric(5,4);default:0" json:"marketplace_commission_rate"`
CanSellOnMarketplace bool `gorm:"not null;default:false" json:"can_sell_on_marketplace"`
HasPrioritySupport bool `gorm:"not null;default:false" json:"has_priority_support"`
HasCollaborationTools bool `gorm:"not null;default:false" json:"has_collaboration_tools"`
HasDistribution bool `gorm:"not null;default:false" json:"has_distribution"`
TrialDays int `gorm:"not null;default:0" json:"trial_days"`
IsActive bool `gorm:"not null;default:true" json:"is_active"`
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (Plan) TableName() string {
@ -81,22 +81,22 @@ func (p *Plan) BeforeCreate(tx *gorm.DB) error {
// UserSubscription represents a user's active or past subscription
type UserSubscription struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
PlanID uuid.UUID `gorm:"type:uuid;not null" json:"plan_id"`
Status SubscriptionStatus `gorm:"not null;size:30;default:'active'" json:"status"`
BillingCycle BillingCycle `gorm:"not null;size:10;default:'monthly'" json:"billing_cycle"`
CurrentPeriodStart time.Time `gorm:"not null" json:"current_period_start"`
CurrentPeriodEnd time.Time `gorm:"not null" json:"current_period_end"`
TrialStart *time.Time `json:"trial_start,omitempty"`
TrialEnd *time.Time `json:"trial_end,omitempty"`
CanceledAt *time.Time `json:"canceled_at,omitempty"`
CancelAtPeriodEnd bool `gorm:"not null;default:false" json:"cancel_at_period_end"`
HyperswitchSubscriptionID string `gorm:"size:255" json:"hyperswitch_subscription_id,omitempty"`
HyperswitchCustomerID string `gorm:"size:255" json:"hyperswitch_customer_id,omitempty"`
PaymentMethodID string `gorm:"size:255" json:"payment_method_id,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
PlanID uuid.UUID `gorm:"type:uuid;not null" json:"plan_id"`
Status SubscriptionStatus `gorm:"not null;size:30;default:'active'" json:"status"`
BillingCycle BillingCycle `gorm:"not null;size:10;default:'monthly'" json:"billing_cycle"`
CurrentPeriodStart time.Time `gorm:"not null" json:"current_period_start"`
CurrentPeriodEnd time.Time `gorm:"not null" json:"current_period_end"`
TrialStart *time.Time `json:"trial_start,omitempty"`
TrialEnd *time.Time `json:"trial_end,omitempty"`
CanceledAt *time.Time `json:"canceled_at,omitempty"`
CancelAtPeriodEnd bool `gorm:"not null;default:false" json:"cancel_at_period_end"`
HyperswitchSubscriptionID string `gorm:"size:255" json:"hyperswitch_subscription_id,omitempty"`
HyperswitchCustomerID string `gorm:"size:255" json:"hyperswitch_customer_id,omitempty"`
PaymentMethodID string `gorm:"size:255" json:"payment_method_id,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
// Relations (loaded via Preload)
Plan Plan `gorm:"foreignKey:PlanID" json:"plan,omitempty"`
@ -125,17 +125,17 @@ func (s *UserSubscription) IsActiveOrTrialing() bool {
// Invoice represents a subscription billing invoice
type Invoice struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
SubscriptionID uuid.UUID `gorm:"type:uuid;not null" json:"subscription_id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
AmountCents int `gorm:"not null" json:"amount_cents"`
Currency string `gorm:"size:3;default:'USD'" json:"currency"`
Status InvoiceStatus `gorm:"not null;size:30;default:'pending'" json:"status"`
BillingPeriodStart time.Time `gorm:"not null" json:"billing_period_start"`
BillingPeriodEnd time.Time `gorm:"not null" json:"billing_period_end"`
HyperswitchPaymentID string `gorm:"size:255" json:"hyperswitch_payment_id,omitempty"`
PaidAt *time.Time `json:"paid_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
SubscriptionID uuid.UUID `gorm:"type:uuid;not null" json:"subscription_id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
AmountCents int `gorm:"not null" json:"amount_cents"`
Currency string `gorm:"size:3;default:'USD'" json:"currency"`
Status InvoiceStatus `gorm:"not null;size:30;default:'pending'" json:"status"`
BillingPeriodStart time.Time `gorm:"not null" json:"billing_period_start"`
BillingPeriodEnd time.Time `gorm:"not null" json:"billing_period_end"`
HyperswitchPaymentID string `gorm:"size:255" json:"hyperswitch_payment_id,omitempty"`
PaidAt *time.Time `json:"paid_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
func (Invoice) TableName() string {

View file

@ -61,11 +61,11 @@ type StreamServiceInterface interface {
// BE-SVC-001: Add cache service for track metadata
// v0.943: Batch operations delegated to TrackBatchService
type TrackService struct {
db *gorm.DB // Write operations (and read fallback when readDB is nil)
readDB *gorm.DB // Optional read replica for read-only operations
logger *zap.Logger
uploadDir string
maxFileSize int64
db *gorm.DB // Write operations (and read fallback when readDB is nil)
readDB *gorm.DB // Optional read replica for read-only operations
logger *zap.Logger
uploadDir string
maxFileSize int64
cacheService *services.CacheService
streamService StreamServiceInterface // INT-02: Optional, triggers HLS transcoding after upload
batchService *TrackBatchService // v0.943: batch operations

View file

@ -27,8 +27,8 @@ type UpdateTrackRequest struct {
Artist *string `json:"artist" binding:"omitempty,max=255" validate:"omitempty,max=255"`
Album *string `json:"album" binding:"omitempty,max=255" validate:"omitempty,max=255"`
Genre *string `json:"genre" binding:"omitempty,max=100" validate:"omitempty,max=100"` // legacy, single
Genres []string `json:"genres"` // v0.10.1: max 3, taxonomy slugs
Tags []string `json:"tags"` // v0.10.1: max 10, 30 chars each
Genres []string `json:"genres"` // v0.10.1: max 3, taxonomy slugs
Tags []string `json:"tags"` // v0.10.1: max 10, 30 chars each
Year *int `json:"year" binding:"omitempty,min=1900,max=2100" validate:"omitempty,min=1900,max=2100"`
BPM *int `json:"bpm" binding:"omitempty,min=0,max=300" validate:"omitempty,min=0,max=300"`
MusicalKey *string `json:"musical_key" binding:"omitempty,max=10" validate:"omitempty,max=10"`

View file

@ -129,4 +129,3 @@ const playlistsMapping = `{
}
}
}`

View file

@ -9,8 +9,8 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/core/marketplace"
apperrors "veza-backend-api/internal/errors"
)
// AdminTransferHandler handles admin transfer dashboard endpoints (v0.701).

View file

@ -0,0 +1,27 @@
package handlers
import (
"encoding/json"
"testing"
)
// FuzzLoginPayload fuzzes the login request parsing.
// The handler should never panic on any input.
func FuzzLoginPayload(f *testing.F) {
f.Add([]byte(`{"email":"test@test.com","password":"Pass123!"}`))
f.Add([]byte(`{"email":"","password":""}`))
f.Add([]byte(`{}`))
f.Add([]byte(`invalid json`))
f.Add([]byte(""))
f.Add([]byte(`{"email":null,"password":123}`))
f.Add([]byte(`{"email":"a@b.c","password":"` + string(make([]byte, 10000)) + `"}`))
f.Fuzz(func(t *testing.T, data []byte) {
// Verify that JSON parsing of arbitrary payloads never panics
var payload struct {
Email string `json:"email"`
Password string `json:"password"`
}
_ = json.Unmarshal(data, &payload)
})
}

View file

@ -137,8 +137,8 @@ func (h *ChatAttachmentHandler) UploadChatAttachment(c *gin.Context) {
}
RespondSuccess(c, http.StatusCreated, gin.H{
"url": signedURL,
"file_id": uuid.New().String(),
"url": signedURL,
"file_id": uuid.New().String(),
"file_name": fileHeader.Filename,
"file_size": fileHeader.Size,
"file_type": fileType,

View file

@ -3,9 +3,9 @@ package handlers
import (
"net/http"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/models"
"veza-backend-api/internal/repositories"
apperrors "veza-backend-api/internal/errors"
chatws "veza-backend-api/internal/websocket/chat"

View file

@ -93,20 +93,20 @@ func (h *ChatSearchHandler) SearchMessages(c *gin.Context) {
out := make([]gin.H, 0, len(messages))
for _, m := range messages {
out = append(out, gin.H{
"id": m.ID,
"conversation_id": m.ConversationID,
"sender_id": m.SenderID,
"content": m.Content,
"message_type": m.MessageType,
"id": m.ID,
"conversation_id": m.ConversationID,
"sender_id": m.SenderID,
"content": m.Content,
"message_type": m.MessageType,
"parent_message_id": m.ParentMessageID,
"reply_to_id": m.ReplyToID,
"is_pinned": m.IsPinned,
"is_edited": m.IsEdited,
"is_deleted": m.IsDeleted,
"edited_at": m.EditedAt,
"status": m.Status,
"created_at": m.CreatedAt,
"updated_at": m.UpdatedAt,
"reply_to_id": m.ReplyToID,
"is_pinned": m.IsPinned,
"is_edited": m.IsEdited,
"is_deleted": m.IsDeleted,
"edited_at": m.EditedAt,
"status": m.Status,
"created_at": m.CreatedAt,
"updated_at": m.UpdatedAt,
})
}

View file

@ -18,10 +18,10 @@ import (
// CoListeningWebSocketHandler handles WebSocket connections for co-listening (v0.10.7 F481)
type CoListeningWebSocketHandler struct {
hub *colistening.Hub
svc *services.CoListeningService
jwtValidator JWTValidator
logger *zap.Logger
hub *colistening.Hub
svc *services.CoListeningService
jwtValidator JWTValidator
logger *zap.Logger
}
// JWTValidator validates access tokens and returns user ID

View file

@ -21,11 +21,11 @@ const exportRateLimitMax = 3
// GDPRExportHandler handles GDPR export endpoints (v0.10.8 F065)
type GDPRExportHandler struct {
db *gorm.DB
gdprExportService *services.GDPRExportService
s3Service *services.S3StorageService
redisClient *redis.Client
logger *zap.Logger
db *gorm.DB
gdprExportService *services.GDPRExportService
s3Service *services.S3StorageService
redisClient *redis.Client
logger *zap.Logger
}
// NewGDPRExportHandler creates a new GDPR export handler
@ -85,10 +85,10 @@ func (h *GDPRExportHandler) RequestExport(c *gin.Context) {
h.gdprExportService.ExportUserDataAsync(c.Request.Context(), export.ID, userID)
c.JSON(http.StatusAccepted, gin.H{
"export_id": export.ID.String(),
"status": "processing",
"estimated_completion": time.Now().Add(15 * time.Minute).Format(time.RFC3339),
"message": "Your export is being prepared. You will receive an email with the download link when ready.",
"export_id": export.ID.String(),
"status": "processing",
"estimated_completion": time.Now().Add(15 * time.Minute).Format(time.RFC3339),
"message": "Your export is being prepared. You will receive an email with the download link when ready.",
})
}
@ -153,10 +153,10 @@ func (h *GDPRExportHandler) GetExport(c *gin.Context) {
}
resp := gin.H{
"export_id": export.ID.String(),
"status": export.Status,
"expires_at": export.ExpiresAt.Format(time.RFC3339),
"created_at": export.CreatedAt.Format(time.RFC3339),
"export_id": export.ID.String(),
"status": export.Status,
"expires_at": export.ExpiresAt.Format(time.RFC3339),
"created_at": export.CreatedAt.Format(time.RFC3339),
}
if export.CompletedAt != nil {
resp["completed_at"] = export.CompletedAt.Format(time.RFC3339)

View file

@ -87,7 +87,7 @@ func (nh *NotificationHandlers) GetNotifications(c *gin.Context) {
"page": result.Page,
"limit": result.Limit,
"total_pages": result.TotalPages,
"unread_count": result.UnreadCount,
"unread_count": result.UnreadCount,
})
}
@ -234,11 +234,11 @@ func (nh *NotificationHandlers) GetPreferences(c *gin.Context) {
}
RespondSuccess(c, http.StatusOK, gin.H{
"push_follow": prefs.PushFollow,
"push_like": prefs.PushLike,
"push_comment": prefs.PushComment,
"push_message": prefs.PushMessage,
"push_mention": prefs.PushMention,
"push_follow": prefs.PushFollow,
"push_like": prefs.PushLike,
"push_comment": prefs.PushComment,
"push_message": prefs.PushMessage,
"push_mention": prefs.PushMention,
"quiet_hours_enabled": prefs.QuietHoursEnabled,
"quiet_hours_start": prefs.QuietHoursStart,
"quiet_hours_end": prefs.QuietHoursEnd,
@ -248,11 +248,11 @@ func (nh *NotificationHandlers) GetPreferences(c *gin.Context) {
// UpdatePreferencesRequest is the DTO for updating preferences (F553: quiet hours)
type UpdatePreferencesRequest struct {
PushFollow *bool `json:"push_follow"`
PushLike *bool `json:"push_like"`
PushComment *bool `json:"push_comment"`
PushMessage *bool `json:"push_message"`
PushMention *bool `json:"push_mention"`
PushFollow *bool `json:"push_follow"`
PushLike *bool `json:"push_like"`
PushComment *bool `json:"push_comment"`
PushMessage *bool `json:"push_message"`
PushMention *bool `json:"push_mention"`
QuietHoursEnabled *bool `json:"quiet_hours_enabled"`
QuietHoursStart *string `json:"quiet_hours_start"` // "22:00"
QuietHoursEnd *string `json:"quiet_hours_end"` // "08:00"

View file

@ -39,11 +39,11 @@ func (h *PayoutHandler) GetSellerBalance(c *gin.Context) {
}
RespondSuccess(c, http.StatusOK, gin.H{
"available_cents": balance.AvailableCents,
"pending_cents": balance.PendingCents,
"total_earned_cents": balance.TotalEarnedCents,
"available_cents": balance.AvailableCents,
"pending_cents": balance.PendingCents,
"total_earned_cents": balance.TotalEarnedCents,
"total_paid_out_cents": balance.TotalPaidOutCents,
"currency": balance.Currency,
"currency": balance.Currency,
})
}

View file

@ -26,11 +26,11 @@ type RoomServiceInterface interface {
GetRoomHistory(ctx context.Context, roomID uuid.UUID, limit, offset int) ([]services.ChatMessageResponse, error)
GetRoomHistoryWithCursor(ctx context.Context, roomID uuid.UUID, limit int, cursor string) (*services.RoomHistoryWithCursorResult, error) // v0.931: cursor pagination
DeleteRoom(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) error // BE-API-010: Delete room method
GetRoomMembers(ctx context.Context, roomID uuid.UUID, requestUserID uuid.UUID) (*services.RoomMembersResponse, error) // v0.9.6 @mention, v0.9.7 roles
CreateInvitation(ctx context.Context, roomID uuid.UUID, inviterID uuid.UUID) (*services.RoomInvitationResponse, error) // v0.9.7
GetRoomMembers(ctx context.Context, roomID uuid.UUID, requestUserID uuid.UUID) (*services.RoomMembersResponse, error) // v0.9.6 @mention, v0.9.7 roles
CreateInvitation(ctx context.Context, roomID uuid.UUID, inviterID uuid.UUID) (*services.RoomInvitationResponse, error) // v0.9.7
JoinByToken(ctx context.Context, token uuid.UUID, userID uuid.UUID) (uuid.UUID, error) // v0.9.7
KickMember(ctx context.Context, roomID uuid.UUID, targetUserID uuid.UUID, requestUserID uuid.UUID) error // v0.9.7
UpdateMemberRole(ctx context.Context, roomID, targetUserID, requestUserID uuid.UUID, newRole string) error // v0.9.7
KickMember(ctx context.Context, roomID uuid.UUID, targetUserID uuid.UUID, requestUserID uuid.UUID) error // v0.9.7
UpdateMemberRole(ctx context.Context, roomID, targetUserID, requestUserID uuid.UUID, newRole string) error // v0.9.7
}
// RoomHandler gère les opérations sur les rooms (conversations)

View file

@ -4,8 +4,8 @@ import (
"net/http"
"strconv"
"veza-backend-api/internal/services"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
)

View file

@ -4,8 +4,8 @@ import (
"errors"
"net/http"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/core/marketplace"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"

View file

@ -5,9 +5,9 @@ import (
"net/http"
"time"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/services"
"veza-backend-api/internal/types"
apperrors "veza-backend-api/internal/errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"

View file

@ -5,9 +5,9 @@ import (
"net/http"
"veza-backend-api/internal/core/social"
"veza-backend-api/internal/utils"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/pagination"
"veza-backend-api/internal/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"

View file

@ -92,7 +92,7 @@ func setupE2ETestRouter(t *testing.T) (*gin.Engine, func()) {
RateLimitLimit: 100,
RateLimitWindow: 60,
AuthRateLimitLoginAttempts: 10,
AuthRateLimitLoginWindow: 15,
AuthRateLimitLoginWindow: 15,
}
// Initialize services (required for AuthMiddleware)

View file

@ -13,13 +13,13 @@ import (
// LogConfig holds the centralized logging configuration loaded from config/logging.toml.
// Environment variables override file values (highest priority).
type LogConfig struct {
Global GlobalConfig `toml:"global"`
Rotation RotationConfig `toml:"rotation"`
Backend BackendConfig `toml:"backend"`
Stream StreamConfig `toml:"stream"`
Frontend FrontendConfig `toml:"frontend"`
Global GlobalConfig `toml:"global"`
Rotation RotationConfig `toml:"rotation"`
Backend BackendConfig `toml:"backend"`
Stream StreamConfig `toml:"stream"`
Frontend FrontendConfig `toml:"frontend"`
Aggregation LogAggregationConfig `toml:"aggregation"`
Permissions PermissionsConfig `toml:"permissions"`
Permissions PermissionsConfig `toml:"permissions"`
}
type GlobalConfig struct {
@ -38,13 +38,13 @@ type RotationConfig struct {
}
type BackendConfig struct {
Module string `toml:"module"`
Modules []string `toml:"modules"`
SlowRequestThresholdMs int `toml:"slow_request_threshold_ms"`
SamplingInitial int `toml:"sampling_initial"`
SamplingThereafter int `toml:"sampling_thereafter"`
BufferSizeKB int `toml:"buffer_size_kb"`
FlushIntervalMs int `toml:"flush_interval_ms"`
Module string `toml:"module"`
Modules []string `toml:"modules"`
SlowRequestThresholdMs int `toml:"slow_request_threshold_ms"`
SamplingInitial int `toml:"sampling_initial"`
SamplingThereafter int `toml:"sampling_thereafter"`
BufferSizeKB int `toml:"buffer_size_kb"`
FlushIntervalMs int `toml:"flush_interval_ms"`
}
type StreamConfig struct {
@ -54,9 +54,9 @@ type StreamConfig struct {
}
type FrontendConfig struct {
Level string `toml:"level"`
Endpoint string `toml:"endpoint"`
SentryEnabled bool `toml:"sentry_enabled"`
Level string `toml:"level"`
Endpoint string `toml:"endpoint"`
SentryEnabled bool `toml:"sentry_enabled"`
}
type LogAggregationConfig struct {
@ -117,13 +117,13 @@ func defaultConfig() *LogConfig {
RustMaxFiles: 5,
},
Backend: BackendConfig{
Module: "backend-api",
Modules: []string{"db", "rabbitmq"},
SlowRequestThresholdMs: 1000,
SamplingInitial: 100,
SamplingThereafter: 100,
BufferSizeKB: 256,
FlushIntervalMs: 100,
Module: "backend-api",
Modules: []string{"db", "rabbitmq"},
SlowRequestThresholdMs: 1000,
SamplingInitial: 100,
SamplingThereafter: 100,
BufferSizeKB: 256,
FlushIntervalMs: 100,
},
Stream: StreamConfig{
Module: "stream",

View file

@ -38,11 +38,11 @@ type apiKeyEntry struct {
// APIKeyRateLimiter rate-limits requests authenticated via API key.
// It tracks read and write operations separately, keyed by API key ID.
type APIKeyRateLimiter struct {
config *APIKeyRateLimiterConfig
readStore map[string]*apiKeyEntry
writeStore map[string]*apiKeyEntry
mu sync.Mutex
stop chan struct{}
config *APIKeyRateLimiterConfig
readStore map[string]*apiKeyEntry
writeStore map[string]*apiKeyEntry
mu sync.Mutex
stop chan struct{}
}
// NewAPIKeyRateLimiter creates a new API key rate limiter

View file

@ -51,8 +51,8 @@ func RequireAPIKeyScope(apiKeyService *services.APIKeyService) gin.HandlerFunc {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"error": gin.H{
"code": "INSUFFICIENT_SCOPE",
"message": "API key does not have the required scope: " + requiredScope,
"code": "INSUFFICIENT_SCOPE",
"message": "API key does not have the required scope: " + requiredScope,
"required_scope": requiredScope,
"key_scopes": key.Scopes,
},

View file

@ -80,4 +80,3 @@ func setCacheHeaders(c *gin.Context, rule CacheRule) {
c.Header("Cache-Control", strings.Join(parts, ", "))
}
}

View file

@ -104,4 +104,3 @@ func TestDefaultCacheHeadersConfig(t *testing.T) {
t.Error("missing API rule")
}
}

View file

@ -17,7 +17,7 @@ type mockCaptchaVerifier struct {
}
func (m *mockCaptchaVerifier) Verify(_ context.Context, _, _ string) error { return m.err }
func (m *mockCaptchaVerifier) IsEnabled() bool { return m.enabled }
func (m *mockCaptchaVerifier) IsEnabled() bool { return m.enabled }
func TestRequireCaptcha_Disabled_PassesThrough(t *testing.T) {
gin.SetMode(gin.TestMode)

View file

@ -76,8 +76,8 @@ func SecurityHeaders() gin.HandlerFunc {
frameAncestors = "'self' http://localhost:3000 http://127.0.0.1:3000 http://localhost:5173 http://127.0.0.1:5173"
}
// SECURITY(MEDIUM-006): Removed 'unsafe-eval' from script-src. Swagger UI works without it.
// Keep 'unsafe-inline' only for style-src (required by Swagger UI's inline styles).
csp := "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' http: https:; font-src 'self' data: https:; frame-ancestors " + frameAncestors
// Keep 'unsafe-inline' only for style-src (required by Swagger UI's inline styles).
csp := "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' http: https:; font-src 'self' data: https:; frame-ancestors " + frameAncestors
c.Header("Content-Security-Policy", csp)
} else {
// default-src 'none': bloque tout par défaut

View file

@ -13,7 +13,7 @@ type ChatMessage struct {
Sender *User `gorm:"foreignKey:SenderID" json:"-"` // v0.9.7: for history sender_username
Content string `gorm:"type:text;not null" json:"content"`
MessageType string `gorm:"type:varchar(50);not null" json:"message_type"` // text, image, audio, etc.
ParentMessageID *uuid.UUID `gorm:"-" json:"parent_message_id,omitempty"` // Not persisted; use ReplyToID for DB
ParentMessageID *uuid.UUID `gorm:"-" json:"parent_message_id,omitempty"` // Not persisted; use ReplyToID for DB
ReplyToID *uuid.UUID `gorm:"column:reply_to_id;type:uuid" json:"reply_to_id,omitempty"`
IsPinned bool `gorm:"default:false;not null" json:"is_pinned"`
IsEdited bool `gorm:"default:false;not null" json:"is_edited"`

View file

@ -9,15 +9,15 @@ import (
// DataExport represents a GDPR data export job (v0.10.8 F065)
type DataExport struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"` // pending, processing, completed, failed
S3Key *string `gorm:"type:text" json:"s3_key,omitempty"`
FileSizeBytes *int64 `json:"file_size_bytes,omitempty"`
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
ErrorMessage *string `gorm:"type:text" json:"error_message,omitempty"`
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"` // pending, processing, completed, failed
S3Key *string `gorm:"type:text" json:"s3_key,omitempty"`
FileSizeBytes *int64 `json:"file_size_bytes,omitempty"`
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
ErrorMessage *string `gorm:"type:text" json:"error_message,omitempty"`
}
// TableName returns the table name for DataExport

View file

@ -10,19 +10,19 @@ import (
// Playlist représente une playlist de tracks
// MIGRATION UUID: Completée. ID et UserID sont des UUIDs.
type Playlist struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id" db:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id" db:"user_id"`
Title string `gorm:"column:name;not null;size:200" json:"title" db:"title"`
Description string `gorm:"type:text" json:"description,omitempty" db:"description"`
IsPublic bool `gorm:"default:true" json:"is_public" db:"is_public"`
CoverURL string `gorm:"size:500" json:"cover_url,omitempty" db:"cover_url"`
TrackCount int `gorm:"default:0" json:"track_count" db:"track_count"`
FollowerCount int `gorm:"default:0" json:"follower_count" db:"follower_count"`
IsEditorial bool `gorm:"default:false" json:"is_editorial" db:"is_editorial"` // v0.10.4 F141
IsDefaultFavorites bool `gorm:"default:false" json:"is_default_favorites" db:"is_default_favorites"` // v0.10.4 F136
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" db:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" db:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" db:"deleted_at"`
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id" db:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id" db:"user_id"`
Title string `gorm:"column:name;not null;size:200" json:"title" db:"title"`
Description string `gorm:"type:text" json:"description,omitempty" db:"description"`
IsPublic bool `gorm:"default:true" json:"is_public" db:"is_public"`
CoverURL string `gorm:"size:500" json:"cover_url,omitempty" db:"cover_url"`
TrackCount int `gorm:"default:0" json:"track_count" db:"track_count"`
FollowerCount int `gorm:"default:0" json:"follower_count" db:"follower_count"`
IsEditorial bool `gorm:"default:false" json:"is_editorial" db:"is_editorial"` // v0.10.4 F141
IsDefaultFavorites bool `gorm:"default:false" json:"is_default_favorites" db:"is_default_favorites"` // v0.10.4 F136
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" db:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" db:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" db:"deleted_at"`
// Relations
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`

View file

@ -9,15 +9,15 @@ import (
// RoomInvitation represents an invitation to join a room (v0.9.7)
type RoomInvitation struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
RoomID uuid.UUID `gorm:"type:uuid;not null" json:"room_id"`
InviterID uuid.UUID `gorm:"type:uuid;not null" json:"inviter_id"`
InviteeID *uuid.UUID `gorm:"type:uuid" json:"invitee_id,omitempty"`
Token uuid.UUID `gorm:"type:uuid;not null;uniqueIndex" json:"token"`
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"` // pending, accepted, expired
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
RoomID uuid.UUID `gorm:"type:uuid;not null" json:"room_id"`
InviterID uuid.UUID `gorm:"type:uuid;not null" json:"inviter_id"`
InviteeID *uuid.UUID `gorm:"type:uuid" json:"invitee_id,omitempty"`
Token uuid.UUID `gorm:"type:uuid;not null;uniqueIndex" json:"token"`
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"` // pending, accepted, expired
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Room Room `gorm:"foreignKey:RoomID" json:"-"`
Inviter User `gorm:"foreignKey:InviterID" json:"-"`

View file

@ -8,19 +8,19 @@ import (
// SellerStripeAccount links a user (seller) to their Stripe Connect Express account
type SellerStripeAccount struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
UserID uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex;not null"`
StripeAccountID string `json:"stripe_account_id" gorm:"type:varchar(255);uniqueIndex;not null"`
ChargesEnabled bool `json:"charges_enabled" gorm:"default:false"`
PayoutsEnabled bool `json:"payouts_enabled" gorm:"default:false"`
OnboardingCompleted bool `json:"onboarding_completed" gorm:"default:false"`
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
UserID uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex;not null"`
StripeAccountID string `json:"stripe_account_id" gorm:"type:varchar(255);uniqueIndex;not null"`
ChargesEnabled bool `json:"charges_enabled" gorm:"default:false"`
PayoutsEnabled bool `json:"payouts_enabled" gorm:"default:false"`
OnboardingCompleted bool `json:"onboarding_completed" gorm:"default:false"`
// v0.13.5 TASK-MKT-001: KYC identity verification
KYCStatus string `json:"kyc_status" gorm:"type:varchar(32);default:'not_started'"`
KYCVerificationSessionID string `json:"kyc_verification_session_id,omitempty" gorm:"type:varchar(255)"`
KYCVerifiedAt *time.Time `json:"kyc_verified_at,omitempty"`
KYCLastError string `json:"kyc_last_error,omitempty" gorm:"type:text"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
KYCStatus string `json:"kyc_status" gorm:"type:varchar(32);default:'not_started'"`
KYCVerificationSessionID string `json:"kyc_verification_session_id,omitempty" gorm:"type:varchar(255)"`
KYCVerifiedAt *time.Time `json:"kyc_verified_at,omitempty"`
KYCLastError string `json:"kyc_last_error,omitempty" gorm:"type:text"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
// TableName specifies the table name for SellerStripeAccount

View file

@ -39,11 +39,11 @@ type Track struct {
// SECURITY(CRIT-002): play_count and like_count are PRIVATE — visible only to the creator
// in their analytics dashboard. Never exposed in public API responses.
// Ref: CLAUDE.md rule #4, ORIGIN_UI_UX_SYSTEM.md §13
PlayCount int64 `gorm:"default:0" json:"-" db:"play_count"`
LikeCount int64 `gorm:"default:0" json:"-" db:"like_count"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" db:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" db:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" db:"deleted_at"`
PlayCount int64 `gorm:"default:0" json:"-" db:"play_count"`
LikeCount int64 `gorm:"default:0" json:"-" db:"like_count"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" db:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" db:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" db:"deleted_at"`
// Relations
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`

View file

@ -8,9 +8,9 @@ import (
// UserGenreFollow v0.10.1 F354: utilisateur suit un genre
type UserGenreFollow struct {
UserID uuid.UUID `gorm:"type:uuid;primaryKey" json:"user_id" db:"user_id"`
GenreSlug string `gorm:"size:50;primaryKey" json:"genre_slug" db:"genre_slug"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UserID uuid.UUID `gorm:"type:uuid;primaryKey" json:"user_id" db:"user_id"`
GenreSlug string `gorm:"size:50;primaryKey" json:"genre_slug" db:"genre_slug"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// TableName for UserGenreFollow

View file

@ -28,18 +28,18 @@ func NewAdminPlatformService(db *gorm.DB, logger *zap.Logger) *AdminPlatformServ
// PlatformMetrics contains platform-wide statistics
type PlatformMetrics struct {
TotalUsers int64 `json:"total_users"`
ActiveUsers int64 `json:"active_users"`
NewUsersToday int64 `json:"new_users_today"`
NewUsersWeek int64 `json:"new_users_week"`
TotalTracks int64 `json:"total_tracks"`
TracksToday int64 `json:"tracks_today"`
TotalPlaylists int64 `json:"total_playlists"`
TotalComments int64 `json:"total_comments"`
BannedUsers int64 `json:"banned_users"`
PendingReports int64 `json:"pending_reports"`
StorageUsedMB float64 `json:"storage_used_mb"`
TotalRevenue float64 `json:"total_revenue"`
TotalUsers int64 `json:"total_users"`
ActiveUsers int64 `json:"active_users"`
NewUsersToday int64 `json:"new_users_today"`
NewUsersWeek int64 `json:"new_users_week"`
TotalTracks int64 `json:"total_tracks"`
TracksToday int64 `json:"tracks_today"`
TotalPlaylists int64 `json:"total_playlists"`
TotalComments int64 `json:"total_comments"`
BannedUsers int64 `json:"banned_users"`
PendingReports int64 `json:"pending_reports"`
StorageUsedMB float64 `json:"storage_used_mb"`
TotalRevenue float64 `json:"total_revenue"`
RevenueThisMonth float64 `json:"revenue_this_month"`
}

View file

@ -29,23 +29,23 @@ func NewAdvancedAnalyticsService(db *gorm.DB, logger *zap.Logger) *AdvancedAnaly
// TrackSegmentStat represents a segment of a track with aggregated listening data
type TrackSegmentStat struct {
SegmentIndex int `json:"segment_index"`
SegmentStartMs int64 `json:"segment_start_ms"`
SegmentEndMs int64 `json:"segment_end_ms"`
ListenCount int64 `json:"listen_count"`
DropOffCount int64 `json:"drop_off_count"`
ReplayCount int64 `json:"replay_count"`
Intensity float64 `json:"intensity"` // normalized 0-1
SegmentIndex int `json:"segment_index"`
SegmentStartMs int64 `json:"segment_start_ms"`
SegmentEndMs int64 `json:"segment_end_ms"`
ListenCount int64 `json:"listen_count"`
DropOffCount int64 `json:"drop_off_count"`
ReplayCount int64 `json:"replay_count"`
Intensity float64 `json:"intensity"` // normalized 0-1
}
// TrackHeatmap represents the full heatmap for a track
type TrackHeatmap struct {
TrackID string `json:"track_id"`
TotalSegments int `json:"total_segments"`
SegmentDurationMs int64 `json:"segment_duration_ms"`
Segments []TrackSegmentStat `json:"segments"`
MaxListens int64 `json:"max_listens"`
AvgDropOff float64 `json:"avg_drop_off"`
TrackID string `json:"track_id"`
TotalSegments int `json:"total_segments"`
SegmentDurationMs int64 `json:"segment_duration_ms"`
Segments []TrackSegmentStat `json:"segments"`
MaxListens int64 `json:"max_listens"`
AvgDropOff float64 `json:"avg_drop_off"`
}
// GetTrackHeatmap returns aggregated heatmap data for a track (F396)
@ -134,8 +134,8 @@ func (s *AdvancedAnalyticsService) GetTrackHeatmap(ctx context.Context, creatorI
// PeriodComparison holds the comparison between two time periods
type PeriodComparison struct {
CurrentPeriod PeriodStats `json:"current_period"`
PreviousPeriod PeriodStats `json:"previous_period"`
CurrentPeriod PeriodStats `json:"current_period"`
PreviousPeriod PeriodStats `json:"previous_period"`
Changes PeriodChanges `json:"changes"`
}
@ -155,11 +155,11 @@ type PeriodStats struct {
// PeriodChanges holds the percentage changes between periods
type PeriodChanges struct {
PlaysChange float64 `json:"plays_change"` // percentage
ListenersChange float64 `json:"listeners_change"` // percentage
CompletionChange float64 `json:"completion_change"` // percentage
RevenueChange float64 `json:"revenue_change"` // percentage
FollowersChange float64 `json:"followers_change"` // percentage
PlayTimeChange float64 `json:"play_time_change"` // percentage
ListenersChange float64 `json:"listeners_change"` // percentage
CompletionChange float64 `json:"completion_change"` // percentage
RevenueChange float64 `json:"revenue_change"` // percentage
FollowersChange float64 `json:"followers_change"` // percentage
PlayTimeChange float64 `json:"play_time_change"` // percentage
}
// ComparePeriods compares analytics between two time periods (F397)
@ -271,14 +271,14 @@ func calcFloatPercentChange(previous, current float64) float64 {
// MarketplaceAnalytics holds marketplace analytics data for a creator
type MarketplaceAnalytics struct {
TotalViews int64 `json:"total_views"`
TotalSales int64 `json:"total_sales"`
TotalRevenue float64 `json:"total_revenue"`
OverallConversion float64 `json:"overall_conversion"` // percentage
TotalViews int64 `json:"total_views"`
TotalSales int64 `json:"total_sales"`
TotalRevenue float64 `json:"total_revenue"`
OverallConversion float64 `json:"overall_conversion"` // percentage
PlatformCommission float64 `json:"platform_commission"`
NetRevenue float64 `json:"net_revenue"`
Products []ProductAnalytics `json:"products"`
RevenueTimeline []MarketplaceRevenue `json:"revenue_timeline"`
NetRevenue float64 `json:"net_revenue"`
Products []ProductAnalytics `json:"products"`
RevenueTimeline []MarketplaceRevenue `json:"revenue_timeline"`
}
// ProductAnalytics holds analytics for a single product

View file

@ -14,9 +14,9 @@ import (
)
type CommentService struct {
db *gorm.DB
logger *zap.Logger
moderationService *CommentModerationService // v0.10.3 F201: optional keyword moderation
db *gorm.DB
logger *zap.Logger
moderationService *CommentModerationService // v0.10.3 F201: optional keyword moderation
}
func NewCommentService(db *gorm.DB, logger *zap.Logger) *CommentService {

View file

@ -30,9 +30,9 @@ type CreatorDashboardStats struct {
TotalPlays int64 `json:"total_plays"`
CompleteListens int64 `json:"complete_listens"`
UniqueListeners int64 `json:"unique_listeners"`
TotalPlayTime int64 `json:"total_play_time"` // seconds
AvgPlayDuration float64 `json:"avg_play_duration"` // seconds
AvgCompletionRate float64 `json:"avg_completion_rate"` // percentage
TotalPlayTime int64 `json:"total_play_time"` // seconds
AvgPlayDuration float64 `json:"avg_play_duration"` // seconds
AvgCompletionRate float64 `json:"avg_completion_rate"` // percentage
TotalTracks int64 `json:"total_tracks"`
TotalFollowers int64 `json:"total_followers"`
TotalRevenue float64 `json:"total_revenue"`
@ -178,20 +178,20 @@ func (s *CreatorAnalyticsService) GetPlayEvolution(ctx context.Context, creatorI
// SalesRecord represents a sales entry for downloads and purchases (F383)
type SalesRecord struct {
Date string `json:"date"`
TrackID string `json:"track_id"`
TrackTitle string `json:"track_title"`
ProductType string `json:"product_type"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
BuyerCount int64 `json:"buyer_count"`
Date string `json:"date"`
TrackID string `json:"track_id"`
TrackTitle string `json:"track_title"`
ProductType string `json:"product_type"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
BuyerCount int64 `json:"buyer_count"`
}
// SalesSummary aggregates sales data (F383)
type SalesSummary struct {
TotalRevenue float64 `json:"total_revenue"`
TotalSales int64 `json:"total_sales"`
RevenueByPeriod []RevenuePeriod `json:"revenue_by_period"`
TotalRevenue float64 `json:"total_revenue"`
TotalSales int64 `json:"total_sales"`
RevenueByPeriod []RevenuePeriod `json:"revenue_by_period"`
TopSellingTracks []TopSellingTrack `json:"top_selling_tracks"`
}
@ -204,10 +204,10 @@ type RevenuePeriod struct {
// TopSellingTrack represents a top-selling track
type TopSellingTrack struct {
TrackID string `json:"track_id"`
Title string `json:"title"`
Revenue float64 `json:"revenue"`
Sales int64 `json:"sales"`
TrackID string `json:"track_id"`
Title string `json:"title"`
Revenue float64 `json:"revenue"`
Sales int64 `json:"sales"`
}
// GetSalesSummary returns sales and download data for the creator (F383)
@ -293,8 +293,8 @@ func (s *CreatorAnalyticsService) GetSalesSummary(ctx context.Context, creatorID
// DiscoverySourceBreakdown represents how users discover the creator's tracks (F381)
type DiscoverySourceBreakdown struct {
Source string `json:"source"`
Count int64 `json:"count"`
Source string `json:"source"`
Count int64 `json:"count"`
Percentage float64 `json:"percentage"`
}
@ -329,7 +329,7 @@ func (s *CreatorAnalyticsService) GetDiscoverySources(ctx context.Context, creat
}
sources[i] = DiscoverySourceBreakdown{
Source: r.Source,
Count: r.Count,
Count: r.Count,
Percentage: pct,
}
}
@ -338,10 +338,10 @@ func (s *CreatorAnalyticsService) GetDiscoverySources(ctx context.Context, creat
// GeographicBreakdown represents geographic distribution of plays (F381)
type GeographicBreakdown struct {
CountryCode string `json:"country_code"`
Region string `json:"region,omitempty"`
PlayCount int64 `json:"play_count"`
UniqueListeners int64 `json:"unique_listeners"`
CountryCode string `json:"country_code"`
Region string `json:"region,omitempty"`
PlayCount int64 `json:"play_count"`
UniqueListeners int64 `json:"unique_listeners"`
Percentage float64 `json:"percentage"`
}
@ -392,8 +392,8 @@ func (s *CreatorAnalyticsService) GetGeographicBreakdown(ctx context.Context, cr
// AudienceProfile represents aggregated audience demographics (F384)
// Only shown when >= 10 unique listeners for privacy
type AudienceProfile struct {
TotalListeners int64 `json:"total_listeners"`
ListenersByGenre []GenreBreakdown `json:"listeners_by_genre"`
TotalListeners int64 `json:"total_listeners"`
ListenersByGenre []GenreBreakdown `json:"listeners_by_genre"`
TopListeningTimes []ListeningTimeSlot `json:"top_listening_times"`
}
@ -406,8 +406,8 @@ type GenreBreakdown struct {
// ListeningTimeSlot shows when the audience listens most
type ListeningTimeSlot struct {
Hour int `json:"hour"`
PlayCount int64 `json:"play_count"`
Hour int `json:"hour"`
PlayCount int64 `json:"play_count"`
}
// GetAudienceProfile returns aggregated audience data (F384)

View file

@ -59,11 +59,11 @@ func setupTestCreatorAnalyticsService(t *testing.T) (*CreatorAnalyticsService, *
// Create listener users and playback data
for i := 0; i < 15; i++ {
listener := &models.User{
Username: "listener" + string(rune('a'+i)),
Email: "listener" + string(rune('a'+i)) + "@test.com",
Username: "listener" + string(rune('a'+i)),
Email: "listener" + string(rune('a'+i)) + "@test.com",
PasswordHash: "hash",
Slug: "listener" + string(rune('a'+i)),
IsActive: true,
Slug: "listener" + string(rune('a'+i)),
IsActive: true,
}
require.NoError(t, db.Create(listener).Error)
@ -213,11 +213,11 @@ func TestCreatorAnalyticsService_GetAudienceProfile_InsufficientListeners(t *tes
// Only create 3 listeners (< 10 minimum)
for i := 0; i < 3; i++ {
listener := &models.User{
Username: "few" + string(rune('a'+i)),
Email: "few" + string(rune('a'+i)) + "@test.com",
Username: "few" + string(rune('a'+i)),
Email: "few" + string(rune('a'+i)) + "@test.com",
PasswordHash: "hash",
Slug: "few" + string(rune('a'+i)),
IsActive: true,
Slug: "few" + string(rune('a'+i)),
IsActive: true,
}
require.NoError(t, db.Create(listener).Error)
pa := &models.PlaybackAnalytics{

View file

@ -29,45 +29,45 @@ func NewDataExportService(db *gorm.DB, logger *zap.Logger) *DataExportService {
// MessageExport représente un message chat exporté (v0.10.8 F065)
type MessageExport struct {
ID uuid.UUID `json:"id"`
RoomID uuid.UUID `json:"room_id"`
Content string `json:"content"`
MessageType string `json:"message_type"`
ReplyToID *uuid.UUID `json:"reply_to_id,omitempty"`
IsEdited bool `json:"is_edited"`
IsPinned bool `json:"is_pinned"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id"`
RoomID uuid.UUID `json:"room_id"`
Content string `json:"content"`
MessageType string `json:"message_type"`
ReplyToID *uuid.UUID `json:"reply_to_id,omitempty"`
IsEdited bool `json:"is_edited"`
IsPinned bool `json:"is_pinned"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PlaybackHistoryExport représente un historique d'écoute exporté (v0.10.8 F065)
type PlaybackHistoryExport struct {
ID uuid.UUID `json:"id"`
TrackID uuid.UUID `json:"track_id"`
PlayedDuration int `json:"played_duration"`
CompletionPercentage int `json:"completion_percentage"`
Source string `json:"source,omitempty"`
DeviceType string `json:"device_type,omitempty"`
PlayedAt time.Time `json:"played_at"`
ID uuid.UUID `json:"id"`
TrackID uuid.UUID `json:"track_id"`
PlayedDuration int `json:"played_duration"`
CompletionPercentage int `json:"completion_percentage"`
Source string `json:"source,omitempty"`
DeviceType string `json:"device_type,omitempty"`
PlayedAt time.Time `json:"played_at"`
}
// UserDataExport représente toutes les données d'un utilisateur à exporter
type UserDataExport struct {
UserID uuid.UUID `json:"user_id"`
ExportedAt time.Time `json:"exported_at"`
Profile *UserProfileExport `json:"profile"`
Settings *UserSettingsExport `json:"settings"`
Tracks []TrackExport `json:"tracks"`
Playlists []PlaylistExport `json:"playlists"`
Comments []CommentExport `json:"comments"`
Likes []LikeExport `json:"likes"`
Messages []MessageExport `json:"messages,omitempty"`
UserID uuid.UUID `json:"user_id"`
ExportedAt time.Time `json:"exported_at"`
Profile *UserProfileExport `json:"profile"`
Settings *UserSettingsExport `json:"settings"`
Tracks []TrackExport `json:"tracks"`
Playlists []PlaylistExport `json:"playlists"`
Comments []CommentExport `json:"comments"`
Likes []LikeExport `json:"likes"`
Messages []MessageExport `json:"messages,omitempty"`
PlaybackHistory []PlaybackHistoryExport `json:"playback_history,omitempty"`
Analytics []AnalyticsExport `json:"analytics"`
FederatedIDs []FederatedIDExport `json:"federated_identities"`
Roles []RoleExport `json:"roles"`
CloudFiles []CloudFileExport `json:"cloud_files,omitempty"`
Gear []GearExport `json:"gear,omitempty"`
Analytics []AnalyticsExport `json:"analytics"`
FederatedIDs []FederatedIDExport `json:"federated_identities"`
Roles []RoleExport `json:"roles"`
CloudFiles []CloudFileExport `json:"cloud_files,omitempty"`
Gear []GearExport `json:"gear,omitempty"`
}
// CloudFileExport represents cloud file metadata for export
@ -401,13 +401,13 @@ func (s *DataExportService) ExportUserData(ctx context.Context, userID uuid.UUID
// 11. Récupérer l'historique d'écoute (v0.10.8 F065) - playback_history ou playback_analytics
var playbackRows []struct {
ID uuid.UUID
TrackID uuid.UUID
PlayedDuration int
ID uuid.UUID
TrackID uuid.UUID
PlayedDuration int
CompletionPercentage int
Source *string
DeviceType *string
PlayedAt time.Time
Source *string
DeviceType *string
PlayedAt time.Time
}
if err := s.db.WithContext(ctx).Raw(`
SELECT id, track_id, played_duration, completion_percentage, source, device_type, played_at
@ -423,13 +423,13 @@ func (s *DataExportService) ExportUserData(ctx context.Context, userID uuid.UUID
dev = *r.DeviceType
}
export.PlaybackHistory[i] = PlaybackHistoryExport{
ID: r.ID,
TrackID: r.TrackID,
PlayedDuration: r.PlayedDuration,
ID: r.ID,
TrackID: r.TrackID,
PlayedDuration: r.PlayedDuration,
CompletionPercentage: r.CompletionPercentage,
Source: src,
DeviceType: dev,
PlayedAt: r.PlayedAt,
Source: src,
DeviceType: dev,
PlayedAt: r.PlayedAt,
}
}
}
@ -440,11 +440,11 @@ func (s *DataExportService) ExportUserData(ctx context.Context, userID uuid.UUID
export.PlaybackHistory = make([]PlaybackHistoryExport, len(analytics))
for i, a := range analytics {
export.PlaybackHistory[i] = PlaybackHistoryExport{
ID: a.ID,
TrackID: a.TrackID,
PlayedDuration: a.PlayTime,
ID: a.ID,
TrackID: a.TrackID,
PlayedDuration: a.PlayTime,
CompletionPercentage: int(a.CompletionRate),
PlayedAt: a.StartedAt,
PlayedAt: a.StartedAt,
}
}
}

View file

@ -15,9 +15,9 @@ import (
// If a MaxMind database is not available, falls back to a no-op resolver.
// To enable MaxMind, set GEOIP_DB_PATH to the path of a GeoLite2-City.mmdb file.
type GeoIPService struct {
logger *zap.Logger
dbPath string
mu sync.RWMutex
logger *zap.Logger
dbPath string
mu sync.RWMutex
// In production, this would use github.com/oschwald/maxminddb-golang
// For now, we implement a basic lookup that can be extended
enabled bool

View file

@ -25,9 +25,9 @@ type PlaylistServiceInterface interface {
UpdateCollaboratorPermission(ctx context.Context, playlistID, userID, collaboratorUserID uuid.UUID, permission models.PlaylistPermission) error
GetCollaborators(ctx context.Context, playlistID, userID uuid.UUID) ([]*models.PlaylistCollaborator, error)
CreateShareLink(ctx context.Context, playlistID, userID uuid.UUID, expiresAt *time.Time) (*models.PlaylistShareLink, error)
GetPlaylistByShareToken(ctx context.Context, token string) (*models.Playlist, error) // v0.10.4 F143
GetPlaylistByShareToken(ctx context.Context, token string) (*models.Playlist, error) // v0.10.4 F143
ImportPlaylistWithTracks(ctx context.Context, userID uuid.UUID, title, description string, isPublic bool, trackIDs []uuid.UUID) (*models.Playlist, error) // v0.10.4 F145
GetOrCreateFavorisPlaylist(ctx context.Context, userID uuid.UUID) (*models.Playlist, error) // v0.10.4 F136
GetOrCreateFavorisPlaylist(ctx context.Context, userID uuid.UUID) (*models.Playlist, error) // v0.10.4 F136
FollowPlaylist(ctx context.Context, playlistID, userID uuid.UUID) error
UnfollowPlaylist(ctx context.Context, playlistID, userID uuid.UUID) error
CheckPermission(ctx context.Context, playlistID, userID uuid.UUID, permission models.PlaylistPermission) (bool, error)

View file

@ -47,10 +47,10 @@ func NewKYCService(db *gorm.DB, secretKey string, logger *zap.Logger) *KYCServic
// KYCSessionResponse contains the verification session info for the frontend
type KYCSessionResponse struct {
SessionID string `json:"session_id"`
SessionID string `json:"session_id"`
ClientSecret string `json:"client_secret"`
Status string `json:"status"`
URL string `json:"url"`
Status string `json:"status"`
URL string `json:"url"`
}
// CreateVerificationSession creates a Stripe Identity VerificationSession for a seller

View file

@ -21,8 +21,8 @@ type LoginHistoryEntry struct {
UserAgent string `json:"user_agent"`
Success bool `json:"success"`
Reason string `json:"reason,omitempty"`
Country string `json:"country,omitempty"` // F025: ISO 3166-1 alpha-2 country code
City string `json:"city,omitempty"` // F025: City name from GeoIP
Country string `json:"country,omitempty"` // F025: ISO 3166-1 alpha-2 country code
City string `json:"city,omitempty"` // F025: City name from GeoIP
CreatedAt time.Time `json:"created_at"`
}
@ -34,9 +34,9 @@ type GeoIPResolver interface {
// LoginHistoryService tracks login attempts for security auditing.
type LoginHistoryService struct {
db *database.Database
logger *zap.Logger
geoIP GeoIPResolver // F025: optional GeoIP resolver
db *database.Database
logger *zap.Logger
geoIP GeoIPResolver // F025: optional GeoIP resolver
}
// NewLoginHistoryService creates a login history service.

View file

@ -32,23 +32,23 @@ func NewModerationService(db *gorm.DB, logger *zap.Logger) *ModerationService {
// ModerationQueueItem represents an item in the moderation queue
type ModerationQueueItem struct {
ID string `json:"id"`
ReporterID string `json:"reporter_id"`
ReportedUserID *string `json:"reported_user_id,omitempty"`
ContentType string `json:"content_type"`
ContentID *string `json:"content_id,omitempty"`
Reason string `json:"reason"`
Category string `json:"category"`
Priority string `json:"priority"`
Status string `json:"status"`
AssignedTo *string `json:"assigned_to,omitempty"`
ResolutionNote string `json:"resolution_note,omitempty"`
ResolutionAction string `json:"resolution_action,omitempty"`
ResolvedBy *string `json:"resolved_by,omitempty"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
ReporterName string `json:"reporter_name,omitempty"`
ReportedName string `json:"reported_name,omitempty"`
ID string `json:"id"`
ReporterID string `json:"reporter_id"`
ReportedUserID *string `json:"reported_user_id,omitempty"`
ContentType string `json:"content_type"`
ContentID *string `json:"content_id,omitempty"`
Reason string `json:"reason"`
Category string `json:"category"`
Priority string `json:"priority"`
Status string `json:"status"`
AssignedTo *string `json:"assigned_to,omitempty"`
ResolutionNote string `json:"resolution_note,omitempty"`
ResolutionAction string `json:"resolution_action,omitempty"`
ResolvedBy *string `json:"resolved_by,omitempty"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
ReporterName string `json:"reporter_name,omitempty"`
ReportedName string `json:"reported_name,omitempty"`
}
// ModerationQueueParams holds filtering parameters for the queue
@ -117,9 +117,9 @@ func (s *ModerationService) GetModerationQueue(ctx context.Context, params Moder
// ModerationAction represents an action taken on a report
type ModerationAction struct {
Action string `json:"action" binding:"required"` // approve, reject, ban_temp, ban_perm, warn, dismiss
Reason string `json:"reason"`
BanDurationDays int `json:"ban_duration_days,omitempty"` // for ban_temp
Action string `json:"action" binding:"required"` // approve, reject, ban_temp, ban_perm, warn, dismiss
Reason string `json:"reason"`
BanDurationDays int `json:"ban_duration_days,omitempty"` // for ban_temp
}
// ProcessReport processes a moderation action on a report (F411)
@ -154,11 +154,11 @@ func (s *ModerationService) ProcessReport(ctx context.Context, reportID uuid.UUI
// Update the report
if err := tx.Table("reports").Where("id = ?", reportID).Updates(map[string]interface{}{
"status": status,
"resolved_by": moderatorID,
"resolved_at": now,
"resolution_note": action.Reason,
"resolved_by": moderatorID,
"resolved_at": now,
"resolution_note": action.Reason,
"resolution_action": action.Action,
"updated_at": now,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("failed to update report: %w", err)
}
@ -231,7 +231,7 @@ type EnhancedReportRequest struct {
ContentType string `json:"content_type" binding:"required"` // track, comment, profile, message
ContentID *uuid.UUID `json:"content_id"`
ReportedUserID *uuid.UUID `json:"reported_user_id"`
Category string `json:"category" binding:"required"` // spam, offensive, copyright, fake, other
Category string `json:"category" binding:"required"` // spam, offensive, copyright, fake, other
Reason string `json:"reason" binding:"required"`
}
@ -275,9 +275,9 @@ func (s *ModerationService) CreateEnhancedReport(ctx context.Context, reporterID
// SpamCheckResult represents the result of a spam check
type SpamCheckResult struct {
IsSpam bool `json:"is_spam"`
Detections []SpamDetection `json:"detections"`
ActionTaken string `json:"action_taken"` // "none", "flagged", "auto_hidden"
IsSpam bool `json:"is_spam"`
Detections []SpamDetection `json:"detections"`
ActionTaken string `json:"action_taken"` // "none", "flagged", "auto_hidden"
}
// SpamDetection represents a single spam detection
@ -537,24 +537,24 @@ func (s *ModerationService) ReviewFingerprint(ctx context.Context, trackID uuid.
// StrikeInfo represents a strike on a user's account
type StrikeInfo struct {
ID string `json:"id"`
Reason string `json:"reason"`
Severity string `json:"severity"`
IsActive bool `json:"is_active"`
Appealed bool `json:"appealed"`
AppealResult *string `json:"appeal_result,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
ID string `json:"id"`
Reason string `json:"reason"`
Severity string `json:"severity"`
IsActive bool `json:"is_active"`
Appealed bool `json:"appealed"`
AppealResult *string `json:"appeal_result,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// UserStrikeSummary summarizes a user's strike history
type UserStrikeSummary struct {
UserID string `json:"user_id"`
ActiveStrikes int `json:"active_strikes"`
TotalStrikes int `json:"total_strikes"`
IsSuspended bool `json:"is_suspended"`
SuspendedUntil *time.Time `json:"suspended_until,omitempty"`
Strikes []StrikeInfo `json:"strikes"`
UserID string `json:"user_id"`
ActiveStrikes int `json:"active_strikes"`
TotalStrikes int `json:"total_strikes"`
IsSuspended bool `json:"is_suspended"`
SuspendedUntil *time.Time `json:"suspended_until,omitempty"`
Strikes []StrikeInfo `json:"strikes"`
}
// GetUserStrikes returns strike summary for a user (F415)
@ -699,11 +699,11 @@ func (s *ModerationService) GetPendingAppeals(ctx context.Context, limit, offset
result := make([]StrikeInfo, len(rows))
for i, r := range rows {
result[i] = StrikeInfo{
ID: r.ID.String(),
Reason: r.Reason,
Severity: r.Severity,
IsActive: r.IsActive,
Appealed: r.Appealed,
ID: r.ID.String(),
Reason: r.Reason,
Severity: r.Severity,
IsActive: r.IsActive,
Appealed: r.Appealed,
CreatedAt: r.CreatedAt,
}
}

View file

@ -14,10 +14,10 @@ import (
// digestTrackItem represents a track for the weekly digest (new releases from followed artists)
type digestTrackItem struct {
TrackID string
Title string
Artist string
Link string
TrackID string
Title string
Artist string
Link string
}
// NotificationDigestWorker sends weekly digest emails to users with weekly_digest_enabled
@ -31,10 +31,10 @@ type NotificationDigestWorker struct {
// NewNotificationDigestWorker creates a new digest worker
func NewNotificationDigestWorker(db *gorm.DB, jobEnqueuer JobEnqueuer, logger *zap.Logger) *NotificationDigestWorker {
return &NotificationDigestWorker{
db: db,
db: db,
jobEnqueuer: jobEnqueuer,
logger: logger,
interval: 24 * time.Hour, // Check daily, process on Sunday
logger: logger,
interval: 24 * time.Hour, // Check daily, process on Sunday
}
}
@ -67,8 +67,8 @@ func (w *NotificationDigestWorker) Start(ctx context.Context) {
func (w *NotificationDigestWorker) runDigest(ctx context.Context) error {
type userWithEmail struct {
UserID uuid.UUID
Email string
UserID uuid.UUID
Email string
Username string
}
@ -139,11 +139,11 @@ func (w *NotificationDigestWorker) runDigest(ctx context.Context) error {
}
templateData := map[string]interface{}{
"Username": u.Username,
"Tracks": trackMaps,
"BaseURL": baseURL,
"FeedURL": baseURL + "/feed",
"TrackCount": len(tracks),
"Username": u.Username,
"Tracks": trackMaps,
"BaseURL": baseURL,
"FeedURL": baseURL + "/feed",
"TrackCount": len(tracks),
}
w.jobEnqueuer.EnqueueEmailJobWithTemplate(

View file

@ -25,7 +25,7 @@ type NotificationWSNotifier interface {
type NotificationService struct {
db *database.Database
logger *zap.Logger
pushService *PushService // optional, for N1.2 Web Push
pushService *PushService // optional, for N1.2 Web Push
wsNotifier NotificationWSNotifier // optional, for F551 real-time
}
@ -403,15 +403,15 @@ func (ns *NotificationService) DeleteAllNotifications(userID uuid.UUID) error {
// NotificationPrefs represents notification preferences (N1.3, F553, F552)
type NotificationPrefs struct {
PushFollow bool `json:"push_follow"`
PushLike bool `json:"push_like"`
PushComment bool `json:"push_comment"`
PushMessage bool `json:"push_message"`
PushMention bool `json:"push_mention"`
QuietHoursEnabled bool `json:"quiet_hours_enabled"`
QuietHoursStart string `json:"quiet_hours_start"` // "22:00"
QuietHoursEnd string `json:"quiet_hours_end"` // "08:00"
WeeklyDigestEnabled bool `json:"weekly_digest_enabled"`
PushFollow bool `json:"push_follow"`
PushLike bool `json:"push_like"`
PushComment bool `json:"push_comment"`
PushMessage bool `json:"push_message"`
PushMention bool `json:"push_mention"`
QuietHoursEnabled bool `json:"quiet_hours_enabled"`
QuietHoursStart string `json:"quiet_hours_start"` // "22:00"
QuietHoursEnd string `json:"quiet_hours_end"` // "08:00"
WeeklyDigestEnabled bool `json:"weekly_digest_enabled"`
}
// GetPreferences returns notification preferences for a user

View file

@ -23,11 +23,11 @@ const bcryptCost = 12
// PasswordService handles password operations
type PasswordService struct {
db *database.Database
logger *zap.Logger
passwordValidator *validators.PasswordValidator
historyService *PasswordHistoryService
expirationDays int // F016: 0 = disabled, >0 = password expires after N days
db *database.Database
logger *zap.Logger
passwordValidator *validators.PasswordValidator
historyService *PasswordHistoryService
expirationDays int // F016: 0 = disabled, >0 = password expires after N days
}
// PasswordResetToken represents a password reset token

View file

@ -294,14 +294,14 @@ func TestPlaybackAnalyticsService_GetTrackStats(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, int64(5), stats.TotalSessions)
assert.Equal(t, int64(600), stats.TotalPlayTime) // 120+180+90+100+110
assert.Equal(t, 120.0, stats.AveragePlayTime) // 600 / 5
assert.Equal(t, int64(10), stats.TotalPauses) // 2+1+3+2+2
assert.Equal(t, 2.0, stats.AveragePauses) // 10 / 5
assert.Equal(t, int64(15), stats.TotalSeeks) // 3+1+5+2+4
assert.Equal(t, 3.0, stats.AverageSeeks) // 15 / 5
assert.InDelta(t, 78.33, stats.AverageCompletion, 0.1) // (66.67+100+50+80+95) / 5
assert.InDelta(t, 40.0, stats.CompletionRate, 0.01) // 2 sessions with >= 90% / 5
assert.Equal(t, int64(600), stats.TotalPlayTime) // 120+180+90+100+110
assert.Equal(t, 120.0, stats.AveragePlayTime) // 600 / 5
assert.Equal(t, int64(10), stats.TotalPauses) // 2+1+3+2+2
assert.Equal(t, 2.0, stats.AveragePauses) // 10 / 5
assert.Equal(t, int64(15), stats.TotalSeeks) // 3+1+5+2+4
assert.Equal(t, 3.0, stats.AverageSeeks) // 15 / 5
assert.InDelta(t, 78.33, stats.AverageCompletion, 0.1) // (66.67+100+50+80+95) / 5
assert.InDelta(t, 40.0, stats.CompletionRate, 0.01) // 2 sessions with >= 90% / 5
}
func TestPlaybackAnalyticsService_GetTrackStats_NoSessions(t *testing.T) {

View file

@ -973,10 +973,10 @@ func (s *PlaylistService) GetOrCreateFavorisPlaylist(ctx context.Context, userID
}
// Create Favoris playlist
playlist := &models.Playlist{
UserID: userID,
Title: "Favoris",
UserID: userID,
Title: "Favoris",
IsDefaultFavorites: true,
IsPublic: false,
IsPublic: false,
}
if err := s.playlistRepo.Create(ctx, playlist); err != nil {
return nil, fmt.Errorf("failed to create favoris playlist: %w", err)

View file

@ -597,8 +597,8 @@ type RoomMemberResponse struct {
// RoomMembersResponse for GET members (v0.9.7) includes my_role for UI (show kick button)
type RoomMembersResponse struct {
Members []RoomMemberResponse `json:"members"`
MyRole string `json:"my_role"` // owner, admin, member
Members []RoomMemberResponse `json:"members"`
MyRole string `json:"my_role"` // owner, admin, member
}
// UpdateMemberRoleRequest for PATCH members (v0.9.7)

View file

@ -14,9 +14,9 @@ import (
)
const (
smsCodeLength = 6
smsCodeExpiry = 5 * time.Minute
smsRateLimit = 3 // max SMS per user per hour
smsCodeLength = 6
smsCodeExpiry = 5 * time.Minute
smsRateLimit = 3 // max SMS per user per hour
)
// SMSProvider defines the interface for sending SMS messages.

View file

@ -32,8 +32,8 @@ type UploadConfig struct {
// Limites de taille
MaxAudioSize int64 // 100MB
MaxImageSize int64 // 10MB
MaxVideoSize int64 // 500MB
MaxFileSize int64 // 50MB (chat attachments, PDF)
MaxVideoSize int64 // 500MB
MaxFileSize int64 // 50MB (chat attachments, PDF)
// Types MIME autorisés
AllowedAudioTypes []string

View file

@ -33,8 +33,8 @@ type UserRepository interface {
// UserService gère les opérations sur les utilisateurs
type UserService struct {
userRepo UserRepository
db *gorm.DB // Optional DB access for settings
cacheService *CacheService // BE-SVC-001: Cache service for user profiles
db *gorm.DB // Optional DB access for settings
cacheService *CacheService // BE-SVC-001: Cache service for user profiles
socialService *SocialService // v0.10.0 F187: Optional, for is_following in profiles
uploadDir string
}

View file

@ -17,11 +17,11 @@ import (
// WebAuthnChallenge represents an in-progress WebAuthn ceremony.
// Challenges are stored ephemerally (Redis or in-memory) with a short TTL.
type WebAuthnChallenge struct {
Challenge string `json:"challenge"`
UserID uuid.UUID `json:"user_id"`
Type string `json:"type"` // "registration" or "authentication"
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
Challenge string `json:"challenge"`
UserID uuid.UUID `json:"user_id"`
Type string `json:"type"` // "registration" or "authentication"
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
}
// WebAuthnService handles FIDO2/WebAuthn passkey operations.
@ -102,9 +102,9 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID uuid.UUI
{"type": "public-key", "alg": -7}, // ES256
{"type": "public-key", "alg": -257}, // RS256
},
"timeout": 60000,
"attestation": "none",
"excludeCredentials": excludeCredentials,
"timeout": 60000,
"attestation": "none",
"excludeCredentials": excludeCredentials,
"authenticatorSelection": map[string]interface{}{
"authenticatorAttachment": "platform",
"residentKey": "preferred",

View file

@ -32,25 +32,25 @@ const (
// Outgoing message types (server -> client)
const (
TypeNewMessage = "NewMessage"
TypeMessageRead = "MessageRead"
TypeMessageDelivered = "MessageDelivered"
TypeUserTyping = "UserTyping"
TypeMessageEdited = "MessageEdited"
TypeMessageDeleted = "MessageDeleted"
TypeReactionAdded = "ReactionAdded"
TypeReactionRemoved = "ReactionRemoved"
TypeHistoryChunk = "HistoryChunk"
TypeSearchResults = "SearchResults"
TypeSyncChunk = "SyncChunk"
TypeActionConfirmed = "ActionConfirmed"
TypeError = "Error"
TypeOutCallOffer = "CallOffer"
TypeOutCallAnswer = "CallAnswer"
TypeOutICECandidate = "ICECandidate"
TypeOutCallHangup = "CallHangup"
TypeCallRejected = "CallRejected"
TypePong = "Pong"
TypeNewMessage = "NewMessage"
TypeMessageRead = "MessageRead"
TypeMessageDelivered = "MessageDelivered"
TypeUserTyping = "UserTyping"
TypeMessageEdited = "MessageEdited"
TypeMessageDeleted = "MessageDeleted"
TypeReactionAdded = "ReactionAdded"
TypeReactionRemoved = "ReactionRemoved"
TypeHistoryChunk = "HistoryChunk"
TypeSearchResults = "SearchResults"
TypeSyncChunk = "SyncChunk"
TypeActionConfirmed = "ActionConfirmed"
TypeError = "Error"
TypeOutCallOffer = "CallOffer"
TypeOutCallAnswer = "CallAnswer"
TypeOutICECandidate = "ICECandidate"
TypeOutCallHangup = "CallHangup"
TypeCallRejected = "CallRejected"
TypePong = "Pong"
TypeMentionedInMessage = "MentionedInMessage"
)
@ -325,8 +325,8 @@ func NewMentionedInMessageResponse(conversationID, messageID, authorID uuid.UUID
preview = preview[:97] + "..."
}
return map[string]interface{}{
"type": TypeMentionedInMessage,
"conversation_id": conversationID,
"type": TypeMentionedInMessage,
"conversation_id": conversationID,
"message_id": messageID,
"author_id": authorID,
"content_preview": preview,

View file

@ -112,9 +112,9 @@ func (h *Hub) UpdateHostState(conn *Conn, positionMs, clientTimestampMs int64) {
sessionID := conn.SessionID
h.mu.Lock()
state := &HostState{
PositionMs: positionMs,
PositionMs: positionMs,
ClientTimestampMs: clientTimestampMs,
ServerTimestampMs: time.Now().UnixMilli(),
ServerTimestampMs: time.Now().UnixMilli(),
}
h.hostState[sessionID] = state
@ -140,9 +140,9 @@ func (h *Hub) UpdateHostState(conn *Conn, positionMs, clientTimestampMs int64) {
}
}
conn.LastState = &HostState{
PositionMs: state.PositionMs,
ClientTimestampMs: state.ClientTimestampMs,
ServerTimestampMs: state.ServerTimestampMs,
PositionMs: state.PositionMs,
ClientTimestampMs: state.ClientTimestampMs,
ServerTimestampMs: state.ServerTimestampMs,
}
}
}
@ -151,9 +151,9 @@ func (h *Hub) UpdateHostState(conn *Conn, positionMs, clientTimestampMs int64) {
func (h *Hub) UpdateListenerState(conn *Conn, positionMs, clientTimestampMs int64) {
h.mu.Lock()
conn.LastState = &HostState{
PositionMs: positionMs,
PositionMs: positionMs,
ClientTimestampMs: clientTimestampMs,
ServerTimestampMs: time.Now().UnixMilli(),
ServerTimestampMs: time.Now().UnixMilli(),
}
state := h.hostState[conn.SessionID]

View file

@ -15,8 +15,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@ -94,21 +94,21 @@ func setupContractRouter(t *testing.T, db *gorm.DB, marketSvc *marketplace.Servi
vezaDB := &database.Database{DB: sqlDB, GormDB: db, Logger: zap.NewNop()}
cfg := &config.Config{
HyperswitchWebhookSecret: "test-secret",
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
HyperswitchWebhookSecret: "test-secret",
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
AuthRateLimitLoginAttempts: 100,
AuthRateLimitLoginWindow: 15,
AuthRateLimitLoginWindow: 15,
MarketplaceServiceOverride: marketSvc,
}
require.NoError(t, cfg.InitServicesForTest())
@ -158,7 +158,7 @@ func TestContract_Register(t *testing.T) {
"email": "contract-" + uuid.New().String() + "@test.com",
"username": "contractuser",
"password": "ValidPassword123!",
"password_confirmation": "ValidPassword123!",
"password_confirmation": "ValidPassword123!",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")

View file

@ -127,9 +127,9 @@ func TestGDPR_ExportAndDeletion_E2E(t *testing.T) {
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"message": "Account scheduled for deletion",
"anonymized": true,
"recovery_days": 30,
"message": "Account scheduled for deletion",
"anonymized": true,
"recovery_days": 30,
},
})
})

View file

@ -61,8 +61,8 @@ func setupOAuthGitHubTestRouter(t *testing.T) (*gin.Engine, *services.OAuthServi
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": "mock_github_access_token",
"token_type": "Bearer",
"expires_in": 3600,
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mock_github_refresh_token",
})
return

View file

@ -57,8 +57,8 @@ func setupOAuthGoogleTestRouter(t *testing.T) (*gin.Engine, *services.OAuthServi
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": "mock_access_token_123",
"token_type": "Bearer",
"expires_in": 3600,
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mock_refresh_token",
})
return

View file

@ -124,21 +124,21 @@ func setupPaymentFlowRouter(t *testing.T, db *gorm.DB, marketService *marketplac
cfg := &config.Config{
HyperswitchWebhookSecret: "test-secret-at-least-32-chars-long",
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
RateLimitWindow: 60,
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
RateLimitWindow: 60,
AuthRateLimitLoginAttempts: 10,
AuthRateLimitLoginWindow: 15,
AuthRateLimitLoginWindow: 15,
MarketplaceServiceOverride: marketService,
}
require.NoError(t, cfg.InitServicesForTest())

View file

@ -66,23 +66,23 @@ func setupRefundFlowRouter(t *testing.T, db *gorm.DB, marketService *marketplace
cfg := &config.Config{
HyperswitchWebhookSecret: "test-secret",
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
RateLimitWindow: 60,
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
RateLimitWindow: 60,
AuthRateLimitLoginAttempts: 10,
AuthRateLimitLoginWindow: 15,
AuthRateLimitLoginWindow: 15,
MarketplaceServiceOverride: marketService,
AuthMiddlewareOverride: &testAuthMiddleware{},
AuthMiddlewareOverride: &testAuthMiddleware{},
}
require.NoError(t, cfg.InitServicesForTest())
require.NoError(t, cfg.InitMiddlewaresForTest())

View file

@ -97,20 +97,20 @@ func TestTrackQuotaEndpoint_GetQuota(t *testing.T) {
// Test 2: Create some tracks and verify quota updates
track1 := &models.Track{
ID: uuid.New(),
Title: "Test Track 1",
UserID: userID,
FileSize: 5 * 1024 * 1024, // 5MB
IsPublic: true,
ID: uuid.New(),
Title: "Test Track 1",
UserID: userID,
FileSize: 5 * 1024 * 1024, // 5MB
IsPublic: true,
}
require.NoError(t, db.Create(track1).Error)
track2 := &models.Track{
ID: uuid.New(),
Title: "Test Track 2",
UserID: userID,
FileSize: 10 * 1024 * 1024, // 10MB
IsPublic: true,
ID: uuid.New(),
Title: "Test Track 2",
UserID: userID,
FileSize: 10 * 1024 * 1024, // 10MB
IsPublic: true,
}
require.NoError(t, db.Create(track2).Error)

View file

@ -48,21 +48,21 @@ func setupWebhookIdempotencyRouter(t *testing.T, db *gorm.DB, marketService *mar
cfg := &config.Config{
HyperswitchWebhookSecret: "test-secret-at-least-32-chars-long",
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
RateLimitWindow: 60,
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
RateLimitWindow: 60,
AuthRateLimitLoginAttempts: 10,
AuthRateLimitLoginWindow: 15,
AuthRateLimitLoginWindow: 15,
MarketplaceServiceOverride: marketService,
}
require.NoError(t, cfg.InitServicesForTest())

View file

@ -55,22 +55,22 @@ func setupWebhookSecurityRouter(t *testing.T, webhookSecret string) *gin.Engine
}
cfg := &config.Config{
HyperswitchWebhookSecret: webhookSecret,
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
RateLimitWindow: 60,
HyperswitchWebhookSecret: webhookSecret,
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
RateLimitWindow: 60,
AuthRateLimitLoginAttempts: 10,
AuthRateLimitLoginWindow: 15,
AuthRateLimitLoginWindow: 15,
}
require.NoError(t, cfg.InitServicesForTest())
require.NoError(t, cfg.InitMiddlewaresForTest())

View file

@ -97,10 +97,10 @@ func setupLoadTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, *redis.Client, fu
// Setup UploadValidator (disable ClamAV for load tests)
uploadConfig := &services.UploadConfig{
ClamAVEnabled: false,
ClamAVRequired: false,
MaxAudioSize: 100 * 1024 * 1024, // 100MB
AllowedAudioTypes: []string{"audio/mpeg", "audio/flac", "audio/wav", "audio/ogg"},
ClamAVEnabled: false,
ClamAVRequired: false,
MaxAudioSize: 100 * 1024 * 1024, // 100MB
AllowedAudioTypes: []string{"audio/mpeg", "audio/flac", "audio/wav", "audio/ogg"},
}
uploadValidator, err := services.NewUploadValidator(uploadConfig, logger)
require.NoError(t, err)
@ -517,7 +517,7 @@ func TestLoad_UploadStatusPolling(t *testing.T) {
ID: trackID,
UserID: userID,
Title: "Test Track",
Status: models.TrackStatusUploading,
Status: models.TrackStatusUploading,
}
db.Create(track)
@ -561,4 +561,3 @@ func TestLoad_UploadStatusPolling(t *testing.T) {
assert.Equal(t, concurrentPolls, successCount, "All status polls should succeed")
assert.Less(t, totalTime, 5*time.Second, "Status polling should complete quickly")
}

View file

@ -305,7 +305,7 @@ func createTestTrack(t *testing.T, db *gorm.DB, userID uuid.UUID, title string)
UserID: userID,
Title: title,
Artist: "Test Artist",
FilePath: fmt.Sprintf("/tracks/%s.mp3", uuid.New().String()),
FilePath: fmt.Sprintf("/tracks/%s.mp3", uuid.New().String()),
Format: "mp3",
FileSize: 5 * 1024 * 1024,
Duration: 180,
@ -790,4 +790,3 @@ func TestMarketplaceFlow_CreateOrder_InactiveProduct(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
}

View file

@ -33,27 +33,27 @@ import (
// PerformanceThresholds définit les seuils de performance acceptables
var PerformanceThresholds = struct {
HealthCheck time.Duration // Health check should be very fast
AuthLogin time.Duration // Authentication should be fast
AuthRegister time.Duration // Registration can be slightly slower
TrackList time.Duration // List operations should be fast
TrackGet time.Duration // Get single item should be fast
TrackCreate time.Duration // Create operations can be slower
PlaylistList time.Duration // List operations should be fast
PlaylistCreate time.Duration // Create operations can be slower
UserList time.Duration // List operations should be fast
UserGet time.Duration // Get single item should be fast
HealthCheck time.Duration // Health check should be very fast
AuthLogin time.Duration // Authentication should be fast
AuthRegister time.Duration // Registration can be slightly slower
TrackList time.Duration // List operations should be fast
TrackGet time.Duration // Get single item should be fast
TrackCreate time.Duration // Create operations can be slower
PlaylistList time.Duration // List operations should be fast
PlaylistCreate time.Duration // Create operations can be slower
UserList time.Duration // List operations should be fast
UserGet time.Duration // Get single item should be fast
}{
HealthCheck: 10 * time.Millisecond,
AuthLogin: 100 * time.Millisecond,
AuthRegister: 200 * time.Millisecond,
TrackList: 50 * time.Millisecond,
TrackGet: 30 * time.Millisecond,
TrackCreate: 500 * time.Millisecond,
PlaylistList: 50 * time.Millisecond,
PlaylistCreate: 200 * time.Millisecond,
UserList: 50 * time.Millisecond,
UserGet: 30 * time.Millisecond,
HealthCheck: 10 * time.Millisecond,
AuthLogin: 100 * time.Millisecond,
AuthRegister: 200 * time.Millisecond,
TrackList: 50 * time.Millisecond,
TrackGet: 30 * time.Millisecond,
TrackCreate: 500 * time.Millisecond,
PlaylistList: 50 * time.Millisecond,
PlaylistCreate: 200 * time.Millisecond,
UserList: 50 * time.Millisecond,
UserGet: 30 * time.Millisecond,
}
// setupPerformanceTestRouter crée un router de test pour les tests de performance
@ -250,11 +250,11 @@ func TestPerformance_AuthLogin(t *testing.T) {
// Create test user
userID := uuid.New()
user := &models.User{
ID: userID,
Email: "test@example.com",
Username: "testuser",
ID: userID,
Email: "test@example.com",
Username: "testuser",
PasswordHash: "$2a$10$abcdefghijklmnopqrstuvwxyz1234567890", // Mock hashed password
IsVerified: true,
IsVerified: true,
}
err := db.Create(user).Error
require.NoError(t, err)
@ -290,8 +290,8 @@ func TestPerformance_AuthRegister(t *testing.T) {
defer cleanup()
registerReq := map[string]interface{}{
"email": fmt.Sprintf("test%d@example.com", time.Now().UnixNano()),
"username": fmt.Sprintf("testuser%d", time.Now().UnixNano()),
"email": fmt.Sprintf("test%d@example.com", time.Now().UnixNano()),
"username": fmt.Sprintf("testuser%d", time.Now().UnixNano()),
"password": "SecurePassword123!",
"password_confirm": "SecurePassword123!",
}
@ -644,4 +644,3 @@ func BenchmarkTrackGet(b *testing.B) {
router.ServeHTTP(w, req)
}
}