Aller au contenu
Candidater

Journal des modifications

Les évolutions de Cohez.io, de la plus récente à la plus ancienne.

174 versions publiées.

Versions 101 à 110 sur 174

0.16.10.0 — 2026-06-08

Corrections

  • Les liens « Finalisez votre paiement » des e-mails de relance HelloAsso fonctionnent à nouveau. Le lien encodait la référence du checkout HelloAsso, qui change à chaque nouvelle tentative de paiement : dès qu'un nouveau checkout était généré (premier clic du payeur, re-soumission), l'ancienne référence devenait orpheline et le payeur tombait sur la page « Le paiement n'a pas pu être finalisé », sans aucun recours. Le lien s'appuie désormais sur l'identité stable du panier (l'ensemble des inscriptions d'une même soumission), de sorte qu'il reste valide quel que soit le nombre de tentatives.
  • Plus de page d'erreur muette au clic sur un lien de relance. Selon l'état réel, le payeur voit maintenant un message clair : « Paiement déjà réglé », « Cette inscription n'est plus active », « Lien expiré » ou une vraie erreur technique avec possibilité de recommencer. Un échec technique de création du checkout ne « brûle » plus la relance : l'inscription reste éligible à un rappel ultérieur.

Ajouts

  • Notion de panier (cart_id) sur les inscriptions. Les inscriptions issues d'une même soumission multi-membres partagent désormais un identifiant de panier stable, posé à la création et jamais réécrit. Il sert de socle au lien de relance fiable et prépare les évolutions à venir (récapitulatif du solde, gestion de commande). Les inscriptions existantes ont été rattachées à un panier par migration (regroupement par payeur et date pour les paiements tiers, panier individuel sinon).

Modifications

  • Traçabilité renforcée du cycle de relance et de paiement HelloAsso. Chaque étape (clic du lien, résolution du panier, décision d'état, re-création du checkout, succès ou échec) est désormais journalisée avec un contexte structuré (préfixe helloasso.*), pour diagnostiquer rapidement en production.

0.16.9.0 — 2026-06-05

Corrections

  • Le formulaire public ne crée plus de « paiement en attente » fantôme pour les règlements hors ligne. Quand un adhérent s'inscrivait en choisissant un règlement par chèque, virement, espèces ou moyen personnalisé, l'application enregistrait aussitôt un paiement « en attente » alors qu'aucun argent n'avait circulé — le choix du moyen n'est qu'une intention, pas un paiement. Désormais seul HelloAsso génère un paiement à la soumission (support du tunnel de règlement). Pour les autres moyens, le choix de l'adhérent est conservé comme information sur l'inscription et affiché tel quel (email de confirmation, fiche back-office) ; l'équipe enregistre le paiement réel à la réception effective des fonds.

Technique

  • PublicRegistrationService ne crée un Payment (Pending) que lorsque le moyen résolu est HelloAsso (cas A acompte HA + paiement intégral HA). Les moyens hors ligne (chèque, virement, espèces, custom) ne matérialisent aucun paiement. Le gate porte sur la variable résolue (en cas A elle est forcée à HelloAsso pour le checkout, même si l'adhérent a coché chèque).
  • Nouvelles colonnes registrations.intended_payment_method (enum) + intended_custom_payment_method_id (FK tenant_payment_methods, nullOnDelete). Aucun backfill : les inscriptions antérieures restent à null (libellé « — »).
  • Registration::intendedPaymentMethodLabel() centralise la résolution du libellé (custom via relation, enum standard, null) ; l'email de confirmation et la fiche back-office la consomment (plus de lecture depuis le paiement supprimé).
  • Tests : flux offline = 0 paiement + intention stockée, HelloAsso = 1 paiement, cas A/B/C, méthode custom, rendu réel de l'email, libellé back-office, et résolution du libellé (null/standard/HelloAsso/custom/custom supprimé).

0.16.8.0 — 2026-06-05

Modifications

  • Paiement manuel : avancement automatique d'une inscription en brouillon : lorsque l'équipe marque un paiement comme reçu sur une inscription en brouillon, celle-ci avance désormais automatiquement, à l'image du flux HelloAsso. Elle passe en Validée si le paiement solde le montant dû (ce qui génère la facture et l'email de validation), ou en En attente de validation s'il s'agit d'un paiement partiel. Auparavant l'inscription restait bloquée en brouillon malgré l'encaissement (place de cours retenue, dossier invisible des relances). Les inscriptions déjà validées, en attente ou annulées ne sont pas affectées : leur statut reste administratif.

Corrections

  • Plus d'abandon d'une réservation dont le chèque a été déposé mais pas encore encaissé : le cron d'abandon (registrations:abandon-expired-deposits) ne libère plus une réservation expirée si elle porte un engagement de paiement — un paiement déjà reçu, ou un paiement manuel (chèque, virement, espèces…) saisi par le back-office et encore en attente d'encaissement. Auparavant, un chèque déposé puis enregistré mais pas encore marqué « reçu » laissait la réservation en brouillon, qu'un délai dépassé pouvait faire abandonner à tort (place libérée alors que le chèque était en main). Le critère « saisie back-office » (paiement avec un auteur, created_by) exclut les paiements Pending créés par le formulaire public à la soumission : une inscription publique jamais réglée doit, elle, être libérée à l'échéance. Les tunnels HelloAsso abandonnés (paiement HelloAsso resté Pending) et les chèques rejetés restent également abandonnables.

Technique

  • PaymentController::markAsReceived enveloppe la mise à jour du paiement, la relecture verrouillée de l'inscription (lockForUpdate) et la transition de statut dans une transaction unique ; l'événement RegistrationValidated est dispatché après commit pour que le listener de facture (CreateRegistrationInvoiceListener, idempotent) lise l'état persisté. La transition ne s'applique qu'aux inscriptions en Draft (re-test du statut sous verrou → pas de double-transition concurrente). Couverture étendue : transition pleine (Validated) / partielle (PendingValidation), statuts non-brouillon inchangés, et PaymentObserverTest mis à jour pour le nouveau contrat.
  • AbandonExpiredDepositsCommand : le garde « ne pas abandonner si payé » devient un garde « engagement » — freshPayments->contains(Received || (Pending && method !== HelloAsso && created_by !== null)). Discriminants : method (chèque en attente d'encaissement vs checkout HelloAsso abandonné) et created_by (saisie back-office vs Payment Pending créé par le formulaire public, jamais réglé). 5 tests ajoutés (chèque manuel back-office protège, virement manuel back-office protège, Pending public abandonné, HelloAsso Pending abandonné, chèque Rejected abandonné).

0.16.7.0 — 2026-06-05

Ajouts

  • Échange de cours sur une inscription (back-office) : l'équipe peut désormais remplacer un cours par un autre sur une inscription existante, directement depuis sa fiche. Chaque ligne de cours dispose d'un bouton « Échanger » ouvrant une fenêtre où l'on choisit le cours cible (les cours complets sont désactivés), avec un aperçu en temps réel du nouveau montant dû, du solde projeté et de la conséquence financière (trop-perçu à rembourser ou complément à régler). Un motif est obligatoire et consigné dans les notes de l'inscription. Fonctionne aussi sur les inscriptions validées et payées : l'argent n'est jamais déplacé automatiquement (le solde/trop-perçu absorbe l'écart, l'équipe gère remboursement ou complément manuellement). L'échange vers un cours complet est strictement bloqué.

Technique

  • Nouveau SwapRegistrationCourseService : échange atomique (transaction + lockForUpdate sur l'inscription et sur le cours cible), recalcul complet du montant dû (rebuild des items, remises cross-items), résolution serveur du tarif (un montant client n'est honoré que pour un cours à prix libre). Deux routes : PATCH registrations/{registration}/swap-course et POST .../swap-course/preview (aperçu sans persistance), toutes deux soumises à la permission UPDATE_MEMBERS.
  • Verrou sur l'inscription (et plus seulement sur le cours cible) avec recheck-after-lock : deux échanges simultanés sur une même inscription sont sérialisés, évitant un lost-update silencieux sur amount_due sur une inscription non validée (sans contrainte unique de snapshot pour servir de filet). lockForUpdate est un no-op SQLite (tests), effectif sur PostgreSQL (dev/prod).
  • L'acompte (deposit_amount_due) est borné au nouveau montant dû lors d'un échange vers un cours moins cher : sans cela, un acompte supérieur au total dû produisait un payment_status incohérent (partial_deposit).
  • Versionnement des snapshots facture : nouvelle colonne registration_invoices.version (source de vérité) + unique composite (registration_id, version). v1 = validation, v2+ = échanges. La migration renumérote les éventuels doublons préexistants avant de poser la contrainte (l'ancien ->unique() était un no-op chaîné sur la clé étrangère → jamais appliqué). Extraction du RegistrationInvoiceSnapshotter partagé entre la validation et l'échange.
  • La relation Registration::invoice() utilise orderByDesc('version') (et non latestOfMany) : latestOfMany générait un MAX(id) que PostgreSQL refuse sur une clé UUID (function max(uuid) does not exist). La contrainte d'unicité garantit l'absence d'égalité de version, l'ordre suffit.
  • Fiche inscription : les comptes d'inscrits par cours (pour griser les cours complets dans l'échange) sont calculés en une seule requête groupée au lieu d'un COUNT par cours, évitant un N+1 à chaque chargement de la page.
  • Mapper d'items tarifaires partagé (PricingCalculatorService::buildItem) entre la création d'inscription, l'estimation et l'échange. Aperçu côté front avec anti-rebond et annulation des requêtes périmées (AbortController).

0.16.6.0 — 2026-06-04

Ajouts

  • Indicateur « Trop-perçu » sur une inscription : lorsqu'un adhérent paie plus que le montant dû (par exemple un chèque qui dépasse le solde restant), l'écran de l'inscription affiche désormais clairement l'état « Trop-perçu » — une carte ambre indiquant le montant payé en trop (montant positif) et un badge « Trop-perçu » en en-tête. Auparavant, ce cas se traduisait par un « Solde : -10,00 € » négatif et déroutant. Le marquage d'un paiement comme « Reçu » n'a jamais été bloqué par un trop-perçu ; seul l'affichage manquait.

0.16.5.0 — 2026-06-04

Corrections

  • Tâches planifiées jamais exécutées en production : le scheduler Laravel n'était jamais déclenché sur Clever Cloud (aucun clevercloud/cron.json, crontab -l vide en prod). Conséquence : helloasso:sync-pending-payments (06:00), registrations:abandon-expired-deposits (07:00), documents:audit et invitations:expire ne tournaient pas — les paiements HelloAsso n'étaient pas réconciliés automatiquement et les réservations en attente expirées n'étaient jamais libérées. Le correctif précédent (0.16.4.0) reposait sur ce cron pour libérer les places ; il est désormais réellement actif.

Technique

  • Ajout de clevercloud/cron.json (appel chaque minute) et clevercloud/cron.sh (lance php artisan schedule:run depuis la racine de l'app). C'est le seul mécanisme cron disponible pour une app PHP Clever Cloud. stdout est jeté pour éviter le bruit « No scheduled commands are ready to run » à chaque minute ; stderr est conservé pour les erreurs. Un seul scaler en production → pas de double exécution.

0.16.4.0 — 2026-06-04

Corrections

  • Cours affiché « complet » alors qu'il restait des places : un cours pouvait apparaître complet sur le formulaire d'inscription public alors que le compte admin affichait encore des places disponibles. Deux causes : (1) le formulaire et l'admin ne comptaient pas le même ensemble de statuts (les inscriptions Draft comptaient côté formulaire mais pas côté admin) ; (2) des inscriptions Draft issues d'un checkout HelloAsso abandonné (paiement jamais finalisé) restaient bloquées indéfiniment et occupaient une place à vie. Le comptage de capacité est désormais unifié sur tous les écrans (formulaire public, Cours, Campagnes, Réglages > Cours) et les réservations en attente non finalisées sont automatiquement libérées après un délai.

Ajouts

  • Délai d'expiration des réservations en attente : toute réservation non finalisée (Draft) reçoit désormais une date limite. Passé ce délai sans paiement, elle est automatiquement annulée et sa place libérée. Le délai par défaut est de 7 jours (configurable via REGISTRATION_DRAFT_TTL_DAYS).
  • Surcharge du délai par campagne : chaque campagne peut définir son propre délai d'expiration des réservations en attente, via un champ dédié dans le formulaire de campagne (laisser vide pour utiliser le défaut global).
  • Édition de campagne accessible : un bouton « Modifier » permet désormais d'éditer une campagne existante depuis sa page (la page d'édition existait mais n'était reliée à aucun lien).

Technique

  • Nouvelle colonne registrations.draft_expires_at (date limite générique de réservation) et campaigns.draft_ttl_days (surcharge par campagne). La date limite d'acompte (deposit_expires_at) reste prioritaire quand elle existe, avec repli sur draft_expires_at.
  • Les échéances de réservation et d'acompte sont désormais calées sur la fin de journée (endOfDay()) à la création comme au backfill, cohérent avec un cron quotidien : l'expiration correspond à la date affichée à l'adhérent et lui laisse sa dernière journée entière.
  • Le job registrations:abandon-expired-deposits annule désormais tout Draft expiré (plus seulement les acomptes) ; il n'expire jamais un PendingValidation via draft_expires_at (un paiement enregistré tient la place). Garde-fou : aucune réservation ayant déjà reçu un paiement n'est annulée (total_paid non nul). Il est désormais planifié une fois par jour à 07:00, après helloasso:sync-pending-payments (06:00) au lieu de toutes les heures : la synchronisation réconcilie d'abord les paiements HelloAsso réellement reçus, évitant d'abandonner par erreur une inscription payée mais pas encore synchronisée (sync ignore les statuts terminaux).
  • Comptage de capacité centralisé sur RegistrationStatus::terminalStatuses() (exclut Cancelled/Refused/Abandoned, compte les Draft) avec COUNT(DISTINCT), partagé par le modèle Course, CourseController, Settings/CourseController et CampaignController. Les listes de membres (roster, export CSV) continuent d'exclure les Draft.
  • Nouvelle commande ponctuelle registrations:backfill-draft-deadlines (avec --dry-run) pour poser une échéance sur les Draft existants créés avant ce correctif. À lancer une fois après déploiement.
  • Le balayage d'abandon parcourt les inscriptions avec chunkById (et non each/offset) : comme il fait sortir chaque ligne du jeu de résultats, une pagination par offset sauterait des lignes au-delà de la première page (places retenues à vie) — chunkById les traite toutes.
  • Garde de fraîcheur : si au moins un tenant utilise HelloAsso, l'abandon ne tourne que si helloasso:sync-pending-payments a réussi dans les 6 dernières heures (heartbeat en cache). Sinon il s'abstient (log d'avertissement) pour ne pas abandonner une inscription payée mais pas encore réconciliée si le sync échoue.

0.16.3.0 — 2026-06-03

Ajouts

  • Traçabilité des emails de relance : chaque email de relance envoyé pour un dossier d'inscription est désormais enregistré (paiement HelloAsso rejeté, paiement abandonné, ou acompte expirant), avec le destinataire, la raison, la référence de checkout et la date d'envoi.
  • Historique des relances sur la vue dossier : une carte « Relances envoyées » liste les relances du dossier, avec un badge coloré par type (rejeté, abandonné, acompte) et l'horodatage de chaque envoi.

Technique

  • Nouvelle table registration_relance_emails (une ligne par email) et table pivot vers les dossiers concernés (un email groupé peut couvrir plusieurs dossiers).
  • L'enregistrement des traces est centralisé dans un recorder best-effort (RegistrationRelanceEmail::record()), branché sur le goulot d'envoi HelloAsso et sur la relance d'acompte. Une défaillance de la trace n'interrompt jamais l'envoi du mail.

0.16.2.0 — 2026-06-03

Corrections

  • Webhook HelloAsso Refused tardif : un webhook de refus livré en retard pour un checkout périmé (la référence ayant été écrasée lors d'un retry) ne re-marque plus Rejected un paiement déjà retenté sous une nouvelle référence. resolvePayments() filtre désormais sur la référence du checkout courant (filtre gardé : repli sur la résolution par registration_id si le checkoutIntentId est absent du payload). Évite qu'une inscription reste bloquée en Draft alors que le paiement a abouti sur le nouveau checkout. Contrat data.checkoutIntentId confirmé sur un webhook de production.

0.16.0.0 — 2026-06-02

Ajouts

  • Recherche par adhérent ou payeur sur la liste des inscriptions : tapez un nom, prénom ou email pour filtrer instantanément. La recherche est insensible à la casse et couvre à la fois le membre et le payeur (le cas échéant).
  • Tri par date d'arrivée : un bouton bascule permet d'afficher les inscriptions du plus récent au plus ancien ou inversement.
  • Bouton « Réinitialiser les filtres » : apparaît dès qu'un filtre actif est détecté (campagne, statut, recherche ou tri non-défaut) ; un clic remet tout à zéro en une seule action.
  • États vides distincts : une icône et un message différenciés pour « aucune inscription » (liste vide) et « aucun résultat » (filtres actifs sans correspondance).

Corrections

  • Les liens de pagination conservent désormais tous les filtres actifs (campagne, statut, recherche, tri) lorsqu'on navigue entre les pages.