# 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: 1. Sélectionner une option de transport depuis les résultats de recherche 2. Remplir un formulaire avec les documents nécessaires 3. Envoyer une demande de booking par email au transporteur 4. Le transporteur peut accepter ou refuser via des boutons dans l'email 5. 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 `Booking` avec: - `id: string` - `userId: string` - `organizationId: string` - `carrierName: string` - `carrierEmail: string` - `origin: PortCode` - `destination: PortCode` - `volumeCBM: number` - `weightKG: number` - `priceEUR: number` - `transitDays: number` - `status: BookingStatus` - `documents: Document[]` (Bill of Lading, Packing List, Commercial Invoice, Certificate of Origin) - `confirmationToken: string` (pour les liens email) - `requestedAt: Date` - `respondedAt?: Date` - `notes?: 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 `Notification` avec: - `id: string` - `userId: string` - `type: NotificationType` - `title: string` - `message: string` - `bookingId?: string` - `isRead: boolean` - `createdAt: 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 `CsvRow` déjà mise à jour avec `companyEmail` - Modifier la méthode `mapToCsvRate()` pour passer `record.companyEmail` au constructeur de `CsvRate` - Ajouter `'companyEmail'` dans le tableau `requiredColumns` de `validateCsvStructure()` **Code à modifier** (ligne ~267): ```typescript 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.ts` - `apps/backend/src/infrastructure/persistence/typeorm/repositories/booking.repository.ts` **Actions**: - Créer l'interface du port avec méthodes: - `create(booking: Booking): Promise` - `findById(id: string): Promise` - `findByUserId(userId: string): Promise` - `findByToken(token: string): Promise` - `update(booking: Booking): Promise` - 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.ts` - `apps/backend/src/infrastructure/persistence/typeorm/repositories/notification.repository.ts` **Actions**: - Créer l'interface du port avec méthodes: - `create(notification: Notification): Promise` - `findByUserId(userId: string, unreadOnly?: boolean): Promise` - `markAsRead(id: string): Promise` - `markAllAsRead(userId: string): Promise` - 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 `nodemailer` ou 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**: ```html

Nouvelle demande de réservation - Xpeditis

Détails du transport

Route: {{origin}} → {{destination}}

Volume: {{volumeCBM}} CBM

Poids: {{weightKG}} kg

Prix: {{priceEUR}} EUR

Transit: {{transitDays}} jours

Documents fournis:

    {{#each documents}}
  • {{this.name}}
  • {{/each}}
``` --- ## 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;` dans `CsvRateSearchResultDto` - 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 `CreateBookingDto` avec validation: ```typescript 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 `confirmationToken` unique (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**: ```typescript @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 { // 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**: ```typescript @Get(':id/accept') @ApiOperation({ summary: 'Accept a booking request (public endpoint)' }) async acceptBooking( @Param('id') bookingId: string, @Query('token') token: string ): Promise { // 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 lu - `PATCH /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: 1. **Étape 1**: Confirmation des détails du transport (lecture seule) 2. **Étape 2**: Upload des documents (Bill of Lading, Packing List, Commercial Invoice, Certificate of Origin) 3. **Étape 3**: Révision et envoi **Structure**: ```typescript 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 `` - 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**: ```typescript
handleFileChange('billOfLading', e.target.files?.[0])} />
{/* Répéter pour les autres documents */}
``` --- ### ✅ 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` - Créer `getBookings(): Promise` - Utiliser `upload()` de `client.ts` pour 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.tsx` dans 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 `useQuery` de TanStack Query avec `refetchInterval: 30000` **Code**: ```typescript 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**: 1. Lancer le backend et le frontend 2. Se connecter au dashboard 3. Faire une recherche de tarifs 4. Cliquer sur "Sélectionner cette option" 5. Remplir le formulaire de booking 6. Uploader des documents (fichiers de test) 7. Soumettre le booking 8. Vérifier que l'email est envoyé (vérifier les logs ou mailhog si configuré) 9. Cliquer sur "Accepter" dans l'email 10. Vérifier la page de confirmation 11. Vérifier que la notification apparaît dans le dashboard 12. 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 ```bash cd apps/backend npm install nodemailer @types/nodemailer npm install handlebars # Pour les templates email npm install uuid @types/uuid ``` ### Frontend ```bash 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`: ```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 ```bash 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 1. **Sécurité des tokens**: Utiliser des UUID v4 pour les confirmation tokens 2. **Expiration des liens**: Ajouter une expiration (ex: 48h) pour les liens d'acceptation/refus 3. **Rate limiting**: Limiter les appels aux endpoints publics (accept/reject) 4. **Stockage des documents**: Considérer S3 pour la production au lieu du filesystem local 5. **Email fallback**: Si l'envoi échoue, logger et permettre un retry 6. **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