diff --git a/control-plane/api/internal/demos/handler.go b/control-plane/api/internal/demos/handler.go index 3fc3cc6..621805d 100644 --- a/control-plane/api/internal/demos/handler.go +++ b/control-plane/api/internal/demos/handler.go @@ -220,6 +220,24 @@ func (h *Handler) SetDomain(c *gin.Context) { } // Extend : POST /demos/:id/extend — prolonge de 30 jours. +// TransferToPremium : POST /demos/:id/premium — passe la démo en +// abonnement payant (n'expire plus). La migration vers le namespace +// premium- tourne en tâche de fond (voir Service.TransferToPaid) : +// la réponse reflète le changement de statut immédiat, pas la fin de la +// migration. +func (h *Handler) TransferToPremium(c *gin.Context) { + d, err := h.svc.TransferToPaidByID(c.Param("id")) + if err != nil { + if errors.Is(err, ErrNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "démo introuvable"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"}) + return + } + c.JSON(http.StatusOK, d) +} + func (h *Handler) Extend(c *gin.Context) { d, err := h.svc.Extend(c.Param("id")) if err != nil { diff --git a/control-plane/api/internal/demos/service.go b/control-plane/api/internal/demos/service.go index 1f26027..29f494d 100644 --- a/control-plane/api/internal/demos/service.go +++ b/control-plane/api/internal/demos/service.go @@ -180,6 +180,22 @@ func (s *Service) TransferToPaid(username string) error { return nil } +// TransferToPaidByID passe en premium la démo désignée par son id (résout +// le username propriétaire puis délègue à TransferToPaid, qui opère par +// client — un client n'a qu'une démo active à la fois, cf. Create). +// Retourne la démo mise à jour (TypeAbo="premium" déjà appliqué ; la +// migration de namespace tourne en tâche de fond, voir TransferToPaid). +func (s *Service) TransferToPaidByID(id string) (Demo, error) { + d, err := s.Get(id) + if err != nil { + return Demo{}, err + } + if err := s.TransferToPaid(d.Username); err != nil { + return Demo{}, err + } + return s.Get(id) +} + // urlFor construit l'URL publique d'une démo pour un namespace donné. Port // omis s'il s'agit du 443 standard ; sinon affiché explicitement (ex. // NodePort HTTPS sans LoadBalancer). Ne concerne que l'URL affichée : le diff --git a/control-plane/api/internal/router/router.go b/control-plane/api/internal/router/router.go index 309c348..e161dae 100644 --- a/control-plane/api/internal/router/router.go +++ b/control-plane/api/internal/router/router.go @@ -83,6 +83,7 @@ func New(d Deps) *gin.Engine { admin.DELETE("/demos/:id", d.DemosH.Delete) admin.POST("/demos/:id/extend", d.DemosH.Extend) admin.POST("/demos/:id/domain", d.DemosH.SetDomain) + admin.POST("/demos/:id/premium", d.DemosH.TransferToPremium) admin.POST("/demos/details", d.DemosH.ListDetails) } } diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 688d8a4..f8d4d65 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -210,6 +210,7 @@ export const api = { deleteDemo: (id: string) => request('DELETE', `/demos/${id}`), setDemoDomain: (id: string, domain: string) => request('POST', `/demos/${id}/domain`, { domain }), + transferDemoToPremium: (id: string) => request('POST', `/demos/${id}/premium`), listCodes: () => request<{ items: CodeBuySub[] }>('GET', '/codes'), createCode: (username: string) => diff --git a/web/src/pages/backoffice/Demos.tsx b/web/src/pages/backoffice/Demos.tsx index da91230..d6d7563 100644 --- a/web/src/pages/backoffice/Demos.tsx +++ b/web/src/pages/backoffice/Demos.tsx @@ -45,6 +45,7 @@ export function Demos() { const [loading, setLoading] = useState(true) const [busy, setBusy] = useState(null) const [toDelete, setToDelete] = useState(null) + const [toPremium, setToPremium] = useState(null) const [createModalOpen, setCreateModalOpen] = useState(false) const [editingDomain, setEditingDomain] = useState(null) @@ -105,6 +106,27 @@ export function Demos() { } } + const confirmTransferToPremium = async () => { + if (!toPremium) return + const d = toPremium + setBusy(d.id) + try { + await api.transferDemoToPremium(d.id) + toast({ + status: 'success', + title: 'Démo passée en premium', + description: 'La migration des données tourne en tâche de fond.', + }) + setToPremium(null) + await load() + } catch (err) { + const msg = err instanceof ApiError ? err.message : 'Erreur' + toast({ status: 'error', title: 'Passage en premium impossible', description: msg }) + } finally { + setBusy(null) + } + } + const toggleRow = async (d: Demo) => { if (expandedId === d.id) { setExpandedId(null) @@ -159,6 +181,18 @@ export function Demos() { > +30 j +