Endpoints automatisables par cron externe (Hermes, GitHub Actions,
ou crontab système) pour gérer les tâches récurrentes de la
plateforme.
src/lib/cron-auth.ts (NEW) : isAuthorizedCronRequest(req) vérifie
l'en-tête Authorization Bearer ${CRON_TOKEN}. Le token est déjà dans
.env.production.
GET /api/cron/reminders :
- Itère bookings carbet CONFIRMED + rentalBookings CONFIRMED dont
startDate ∈ [now+22h, now+26h] (fenêtre 4h pour absorber les
éventuels retards de cron).
- Envoie sendBookingReminder (carbet) ou sendRentalReminder (rental).
- Compte bookingSent/bookingErrors et rentalSent/rentalErrors.
- Audit log scope=cron event=cron.reminders.run avec stats.
- Retourne JSON {ok, window, booking:{candidates,sent,errors},
rental:{candidates,sent,errors}}.
GET /api/cron/cleanup :
- Purge OrgInviteToken expirés depuis > 30j.
- Booking PENDING + paymentStatus≠SUCCEEDED + createdAt > 7j →
status=CANCELLED + paymentStatus=FAILED (libère le créneau).
- RentalBooking idem + delete RentalItemAvailability associée
(libère stock) en transaction.
- Audit log scope=cron event=cron.cleanup.run avec compteurs.
src/lib/email.ts :
- sendBookingReminder(to, firstName, bookingId, title, startDate,
slug) : email rappel J-1 avec CTA vers /reservations/[id].
- sendRentalReminder(to, firstName, rbId, providerName, startDate,
contactInfo) : rappel pour récup matériel, affiche contacts
provider (phone + email).
tests/lib/cron-auth.test.ts (6 cas) :
- Refus si CRON_TOKEN absent, header absent, format incorrect (Basic
ou Token), token mismatch.
- Accept si match exact, accept avec espaces autour du token (defensive).
Total tests : 89/89 ✓.
Schedule recommandé (à brancher côté Hermes ou crontab) :
- GET /api/cron/reminders : 1× par jour à 9h (Authorization: Bearer
$CRON_TOKEN)
- GET /api/cron/cleanup : 1× par semaine
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seed plugin `demo-ce-seed` :
- Nouveau descriptor dans registry (category visual).
- src/lib/plugins/seeds/demo-ce.ts : seedDemoCe() + archiveDemoCe().
Crée org « Comité ESA Kourou (démo) » approved=true + 2 CE_MANAGERs
+ 3 CE_MEMBERs (password "demo") + 2 carbets co-gérés
(OrganizationCarbetMembership) + 1 RentalProvider org-scoped +
4 items (hamac, moustiquaire, kayak, réchaud).
- Idempotent : check existence par slug/email avant create. Upsert
pour users.
- Disable : soft-archive carbets (status=ARCHIVED), delete
RentalProvider démo (best-effort si pas de booking), delete users
démo (cascade memberships) + delete org.
- Branchement hooks onEnable/onDisable dans plugins/hooks.ts.
Permet de visualiser le module CE end-to-end sans signup manuel :
admin active le plugin → l'org démo et son écosystème apparaissent
sur le site (badge « Géré par le CE Comité ESA Kourou (démo) » sur
les fiches carbet, items rental dans le catalogue /materiel).
ce-invites.ts refactor :
- Exporte hashToken (déjà sha256, désormais documenté).
- Extrait isInviteValid(row, now=Date) : helper pur testable. La
fonction getOrgInviteByToken le réutilise.
tests/lib/ce-invites.test.ts (9 cas) :
- hashToken : déterminisme, format sha256 64-hex, inputs différents,
pas de fuite du plain.
- isInviteValid : non consommé+non expiré → vrai ; consommé → faux ;
expiré → faux ; les 2 raisons → faux ; injection now pour tests
temporels.
Total tests : 83/83 ✓ (74 précédents + 9 nouveaux).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marketplace encaisse centralisé sur System D → besoin de tracer les
virements mensuels aux prestataires tiers. Migration appliquée prod.
Schema :
- Modèle RentalPayoutMark { id, providerId, periodMonth, amount,
reference, paidAt, paidByEmail }. Unique (providerId, periodMonth)
→ 1 mark = 1 mois = 1 virement par provider.
Lib src/lib/payouts.ts :
- monthKey(d) → 1er du mois minuit UTC (clé de période).
- listProviderPayouts({monthsBack=6}) → grid provider × mois avec
bookingsCount + grossAmount (itemsTotal) + commission + netAmount
(gross-commission) + statut paid via RentalPayoutMark. Exclut
System D (commission 0%, géré par l'asso). Statut « payé » lu
depuis les marks. Tri : mois desc puis providerName.
- createPayoutMark (idempotent via findUnique avant insert) +
deletePayoutMark.
Politique : net dû = itemsTotal - commissionAmount (depositTotal
hors flux, collecté par le provider auprès du client). Politique
documentée dans le commentaire en tête de payouts.ts.
/admin/payouts/page.tsx :
- 3 KPIs (À payer / Déjà payé / Mois affichés).
- Une section par mois (6 derniers), tableau provider × CA brut +
commission + net dû + statut.
- MarkPaidForm : bouton « Marquer payé » → form inline (amount
pré-rempli avec net dû, reference optionnelle) → action
markPayoutPaidAction. Statut payé montre amount + ref + bouton
« Annuler marquage ».
Server actions :
- markPayoutPaidAction (admin only, idempotent, audit
admin.payouts/payout.mark + payout.already_marked) → envoie
sendPayoutSent au contactEmail du provider (best-effort).
- unmarkPayoutPaidAction → delete + audit payout.unmark.
Email sendPayoutSent : notification au provider quand un virement est
marqué payé. Inclut amount + reference + lien dashboard.
Sidebar admin gagne entrée « Reversements » sous Activité.
Tests vitest tests/lib/payouts.test.ts (4 cas) : monthKey
normalisation UTC + idempotence + janvier sans bug, formatMonth fr-FR.
Total : 74/74 ✓.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Politique de remboursement v1 :
- > 7 jours du début → FULL (location + caution)
- 1 à 7 jours → PARTIAL_50 (50% location + caution intégrale)
- < 24h ou passé → DEPOSIT_ONLY (caution seulement, pas de remboursement
sur la location)
src/lib/rental-refund.ts (NEW) : computeRentalRefund({startDate,
itemsTotal, depositTotal}) → { itemsRefund, depositRefund, totalRefund,
policy, policyLabel }. Arrondi au centime, support de Decimal.
POST /api/rentals/[id]/cancel :
- Auth multi-rôle : tenant de la booking, RENTAL_PROVIDER nominal ou
CE_MANAGER de l'org du provider, ADMIN. Détecte `cancelledBy` pour
adapter l'email.
- Refuse si status ∉ {PENDING, CONFIRMED} (HANDED_OVER → non
annulable, contacter Karbé).
- Calcule le refund selon la politique.
- Stripe refund best-effort si paymentStatus=SUCCEEDED + stripeSessionId
existante + isStripeConfigured + totalRefund > 0. Retrieve session →
payment_intent → refunds.create. Échec Stripe = audit-logged mais
le flip status continue (l'asso pourra rembourser manuellement).
- Transaction : update RentalBooking (CANCELLED + paymentStatus
REFUNDED si SUCCEEDED sinon FAILED) + delete RentalItemAvailability
(libère stock).
- Audit log rental.cancel avec montants, policy, cancelledBy,
stripeRefundId, stripeRefundError.
- Email best-effort : sendRentalCancelled à tenant + provider (sauf si
provider est le canceller).
src/components/CancelRentalButton.tsx : composant client confirm dialog
inline avec textarea motif (max 500 chars). Branché sur :
- /mes-locations : « Annuler ma location » sur résa PENDING/CONFIRMED
- BookingDecision (utilisé par /espace-prestataire/reservations ET
/espace-ce/materiel/reservations) : remplace l'ancienne mini-confirm
qui flippait juste le status, désormais via la vraie API refund
sendRentalCancelled email : adapté selon cancelledBy ("Vous avez annulé"
/ "<Provider> a annulé" / "L'équipe Karbé a annulé").
tests/lib/rental-refund.test.ts : 8 cas (FULL @ 10+ et 7j, PARTIAL_50,
DEPOSIT_ONLY < 24h et passé, arrondi centime, zéro caution, policyLabel).
Total projet : 70/70 ✓.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Public badge sur fiche carbet :
- carbet-public.ts charge les OrganizationCarbetMembership (org
approuvée uniquement) + expose `organizations: {id,name,slug}[]`.
- /carbets/[slug] affiche « Géré par le CE <name> » sous le header
si au moins 1 org liée.
Invites CE_MEMBER :
- Migration 20260603300000_org_invite_token : OrgInviteToken
(tokenHash, organizationId, email?, createdByUserId, expiresAt,
usedAt). Cascade sur Organization. Index expiresAt + organizationId.
- src/lib/ce-invites.ts : createOrgInviteToken (TTL 14j),
listOrgInviteTokens, getOrgInviteByToken (validité + expiry),
markOrgInviteConsumed, revokeOrgInviteToken. Token = 24 bytes
base64url, hash sha256.
- /espace-ce/membres : liste membres (CE_MANAGER + CE_MEMBER actifs)
+ form de génération de lien (email optionnel = lock email côté
signup) + liste des invitations avec statut actif/consommé/expiré +
bouton révoquer.
- /espace-ce/membres/actions.ts : createInviteAction +
revokeInviteAction. Audit log scope=ce.invite.
- API /api/signup étendue : zod accepte inviteToken, branche dédiée
qui crée User CE_MEMBER + organizationId du token + marquage
usedAt. Vérif email match si email fourni dans le token.
- /inscription?invite=TOKEN : récupère l'invite, pré-affiche org name,
lock email si fourni, masque les fieldsets type de compte (forcé
CE_MEMBER).
CTA marketing :
- /pour-comites-entreprise : section CTA « Créer mon espace CE » sous
le rendu content-pages, conditionnée par plugin ce-management.
Tests vitest (tests/lib/ce-access.test.ts) :
- canManageCarbet : admin always, owner direct, CE_MANAGER via org
match, refus si autre org / pas d'org / TOURIST / pas de membership.
- 9 tests, mocks next-auth + @/auth + @/lib/authorization pour éviter
next/server (incompatible vitest sans setup).
- Total tests projet : 62/62 ✓.
Dashboard /espace-ce : lien vers /espace-ce/membres en bas.
Migration prod appliquée.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>