This commit is contained in:
LathanDevers
2026-07-16 09:05:24 +02:00
parent eb48f506cf
commit 151aca69de
3 changed files with 133 additions and 13 deletions
+101
View File
@@ -0,0 +1,101 @@
### 🛒 Épique 1 : Souscription et Achat (Abonnement Web)
**US 1.1 : Souscription initiale et information légale**
> **En tant que** client,
> **Je veux** pouvoir choisir un abonnement web et le payer en ligne,
> **Afin d'** y accéder immédiatement tout en étant informé de mes droits.
* **CA 1 :** Le client doit pouvoir sélectionner un plan d'hébergement et un cycle de facturation (ex: 1 mois, 1 an).
* **CA 2 :** Dès validation du paiement, le compte HestiaCP est provisionné automatiquement et les accès sont envoyés par email.
* **CA 3 :** Le système enregistre l'horodatage exact (date et heure) de l'activation, définissant le "Jour 0".
* **CA 4 :** L'email de confirmation de commande **doit explicitement mentionner** le droit de rétractation de 14 jours prévu par le Code de droit économique belge.
---
### 🛑 Épique 2 : Résiliation et Rétractation
**US 2.1 : Droit de rétractation (Résiliation Jours 0 à 14 inclus)**
> **En tant que** client dans ma période d'essai légale de 14 jours,
> **Je veux** pouvoir annuler mon abonnement d'un simple clic depuis mon espace,
> **Afin de** stopper le service immédiatement et d'obtenir un remboursement complet.
* **CA 1 :** Un bouton "Annuler mon abonnement" est disponible sur le détail du service.
* **CA 2 :** **Règle métier :** Si la demande d'annulation intervient avant ou le 14ème jour à 23h59 suivant l'activation, le système affiche un message indiquant que le remboursement sera intégral et immédiat.
* **CA 3 :** Dès confirmation du client, le service (FOSSBilling + HestiaCP) est **immédiatement suspendu ou supprimé**.
* **CA 4 :** Une facture de remboursement (montant négatif) est générée, et l'ordre de remboursement est envoyé à la passerelle de paiement.
**US 2.2 : Résiliation standard (Après 14 jours)**
> **En tant que** client abonné depuis plus de 14 jours,
> **Je veux** pouvoir annuler le renouvellement automatique de mon abonnement,
> **Afin de** ne pas être facturé à la prochaine échéance.
* **CA 1 :** **Règle métier :** Si la demande d'annulation intervient après le 14ème jour, aucun remboursement n'est proposé.
* **CA 2 :** L'interface affiche clairement : *"Votre abonnement restera actif jusqu'au [Date de fin de période payée] et ne sera pas renouvelé."*
* **CA 3 :** Le statut de l'abonnement dans FOSSBilling passe en "Annulé à l'échéance".
* **CA 4 :** Le système coupe le service HestiaCP **uniquement à la date d'échéance**, sans action manuelle de l'administrateur.
---
### ⬆️ Épique 3 : Upgrade (Montée en gamme)
**US 3.1 : Upgrade d'une offre en cours de cycle**
> **En tant que** client,
> **Je veux** pouvoir passer à une offre supérieure à tout moment,
> **Afin de** bénéficier de plus de ressources immédiatement.
* **CA 1 :** L'interface propose les plans supérieurs disponibles et calcule le prix à payer au *Prorata Temporis* (différence de prix calculée au prorata des jours restants avant le prochain renouvellement).
* **CA 2 :** Le changement de ressources techniques sur HestiaCP et la mise à jour des paramètres sur FOSSBilling ont lieu **immédiatement après validation du paiement de l'upgrade**.
* **CA 3 :** **Légal :** L'upgrade constituant un nouveau contrat/avenant, si le client se rétracte dans les 14 jours suivant cet upgrade, il est remboursé du prorata payé et redescendu à son offre initiale (ses ressources HestiaCP sont rétrogradées).
---
### ⬇️ Épique 4 : Downgrade (Descente en gamme standard)
**US 4.1 : Demande de Downgrade pour le prochain cycle**
> **En tant que** client,
> **Je veux** pouvoir demander le passage à une offre inférieure,
> **Afin de** réduire mes coûts lors de mon prochain renouvellement.
* **CA 1 :** L'interface permet de sélectionner un plan inférieur.
* **CA 2 :** Un message d'avertissement clair doit s'afficher : *"Votre demande est enregistrée. Le changement d'offre (ressources et tarif) prendra effet à votre prochaine date de renouvellement, le [Date]. Aucun remboursement partiel ne sera effectué pour la période en cours."*
* **CA 3 :** Le système enregistre l'intention de downgrade (Modification du `product_id` et du tarif pour la prochaine facture, mais maintien du package actuel sur HestiaCP jusqu'à la date d'échéance).
* **CA 4 :** À la date anniversaire, un script Cron effectue la bascule effective des ressources sur HestiaCP et génère la facture au nouveau tarif.
### 🟢 Phase 1 : Le "Quick Win" légal (Épique 1)
C'est le plus facile et on peut le régler en 5 minutes directement dans l'interface de FOSSBilling, sans toucher au code.
* **L'objectif :** Modifier les templates d'emails automatiques envoyés lors d'une commande pour y inclure la mention légale obligatoire du droit de rétractation de 14 jours.
* **L'action :** Éditer le template `mod_email_order_activated` dans FOSSBilling.
### 🟠 Phase 2 : Le cœur du réacteur - Les 14 jours (Épique 2)
C'est ici qu'on va faire du vrai développement PHP. Par défaut, quand un client annule sur FOSSBilling, ça coupe à la fin du mois sans rembourser. On doit créer un "intercepteur" (un Hook).
* **L'objectif :** Quand le client clique sur "Annuler", le système calcule la différence entre la date d'aujourd'hui et la date de la commande.
* **La logique :**
* Si `<= 14 jours` : On déclenche l'API `refund` de FOSSBilling, on suspend HestiaCP immédiatement, et on passe la commande en annulée.
* Si `> 14 jours` : On laisse FOSSBilling faire son comportement par défaut (Annulation à la prochaine date d'échéance).
### 🔵 Phase 3 : L'adaptation du Downgrade (Épique 4)
Tu te souviens de notre script de Downgrade instantané qu'on avait fait il y a quelques jours ? Puisque tu as choisi le modèle "Standard SaaS" (le downgrade ne s'applique qu'à la prochaine facture), on va devoir le modifier !
* **L'objectif :** Le clic sur "Downgrade" ne doit plus modifier HestiaCP immédiatement. Il doit juste préparer la prochaine facture dans FOSSBilling, et c'est un Cron (une tâche planifiée) qui réduira les ressources HestiaCP le jour du renouvellement.
### 🟣 Phase 4 : L'Upgrade (Épique 3)
FOSSBilling gère plutôt bien les upgrades nativement (avec calcul du prorata). On devra juste s'assurer que l'appel API vers HestiaCP se fait correctement dès que la facture de prorata est payée.
---
**Comment on attaque ?**
Est-ce qu'on se met en chauffe avec la **Phase 1 (Les templates d'emails)**, ou est-ce qu'on plonge directement dans le code PHP avec la **Phase 2 (La logique de rétractation des 14 jours)** ?
@@ -214,7 +214,7 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
const currentPlanFromCatalog = catalog.find(p => p.id === currentPlanId); const currentPlanFromCatalog = catalog.find(p => p.id === currentPlanId);
const baseCurrent = parseFloat(currentPlanFromCatalog?.pricing?.recurrent?.['1M']?.price || 0); const baseCurrent = parseFloat(currentPlanFromCatalog?.pricing?.recurrent?.['1M']?.price || 0);
const baseSelected = parseFloat(selectedPlan?.pricing?.recurrent?.['1M']?.price || 0); const baseSelected = parseFloat(selectedPlan?.pricing?.recurrent?.['1M']?.price || 0);
if (selectedPlan.id === currentPlanId) { if (selectedPlan.id === currentPlanId) {
actionType = 'cycle'; actionType = 'cycle';
} else if (baseSelected < baseCurrent) { } else if (baseSelected < baseCurrent) {
@@ -225,11 +225,11 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
const resp = await fetch(`${CUSTOM_API_BASE_URL}/custom_api/nexus_subscription.php`, { const resp = await fetch(`${CUSTOM_API_BASE_URL}/custom_api/nexus_subscription.php`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ body: new URLSearchParams({
action: 'migrate', action: 'migrate',
order_id: order.id, order_id: order.id,
target_plan_id: selectedPlan.id, target_plan_id: selectedPlan.id,
new_period: billingPeriod, new_period: billingPeriod,
new_price: finalInvoicePrice.toFixed(2), new_price: finalInvoicePrice.toFixed(2),
action_type: actionType // 🎯 On envoie l'information au backend ! action_type: actionType // 🎯 On envoie l'information au backend !
}) })
@@ -240,18 +240,29 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
onClose?.(); onClose?.();
setTimeout(() => window.location.href = '/facturation', 2000); setTimeout(() => window.location.href = '/facturation', 2000);
} else { } else {
onAlert?.("Erreur", data.error, "error"); onAlert?.("Erreur", data.error, "error");
} }
} catch (err) { } catch (err) {
onAlert?.("Erreur", "Problème réseau", "error"); onAlert?.("Erreur", "Problème réseau", "error");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleCancel = async () => { const handleCancel = async () => {
if (!window.confirm("Êtes-vous sûr de vouloir résilier cet abonnement ? Le service sera désactivé au prochain cycle de facturation.")) { if (!window.confirm("Êtes-vous sûr de vouloir résilier cet abonnement ? Le service sera désactivé au prochain cycle de facturation.")) {
return; try {
const response = await cancelSubscription(user.id, order.id);
// Le message s'adapte tout seul !
// Soit: "Annulé avec effet immédiat et remboursé"
// Soit: "Actif jusqu'à la fin de la période"
alert(response.message);
// Rafraîchir la liste des abonnements...
} catch (error) {
alert("Erreur lors de l'annulation : " + error.message);
}
} }
setCancelLoading(true); setCancelLoading(true);
+9 -1
View File
@@ -235,4 +235,12 @@ export const getInvoicesHistory = (clientId) =>
// (Optionnel) FOSSBilling Native : Récupère les détails complets d'une facture // (Optionnel) FOSSBilling Native : Récupère les détails complets d'une facture
export const getInvoiceDetails = (invoiceHash) => export const getInvoiceDetails = (invoiceHash) =>
apiCall(`${BASE_URL}/api/client/invoice/get`, 'POST', { hash: invoiceHash }); apiCall(`${BASE_URL}/api/client/invoice/get`, 'POST', { hash: invoiceHash });
// Annule un abonnement (Gère automatiquement la règle des 14 jours côté serveur)
export const cancelSubscription = (clientId, orderId) =>
apiCall(`${CUSTOM_API_BASE_URL}/custom_api/nexus_subscription.php`, 'POST', {
action: 'cancel_order',
client_id: clientId,
order_id: orderId
});