Entités métier
Les concepts centraux de Bell et leurs relations — guest, room, booking, conversation, ticket, integration
Avant de regarder le schéma Drizzle, lis ici. Cette page décrit le quoi (les concepts), la page database-schema décrit le comment (les colonnes).
Vue d'ensemble
Bell est multi-tenant : une organization = un hôtel. Toutes les entités métier sont scopées par organizationId. Un user peut être membre de plusieurs orgs (staff multi-hôtel) mais un guest appartient à une org (on ne partage pas les profils guests entre hôtels).
Les 10 entités principales
1. organization
C'est : un hôtel.
Ce qui la définit : nom, slug (URL-friendly), metadata (adresse, logo, fuseau horaire, devise), timestamp de création.
Pourquoi multi-tenant : Bell vise à terme une gamme d'hôtels. Toutes les requêtes sont scopées par org, les joins cross-org sont impossibles via les procedures tRPC/Eden.
Géré par : Better Auth organization plugin.
2. user
C'est : un humain qui se connecte à Bell, côté staff ou côté guest.
Ce qui le définit : email (unique), nom, mot de passe hashé, timestamp création.
Rôles : via la table member ci-dessous — un user peut avoir différents rôles dans différentes orgs.
Géré par : Better Auth.
3. member (user × organization)
C'est : le lien entre un user et une org, avec un rôle.
Rôles :
| Role | Qui | Peut |
|---|---|---|
owner | fondateur HOAIY, CEO hôtel | tout, y compris facturation et désactiver l'org |
admin | GM, head of staff | tout sauf la facturation — configure les intégrations, invite des staff |
manager | chef de service, night auditor | ops quotidiennes : cardex, chats, analytics |
staff | réceptionniste, serveur | cardex read + write sur son périmètre, chats |
guest | invité de l'hôtel | PWA guest : commandes, chat, voir son profil |
Les 4 premiers sont "staff" côté API (voir staffProcedure dans le middleware).
4. guest
C'est : un profil d'invité dans le cardex d'un hôtel. Existe avant que le guest ne crée son compte user.
Flow :
Colonnes clés : organizationId, roomId?, userId? (null tant qu'il n'a pas signup), checkInStatus (pgEnum — cf. state machines), externalGuestId (ID côté PMS, null si hôtel sans PMS), externalSource (mews, opera, …), externalChannel (booking.com, expedia, direct, … — déduit de Mews).
5. guest_group
C'est : un groupe corporatif ou événementiel (mariage, conférence). Contient plusieurs guest.
Utile pour : bulk actions, contact person unique pour toute la réservation de groupe.
6. room
C'est : une chambre physique de l'hôtel.
Ce qui la définit : numéro, étage, type (single, double, suite, deluxe, penthouse), status (pgEnum : available, occupied, cleaning, maintenance, reserved), prix/nuit, amenities (JSONB : wifi, vue, jacuzzi…), externalRoomId (sync PMS).
Lien bidirectionnel PMS : quand Mews dit "room 302 = Dirty", on set status cleaning. Quand on termine un checkout, on push vers Mews setRoomStatus(302, dirty).
7. conversation + message
C'est : une conversation entre un guest et Bell. Un seul modèle unifié (vs ancien projet qui avait deux systèmes dupliqués — voir ADR futur merger).
Audiences (pgEnum) :
guest_ai: guest discute avec l'IA concierge (par défaut)guest_staff: escalade à un humain après que l'IA a passé la mainstaff_internal: note interne entre staff sur un guest (pas visible par le guest)
Messages :
role:user(le guest),assistant(AI),staff,systemcontent: texte (MD supporté)metadata(JSONB typé) : tool calls si AI, AI cost, hotel tags
8. restaurant_booking / room_service_order / laundry_order / spa_booking
C'est : les 4 types de commandes/réservations qu'un guest peut faire.
Communs :
userId,organizationId,guestIdstatus(pgEnum propre à chaque type — voir state machines)paymentIntentId(Stripe),paymentStatustotalAmount,currency- Timestamps du cycle de vie (
deliveredAt,completedAt, etc.)
Spécificités :
| Type | Champs propres |
|---|---|
restaurant_booking | peopleCount, date, time, specialRequest |
room_service_order | items (JSONB array), deliveryFee, deliveryAddress |
laundry_order | items (JSONB array), specialRequest, collection/delivery times |
spa_booking | serviceId, duration, date, time, therapistId? |
9. ticket
C'est : une escalade créée par le staff depuis un chat.
Ce qui la définit : ticketNumber humain (TKT-00042), catégorie, priorité, status (open → in_progress → waiting → resolved → closed), lien optionnel vers conversationId.
Utilité : un chat se résout en 90 % des cas par le staff directement. Les 10 % restants (plainte, demande spéciale, incident) deviennent des tickets que l'admin voit dans /admin/tickets.
Commentaires : ticket_comment — audit trail des actions staff.
10. integration
C'est : la config d'un PMS (ou OTA futur) connecté à une org.
Ce qui la définit :
provider(pgEnum :mews,opera,cloudbeds,booking,apaleo, …)name(display : "Mews PMS - Oceania Paris")credentials(JSONB chiffré : tokens, keys)platformUrl(endpoint API du provider)externalPropertyId(ID de l'hôtel chez le provider)config(JSONB : options spécifiques)isActive,lastSyncAt,syncStatus(idle/syncing/error)syncError(message d'erreur si le dernier sync a échoué)webhookSecret(pour valider les signatures HMAC des webhooks entrants)
Un hôtel = 0 ou 1 integration par provider. Peut avoir plusieurs providers simultanés si pertinent (ex: Mews pour PMS + Booking API directe pour du channel management avancé — rare).
Log : integration_sync_log trace chaque action sync pour debug.
Entités secondaires (résumé)
| Entité | Rôle |
|---|---|
staff_note | notes internes du staff (cardex, today) avec priorité, assignation |
note_comment | audit trail des notes |
menu_category + menu_item | catalogue des services proposés (restaurant menu, spa soins, laundry items) |
activity_log | journal d'activité métier (booking_created, chat_started, payment_received…) |
interaction_log (hypertable TimescaleDB future) | logs analytics fine-grained |
hotel_event | événements internes hôtel (conférences, mariages) — affichage calendar |
invitation (Better Auth) | invitations staff pending |
session, account, verification | tables Better Auth standard |
Règles d'intégrité
Cascade deletes
organizationsupprimée → tout ce qui lui appartient supprimé (guest, room, bookings, …)usersupprimé →membercascade, mais les guests et bookings restent (pour compliance comptable/RGPD, droit à l'oubli géré séparément)guestsupprimé (soft delete) → bookings restent,guest.deletedAtposé
Contraintes
guest.emailunique parorganizationId(pas par global — un même email peut être guest de 2 hôtels)room.roomNumberunique parorganizationIdintegrationunique par(organizationId, provider)— 1 Mews par org maxuser.emailunique global (Better Auth)
Soft delete
Les entités métier critiques ont deletedAt: timestamp | null. Un soft delete preserve les FK et l'audit. Purge physique après X mois (configurable, voir RGPD).
Entités concernées : guest, booking, conversation, ticket, staff_note.
Entités sans soft delete (purge physique OK) : session, activity_log, integration_sync_log.
Canaux de réservation (channel tracking)
La feature différenciante Bell : détecter qu'un guest vient de Booking.com, Expedia, Airbnb, etc. sans intégration OTA directe, via le CommanderOrigin que Mews remonte dans chaque réservation.
Mapping dans packages/api/src/services/integrations/adapters/mews.ts, fonction inferChannel() :
Mews CommanderOrigin → externalChannel
"Booking.com" → "booking"
"Expedia" → "expedia"
"Airbnb" → "airbnb"
"HRS" → "hrs"
"Hotelbeds" → "hotelbeds"
"Agoda" → "agoda"
"Direct" / null → "direct"
(autre) → "other"Stocké sur guest.externalChannel. Permet :
- Badge coloré dans le cardex (revenue manager voit instantanément la répartition)
- Analytics (revenue par channel, conversion, upsells par canal)
- Sans intégrer aucune OTA nous-mêmes — Mews agrège pour nous
Multi-devise, multi-langue (futur)
- Multi-devise :
currencysur chaque booking +organization.defaultCurrency. Pas de conversion multi-devise au MVP — on affiche dans la devise du booking. - Multi-langue :
user.locale+ templates email multi-langues. Fallback FR. Pas MVP.
Prochaine étape
- Schéma DB complet — toutes les tables, colonnes, index, relations en Drizzle
- State machines — tous les statuts et transitions autorisées