feat: Implement video workflow repository and related services
- Added videoWorkflowRepository with methods to manage video and user interactions. - Introduced catalog_mapper for converting database models to protobuf representations. - Created domain_helpers for normalizing domain and ad format values. - Defined service interfaces for payment, account, notification, domain, ad template, player config, video, and user management. - Implemented OAuth helpers for generating state and caching keys. - Developed payment_proto_helpers for mapping payment-related models to protobuf. - Added service policy helpers to enforce plan requirements and user permissions. - Created user_mapper for converting user payloads to protobuf format. - Implemented value_helpers for handling various value conversions and nil checks. - Developed video_helpers for normalizing video statuses and managing storage types. - Created video_mapper for mapping video models to protobuf format. - Implemented render workflow for managing video creation and job processing.
This commit is contained in:
73
internal/repository/account_repository.go
Normal file
73
internal/repository/account_repository.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type accountRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAccountRepository(db *gorm.DB) *accountRepository {
|
||||
return &accountRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *accountRepository) DeleteUserAccount(ctx context.Context, userID string) error {
|
||||
userID = strings.TrimSpace(userID)
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.Notification{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.Domain{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.AdTemplate{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.WalletTransaction{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.PlanSubscription{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.UserPreference{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.Payment{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.Video{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("id = ?", userID).Delete(&model.User{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *accountRepository) ClearUserData(ctx context.Context, userID string) error {
|
||||
userID = strings.TrimSpace(userID)
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.Notification{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.Domain{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.AdTemplate{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.Video{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]any{"storage_used": 0}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
152
internal/repository/ad_template_repository.go
Normal file
152
internal/repository/ad_template_repository.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type adTemplateRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAdTemplateRepository(db *gorm.DB) *adTemplateRepository {
|
||||
return &adTemplateRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) ListByUser(ctx context.Context, userID string) ([]model.AdTemplate, error) {
|
||||
var items []model.AdTemplate
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("user_id = ?", strings.TrimSpace(userID)).
|
||||
Order("is_default DESC").
|
||||
Order("created_at DESC").
|
||||
Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) ListForAdmin(ctx context.Context, search string, userID string, limit int32, offset int) ([]model.AdTemplate, int64, error) {
|
||||
db := r.db.WithContext(ctx).Model(&model.AdTemplate{})
|
||||
if trimmedSearch := strings.TrimSpace(search); trimmedSearch != "" {
|
||||
like := "%" + trimmedSearch + "%"
|
||||
db = db.Where("name ILIKE ?", like)
|
||||
}
|
||||
if trimmedUserID := strings.TrimSpace(userID); trimmedUserID != "" {
|
||||
db = db.Where("user_id = ?", trimmedUserID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var templates []model.AdTemplate
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(int(limit)).Find(&templates).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return templates, total, nil
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) CountAll(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.AdTemplate{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) GetByID(ctx context.Context, id string) (*model.AdTemplate, error) {
|
||||
var item model.AdTemplate
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id = ?", strings.TrimSpace(id)).
|
||||
First(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) GetByIDAndUser(ctx context.Context, id string, userID string) (*model.AdTemplate, error) {
|
||||
var item model.AdTemplate
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
First(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) ExistsByIDAndUser(ctx context.Context, id string, userID string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.AdTemplate{}).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) CreateWithDefault(ctx context.Context, userID string, item *model.AdTemplate) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if item.IsDefault {
|
||||
if err := tx.Model(&model.AdTemplate{}).
|
||||
Where("user_id = ?", strings.TrimSpace(userID)).
|
||||
Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Create(item).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) SaveWithDefault(ctx context.Context, userID string, item *model.AdTemplate) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if item.IsDefault {
|
||||
if err := tx.Model(&model.AdTemplate{}).
|
||||
Where("user_id = ? AND id <> ?", strings.TrimSpace(userID), item.ID).
|
||||
Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Save(item).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) DeleteByIDAndUserAndClearVideos(ctx context.Context, id string, userID string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.Video{}).
|
||||
Where("user_id = ? AND ad_id = ?", strings.TrimSpace(userID), strings.TrimSpace(id)).
|
||||
Update("ad_id", nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res := tx.Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
Delete(&model.AdTemplate{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *adTemplateRepository) DeleteByIDAndClearVideos(ctx context.Context, id string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.Video{}).
|
||||
Where("ad_id = ?", strings.TrimSpace(id)).
|
||||
Update("ad_id", nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res := tx.Where("id = ?", strings.TrimSpace(id)).
|
||||
Delete(&model.AdTemplate{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
41
internal/repository/billing_repository.go
Normal file
41
internal/repository/billing_repository.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type billingRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewBillingRepository(db *gorm.DB) *billingRepository {
|
||||
return &billingRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *billingRepository) GetWalletBalance(ctx context.Context, userID string) (float64, error) {
|
||||
return model.GetWalletBalance(ctx, r.db, userID)
|
||||
}
|
||||
|
||||
func (r *billingRepository) GetWalletBalanceTx(tx *gorm.DB, ctx context.Context, userID string) (float64, error) {
|
||||
return model.GetWalletBalance(ctx, tx, userID)
|
||||
}
|
||||
|
||||
func (r *billingRepository) GetLatestPlanSubscription(ctx context.Context, userID string) (*model.PlanSubscription, error) {
|
||||
return model.GetLatestPlanSubscription(ctx, r.db, userID)
|
||||
}
|
||||
|
||||
func (r *billingRepository) GetLatestPlanSubscriptionTx(tx *gorm.DB, ctx context.Context, userID string) (*model.PlanSubscription, error) {
|
||||
return model.GetLatestPlanSubscription(ctx, tx, userID)
|
||||
}
|
||||
|
||||
func (r *billingRepository) CountActiveSubscriptions(ctx context.Context, now time.Time) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.PlanSubscription{}).Where("expires_at > ?", now).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
46
internal/repository/domain_repository.go
Normal file
46
internal/repository/domain_repository.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type domainRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewDomainRepository(db *gorm.DB) *domainRepository {
|
||||
return &domainRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *domainRepository) ListByUser(ctx context.Context, userID string) ([]model.Domain, error) {
|
||||
var rows []model.Domain
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("user_id = ?", strings.TrimSpace(userID)).
|
||||
Order("created_at DESC").
|
||||
Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (r *domainRepository) CountByUserAndName(ctx context.Context, userID string, name string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.Domain{}).
|
||||
Where("user_id = ? AND name = ?", strings.TrimSpace(userID), strings.TrimSpace(name)).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *domainRepository) Create(ctx context.Context, item *model.Domain) error {
|
||||
return r.db.WithContext(ctx).Create(item).Error
|
||||
}
|
||||
|
||||
func (r *domainRepository) DeleteByIDAndUser(ctx context.Context, id string, userID string) (int64, error) {
|
||||
res := r.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
Delete(&model.Domain{})
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
102
internal/repository/job_repository.go
Normal file
102
internal/repository/job_repository.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
"stream.api/internal/database/query"
|
||||
)
|
||||
|
||||
type jobRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewJobRepository(db *gorm.DB) *jobRepository {
|
||||
return &jobRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *jobRepository) ListByOffset(ctx context.Context, agentID string, offset int, limit int) ([]*model.Job, int64, error) {
|
||||
q := query.Job.WithContext(ctx).Order(query.Job.CreatedAt.Desc(), query.Job.ID.Desc())
|
||||
if trimmedAgentID := strings.TrimSpace(agentID); trimmedAgentID != "" {
|
||||
agentNumeric, err := strconv.ParseInt(trimmedAgentID, 10, 64)
|
||||
if err != nil {
|
||||
return []*model.Job{}, 0, nil
|
||||
}
|
||||
q = q.Where(query.Job.AgentID.Eq(agentNumeric))
|
||||
}
|
||||
|
||||
jobs, total, err := q.FindByPage(offset, limit)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items := make([]*model.Job, 0, len(jobs))
|
||||
for _, job := range jobs {
|
||||
items = append(items, job)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *jobRepository) ListByCursor(ctx context.Context, agentID string, cursorTime time.Time, cursorID string, limit int) ([]*model.Job, bool, error) {
|
||||
q := query.Job.WithContext(ctx).Order(query.Job.CreatedAt.Desc(), query.Job.ID.Desc())
|
||||
if trimmedAgentID := strings.TrimSpace(agentID); trimmedAgentID != "" {
|
||||
agentNumeric, err := strconv.ParseInt(trimmedAgentID, 10, 64)
|
||||
if err != nil {
|
||||
return []*model.Job{}, false, nil
|
||||
}
|
||||
q = q.Where(query.Job.AgentID.Eq(agentNumeric))
|
||||
}
|
||||
|
||||
queryDB := q.UnderlyingDB()
|
||||
if !cursorTime.IsZero() && strings.TrimSpace(cursorID) != "" {
|
||||
queryDB = queryDB.Where("(created_at < ?) OR (created_at = ? AND id < ?)", cursorTime, cursorTime, strings.TrimSpace(cursorID))
|
||||
}
|
||||
|
||||
var jobs []*model.Job
|
||||
if err := queryDB.Limit(limit + 1).Find(&jobs).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
hasMore := len(jobs) > limit
|
||||
if hasMore {
|
||||
jobs = jobs[:limit]
|
||||
}
|
||||
return jobs, hasMore, nil
|
||||
}
|
||||
|
||||
func (r *jobRepository) Create(ctx context.Context, job *model.Job) error {
|
||||
return query.Job.WithContext(ctx).Create(job)
|
||||
}
|
||||
|
||||
func (r *jobRepository) GetByID(ctx context.Context, id string) (*model.Job, error) {
|
||||
return query.Job.WithContext(ctx).Where(query.Job.ID.Eq(strings.TrimSpace(id))).First()
|
||||
}
|
||||
|
||||
func (r *jobRepository) Save(ctx context.Context, job *model.Job) error {
|
||||
return query.Job.WithContext(ctx).Save(job)
|
||||
}
|
||||
|
||||
func (r *jobRepository) UpdateVideoStatus(ctx context.Context, videoID string, statusValue string, processingStatus string) error {
|
||||
videoID = strings.TrimSpace(videoID)
|
||||
if videoID == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := query.Video.WithContext(ctx).
|
||||
Where(query.Video.ID.Eq(videoID)).
|
||||
Updates(map[string]any{"status": statusValue, "processing_status": processingStatus})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *jobRepository) GetLatestByVideoID(ctx context.Context, videoID string) (*model.Job, error) {
|
||||
var job model.Job
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("config::jsonb ->> 'video_id' = ?", strings.TrimSpace(videoID)).
|
||||
Order("created_at DESC").
|
||||
First(&job).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
54
internal/repository/notification_repository.go
Normal file
54
internal/repository/notification_repository.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type notificationRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewNotificationRepository(db *gorm.DB) *notificationRepository {
|
||||
return ¬ificationRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *notificationRepository) ListByUser(ctx context.Context, userID string) ([]model.Notification, error) {
|
||||
var rows []model.Notification
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("user_id = ?", strings.TrimSpace(userID)).
|
||||
Order("created_at DESC").
|
||||
Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (r *notificationRepository) MarkReadByIDAndUser(ctx context.Context, id string, userID string) (int64, error) {
|
||||
res := r.db.WithContext(ctx).
|
||||
Model(&model.Notification{}).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
Update("is_read", true)
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
func (r *notificationRepository) MarkAllReadByUser(ctx context.Context, userID string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&model.Notification{}).
|
||||
Where("user_id = ? AND is_read = ?", strings.TrimSpace(userID), false).
|
||||
Update("is_read", true).Error
|
||||
}
|
||||
|
||||
func (r *notificationRepository) DeleteByIDAndUser(ctx context.Context, id string, userID string) (int64, error) {
|
||||
res := r.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
Delete(&model.Notification{})
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
func (r *notificationRepository) DeleteAllByUser(ctx context.Context, userID string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("user_id = ?", strings.TrimSpace(userID)).
|
||||
Delete(&model.Notification{}).Error
|
||||
}
|
||||
469
internal/repository/payment_repository.go
Normal file
469
internal/repository/payment_repository.go
Normal file
@@ -0,0 +1,469 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"stream.api/internal/database/model"
|
||||
"stream.api/internal/database/query"
|
||||
)
|
||||
|
||||
type PaymentHistoryRow struct {
|
||||
ID string `gorm:"column:id"`
|
||||
Amount float64 `gorm:"column:amount"`
|
||||
Currency *string `gorm:"column:currency"`
|
||||
Status *string `gorm:"column:status"`
|
||||
PlanID *string `gorm:"column:plan_id"`
|
||||
PlanName *string `gorm:"column:plan_name"`
|
||||
InvoiceID string `gorm:"column:invoice_id"`
|
||||
Kind string `gorm:"column:kind"`
|
||||
TermMonths *int32 `gorm:"column:term_months"`
|
||||
PaymentMethod *string `gorm:"column:payment_method"`
|
||||
ExpiresAt *time.Time `gorm:"column:expires_at"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
type paymentRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPaymentRepository(db *gorm.DB) *paymentRepository {
|
||||
return &paymentRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *paymentRepository) ListHistoryByUser(ctx context.Context, userID string, subscriptionKind string, walletTopupKind string, topupType string, limit int32, offset int) ([]PaymentHistoryRow, int64, error) {
|
||||
baseQuery := `
|
||||
WITH history AS (
|
||||
SELECT
|
||||
p.id AS id,
|
||||
p.amount AS amount,
|
||||
p.currency AS currency,
|
||||
p.status AS status,
|
||||
p.plan_id AS plan_id,
|
||||
pl.name AS plan_name,
|
||||
p.id AS invoice_id,
|
||||
? AS kind,
|
||||
ps.term_months AS term_months,
|
||||
ps.payment_method AS payment_method,
|
||||
ps.expires_at AS expires_at,
|
||||
p.created_at AS created_at
|
||||
FROM payment AS p
|
||||
LEFT JOIN plan AS pl ON pl.id = p.plan_id
|
||||
LEFT JOIN plan_subscriptions AS ps ON ps.payment_id = p.id
|
||||
WHERE p.user_id = ?
|
||||
UNION ALL
|
||||
SELECT
|
||||
wt.id AS id,
|
||||
wt.amount AS amount,
|
||||
wt.currency AS currency,
|
||||
'SUCCESS' AS status,
|
||||
NULL AS plan_id,
|
||||
NULL AS plan_name,
|
||||
wt.id AS invoice_id,
|
||||
? AS kind,
|
||||
NULL AS term_months,
|
||||
NULL AS payment_method,
|
||||
NULL AS expires_at,
|
||||
wt.created_at AS created_at
|
||||
FROM wallet_transactions AS wt
|
||||
WHERE wt.user_id = ? AND wt.type = ? AND wt.payment_id IS NULL
|
||||
)
|
||||
`
|
||||
|
||||
var total int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Raw(baseQuery+`SELECT COUNT(*) FROM history`, subscriptionKind, userID, walletTopupKind, userID, topupType).
|
||||
Scan(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var rows []PaymentHistoryRow
|
||||
if err := r.db.WithContext(ctx).
|
||||
Raw(baseQuery+`SELECT * FROM history ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?`, subscriptionKind, userID, walletTopupKind, userID, topupType, limit, offset).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
func (r *paymentRepository) ListForAdmin(ctx context.Context, userID string, status string, limit int32, offset int) ([]model.Payment, int64, error) {
|
||||
db := r.db.WithContext(ctx).Model(&model.Payment{})
|
||||
if trimmedUserID := strings.TrimSpace(userID); trimmedUserID != "" {
|
||||
db = db.Where("user_id = ?", trimmedUserID)
|
||||
}
|
||||
if trimmedStatus := strings.TrimSpace(status); trimmedStatus != "" {
|
||||
db = db.Where("UPPER(status) = ?", strings.ToUpper(trimmedStatus))
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var payments []model.Payment
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(int(limit)).Find(&payments).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return payments, total, nil
|
||||
}
|
||||
|
||||
func (r *paymentRepository) CountAll(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.Payment{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *paymentRepository) SumSuccessfulAmount(ctx context.Context) (float64, error) {
|
||||
var total float64
|
||||
if err := r.db.WithContext(ctx).Model(&model.Payment{}).Where("status = ?", "SUCCESS").Select("COALESCE(SUM(amount), 0)").Scan(&total).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *paymentRepository) GetByID(ctx context.Context, paymentID string) (*model.Payment, error) {
|
||||
return query.Payment.WithContext(ctx).
|
||||
Where(query.Payment.ID.Eq(strings.TrimSpace(paymentID))).
|
||||
First()
|
||||
}
|
||||
|
||||
func (r *paymentRepository) GetByIDAndUser(ctx context.Context, paymentID string, userID string) (*model.Payment, error) {
|
||||
return query.Payment.WithContext(ctx).
|
||||
Where(query.Payment.ID.Eq(strings.TrimSpace(paymentID)), query.Payment.UserID.Eq(strings.TrimSpace(userID))).
|
||||
First()
|
||||
}
|
||||
|
||||
func (r *paymentRepository) GetStandaloneTopupByIDAndUser(ctx context.Context, id string, userID string, topupType string) (*model.WalletTransaction, error) {
|
||||
var topup model.WalletTransaction
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ? AND type = ? AND payment_id IS NULL", strings.TrimSpace(id), strings.TrimSpace(userID), topupType).
|
||||
First(&topup).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &topup, nil
|
||||
}
|
||||
|
||||
func (r *paymentRepository) GetSubscriptionByPaymentID(ctx context.Context, paymentID string) (*model.PlanSubscription, error) {
|
||||
var subscription model.PlanSubscription
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("payment_id = ?", strings.TrimSpace(paymentID)).
|
||||
Order("created_at DESC").
|
||||
First(&subscription).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &subscription, nil
|
||||
}
|
||||
|
||||
func (r *paymentRepository) CountByPlanID(ctx context.Context, planID string) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.Payment{}).Where("plan_id = ?", strings.TrimSpace(planID)).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *paymentRepository) CreatePayment(ctx context.Context, payment *model.Payment) error {
|
||||
return r.db.WithContext(ctx).Create(payment).Error
|
||||
}
|
||||
|
||||
func (r *paymentRepository) Save(ctx context.Context, payment *model.Payment) error {
|
||||
return r.db.WithContext(ctx).Save(payment).Error
|
||||
}
|
||||
|
||||
func (r *paymentRepository) CreatePaymentTx(tx *gorm.DB, ctx context.Context, payment *model.Payment) error {
|
||||
return tx.Create(payment).Error
|
||||
}
|
||||
|
||||
func (r *paymentRepository) CreateWalletTransactionTx(tx *gorm.DB, ctx context.Context, txRecord *model.WalletTransaction) error {
|
||||
return tx.Create(txRecord).Error
|
||||
}
|
||||
|
||||
func (r *paymentRepository) CreatePlanSubscriptionTx(tx *gorm.DB, ctx context.Context, subscription *model.PlanSubscription) error {
|
||||
return tx.Create(subscription).Error
|
||||
}
|
||||
|
||||
func (r *paymentRepository) UpdateUserPlanID(ctx context.Context, userID string, planID string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", userID).Update("plan_id", planID).Error
|
||||
}
|
||||
|
||||
func (r *paymentRepository) UpdateUserPlanIDTx(tx *gorm.DB, ctx context.Context, userID string, planID string) error {
|
||||
return tx.WithContext(ctx).Model(&model.User{}).Where("id = ?", strings.TrimSpace(userID)).Update("plan_id", strings.TrimSpace(planID)).Error
|
||||
}
|
||||
|
||||
func (r *paymentRepository) CreateNotificationTx(tx *gorm.DB, ctx context.Context, notification *model.Notification) error {
|
||||
return tx.Create(notification).Error
|
||||
}
|
||||
|
||||
func (r *paymentRepository) CreateWalletTopupAndNotification(ctx context.Context, userID string, transaction *model.WalletTransaction, notification *model.Notification) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if _, err := r.lockUserForUpdateTx(tx, ctx, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.CreateWalletTransactionTx(tx, ctx, transaction); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.CreateNotificationTx(tx, ctx, notification)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *paymentRepository) ExecuteSubscriptionPayment(ctx context.Context, userID string, plan *model.Plan, termMonths int32, paymentMethod string, paymentRecord *model.Payment, invoiceID string, now time.Time, validateFunding func(currentWalletBalance float64) (float64, error)) (*model.PlanSubscription, float64, error) {
|
||||
var (
|
||||
subscription *model.PlanSubscription
|
||||
walletBalance float64
|
||||
)
|
||||
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
referee, err := r.lockUserForUpdateTx(tx, ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newExpiry, err := r.loadPaymentExpiryTx(tx, ctx, userID, termMonths, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentWalletBalance, err := model.GetWalletBalance(ctx, tx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validatedTopupAmount, err := validateFunding(currentWalletBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.CreatePaymentTx(tx, ctx, paymentRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.createPaymentWalletTransactionsTx(tx, ctx, userID, plan, termMonths, paymentMethod, paymentRecord, paymentRecord.Amount, validatedTopupAmount, model.StringValue(paymentRecord.Currency)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subscription = &model.PlanSubscription{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
PaymentID: paymentRecord.ID,
|
||||
PlanID: plan.ID,
|
||||
TermMonths: termMonths,
|
||||
PaymentMethod: paymentMethod,
|
||||
WalletAmount: paymentRecord.Amount,
|
||||
TopupAmount: validatedTopupAmount,
|
||||
StartedAt: now,
|
||||
ExpiresAt: newExpiry,
|
||||
}
|
||||
if err := r.CreatePlanSubscriptionTx(tx, ctx, subscription); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.UpdateUserPlanIDTx(tx, ctx, userID, plan.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notification := &model.Notification{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
Type: "billing.subscription",
|
||||
Title: "Subscription activated",
|
||||
Message: fmt.Sprintf("Your subscription to %s is active until %s.", plan.Name, subscription.ExpiresAt.UTC().Format("2006-01-02")),
|
||||
Metadata: model.StringPtr(mustMarshalJSON(map[string]any{
|
||||
"payment_id": paymentRecord.ID,
|
||||
"invoice_id": invoiceID,
|
||||
"plan_id": plan.ID,
|
||||
"term_months": subscription.TermMonths,
|
||||
"payment_method": subscription.PaymentMethod,
|
||||
"wallet_amount": subscription.WalletAmount,
|
||||
"topup_amount": subscription.TopupAmount,
|
||||
"plan_expires_at": subscription.ExpiresAt.UTC().Format(time.RFC3339),
|
||||
})),
|
||||
}
|
||||
if err := r.CreateNotificationTx(tx, ctx, notification); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.maybeGrantReferralRewardTx(tx, ctx, referee, plan, paymentRecord, subscription); err != nil {
|
||||
return err
|
||||
}
|
||||
walletBalance, err = model.GetWalletBalance(ctx, tx, userID)
|
||||
return err
|
||||
})
|
||||
|
||||
return subscription, walletBalance, err
|
||||
}
|
||||
|
||||
func (r *paymentRepository) lockUserForUpdateTx(tx *gorm.DB, ctx context.Context, userID string) (*model.User, error) {
|
||||
trimmedUserID := strings.TrimSpace(userID)
|
||||
if tx.Dialector.Name() == "sqlite" {
|
||||
res := tx.WithContext(ctx).Exec("UPDATE user SET id = id WHERE id = ?", trimmedUserID)
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := tx.WithContext(ctx).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ?", trimmedUserID).
|
||||
First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *paymentRepository) loadPaymentExpiryTx(tx *gorm.DB, ctx context.Context, userID string, termMonths int32, now time.Time) (time.Time, error) {
|
||||
currentSubscription, err := model.GetLatestPlanSubscription(ctx, tx, userID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return time.Time{}, err
|
||||
}
|
||||
baseExpiry := now
|
||||
if currentSubscription != nil && currentSubscription.ExpiresAt.After(baseExpiry) {
|
||||
baseExpiry = currentSubscription.ExpiresAt.UTC()
|
||||
}
|
||||
return baseExpiry.AddDate(0, int(termMonths), 0), nil
|
||||
}
|
||||
|
||||
func (r *paymentRepository) createPaymentWalletTransactionsTx(tx *gorm.DB, ctx context.Context, userID string, plan *model.Plan, termMonths int32, paymentMethod string, paymentRecord *model.Payment, totalAmount, topupAmount float64, currency string) error {
|
||||
if paymentMethod == "topup" {
|
||||
topupTransaction := &model.WalletTransaction{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
Type: "topup",
|
||||
Amount: topupAmount,
|
||||
Currency: model.StringPtr(currency),
|
||||
Note: model.StringPtr(fmt.Sprintf("Wallet top-up for %s (%d months)", plan.Name, termMonths)),
|
||||
PaymentID: &paymentRecord.ID,
|
||||
PlanID: &plan.ID,
|
||||
TermMonths: &termMonths,
|
||||
}
|
||||
if err := r.CreateWalletTransactionTx(tx, ctx, topupTransaction); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
debitTransaction := &model.WalletTransaction{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
Type: "subscription_debit",
|
||||
Amount: -totalAmount,
|
||||
Currency: model.StringPtr(currency),
|
||||
Note: model.StringPtr(fmt.Sprintf("Subscription payment for %s (%d months)", plan.Name, termMonths)),
|
||||
PaymentID: &paymentRecord.ID,
|
||||
PlanID: &plan.ID,
|
||||
TermMonths: &termMonths,
|
||||
}
|
||||
return r.CreateWalletTransactionTx(tx, ctx, debitTransaction)
|
||||
}
|
||||
|
||||
func (r *paymentRepository) maybeGrantReferralRewardTx(tx *gorm.DB, ctx context.Context, referee *model.User, plan *model.Plan, paymentRecord *model.Payment, subscription *model.PlanSubscription) error {
|
||||
if paymentRecord == nil || subscription == nil || plan == nil || referee == nil {
|
||||
return nil
|
||||
}
|
||||
if subscription.PaymentMethod != "wallet" && subscription.PaymentMethod != "topup" {
|
||||
return nil
|
||||
}
|
||||
if referee.ReferredByUserID == nil || strings.TrimSpace(*referee.ReferredByUserID) == "" {
|
||||
return nil
|
||||
}
|
||||
if referee.ReferralRewardGrantedAt != nil || (referee.ReferralRewardPaymentID != nil && strings.TrimSpace(*referee.ReferralRewardPaymentID) != "") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var subscriptionCount int64
|
||||
if err := tx.WithContext(ctx).Model(&model.PlanSubscription{}).Where("user_id = ?", referee.ID).Count(&subscriptionCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if subscriptionCount != 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
referrer, err := r.lockUserForUpdateTx(tx, ctx, strings.TrimSpace(*referee.ReferredByUserID))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if referrer.ID == referee.ID || (referrer.ReferralEligible != nil && !*referrer.ReferralEligible) {
|
||||
return nil
|
||||
}
|
||||
|
||||
bps := int32(500)
|
||||
if referrer.ReferralRewardBps != nil {
|
||||
bps = *referrer.ReferralRewardBps
|
||||
if bps < 0 {
|
||||
bps = 0
|
||||
}
|
||||
if bps > 10000 {
|
||||
bps = 10000
|
||||
}
|
||||
}
|
||||
if bps <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
baseAmount := plan.Price * float64(subscription.TermMonths)
|
||||
if baseAmount <= 0 {
|
||||
return nil
|
||||
}
|
||||
rewardAmount := baseAmount * float64(bps) / 10000
|
||||
if rewardAmount <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
refereeLabel := strings.TrimSpace(referee.Email)
|
||||
if username := strings.TrimSpace(model.StringValue(referee.Username)); username != "" {
|
||||
refereeLabel = "@" + username
|
||||
}
|
||||
|
||||
rewardTransaction := &model.WalletTransaction{
|
||||
ID: uuid.New().String(),
|
||||
UserID: referrer.ID,
|
||||
Type: "referral_reward",
|
||||
Amount: rewardAmount,
|
||||
Currency: paymentRecord.Currency,
|
||||
Note: model.StringPtr(fmt.Sprintf("Referral reward for %s first subscription", referee.Email)),
|
||||
PaymentID: &paymentRecord.ID,
|
||||
PlanID: &plan.ID,
|
||||
}
|
||||
if err := tx.Create(rewardTransaction).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
notification := &model.Notification{
|
||||
ID: uuid.New().String(),
|
||||
UserID: referrer.ID,
|
||||
Type: "billing.referral_reward",
|
||||
Title: "Referral reward granted",
|
||||
Message: fmt.Sprintf("You received %.2f USD from %s's first subscription.", rewardAmount, refereeLabel),
|
||||
Metadata: model.StringPtr(mustMarshalJSON(map[string]any{
|
||||
"payment_id": paymentRecord.ID,
|
||||
"referee_id": referee.ID,
|
||||
"amount": rewardAmount,
|
||||
})),
|
||||
}
|
||||
if err := tx.Create(notification).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
return tx.WithContext(ctx).Model(&model.User{}).Where("id = ?", referee.ID).Updates(map[string]any{
|
||||
"referral_reward_granted_at": now,
|
||||
"referral_reward_payment_id": paymentRecord.ID,
|
||||
"referral_reward_amount": rewardAmount,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func mustMarshalJSON(value any) string {
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
69
internal/repository/plan_repository.go
Normal file
69
internal/repository/plan_repository.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type planRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPlanRepository(db *gorm.DB) *planRepository {
|
||||
return &planRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *planRepository) GetByID(ctx context.Context, planID string) (*model.Plan, error) {
|
||||
var plan model.Plan
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(planID)).First(&plan).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &plan, nil
|
||||
}
|
||||
|
||||
func (r *planRepository) ListActive(ctx context.Context) ([]model.Plan, error) {
|
||||
var plans []model.Plan
|
||||
if err := r.db.WithContext(ctx).Where("is_active = ?", true).Find(&plans).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
func (r *planRepository) ListAll(ctx context.Context) ([]model.Plan, error) {
|
||||
var plans []model.Plan
|
||||
if err := r.db.WithContext(ctx).Order("price ASC").Find(&plans).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
func (r *planRepository) Create(ctx context.Context, plan *model.Plan) error {
|
||||
return r.db.WithContext(ctx).Create(plan).Error
|
||||
}
|
||||
|
||||
func (r *planRepository) Save(ctx context.Context, plan *model.Plan) error {
|
||||
return r.db.WithContext(ctx).Save(plan).Error
|
||||
}
|
||||
|
||||
func (r *planRepository) CountPaymentsByPlan(ctx context.Context, planID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.Payment{}).Where("plan_id = ?", strings.TrimSpace(planID)).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *planRepository) CountSubscriptionsByPlan(ctx context.Context, planID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.PlanSubscription{}).Where("plan_id = ?", strings.TrimSpace(planID)).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *planRepository) SetActive(ctx context.Context, planID string, isActive bool) error {
|
||||
return r.db.WithContext(ctx).Model(&model.Plan{}).Where("id = ?", strings.TrimSpace(planID)).Update("is_active", isActive).Error
|
||||
}
|
||||
|
||||
func (r *planRepository) DeleteByID(ctx context.Context, planID string) error {
|
||||
return r.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(planID)).Delete(&model.Plan{}).Error
|
||||
}
|
||||
277
internal/repository/player_config_repository.go
Normal file
277
internal/repository/player_config_repository.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type playerConfigRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPlayerConfigRepository(db *gorm.DB) *playerConfigRepository {
|
||||
return &playerConfigRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) ListByUser(ctx context.Context, userID string) ([]model.PlayerConfig, error) {
|
||||
var items []model.PlayerConfig
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("user_id = ?", strings.TrimSpace(userID)).
|
||||
Order("is_default DESC").
|
||||
Order("created_at DESC").
|
||||
Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) ListForAdmin(ctx context.Context, search string, userID string, limit int32, offset int) ([]model.PlayerConfig, int64, error) {
|
||||
db := r.db.WithContext(ctx).Model(&model.PlayerConfig{})
|
||||
if trimmedSearch := strings.TrimSpace(search); trimmedSearch != "" {
|
||||
like := "%" + trimmedSearch + "%"
|
||||
db = db.Where("name ILIKE ?", like)
|
||||
}
|
||||
if trimmedUserID := strings.TrimSpace(userID); trimmedUserID != "" {
|
||||
db = db.Where("user_id = ?", trimmedUserID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var items []model.PlayerConfig
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(int(limit)).Find(&items).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) CountByUser(ctx context.Context, userID string) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&model.PlayerConfig{}).
|
||||
Where("user_id = ?", strings.TrimSpace(userID)).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) CountByUserTx(tx *gorm.DB, ctx context.Context, userID string) (int64, error) {
|
||||
var count int64
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&model.PlayerConfig{}).
|
||||
Where("user_id = ?", strings.TrimSpace(userID)).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) Create(ctx context.Context, item *model.PlayerConfig) error {
|
||||
return r.db.WithContext(ctx).Create(item).Error
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) CreateWithDefault(ctx context.Context, userID string, item *model.PlayerConfig) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if item.IsDefault {
|
||||
if err := r.UnsetDefaultForUserTx(tx, userID, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Create(item).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) CreateTx(tx *gorm.DB, ctx context.Context, item *model.PlayerConfig) error {
|
||||
return tx.Create(item).Error
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) GetByIDAndUser(ctx context.Context, id string, userID string) (*model.PlayerConfig, error) {
|
||||
var item model.PlayerConfig
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
First(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) GetByID(ctx context.Context, id string) (*model.PlayerConfig, error) {
|
||||
var item model.PlayerConfig
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id = ?", strings.TrimSpace(id)).
|
||||
First(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) GetByIDAndUserTx(tx *gorm.DB, ctx context.Context, id string, userID string) (*model.PlayerConfig, error) {
|
||||
var item model.PlayerConfig
|
||||
if err := tx.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
First(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) Save(ctx context.Context, item *model.PlayerConfig) error {
|
||||
return r.db.WithContext(ctx).Save(item).Error
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) SaveWithDefault(ctx context.Context, userID string, item *model.PlayerConfig) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if item.IsDefault {
|
||||
if err := r.UnsetDefaultForUserTx(tx, userID, item.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Save(item).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) SaveTx(tx *gorm.DB, ctx context.Context, item *model.PlayerConfig) error {
|
||||
return tx.Save(item).Error
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) DeleteByIDAndUser(ctx context.Context, id string, userID string) (int64, error) {
|
||||
res := r.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
Delete(&model.PlayerConfig{})
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) DeleteByID(ctx context.Context, id string) (int64, error) {
|
||||
res := r.db.WithContext(ctx).
|
||||
Where("id = ?", strings.TrimSpace(id)).
|
||||
Delete(&model.PlayerConfig{})
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) DeleteByIDAndUserTx(tx *gorm.DB, ctx context.Context, id string, userID string) (int64, error) {
|
||||
res := tx.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(id), strings.TrimSpace(userID)).
|
||||
Delete(&model.PlayerConfig{})
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) UnsetDefaultForUser(ctx context.Context, userID string, excludeID string) error {
|
||||
updates := map[string]any{"is_default": false}
|
||||
db := r.db.WithContext(ctx).Model(&model.PlayerConfig{}).Where("user_id = ?", strings.TrimSpace(userID))
|
||||
if strings.TrimSpace(excludeID) != "" {
|
||||
db = db.Where("id != ?", strings.TrimSpace(excludeID))
|
||||
}
|
||||
return db.Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) UnsetDefaultForUserTx(tx *gorm.DB, userID string, excludeID string) error {
|
||||
updates := map[string]any{"is_default": false}
|
||||
db := tx.Model(&model.PlayerConfig{}).Where("user_id = ?", strings.TrimSpace(userID))
|
||||
if strings.TrimSpace(excludeID) != "" {
|
||||
db = db.Where("id != ?", strings.TrimSpace(excludeID))
|
||||
}
|
||||
return db.Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) CreateManaged(ctx context.Context, userID string, item *model.PlayerConfig, validate func(*model.User, int64) error) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
lockedUser, err := r.lockUserForUpdateTx(tx, ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configCount, err := r.CountByUserTx(tx, ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validate(lockedUser, configCount); err != nil {
|
||||
return err
|
||||
}
|
||||
if item.IsDefault {
|
||||
if err := r.UnsetDefaultForUserTx(tx, userID, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return r.CreateTx(tx, ctx, item)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) UpdateManaged(ctx context.Context, userID string, id string, mutateAndValidate func(*model.PlayerConfig, *model.User, int64) error) (*model.PlayerConfig, error) {
|
||||
var item *model.PlayerConfig
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
lockedUser, err := r.lockUserForUpdateTx(tx, ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configCount, err := r.CountByUserTx(tx, ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item, err = r.GetByIDAndUserTx(tx, ctx, id, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mutateAndValidate(item, lockedUser, configCount); err != nil {
|
||||
return err
|
||||
}
|
||||
if item.IsDefault {
|
||||
if err := r.UnsetDefaultForUserTx(tx, userID, item.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return r.SaveTx(tx, ctx, item)
|
||||
})
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) DeleteManaged(ctx context.Context, userID string, id string, validate func(*model.User, int64) error) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
lockedUser, err := r.lockUserForUpdateTx(tx, ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configCount, err := r.CountByUserTx(tx, ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validate(lockedUser, configCount); err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := r.DeleteByIDAndUserTx(tx, ctx, id, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *playerConfigRepository) lockUserForUpdateTx(tx *gorm.DB, ctx context.Context, userID string) (*model.User, error) {
|
||||
trimmedUserID := strings.TrimSpace(userID)
|
||||
if tx.Dialector.Name() == "sqlite" {
|
||||
res := tx.WithContext(ctx).Exec("UPDATE user SET id = id WHERE id = ?", trimmedUserID)
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := tx.WithContext(ctx).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ?", trimmedUserID).
|
||||
First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
24
internal/repository/user_preference_repository.go
Normal file
24
internal/repository/user_preference_repository.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type userPreferenceRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserPreferenceRepository(db *gorm.DB) *userPreferenceRepository {
|
||||
return &userPreferenceRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userPreferenceRepository) FindOrCreateByUserID(ctx context.Context, userID string) (*model.UserPreference, error) {
|
||||
return model.FindOrCreateUserPreference(ctx, r.db, userID)
|
||||
}
|
||||
|
||||
func (r *userPreferenceRepository) Save(ctx context.Context, pref *model.UserPreference) error {
|
||||
return r.db.WithContext(ctx).Save(pref).Error
|
||||
}
|
||||
173
internal/repository/user_repository.go
Normal file
173
internal/repository/user_repository.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"stream.api/internal/database/model"
|
||||
"stream.api/internal/database/query"
|
||||
)
|
||||
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *userRepository {
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {
|
||||
u := query.User
|
||||
return u.WithContext(ctx).Where(u.Email.Eq(strings.TrimSpace(email))).First()
|
||||
}
|
||||
|
||||
func (r *userRepository) CountByEmail(ctx context.Context, email string) (int64, error) {
|
||||
u := query.User
|
||||
return u.WithContext(ctx).Where(u.Email.Eq(strings.TrimSpace(email))).Count()
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByID(ctx context.Context, userID string) (*model.User, error) {
|
||||
u := query.User
|
||||
return u.WithContext(ctx).Where(u.ID.Eq(strings.TrimSpace(userID))).First()
|
||||
}
|
||||
|
||||
func (r *userRepository) ListForAdmin(ctx context.Context, search string, role string, limit int32, offset int) ([]model.User, int64, error) {
|
||||
db := r.db.WithContext(ctx).Model(&model.User{})
|
||||
if trimmedSearch := strings.TrimSpace(search); trimmedSearch != "" {
|
||||
like := "%" + trimmedSearch + "%"
|
||||
db = db.Where("email ILIKE ? OR username ILIKE ?", like, like)
|
||||
}
|
||||
if trimmedRole := strings.TrimSpace(role); trimmedRole != "" {
|
||||
db = db.Where("UPPER(role) = ?", strings.ToUpper(trimmedRole))
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var users []model.User
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(int(limit)).Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) CountAll(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.User{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) CountCreatedSince(ctx context.Context, since time.Time) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.User{}).Where("created_at >= ?", since).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) SumStorageUsed(ctx context.Context) (int64, error) {
|
||||
var total int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.User{}).Select("COALESCE(SUM(storage_used), 0)").Scan(&total).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetEmailByID(ctx context.Context, userID string) (*string, error) {
|
||||
var user model.User
|
||||
if err := r.db.WithContext(ctx).Select("id, email").Where("id = ?", strings.TrimSpace(userID)).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.Email, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetReferralSummaryByID(ctx context.Context, userID string) (*model.User, error) {
|
||||
var user model.User
|
||||
if err := r.db.WithContext(ctx).Select("id, email, username").Where("id = ?", strings.TrimSpace(userID)).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) CountByPlanID(ctx context.Context, planID string) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.User{}).Where("plan_id = ?", strings.TrimSpace(planID)).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) LockByIDTx(tx *gorm.DB, ctx context.Context, userID string) (*model.User, error) {
|
||||
trimmedUserID := strings.TrimSpace(userID)
|
||||
if tx.Dialector.Name() == "sqlite" {
|
||||
res := tx.WithContext(ctx).Exec("UPDATE user SET id = id WHERE id = ?", trimmedUserID)
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := tx.WithContext(ctx).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ?", trimmedUserID).
|
||||
First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Create(ctx context.Context, user *model.User) error {
|
||||
return query.User.WithContext(ctx).Create(user)
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdateFieldsByID(ctx context.Context, userID string, updates map[string]any) error {
|
||||
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", strings.TrimSpace(userID)).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdateFieldsByIDTx(tx *gorm.DB, ctx context.Context, userID string, updates map[string]any) error {
|
||||
return tx.WithContext(ctx).Model(&model.User{}).Where("id = ?", strings.TrimSpace(userID)).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdatePassword(ctx context.Context, userID string, passwordHash string) error {
|
||||
_, err := query.User.WithContext(ctx).
|
||||
Where(query.User.ID.Eq(strings.TrimSpace(userID))).
|
||||
Update(query.User.Password, passwordHash)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByReferralUsername(ctx context.Context, username string, limit int) ([]model.User, error) {
|
||||
trimmed := strings.TrimSpace(username)
|
||||
if trimmed == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var users []model.User
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("LOWER(username) = LOWER(?)", trimmed).
|
||||
Order("created_at ASC, id ASC").
|
||||
Limit(limit).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) CountSubscriptionsByUser(ctx context.Context, userID string) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&model.PlanSubscription{}).
|
||||
Where("user_id = ?", strings.TrimSpace(userID)).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
210
internal/repository/video_repository.go
Normal file
210
internal/repository/video_repository.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type videoRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewVideoRepository(db *gorm.DB) *videoRepository {
|
||||
return &videoRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *videoRepository) ListByUser(ctx context.Context, userID string, search string, status string, offset int, limit int) ([]model.Video, int64, error) {
|
||||
db := r.db.WithContext(ctx).Model(&model.Video{}).Where("user_id = ?", strings.TrimSpace(userID))
|
||||
if trimmedSearch := strings.TrimSpace(search); trimmedSearch != "" {
|
||||
like := "%" + trimmedSearch + "%"
|
||||
db = db.Where("title ILIKE ? OR description ILIKE ?", like, like)
|
||||
}
|
||||
if trimmedStatus := strings.TrimSpace(status); trimmedStatus != "" && !strings.EqualFold(trimmedStatus, "all") {
|
||||
db = db.Where("status = ?", trimmedStatus)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var videos []model.Video
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&videos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return videos, total, nil
|
||||
}
|
||||
|
||||
func (r *videoRepository) ListForAdmin(ctx context.Context, search string, userID string, status string, offset int, limit int) ([]model.Video, int64, error) {
|
||||
db := r.db.WithContext(ctx).Model(&model.Video{})
|
||||
if trimmedSearch := strings.TrimSpace(search); trimmedSearch != "" {
|
||||
like := "%" + trimmedSearch + "%"
|
||||
db = db.Where("title ILIKE ?", like)
|
||||
}
|
||||
if trimmedUserID := strings.TrimSpace(userID); trimmedUserID != "" {
|
||||
db = db.Where("user_id = ?", trimmedUserID)
|
||||
}
|
||||
if trimmedStatus := strings.TrimSpace(status); trimmedStatus != "" && !strings.EqualFold(trimmedStatus, "all") {
|
||||
db = db.Where("status = ?", trimmedStatus)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var videos []model.Video
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&videos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return videos, total, nil
|
||||
}
|
||||
|
||||
func (r *videoRepository) CountAll(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.Video{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *videoRepository) CountCreatedSince(ctx context.Context, since time.Time) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.Video{}).Where("created_at >= ?", since).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *videoRepository) IncrementViews(ctx context.Context, videoID string, userID string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&model.Video{}).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(videoID), strings.TrimSpace(userID)).
|
||||
UpdateColumn("views", gorm.Expr("views + ?", 1)).Error
|
||||
}
|
||||
|
||||
func (r *videoRepository) GetByIDAndUser(ctx context.Context, videoID string, userID string) (*model.Video, error) {
|
||||
var video model.Video
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(videoID), strings.TrimSpace(userID)).
|
||||
First(&video).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &video, nil
|
||||
}
|
||||
|
||||
func (r *videoRepository) GetByID(ctx context.Context, videoID string) (*model.Video, error) {
|
||||
var video model.Video
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id = ?", strings.TrimSpace(videoID)).
|
||||
First(&video).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &video, nil
|
||||
}
|
||||
|
||||
func (r *videoRepository) UpdateByIDAndUser(ctx context.Context, videoID string, userID string, updates map[string]any) (int64, error) {
|
||||
res := r.db.WithContext(ctx).
|
||||
Model(&model.Video{}).
|
||||
Where("id = ? AND user_id = ?", strings.TrimSpace(videoID), strings.TrimSpace(userID)).
|
||||
Updates(updates)
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
func (r *videoRepository) CountByUser(ctx context.Context, userID string) (int64, error) {
|
||||
var total int64
|
||||
err := r.db.WithContext(ctx).Model(&model.Video{}).Where("user_id = ?", strings.TrimSpace(userID)).Count(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
func (r *videoRepository) DeleteByIDAndUserWithStorageUpdate(ctx context.Context, videoID string, userID string, videoSize int64) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return r.deleteByIDAndUserWithStorageUpdateTx(tx, ctx, videoID, userID, videoSize)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *videoRepository) deleteByIDAndUserWithStorageUpdateTx(tx *gorm.DB, ctx context.Context, videoID string, userID string, videoSize int64) error {
|
||||
if err := tx.WithContext(ctx).Where("id = ? AND user_id = ?", strings.TrimSpace(videoID), strings.TrimSpace(userID)).Delete(&model.Video{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.User{}).
|
||||
Where("id = ?", strings.TrimSpace(userID)).
|
||||
UpdateColumn("storage_used", gorm.Expr("storage_used - ?", videoSize)).Error
|
||||
}
|
||||
|
||||
func (r *videoRepository) DeleteByIDWithStorageUpdate(ctx context.Context, videoID string, userID string, videoSize int64) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.WithContext(ctx).Where("id = ?", strings.TrimSpace(videoID)).Delete(&model.Video{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.User{}).
|
||||
Where("id = ?", strings.TrimSpace(userID)).
|
||||
UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used - ?, 0)", videoSize)).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *videoRepository) UpdateAdminVideo(ctx context.Context, video *model.Video, oldUserID string, oldSize int64, adTemplateID *string) error {
|
||||
if video == nil {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(video).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(oldUserID) == strings.TrimSpace(video.UserID) {
|
||||
delta := video.Size - oldSize
|
||||
if delta != 0 {
|
||||
if err := tx.Model(&model.User{}).
|
||||
Where("id = ?", strings.TrimSpace(video.UserID)).
|
||||
UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used + ?, 0)", delta)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := tx.Model(&model.User{}).
|
||||
Where("id = ?", strings.TrimSpace(oldUserID)).
|
||||
UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used - ?, 0)", oldSize)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.User{}).
|
||||
Where("id = ?", strings.TrimSpace(video.UserID)).
|
||||
UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if adTemplateID == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
trimmedAdID := strings.TrimSpace(*adTemplateID)
|
||||
if trimmedAdID == "" {
|
||||
if err := tx.WithContext(ctx).Model(&model.Video{}).Where("id = ?", video.ID).Update("ad_id", nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
video.AdID = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var template model.AdTemplate
|
||||
if err := tx.WithContext(ctx).Select("id").Where("id = ? AND user_id = ?", trimmedAdID, strings.TrimSpace(video.UserID)).First(&template).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.Video{}).Where("id = ?", video.ID).Update("ad_id", template.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
video.AdID = &template.ID
|
||||
return nil
|
||||
})
|
||||
}
|
||||
67
internal/repository/video_workflow_repository.go
Normal file
67
internal/repository/video_workflow_repository.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
)
|
||||
|
||||
type videoWorkflowRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewVideoWorkflowRepository(db *gorm.DB) *videoWorkflowRepository {
|
||||
return &videoWorkflowRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *videoWorkflowRepository) GetUserByID(ctx context.Context, userID string) (*model.User, error) {
|
||||
var user model.User
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(userID)).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *videoWorkflowRepository) CreateVideoWithStorageAndAd(ctx context.Context, video *model.Video, userID string, adTemplateID *string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(video).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.User{}).
|
||||
Where("id = ?", strings.TrimSpace(userID)).
|
||||
UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if video == nil || adTemplateID == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
trimmed := strings.TrimSpace(*adTemplateID)
|
||||
if trimmed == "" {
|
||||
if err := tx.WithContext(ctx).Model(&model.Video{}).Where("id = ?", video.ID).Update("ad_id", nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
video.AdID = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var template model.AdTemplate
|
||||
if err := tx.WithContext(ctx).Select("id").Where("id = ? AND user_id = ?", trimmed, strings.TrimSpace(userID)).First(&template).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.Video{}).Where("id = ?", video.ID).Update("ad_id", template.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
video.AdID = &template.ID
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *videoWorkflowRepository) MarkVideoJobFailed(ctx context.Context, videoID string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&model.Video{}).
|
||||
Where("id = ?", strings.TrimSpace(videoID)).
|
||||
Updates(map[string]any{"status": "failed", "processing_status": "FAILED"}).Error
|
||||
}
|
||||
Reference in New Issue
Block a user