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:
parent
eb97cad991
commit
a1000ce7fb
86 changed files with 790 additions and 769 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -129,4 +129,3 @@ const playlistsMapping = `{
|
|||
}
|
||||
}
|
||||
}`
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
27
veza-backend-api/internal/handlers/auth_fuzz_test.go
Normal file
27
veza-backend-api/internal/handlers/auth_fuzz_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -80,4 +80,3 @@ func setCacheHeaders(c *gin.Context, rule CacheRule) {
|
|||
c.Header("Cache-Control", strings.Join(parts, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,4 +104,3 @@ func TestDefaultCacheHeadersConfig(t *testing.T) {
|
|||
t.Error("missing API rule")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:"-"`
|
||||
|
|
|
|||
|
|
@ -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:"-"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:"-"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue