chore: update
ci-api / test (push) Successful in 22m30s
ci-web / test (push) Successful in 13m15s

This commit is contained in:
Nuxgrid
2026-08-01 23:33:11 +02:00
parent cb966fcaf0
commit fb10d25ee2
21 changed files with 612 additions and 81 deletions
+49 -20
View File
@@ -11,7 +11,6 @@ import (
)
var (
ErrCapacityReached = errors.New("capacité maximale de démos atteinte")
ErrNotFound = errors.New("démo introuvable")
ErrNotExtendable = errors.New("démo non prolongeable dans son état actuel")
ErrUserHasActiveDemo = errors.New("ce client a déjà une démo active")
@@ -22,7 +21,6 @@ type Config struct {
BaseDomain string // ex. "demo.omnex.app"
HTTPSPort string // port public HTTPS pour l'URL affichée ("443" = omis, sinon ex. NodePort)
TTL time.Duration // durée de vie
MaxDemos int // capacité
}
// Service : logique métier des démos (indépendante du transport HTTP).
@@ -38,9 +36,6 @@ func NewService(store Store, pool Pool, prov Provisioner, cfg Config) *Service {
if cfg.TTL == 0 {
cfg.TTL = TTL
}
if cfg.MaxDemos == 0 {
cfg.MaxDemos = MaxConcurrentDemos
}
if cfg.BaseDomain == "" {
cfg.BaseDomain = "demo.omnex.app"
}
@@ -52,8 +47,12 @@ func NewService(store Store, pool Pool, prov Provisioner, cfg Config) *Service {
// Create réserve la capacité + le pool, persiste la démo et déclenche le worker.
// username (optionnel) rattache la démo à un client existant. Un client n'a
// droit qu'à une seule démo active à la fois.
func (s *Service) Create(leadID, username string) (Demo, error) {
// droit qu'à une seule démo active à la fois. cfg (bot Telegram, stockage)
// n'est jamais persisté : transmis tel quel au provisioning.
func (s *Service) Create(leadID, username string, cfg ProvisionConfig) (Demo, error) {
if err := cfg.Validate(); err != nil {
return Demo{}, err
}
normalizedUsername := auth.NormalizeUsername(username)
if normalizedUsername != "" {
existing, err := s.store.ListByUsername(normalizedUsername)
@@ -67,14 +66,6 @@ func (s *Service) Create(leadID, username string) (Demo, error) {
}
}
active, err := s.store.CountActive()
if err != nil {
return Demo{}, err
}
if active >= s.cfg.MaxDemos {
return Demo{}, ErrCapacityReached
}
id := uuid.NewString()
demo := Demo{
ID: id,
@@ -95,12 +86,11 @@ func (s *Service) Create(leadID, username string) (Demo, error) {
}
demo.URL = "https://" + demo.Namespace + "." + s.cfg.BaseDomain + portSuffix
// Réserve les ressources externes ; ErrPoolExhausted => capacité atteinte.
// Réserve les ressources externes (slots Telegram/NowPayments pré-
// provisionnés) — contrainte réelle indépendante du nombre de démos,
// non concernée par la suppression du quota.
resources, err := s.pool.Borrow(id)
if err != nil {
if errors.Is(err, ErrPoolExhausted) {
return Demo{}, ErrCapacityReached
}
return Demo{}, err
}
@@ -113,7 +103,7 @@ func (s *Service) Create(leadID, username string) (Demo, error) {
// Lance le déploiement de manière asynchrone (worker inline via goroutine).
go func() {
if err := s.prov.Provision(created, resources); err != nil {
if err := s.prov.Provision(created, resources, cfg); err != nil {
log.Printf("Provisioning échoué pour %s: %v", created.ID, err)
created.Status = StatusFailed
_, _ = s.store.Update(created)
@@ -144,6 +134,45 @@ func (s *Service) ListForUser(username string) ([]Demo, error) {
return s.store.ListByUsername(auth.NormalizeUsername(username))
}
// TransferToPaid marque la démo active du client comme abonnement payant
// (n'expire plus) et déclenche la bascule en stockage persistant (migration
// des données postgres incluse, voir Provisioner.UpgradeToPaid). No-op si le
// client n'a pas de démo active — ce n'est pas une erreur (ex: compte créé
// sans jamais avoir demandé de démo). L'upgrade infra est lent (dump/
// restore/helm) : il tourne en tâche de fond, cette méthode ne bloque pas.
func (s *Service) TransferToPaid(username string) error {
existing, err := s.store.ListByUsername(auth.NormalizeUsername(username))
if err != nil {
return err
}
var active *Demo
for i := range existing {
if existing[i].Status.Active() {
active = &existing[i]
break
}
}
if active == nil {
return nil
}
active.TypeAbo = "premium"
active.ExpiresAt = s.now().UTC().AddDate(10, 0, 0) // n'expire plus, en pratique
updated, err := s.store.Update(*active)
if err != nil {
return err
}
go func() {
if err := s.prov.UpgradeToPaid(updated); err != nil {
log.Printf("Transfert payant échoué pour %s: %v", updated.Namespace, err)
}
}()
return nil
}
// Delete déclenche le teardown et libère le pool.
func (s *Service) Delete(id string) (Demo, error) {
d, ok := s.store.Get(id)