Files
Xor290 0f28d4757e
ci-web / test (push) Canceled after 0s
ci-api / test (push) Canceled after 24s
chore: build
2026-08-22 21:58:39 +02:00

210 lines
5.9 KiB
Go

package sub
import (
"errors"
"sync"
"time"
"github.com/google/uuid"
"github.com/omnex/control-plane/api/internal/auth"
"gorm.io/gorm"
)
// PremiumDuration : durée d'un abonnement premium débloqué par un code
// (30 jours). Sert à la fois à fixer ExpiredAt lors de l'activation et à
// retrouver la date de saisie du code (ExpiredAt - PremiumDuration).
const PremiumDuration = 30 * 24 * time.Hour
// Store définit les opérations de persistance pour les codes de souscription.
type Store interface {
// CreateCodeForSub crée un nouveau code pour un utilisateur.
CreateCodeForSub(username, code string) (CodeBuySub, error)
// GetCodeForSub récupère un code pour un utilisateur.
GetCodeForSub(username, code string) (CodeBuySub, error)
// ListCodes liste tous les codes.
ListCodes() ([]CodeBuySub, error)
AddCodeForSub(username, code string) (auth.User, error)
// GetUserPremium liste les clients actuellement en abonnement premium.
GetUserPremium() ([]PremiumUser, error)
}
// GormStore implémente Store avec PostgreSQL via GORM.
type GormStore struct {
db *gorm.DB
}
// NewGormStore crée un nouveau GormStore.
func NewGormStore(db *gorm.DB) *GormStore {
return &GormStore{db: db}
}
// CreateCodeForSub crée un nouveau code pour un utilisateur.
func (s *GormStore) CreateCodeForSub(username, code string) (CodeBuySub, error) {
c := CodeBuySub{
ID: uuid.NewString(),
Username: auth.NormalizeUsername(username),
CodeBuy: code,
}
if err := s.db.Create(&c).Error; err != nil {
return CodeBuySub{}, err
}
// S'assurer que CreatedAt est bien rempli
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now().UTC()
if err := s.db.Save(&c).Error; err != nil {
return CodeBuySub{}, err
}
}
return c, nil
}
// GetCodeForSub récupère un code pour un utilisateur.
func (s *GormStore) GetCodeForSub(username, code string) (CodeBuySub, error) {
var c CodeBuySub
if err := s.db.Where("username = ? AND code_buy = ?", username, code).First(&c).Error; err != nil {
return CodeBuySub{}, err
}
return c, nil
}
func (s *GormStore) AddCodeForSub(username, code string) (auth.User, error) {
var c CodeBuySub
err := s.db.Where("username = ? AND code_buy = ?", username, code).First(&c).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return auth.User{}, errors.New("code invalide")
}
return auth.User{}, err
}
var u auth.User
if err := s.db.Where("username = ?", auth.NormalizeUsername(username)).First(&u).Error; err != nil {
return auth.User{}, err
}
u.TypeAbo = "premium"
u.ExpiredAt = time.Now().UTC().Add(PremiumDuration)
if err := s.db.Model(&u).Updates(map[string]interface{}{
"type_abo": u.TypeAbo,
"expired_at": u.ExpiredAt,
}).Error; err != nil {
return auth.User{}, err
}
if err := s.db.Delete(&c).Error; err != nil {
return auth.User{}, err
}
return u, nil
}
// ListCodes liste tous les codes, triés par date de création.
func (s *GormStore) ListCodes() ([]CodeBuySub, error) {
var codes []CodeBuySub
// Essayer avec tri par date, sinon sans tri pour compatibilité
if err := s.db.Order("created_at desc").Find(&codes).Error; err != nil {
// Si colonne manquante, on liste sans tri
if orderErr := s.db.Find(&codes).Error; orderErr != nil {
return nil, orderErr
}
}
return codes, nil
}
// GetUserPremium liste les clients dont l'abonnement est premium, avec la
// date de saisie du code (déduite : ExpiredAt - PremiumDuration) et la date
// de fin d'abonnement (ExpiredAt). Trié par échéance la plus proche.
func (s *GormStore) GetUserPremium() ([]PremiumUser, error) {
var users []auth.User
if err := s.db.Where("type_abo = ?", "premium").Order("expired_at asc").Find(&users).Error; err != nil {
return nil, err
}
out := make([]PremiumUser, 0, len(users))
for _, u := range users {
out = append(out, PremiumUser{
Username: u.Username,
ActivatedAt: u.ExpiredAt.Add(-PremiumDuration),
ExpiredAt: u.ExpiredAt,
})
}
return out, nil
}
// MemStore implémente Store en mémoire (pour dev/tests).
type MemStore struct {
mu sync.RWMutex
items map[string]CodeBuySub
byCode map[string]CodeBuySub
}
// NewMemStore crée un nouveau MemStore.
func NewMemStore() *MemStore {
return &MemStore{
items: make(map[string]CodeBuySub),
byCode: make(map[string]CodeBuySub),
}
}
// CreateCodeForSub crée un nouveau code pour un utilisateur.
func (m *MemStore) CreateCodeForSub(username, code string) (CodeBuySub, error) {
m.mu.Lock()
defer m.mu.Unlock()
c := CodeBuySub{
ID: uuid.NewString(),
Username: username,
CodeBuy: code,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
m.items[c.ID] = c
m.byCode[code] = c
return c, nil
}
// GetCodeForSub récupère un code pour un utilisateur.
func (m *MemStore) GetCodeForSub(username, code string) (CodeBuySub, error) {
m.mu.RLock()
defer m.mu.RUnlock()
c, ok := m.byCode[code]
if !ok {
return CodeBuySub{}, nil
}
return c, nil
}
// ListCodes liste tous les codes.
func (m *MemStore) ListCodes() ([]CodeBuySub, error) {
m.mu.RLock()
defer m.mu.RUnlock()
out := make([]CodeBuySub, 0, len(m.items))
for _, c := range m.items {
out = append(out, c)
}
return out, nil
}
func (m *MemStore) AddCodeForSub(username, code string) (auth.User, error) {
m.mu.Lock()
defer m.mu.Unlock()
c, ok := m.byCode[code]
if !ok || c.Username != username {
return auth.User{}, errors.New("code invalide")
}
delete(m.byCode, code)
delete(m.items, c.ID)
// Le MemStore ne connaît pas les utilisateurs (pas de référence à memUsers ici) :
// il ne peut donc pas mettre à jour TypeAbo lui-même. Cf. point ci-dessous.
return auth.User{Username: username, TypeAbo: "premium"}, nil
}
// GetUserPremium : le MemStore (dev/tests) ne connaît pas la table des
// utilisateurs (voir AddCodeForSub) — il ne peut donc pas lister les clients
// premium. Renvoie une liste vide plutôt qu'une erreur.
func (m *MemStore) GetUserPremium() ([]PremiumUser, error) {
return []PremiumUser{}, nil
}