Vue d'ensemble
Flows métier
Les parcours critiques de bout en bout
Cette page décrit les flows complets pour comprendre comment toutes les pièces s'assemblent.
Flow 1, Création d'une demande par email
1. Email arrive sur demandes@reciprok.com
2. Webhook Resend → POST /api/webhooks/email/inbound
3. Backend :
a. Parse l'email (from, subject, body)
b. Cherche un Organizer existant par email
- Si trouvé : utilise son ID
- Sinon : créé un Organizer minimal (email + companyName extrait du domaine)
c. Appel à Claude (ai.ingest.parseEmail) pour extraire les champs structurés
d. Crée une Request en statut "qualifying" (besoin de validation humaine)
- Champs extraits + rawSource = email original
e. Log timeline_event:
- request_created (actor: ai, source: email)
- manual_note avec les champs détectés et ceux manquants
f. Crée la AiConversation liée
g. Lance la recherche initiale en background (search.members.byRequest)
4. Push notification à l'équipe : "Nouvelle demande créée"
5. L'utilisateur ouvre la demande, valide/corrige les champs, passe en "searching"Flow 2, Création d'une demande manuelle
1. L'utilisateur clique "Nouvelle demande" (dialog)
2. Étape 1 : choix du Pool (obligatoire), détermine le type de demande
3. Étape 2 : formulaire adapté au pool
- Champs communs : organisateur, description, dates, budget, format, lieu
- Champs dynamiques pool.extraFields (ex: Hotels → "Nombre de chambres")
4. Submit → api.requests.crud.create({ poolId, organizerId, ... })
5. Backend :
a. Crée ou trouve l'Organizer (par email)
b. Crée la Request avec poolId obligatoire, statut "searching"
c. Stocke les données pool-spécifiques dans poolExtraData (JSONB)
d. Crée la AiConversation
e. Si l'organisateur a été référé par un membre, log un member_added
f. Log request_created (actor: user)
g. Lance la recherche initiale (filtrée par pool)
6. Redirect vers /requests/[id], le hub central de la demande
7. Les premiers résultats arrivent dans les ~3-5 secondesFlow 3, Recherche initiale et ranking
Trigger : à la création de la demande, ou re-déclenché manuellement
1. api.search.members.byRequest({ requestId })
2. Étape 1, Hard filtering SQL :
a. Lit la Request (dont poolId obligatoire)
b. Construit la clause WHERE :
- poolId = request.poolId (filtre primaire)
- geo (postalCode, area, rayon GPS si lat/lng dispo)
- capacité, dates, statut actif
c. Exécute → candidats dans le même pool uniquement
3. Étape 2, Vector search :
a. Construit le texte de query depuis description + complementaryInfo + format + budget
b. Génère l'embedding (OpenAI)
c. SQL pgvector ORDER BY distance LIMIT 50 (toujours filtré par poolId)
4. Étape 3, LLM ranking :
a. Construit un prompt avec les 50 finalistes (résumés)
b. Appel à Claude
c. Reçoit { recommended[], compatible[], rejected[] }
5. Persistence :
a. Pour chaque membre dans recommended/compatible : INSERT request_result avec aiScore + aiExplanation + stage
b. isHidden = true par défaut (invisible dans le catalogue public tant que pas toggle)
c. Log timeline_event: ai_suggestion avec le détail
6. Retourne au frontend → affichage immédiatFlow 4, Conversation avec l'IA
1. L'utilisateur ouvre la demande, clique sur le chat
2. Charge ai.chat.listMessages({ requestId }) → historique
3. L'utilisateur écrit "Trouve-moi un rooftop dans le 8ème pour 100 personnes"
4. api.ai.chat.sendMessage({ requestId, content })
5. Backend :
a. Append user message dans aiMessage
b. Construit le contexte : Request + résultats actuels + KB pertinente
c. Boucle Claude :
- Premier appel avec system + context + history
- Claude répond avec un tool_use : search_members({ postalCodes: ["75008"], minCapacity: 100, semanticQuery: "rooftop" })
- Backend exécute search.members.byPrompt
- Retourne les résultats à Claude
- Claude formule une réponse avec les codes #102 et #205
d. Stream la réponse au frontend (SSE)
e. Append assistant message
6. L'utilisateur voit la réponse en streaming
7. L'utilisateur clique sur "Ajouter #102 aux résultats" depuis le chat
8. Tool call : add_member_to_results
- Mutation request_result.create
- Log timeline_event: member_added (actor: ai, then accepted by user)Flow 5, Composition et envoi du catalogue
1. Sur la page hub /requests/[id], section "Catalogue" :
- Les membres ajoutés via la recherche sont listés
- Toggle oeil pour montrer/cacher chaque membre (isHidden)
- Drag & drop pour réordonner (position)
- Compteur "X visibles / Y total"
2. L'utilisateur compose le catalogue en rendant visibles les bons membres
3. api.requests.catalog.compose({ requestId, recommended[], compatible[] })
→ Met à jour les pipelineStage des request_result
4. L'utilisateur clique "Envoyer le catalogue"
5. api.requests.catalog.send({ requestId, withWhatsapp? })
6. Backend :
a. Génère un access token pour le catalogue
b. Crée le Catalog en DB avec publicUrl persisté (/public/catalogs/:token)
c. Génère l'email avec react-email (templates avec les fiches membres)
d. Envoie via Resend
e. Si withWhatsapp : envoie aussi via Meta Cloud API
f. Log timeline_event: catalog_sent + email_sent + whatsapp_sent
g. Status de la demande → catalog_sent
7. L'organisateur reçoit l'email avec le lien magic
8. Le lien public affiche les membres visibles sur une carte Mapbox + liste
9. publicUrl persiste, le lien reste valide pour ré-accès sans regénérer
- Bouton "Copier le lien" + "Ouvrir" sur la card du catalogue
- Bouton "Regénérer le lien" si nécessaireFlow 6, Requalification
1. L'utilisateur identifie qu'il manque des infos (budget, format)
2. Click "Requalifier"
3. Modal : choix des champs manquants à demander, message personnalisé
4. L'IA propose un brouillon d'email
5. L'utilisateur valide
6. api.requests.qualification.send
7. Backend :
a. Génère un token magic pour la réponse
b. Construit l'URL : reciprok.com/q/<token>
c. Envoie l'email
d. Optionnellement : WhatsApp jumelé
e. Log timeline_event: qualification_sent
f. Status → qualifying
8. L'organisateur clique le lien
9. Page publique avec un mini formulaire (les champs manquants seulement)
10. Submit → api.public.qualification.respond({ token, payload })
11. Backend :
a. Valide le token
b. Update les champs de la Request
c. Log timeline_event: qualification_received
d. Push notification à l'utilisateur
12. (Si réponse vocale) :
- L'organisateur a aussi la possibilité d'enregistrer un audio sur la même page
- Whisper transcrit
- L'IA extrait les champs
- Update + notificationFlow 7, Demande de disponibilité
1. L'utilisateur sélectionne plusieurs membres dans les résultats
2. Click "Demander leur dispo"
3. api.members.availability.request({ memberId, requestId, dates })
4. Backend :
a. Pour chaque membre :
- Génère un token magic
- Crée un email avec les détails et le lien
- Envoie
- Log availability_requested
5. Le membre reçoit l'email
6. Click sur le lien → page publique
7. Choix : Disponible / Partiellement / Non disponible (+ notes)
8. Submit → api.public.availability.respond({ token, status, notes })
9. Backend :
a. Met à jour `request_availability_check.respondedAt` + `acceptedDateIds`
b. Log availability_response sur la timeline
c. Push à l'équipeFlow 8, Sélection du gagnant et commission
1. L'utilisateur identifie le membre choisi par l'organisateur
2. Click "Sélectionner comme gagnant" sur la carte du membre
3. api.requests.results.selectWinner({ requestId, memberId })
4. Backend :
a. Update request_result du gagnant : pipelineStage → "won"
b. Update les autres request_result : pipelineStage → "lost"
c. Update la Request : status → "won"
d. Log winner_selected, status_changed
e. Génère deux emails :
- Au gagnant : "Vous avez été sélectionné"
- Aux non-retenus : "Cette fois ne sera pas la bonne"
f. Envoie les emails (avec confirmation utilisateur si cochée)
g. Log les emails
5. L'utilisateur clique "Créer la commission"
6. Modal : montant, isFirstClient, referredBy (auto-rempli)
7. api.requests.commission.create
8. Status → "commission"
9. Plus tard : sendInvoice → status → "completed"Flow 9, Mise à jour de KB par un membre
1. Le membre reçoit son lien dashboard à l'onboarding
2. Click → vérification du token → page member dashboard
3. Onglet "Ma présentation"
4. Click "Enregistrer un audio de présentation"
5. Frontend : MediaRecorder, upload vers /api/voice-upload?token=xxx
6. Backend :
a. Vérifie le token
b. Upload sur S3 (member-audios)
c. Créé un job en queue : "transcribe-and-structure"
7. Worker async :
a. Whisper → transcription
b. Sauvegarde le transcript brut
c. Claude structure → liste de KnowledgeEntry
d. Pour chaque entry : embedding + INSERT
e. Re-embed le membre (concat de tout le contenu)
f. Notifie l'équipe : "Nouveau contenu KB pour #102"
8. Le membre voit "Audio en cours de traitement..." puis "Traité, X entrées créées"
9. L'équipe valide / corrige les entrées dans son interface adminFlow 10, Cas critique : édition en contexte
C'est LE point central pour le fondateur.
Scénario : l'utilisateur regarde une demande, voit que le membre #102 dans les résultats n'a pas de photos.
1. Sur la carte du membre #102, click "Modifier la fiche"
2. Modal latérale s'ouvre (pas de navigation, on reste sur la demande)
3. Modal contient :
- Informations générales (nom, description, contact)
- Photos (galerie + bouton upload)
- Tags
- Capacités
- Espaces / salles
4. L'utilisateur upload 4 photos
5. api.members.photos.upload (en parallèle)
6. Les photos apparaissent dans la modal au fur et à mesure
7. L'utilisateur ferme la modal
8. La carte du membre dans les résultats se rafraîchit avec les nouvelles photos
9. L'utilisateur n'a JAMAIS quitté la page de la demandePour que ce flow marche :
- Toutes les mutations sont des routes tRPC simples (pas de redirect après save)
- L'invalidation de cache est ciblée (TanStack Query), invalider
member.byId({ id: 102 })met à jour partout dans l'UI - Le state du formulaire de la modal est local (TanStack Form), pas de state global qui pollue
Flow 11, Training de l'IA
1. L'utilisateur ouvre une demande passée (ou une demande fictive générée par l'IA)
2. Voit le catalogue qui avait été proposé (par l'IA ou par lui à l'époque)
3. Mode training : l'utilisateur corrige
- Retire le membre #157 ("trop cher pour ce profil")
- Ajoute le membre #298 ("ils sont parfaits pour ça")
- Réordonne
4. Click "Enregistrer la correction"
5. api.ai.training.recordCorrection
- originalSuggestion: snapshot de l'avant
- correction: snapshot de l'après
- notes: optionnel
6. Stocké dans training_session
7. Plus tard, ces corrections alimentent :
- Un fine-tuning éventuel
- Un système de RAG sur les "patterns de correction"
- Un meilleur prompt système ("Note : pour les événements pro haut de gamme, préfère les lieux avec ces caractéristiques...")