Reciprok Docs
Données

Entités

Modèle de données, entités principales et relations

Vue d'ensemble des entités qui composent le domaine. Les schémas Drizzle détaillés sont dans la page suivante (database-schema).

Entités structurantes

Pool

Catégorie métier. Chaque membre et chaque demande appartient obligatoirement à un pool. Le formulaire de création de demande s'adapte dynamiquement au pool choisi via extraFields.

Champs clés :

  • id (uuid)
  • name (unique, ex: "Hotels", "Restaurants", "Traiteurs")
  • slug (unique)
  • description, iconName
  • extraFields (JSONB, champs supplémentaires du formulaire de demande, ex: Hotels → { roomCount: { type: "number", label: "Nombre de chambres" } })
  • position (int, ordre d'affichage)
  • parentPoolId (FK optional → Pool, un seul niveau de hiérarchie)
  • createdAt, updatedAt

Sub-pools : un pool peut être un sous-ensemble d'un autre (ex: RestaurantSalon privé). Le filtrage par un pool racine inclut automatiquement les items de ses descendants. La hiérarchie n'a qu'un seul niveau.

Group

Groupe d'entreprise (chaîne, holding, multi-établissements). Les indépendants appartiennent au group built-in "INDEPENDANT".

Champs clés : id, name, description, logoUrl, legacyId?.

Pool vs Group : Pool = catégorie métier (Hotels), Group = entreprise (Groupe Alcazar = 7 restaurants). Un membre a obligatoirement les deux.

Entités centrales

Member

Établissement ou prestataire. Entité unifiée : pas de séparation Restaurant/Lieu/Prestataire, le profil métier est porté par les tags, les attributes polymorphes et le pool.

Champs clés :

  • id, code (string unique court, ex: #102)
  • name, description
  • address, city, postalCode, country, lat, lng
  • email, phone, contactName, website
  • poolId (FK obligatoire → Pool), groupId (FK obligatoire → Group)
  • Capacités venue-level (privatisation totale, indépendantes des rooms) : maxCapacityOverall, maxCapacityCocktail, maxCapacityBanquet, maxCapacityTheater
  • budgetLow, budgetHigh, minBookingAmount
  • transport (JSONB, Array<{ type, details }>, ex: Métro ligne 1, Bus 21, Parking Vinci)
  • pricingDetails (JSONB, deposit, min-booking par période, dry-hire, price-by-day-of-week)
  • legacyExtras (JSONB, { chefName?, menu?, info? } issus de V1)
  • memo (text, bloc-notes équipe, visible par l'IA comme contexte)
  • status (enum: active / inactive / configuring)
  • openingHours (JSONB structuré par jour/créneau)
  • embedding (vector pgvector, recherche sémantique)
  • isFavorite (bool, boost le ranking dans les suggestions de catalogue)
  • legacyId, legacySource, deletedAt (soft delete RGPD)

Les capacités précises par configuration sont stockées dans RoomCapacity (lié aux rooms). Les capacités venue-level servent au mode "Établissement entier" / privatisation totale.

Relations : N..1 Pool · N..1 Group · 1..N Room · 1..N MemberPhoto · N..M Tag · 1..N KnowledgeEntry · N..M Attribute · 1..N RequestResult · 1..N Commission.

Room

Espace/salle au sein d'un membre. Tarifs et horaires propres.

Champs clés : id, memberId, name, description, imageUrl, minPriceLunch, minPriceDinner, privatizationFee, openingHoursLunch, openingHoursDinner, properties (JSONB).

RoomCapacity

Capacité d'une salle pour une configuration événementielle Kactus (7 standards). Table dédiée car une salle ne supporte pas forcément les 7 configs, et l'indexation (configuration, max_capacity) accélère les filtres SQL.

Champs : id, roomId, configuration (enum), maxCapacity, notes. Unique sur (roomId, configuration).

Organizer

Le client qui cherche un lieu.

Champs clés : id, companyName, type (enum: company / individual / agency / traiteur / prestataire), contactName, position, address, postalCode, email (unique), phone, isFavorite, memo (bloc-notes équipe inclus dans le contexte IA), firstContactAt, referredByMemberId?.

Request

L'entité centrale. Une demande d'événement.

Champs clés :

  • id, code (ex: REQ-2026-0142)
  • organizerId, poolId (FK obligatoire, détermine le formulaire)
  • description, participantsCount, budget
  • formats (array enum)
  • flexibleDates, postalCode, area
  • complementaryInfo
  • referredByMemberId?, isFirstRequest (impact commission)
  • source (enum: manual / email / voice / ai_agent), rawSource (JSONB)
  • poolExtraData (JSONB, données pool-spécifiques, ex: { roomCount: 30 })
  • tagIds (text array, tags portés par la demande pour le matching)
  • memo (bloc-notes équipe, inclus dans le contexte IA)
  • status (enum), version (int, locks optimistes)
  • externalVenueName, externalVenueCity (organisateur a réservé hors-réseau, pas de commission)
  • lostReason
  • legacyId?, importedFromV1 (V1 = read-only, exclu du workflow actif)
  • embedding (vector, feed le tool find_similar_requests)
  • createdAt, updatedAt, closedAt

RequestDate

Plusieurs dates candidates par demande (l'organisateur hésite entre 2-3 options). priority pour l'ordre de préférence, isFlexible pour ajustement.

RequestResult

Liaison demande ↔ membre dans le pipeline.

Champs clés :

  • id, requestId, memberId
  • roomId? (pin une salle précise quand la recherche matche une salle plutôt que le membre entier)
  • pipelineStage (enum: search_result / catalog_recommended / catalog_compatible / sent / negotiating / won / lost)
  • position (int, ordre dans le catalogue)
  • aiScore (0..1), aiExplanation
  • isHidden (bool, default true, caché du catalogue public par défaut)
  • isShortlisted, shortlistedAt (pre-sélection orga côté public, avant le choix final)
  • catalogSentAt (dernier envoi du lien catalogue à ce membre)
  • addedBy (ai / user), addedAt, updatedAt

Un même membre peut apparaître plusieurs fois dans une demande (une entrée par salle distincte du membre).

Catalog

La sélection finale envoyée à l'organisateur (recommandés + compatibles).

Champs : id, requestId, sentAt, viewedAt, accessTokenHash, publicUrl (chemin persistant /public/catalogs/:token).

RequestAvailabilityCheck

Magic link envoyé au membre : "Dispo le 12 mai ?". Le membre coche les dates qu'il accepte parmi celles de la demande. Une seule check active par requestResult.

Champs : id, resultId (unique), tokenHash, sentAt, respondedAt, acceptedDateIds (sous-set de RequestDate.id), respondedBy ("member" via magic link / "admin" override manuel).

RequestPositioningCheck

"Souhaitez-vous vous positionner sur cet événement ?". Réponse binaire avec un null possible (en attente).

Champs : id, resultId (unique), tokenHash, sentAt, respondedAt, accepted (bool?), respondedBy.

RequestRequalification

Token public envoyé à l'organisateur pour qu'il complète les champs manquants identifiés par l'IA (budget, capacité, format…). Historique préservé (une row par envoi).

Champs : id, requestId, tokenHash, publicUrl, missingFields (JSONB array), personalMessage, sentAt, viewedAt, submittedAt, submittedPayload, appliedAt, appliedBy.

Attributs polymorphes

Système de matching avancé. Quatre tables qui définissent les dimensions métier (Typologie, Capacité assise/debout, Budget par pax, Min spend, Privatisation, Formats, Prestations, Ambiance…) avec un scoring pondéré.

AttributeType

Définit la dimension. kind détermine la forme de la valeur.

Champs : id, name (unique), slug, kind (enum: enum / multi_enum / boolean / number_range), description, unit ("pax" / "EUR" / null), isHard (bool, violation élimine), weight (poids dans le score), rangePresets (JSONB, presets sliders pour number_range).

AttributeOption

Option d'un AttributeType (pour les kinds enum / multi_enum).

Champs : id, attributeTypeId, label, slug, description, position.

MemberAttribute / RequestAttribute

Valeur portée par un membre (ou une demande). Une seule colonne est remplie selon le kind : optionId (enum/multi_enum), numberMin+numberMax (number_range), boolValue (boolean).

Champs partagés :

  • id, (memberId | requestId), attributeTypeId
  • optionId?, numberMin?, numberMax?, boolValue?
  • confidence (0..1, 1.0 si humain, inférieur à 1 si extraction IA)
  • source ("user" / "ai" / "import")
  • RequestAttribute peut surcharger isHard au cas par cas

Pipeline matching : hard filters SQL → soft scoring pondéré → ranking avec explication par critère (modules/attributes/matching.engine.ts).

Tags

TagCategory

Catégorie de tags (Type, Ambiance, Service, Cuisine, Autre, …). Table dédiée pour permettre l'ajout de catégories sans migration de schéma.

Champs : id, name (unique), slug, description, iconName, position.

Tag

Tag classifié sous une catégorie. Unique sur (name, categoryId), deux catégories peuvent porter un tag du même libellé.

Champs : id, name, categoryId.

RequestLabel

Label coloré attaché aux demandes (étiquette de tri/visualisation interne). N..M via RequestLabelAssignment.

Champs : id, name, color, legacyId?.

Timeline, communications, IA

TimelineEvent

Append-only log de tout ce qui se passe sur une demande. Codes typés via @reciprok/domain/event-codes.

Champs : id, requestId, type (text), payload (JSONB), actor (enum), actorId.

EmailThread / EmailMessage

Toutes les communications email liées à une demande. EmailMessage.providerId = id Resend, sert au reconciliation des webhooks status (delivered / opened / bounced).

Routing inbound : webhook Resend matche le request.code dans le subject ou le body pour rattacher au bon thread. Sans match → message orphelin (queue inbox).

WhatsAppMessage

Messages WhatsApp via Meta Cloud API. Indépendants des threads email.

Champs : id, requestId?, direction ("inbound" / "outbound"), phoneFrom, phoneTo, body, wamid (Meta message id), status (pending / sent / delivered / read / failed / received), sentAt, deliveredAt, readAt, metadata.

AiConversation / AiMessage

Chat IA lié à une demande. Une conversation par demande, persistante. AiMessage.content est JSONB (texte, tool calls, tool results).

KnowledgeEntry

Entrée dans la base de connaissances d'un membre. Sourcée et indexée (pgvector).

Champs : id, memberId, type (general_description / room_description / pricing / ambiance / availability_rule / internal_note / audit_note), content, source (voice_transcript / manual / audit / ai_structured), sourceRef (JSONB), embedding.

TrainingSession

Corrections de l'utilisateur sur des suggestions IA (catalogues, recommendations). Sert au training continu.

Champs : id, userId, requestId?, originalSuggestion (JSONB), correction (JSONB), notes.

AiUsage

Log agrégé par (userId, date) des tokens consommés et du coût en EUR. Alimenté à chaque appel Claude/OpenAI, lu par la page /settings/usage.

Champs : id, userId, date, provider, model, inputTokens, outputTokens, cachedTokens, costEur, context.

Commissions

Commission

Champs : id, requestId, memberId, amount, currency (default EUR), rate, rateReason (explique le taux résolu : "default" / "new_client" / "recurring_referral" / "member_override" / "request_override"), isFirstClient, referredByMemberId?, invoicedAt, paidAt, status (pending / invoiced / paid / cancelled).

CommissionRule

Règles de calcul. Un default + overrides par membre ou par demande.

Champs : id, scope (default / member / request), scopeId (memberId ou requestId selon scope, null pour default), defaultRate, newClientRate?, recurringClientRate?, firstReferralRate?, recurringReferralRate?, effectiveFrom, effectiveTo?.

Résolution : request overridemember overridedefault. Première règle effective qui matche gagne.

Magic tokens, member info requests, travel cache

MagicToken

Accès public par token pour les membres (dashboard) ou organisateurs (catalogue, requalification). Stocké hashé.

Champs : id, scope (member / organizer), scopeId, tokenHash, expiresAt, usedAt, revokedAt.

MemberInfoRequest

Magic-link envoyé à un membre pour qu'il remplisse sa fiche (description, salles, capacités, formats, photos…). Auto-extraction IA via parseMemberBrief puis review humaine avant écriture.

Champs : id, memberId, tokenHash, initiatedBy ("team" / "member"), personalMessage, createdByUserId, sentAt, viewedAt, submittedAt, cancelledAt, submittedPayload (JSONB, texte + transcripts vocaux), aiStructured (JSONB, diff extrait), aiParsedAt, appliedAt, appliedBy, appliedFields (string[], audit des champs réécrits).

Index unique partiel : une seule demande active par membre (submittedAt IS NULL AND appliedAt IS NULL AND cancelledAt IS NULL).

TravelEstimateCache

Cache des temps de trajet IA-générés affichés sur le catalogue public (4 modes × N membres). Clé sha256 sur (catalogToken, roundedOrigin@100m, sortedMemberIds).

Champs : cacheKey (PK), data (JSONB), createdAt. TTL 24h enforced en code.

Utilisateurs internes & ticketing

User

Géré par Better Auth (user, session, account, verification). Ajoute un enum userRoleEnum (dev / admin / user) :

  • dev voit tout (Sync, Crons, RGPD, /showcase, V1 toggle)
  • admin voit l'app métier mais pas les ops dev-only
  • user est le défaut pour tout nouveau compte (pas d'accès admin)

Notification / PushSubscription

Notif interne + abonnement Web Push pour le browser. Notification.type enuméré.

Ticket / TicketComment / TicketAttachment

Suivi interne des bugs et features. Workflow : todoin_progressawaiting_validation → admin valide → closed.

Partenaires

Partner / PartnerPhoto

Partenaires commerciaux/techniques (audio/vidéo, production, etc.) exposés dans les catalogues à côté des membres.

Champs Partner : id, name, description, contactEmail, contactPhone, websiteUrl, categoryName, isFeatured, featuredPosition, legacyId?.

Relations principales

Vue d'ensemble, Request au centre

Le membre et son écosystème

Les règles de commission

Logique de résolution : request overridemember overridedefault. La première règle qui matche est appliquée.

On this page