17 KiB
Booking Workflow - Todo List
Ce document détaille toutes les tâches nécessaires pour implémenter le workflow complet de booking avec système d'acceptation/refus par email et notifications.
Vue d'ensemble
Le workflow permet à un utilisateur de:
- Sélectionner une option de transport depuis les résultats de recherche
- Remplir un formulaire avec les documents nécessaires
- Envoyer une demande de booking par email au transporteur
- Le transporteur peut accepter ou refuser via des boutons dans l'email
- L'utilisateur reçoit une notification sur son dashboard
Backend - Domain Layer (3 tâches)
✅ Task 2: Créer l'entité Booking dans le domain
Fichier: apps/backend/src/domain/entities/booking.entity.ts (à créer)
Actions:
- Créer l'enum
BookingStatus(PENDING, ACCEPTED, REJECTED, CANCELLED) - Créer la classe
Bookingavec:id: stringuserId: stringorganizationId: stringcarrierName: stringcarrierEmail: stringorigin: PortCodedestination: PortCodevolumeCBM: numberweightKG: numberpriceEUR: numbertransitDays: numberstatus: BookingStatusdocuments: Document[](Bill of Lading, Packing List, Commercial Invoice, Certificate of Origin)confirmationToken: string(pour les liens email)requestedAt: DaterespondedAt?: Datenotes?: string
- Méthodes:
accept(),reject(),cancel(),isExpired()
✅ Task 3: Créer l'entité Notification dans le domain
Fichier: apps/backend/src/domain/entities/notification.entity.ts (à créer)
Actions:
- Créer l'enum
NotificationType(BOOKING_ACCEPTED, BOOKING_REJECTED, BOOKING_CREATED) - Créer la classe
Notificationavec:id: stringuserId: stringtype: NotificationTypetitle: stringmessage: stringbookingId?: stringisRead: booleancreatedAt: Date
- Méthodes:
markAsRead(),isRecent()
Backend - Infrastructure Layer (4 tâches)
✅ Task 4: Mettre à jour le CSV loader pour passer companyEmail
Fichier: apps/backend/src/infrastructure/carriers/csv-loader/csv-rate-loader.adapter.ts
Actions:
- ✅ Interface
CsvRowdéjà mise à jour aveccompanyEmail - Modifier la méthode
mapToCsvRate()pour passerrecord.companyEmailau constructeur deCsvRate - Ajouter
'companyEmail'dans le tableaurequiredColumnsdevalidateCsvStructure()
Code à modifier (ligne ~267):
return new CsvRate(
record.companyName.trim(),
record.companyEmail.trim(), // NOUVEAU
PortCode.create(record.origin),
// ... reste
)
✅ Task 5: Créer le repository BookingRepository
Fichiers à créer:
apps/backend/src/domain/ports/out/booking.repository.ts(interface)apps/backend/src/infrastructure/persistence/typeorm/entities/booking.orm-entity.tsapps/backend/src/infrastructure/persistence/typeorm/repositories/booking.repository.ts
Actions:
- Créer l'interface du port avec méthodes:
create(booking: Booking): Promise<Booking>findById(id: string): Promise<Booking | null>findByUserId(userId: string): Promise<Booking[]>findByToken(token: string): Promise<Booking | null>update(booking: Booking): Promise<Booking>
- Créer l'entité ORM avec décorateurs TypeORM
- Implémenter le repository avec TypeORM
✅ Task 6: Créer le repository NotificationRepository
Fichiers à créer:
apps/backend/src/domain/ports/out/notification.repository.ts(interface)apps/backend/src/infrastructure/persistence/typeorm/entities/notification.orm-entity.tsapps/backend/src/infrastructure/persistence/typeorm/repositories/notification.repository.ts
Actions:
- Créer l'interface du port avec méthodes:
create(notification: Notification): Promise<Notification>findByUserId(userId: string, unreadOnly?: boolean): Promise<Notification[]>markAsRead(id: string): Promise<void>markAllAsRead(userId: string): Promise<void>
- Créer l'entité ORM
- Implémenter le repository
✅ Task 7: Créer le service d'envoi d'email
Fichier: apps/backend/src/infrastructure/email/email.service.ts (à créer)
Actions:
- Utiliser
nodemailerou un service comme SendGrid/Mailgun - Créer la méthode
sendBookingRequest(booking: Booking, acceptUrl: string, rejectUrl: string) - Créer le template HTML avec:
- Récapitulatif du booking (origine, destination, volume, poids, prix)
- Liste des documents joints
- 2 boutons CTA: "Accepter la demande" (vert) et "Refuser la demande" (rouge)
- Design responsive
Template email:
<!DOCTYPE html>
<html>
<head>
<style>
/* Styles inline pour compatibilité email */
</style>
</head>
<body>
<h1>Nouvelle demande de réservation - Xpeditis</h1>
<div class="summary">
<h2>Détails du transport</h2>
<p><strong>Route:</strong> {{origin}} → {{destination}}</p>
<p><strong>Volume:</strong> {{volumeCBM}} CBM</p>
<p><strong>Poids:</strong> {{weightKG}} kg</p>
<p><strong>Prix:</strong> {{priceEUR}} EUR</p>
<p><strong>Transit:</strong> {{transitDays}} jours</p>
</div>
<div class="documents">
<h3>Documents fournis:</h3>
<ul>
{{#each documents}}
<li>{{this.name}}</li>
{{/each}}
</ul>
</div>
<div class="actions">
<a href="{{acceptUrl}}" class="btn btn-accept">✓ Accepter la demande</a>
<a href="{{rejectUrl}}" class="btn btn-reject">✗ Refuser la demande</a>
</div>
</body>
</html>
Backend - Application Layer (5 tâches)
✅ Task 8: Ajouter companyEmail dans le DTO de réponse
Fichier: apps/backend/src/application/dto/csv-rate-search.dto.ts
Actions:
- Ajouter
@ApiProperty() companyEmail: string;dansCsvRateSearchResultDto - Mettre à jour le mapper pour inclure
companyEmail
✅ Task 9: Créer les DTOs pour créer un booking
Fichier: apps/backend/src/application/dto/booking.dto.ts (à créer)
Actions:
- Créer
CreateBookingDtoavec validation:
export class CreateBookingDto {
@ApiProperty()
@IsString()
carrierName: string;
@ApiProperty()
@IsEmail()
carrierEmail: string;
@ApiProperty()
@IsString()
origin: string;
@ApiProperty()
@IsString()
destination: string;
@ApiProperty()
@IsNumber()
@Min(0)
volumeCBM: number;
@ApiProperty()
@IsNumber()
@Min(0)
weightKG: number;
@ApiProperty()
@IsNumber()
@Min(0)
priceEUR: number;
@ApiProperty()
@IsNumber()
@Min(1)
transitDays: number;
@ApiProperty({ type: 'array', items: { type: 'string', format: 'binary' } })
documents: Express.Multer.File[];
@ApiProperty({ required: false })
@IsOptional()
@IsString()
notes?: string;
}
- Créer
BookingResponseDto - Créer
NotificationDto
✅ Task 10: Créer l'endpoint POST /api/v1/bookings
Fichier: apps/backend/src/application/controllers/booking.controller.ts (à créer)
Actions:
- Créer le controller avec méthode
createBooking() - Utiliser
@UseInterceptors(FilesInterceptor('documents'))pour l'upload - Générer un
confirmationTokenunique (UUID) - Sauvegarder les documents sur le système de fichiers ou S3
- Créer le booking avec status PENDING
- Générer les URLs d'acceptation/refus
- Envoyer l'email au transporteur
- Créer une notification pour l'utilisateur (BOOKING_CREATED)
- Retourner le booking créé
Endpoint:
@Post()
@UseGuards(JwtAuthGuard)
@UseInterceptors(FilesInterceptor('documents', 10))
@ApiOperation({ summary: 'Create a new booking request' })
@ApiResponse({ status: 201, type: BookingResponseDto })
async createBooking(
@Body() dto: CreateBookingDto,
@UploadedFiles() files: Express.Multer.File[],
@Request() req
): Promise<BookingResponseDto> {
// Implementation
}
✅ Task 11: Créer l'endpoint GET /api/v1/bookings/:id/accept
Fichier: apps/backend/src/application/controllers/booking.controller.ts
Actions:
- Endpoint PUBLIC (pas de auth guard)
- Vérifier le token de confirmation
- Trouver le booking par token
- Vérifier que le status est PENDING
- Mettre à jour le status à ACCEPTED
- Créer une notification pour l'utilisateur (BOOKING_ACCEPTED)
- Rediriger vers
/booking/confirm/:token(frontend)
Endpoint:
@Get(':id/accept')
@ApiOperation({ summary: 'Accept a booking request (public endpoint)' })
async acceptBooking(
@Param('id') bookingId: string,
@Query('token') token: string
): Promise<void> {
// Validation + Update + Notification + Redirect
}
✅ Task 12: Créer l'endpoint GET /api/v1/bookings/:id/reject
Fichier: apps/backend/src/application/controllers/booking.controller.ts
Actions:
- Endpoint PUBLIC (pas de auth guard)
- Même logique que accept mais avec status REJECTED
- Créer une notification BOOKING_REJECTED
- Rediriger vers
/booking/reject/:token(frontend)
✅ Task 13: Créer l'endpoint GET /api/v1/notifications
Fichier: apps/backend/src/application/controllers/notification.controller.ts (à créer)
Actions:
- Endpoint protégé (JwtAuthGuard)
- Query param optionnel
?unreadOnly=true - Retourner les notifications de l'utilisateur
Endpoints supplémentaires:
PATCH /api/v1/notifications/:id/read- Marquer comme luPATCH /api/v1/notifications/read-all- Tout marquer comme lu
Frontend (9 tâches)
✅ Task 14: Modifier la page results pour rendre les boutons Sélectionner cliquables
Fichier: apps/frontend/app/dashboard/search/results/page.tsx
Actions:
- Modifier le bouton "Sélectionner cette option" pour rediriger vers
/dashboard/booking/new - Passer les données du rate via query params ou state
- Exemple:
/dashboard/booking/new?rateData=${encodeURIComponent(JSON.stringify(option))}
✅ Task 15: Créer la page /dashboard/booking/new avec formulaire multi-étapes
Fichier: apps/frontend/app/dashboard/booking/new/page.tsx (à créer)
Actions:
- Créer un formulaire en 3 étapes:
- Étape 1: Confirmation des détails du transport (lecture seule)
- Étape 2: Upload des documents (Bill of Lading, Packing List, Commercial Invoice, Certificate of Origin)
- Étape 3: Révision et envoi
Structure:
interface BookingForm {
// Données du rate (pré-remplies)
carrierName: string;
carrierEmail: string;
origin: string;
destination: string;
volumeCBM: number;
weightKG: number;
priceEUR: number;
transitDays: number;
// Documents à uploader
documents: {
billOfLading?: File;
packingList?: File;
commercialInvoice?: File;
certificateOfOrigin?: File;
};
// Notes optionnelles
notes?: string;
}
✅ Task 16: Ajouter upload de documents
Fichier: apps/frontend/app/dashboard/booking/new/page.tsx
Actions:
- Utiliser
<input type="file" multiple accept=".pdf,.doc,.docx" /> - Afficher la liste des fichiers sélectionnés avec possibilité de supprimer
- Validation: taille max 5MB par fichier, formats acceptés (PDF, DOC, DOCX)
- Preview des noms de fichiers
Composant:
<div className="space-y-4">
<div>
<label>Bill of Lading *</label>
<input
type="file"
accept=".pdf,.doc,.docx"
onChange={(e) => handleFileChange('billOfLading', e.target.files?.[0])}
/>
</div>
{/* Répéter pour les autres documents */}
</div>
✅ Task 17: Créer l'API client pour les bookings
Fichier: apps/frontend/src/lib/api/bookings.ts (à créer)
Actions:
- Créer
createBooking(formData: FormData): Promise<BookingResponse> - Créer
getBookings(): Promise<Booking[]> - Utiliser
upload()declient.tspour les fichiers
✅ Task 18: Créer la page /booking/confirm/:token (acceptation publique)
Fichier: apps/frontend/app/booking/confirm/[token]/page.tsx (à créer)
Actions:
- Page publique (pas de layout dashboard)
- Afficher un message de succès avec animation
- Afficher le récapitulatif du booking accepté
- Message: "Merci d'avoir accepté cette demande de transport. Le client a été notifié."
- Design: card centrée avec icône ✓ verte
✅ Task 19: Créer la page /booking/reject/:token (refus publique)
Fichier: apps/frontend/app/booking/reject/[token]/page.tsx (à créer)
Actions:
- Page publique
- Formulaire optionnel pour raison du refus
- Message: "Vous avez refusé cette demande de transport. Le client a été notifié."
- Design: card centrée avec icône ✗ rouge
✅ Task 20: Ajouter le composant NotificationBell dans le dashboard
Fichier: apps/frontend/src/components/NotificationBell.tsx (à créer)
Actions:
- Icône de cloche dans le header du dashboard
- Badge rouge avec le nombre de notifications non lues
- Dropdown au clic avec liste des notifications
- Marquer comme lu au clic
- Lien vers le booking concerné
Intégration:
- Ajouter dans
apps/frontend/app/dashboard/layout.tsxdans le header (ligne ~154, à côté du User Role Badge)
✅ Task 21: Créer le hook useNotifications pour polling
Fichier: apps/frontend/src/hooks/useNotifications.ts (à créer)
Actions:
- Hook custom qui fait du polling toutes les 30 secondes
- Retourne:
{ notifications, unreadCount, markAsRead, markAllAsRead, isLoading } - Utiliser
useQueryde TanStack Query avecrefetchInterval: 30000
Code:
export function useNotifications() {
const { data, isLoading, refetch } = useQuery({
queryKey: ['notifications'],
queryFn: () => notificationsApi.getNotifications(),
refetchInterval: 30000, // 30 seconds
});
const markAsRead = async (id: string) => {
await notificationsApi.markAsRead(id);
refetch();
};
return {
notifications: data?.notifications || [],
unreadCount: data?.unreadCount || 0,
markAsRead,
isLoading,
};
}
✅ Task 22: Tester le workflow complet end-to-end
Actions:
- Lancer le backend et le frontend
- Se connecter au dashboard
- Faire une recherche de tarifs
- Cliquer sur "Sélectionner cette option"
- Remplir le formulaire de booking
- Uploader des documents (fichiers de test)
- Soumettre le booking
- Vérifier que l'email est envoyé (vérifier les logs ou mailhog si configuré)
- Cliquer sur "Accepter" dans l'email
- Vérifier la page de confirmation
- Vérifier que la notification apparaît dans le dashboard
- Répéter avec "Refuser"
Checklist de test:
- Création de booking réussie
- Email reçu avec les bonnes informations
- Bouton Accepter fonctionne et redirige correctement
- Bouton Refuser fonctionne et redirige correctement
- Notifications apparaissent dans le dashboard
- Badge de notification se met à jour
- Documents sont bien stockés
- Données cohérentes en base de données
Dépendances NPM à ajouter
Backend
cd apps/backend
npm install nodemailer @types/nodemailer
npm install handlebars # Pour les templates email
npm install uuid @types/uuid
Frontend
cd apps/frontend
# Tout est déjà installé (React Hook Form, TanStack Query, etc.)
Configuration requise
Variables d'environnement backend
Ajouter dans apps/backend/.env:
# Email configuration (exemple avec Gmail)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_SECURE=false
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-app-password
EMAIL_FROM=noreply@xpeditis.com
# Frontend URL for email links
FRONTEND_URL=http://localhost:3000
# File upload
MAX_FILE_SIZE=5242880 # 5MB
UPLOAD_DEST=./uploads/documents
Migrations de base de données
Backend - TypeORM migrations
cd apps/backend
# Générer les migrations
npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/CreateBookingAndNotification
# Appliquer les migrations
npm run migration:run
Tables à créer:
bookings(id, user_id, organization_id, carrier_name, carrier_email, origin, destination, volume_cbm, weight_kg, price_eur, transit_days, status, confirmation_token, documents_path, notes, requested_at, responded_at, created_at, updated_at)notifications(id, user_id, type, title, message, booking_id, is_read, created_at)
Estimation de temps
| Partie | Tâches | Temps estimé |
|---|---|---|
| Backend - Domain | 3 | 2-3 heures |
| Backend - Infrastructure | 4 | 3-4 heures |
| Backend - Application | 5 | 3-4 heures |
| Frontend | 8 | 4-5 heures |
| Testing & Debug | 1 | 2-3 heures |
| TOTAL | 22 | 14-19 heures |
Notes importantes
- Sécurité des tokens: Utiliser des UUID v4 pour les confirmation tokens
- Expiration des liens: Ajouter une expiration (ex: 48h) pour les liens d'acceptation/refus
- Rate limiting: Limiter les appels aux endpoints publics (accept/reject)
- Stockage des documents: Considérer S3 pour la production au lieu du filesystem local
- Email fallback: Si l'envoi échoue, logger et permettre un retry
- Notifications temps réel: Pour une V2, considérer WebSockets au lieu du polling
Prochaines étapes
Une fois cette fonctionnalité complète, on pourra ajouter:
- Page de liste des bookings (
/dashboard/bookings) - Filtres et recherche dans les bookings
- Export des bookings en PDF/Excel
- Historique des statuts (timeline)
- Chat intégré avec le transporteur
- Système de rating après livraison