Explorar el Código

(feat) ajout des mode de payement cinetpay et un autre du senegal en back v1

Brin AMONKONAN hace 1 año
padre
commit
b48272c756
Se han modificado 26 ficheros con 2341 adiciones y 132 borrados
  1. 433 0
      app_transport_front/src/components/ReservationWorkflowModal.vue
  2. 2 2
      app_transport_front/src/services/CompanyService.ts
  3. 0 42
      app_transport_front/src/stores/useUserStore.ts
  4. 1 1
      app_transport_front/src/types/index.ts
  5. 2 5
      app_transport_front/src/views/EmployeeView.vue
  6. 60 40
      app_transport_front/src/views/ReservationView.vue
  7. 33 0
      node_app/.env.example
  8. 0 12
      node_app/prisma/migrations/20250605154937_add_company_relation_and_place_availability/migration.sql
  9. 46 0
      node_app/prisma/migrations/20250605182700_add_paydunya_fields_to_transaction/migration.sql
  10. 25 13
      node_app/prisma/schema.prisma
  11. 64 0
      node_app/scripts/setup-paydunya.sh
  12. 4 0
      node_app/src/app.module.ts
  13. 229 0
      node_app/src/application/dto/paydunya-payment.dto.ts
  14. 275 0
      node_app/src/application/use-cases/payment/process-reservation-payment.use-case.ts
  15. 19 0
      node_app/src/application/use-cases/reservation/delete-reservation.use-case.ts
  16. 14 0
      node_app/src/application/use-cases/reservation/find-all-reservations.use-case.ts
  17. 19 0
      node_app/src/application/use-cases/reservation/find-reservation-by-id.use-case.ts
  18. 9 5
      node_app/src/infrastructure/database/repositories/transaction.repository.ts
  19. 16 0
      node_app/src/infrastructure/mappers/transaction.mapper.ts
  20. 163 0
      node_app/src/infrastructure/payment/README.md
  21. 27 0
      node_app/src/infrastructure/payment/paydunya.module.ts
  22. 225 0
      node_app/src/infrastructure/services/paydunya.service.spec.ts
  23. 251 0
      node_app/src/infrastructure/services/paydunya.service.ts
  24. 281 0
      node_app/src/presentation/controllers/payment.controller.ts
  25. 106 12
      node_app/src/presentation/controllers/reservation.controller.ts
  26. 37 0
      node_app/src/presentation/modules/reservation.module.ts

+ 433 - 0
app_transport_front/src/components/ReservationWorkflowModal.vue

@@ -0,0 +1,433 @@
+<template>
+  <div v-if="isOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+    <div class="bg-white rounded-lg p-6 w-full max-w-md mx-4 max-h-[90vh] overflow-y-auto">
+      <!-- Header -->
+      <div class="flex justify-between items-center mb-6">
+        <h2 class="text-xl font-bold text-gray-800">Confirmation de réservation</h2>
+        <button @click="closeModal" class="text-gray-500 hover:text-gray-700">
+          <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+          </svg>
+        </button>
+      </div>
+
+      <!-- Progress Steps -->
+      <div class="mb-6">
+        <div class="flex items-center justify-between">
+          <div class="flex items-center">
+            <div :class="stepClasses(1)" class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium">
+              1
+            </div>
+            <span class="ml-2 text-sm font-medium" :class="currentStep >= 1 ? 'text-blue-600' : 'text-gray-500'">Utilisateur</span>
+          </div>
+          <div class="flex-1 h-1 mx-4 bg-gray-200 rounded">
+            <div class="h-1 bg-blue-600 rounded transition-all duration-300" :style="{ width: currentStep >= 2 ? '100%' : '0%' }"></div>
+          </div>
+          <div class="flex items-center">
+            <div :class="stepClasses(2)" class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium">
+              2
+            </div>
+            <span class="ml-2 text-sm font-medium" :class="currentStep >= 2 ? 'text-blue-600' : 'text-gray-500'">Réservation</span>
+          </div>
+          <div class="flex-1 h-1 mx-4 bg-gray-200 rounded">
+            <div class="h-1 bg-blue-600 rounded transition-all duration-300" :style="{ width: currentStep >= 3 ? '100%' : '0%' }"></div>
+          </div>
+          <div class="flex items-center">
+            <div :class="stepClasses(3)" class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium">
+              3
+            </div>
+            <span class="ml-2 text-sm font-medium" :class="currentStep >= 3 ? 'text-blue-600' : 'text-gray-500'">Paiement</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- Step Content -->
+      <div class="min-h-[200px]">
+        <!-- Step 1: User Creation -->
+        <div v-if="currentStep === 1" class="space-y-4">
+          <h3 class="text-lg font-semibold mb-4">Création du compte utilisateur</h3>
+          
+          <div v-if="passengers.length > 1" class="mb-4">
+            <label class="block text-sm font-medium text-gray-700 mb-2">
+              Sélectionnez le passager principal :
+            </label>
+            <select v-model="selectedMainPassenger" class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
+              <option v-for="(passenger, index) in passengers" :key="index" :value="index">
+                {{ passenger.name }} {{ passenger.otherName }}
+              </option>
+            </select>
+          </div>
+
+          <div class="bg-gray-50 p-4 rounded-md">
+            <h4 class="font-medium mb-2">Informations du compte :</h4>
+            <p><strong>Nom :</strong> {{ mainPassenger?.name }} {{ mainPassenger?.otherName }}</p>
+            <p><strong>Téléphone :</strong> {{ mainPassenger?.phone }}</p>
+            <p><strong>Email :</strong> {{ generatedEmail }}</p>
+            <p><strong>Mot de passe :</strong> {{ generatedPassword }}</p>
+          </div>
+
+          <div v-if="step1Error" class="text-red-600 text-sm">
+            {{ step1Error }}
+          </div>
+        </div>
+
+        <!-- Step 2: Reservation Creation -->
+        <div v-if="currentStep === 2" class="space-y-4">
+          <h3 class="text-lg font-semibold mb-4">Création de la réservation</h3>
+          
+          <div class="bg-gray-50 p-4 rounded-md">
+            <h4 class="font-medium mb-2">Détails de la réservation :</h4>
+            <p><strong>Trajet :</strong> {{ tripDetails }}</p>
+            <p><strong>Nombre de passagers :</strong> {{ passengers.length }}</p>
+            <p><strong>Places :</strong> {{ selectedSeats.join(', ') }}</p>
+            <p><strong>Total :</strong> {{ formatPrice(totalAmount) }} FCFA</p>
+          </div>
+
+          <div v-if="step2Error" class="text-red-600 text-sm">
+            {{ step2Error }}
+          </div>
+        </div>
+
+        <!-- Step 3: Payment -->
+        <div v-if="currentStep === 3" class="space-y-4">
+          <h3 class="text-lg font-semibold mb-4">Paiement</h3>
+          
+          <div class="bg-gray-50 p-4 rounded-md">
+            <h4 class="font-medium mb-2">Récapitulatif du paiement :</h4>
+            <p><strong>Réservation ID :</strong> {{ createdReservationId }}</p>
+            <p><strong>Montant :</strong> {{ formatPrice(totalAmount) }} FCFA</p>
+            <p><strong>Méthode :</strong> Mobile Money</p>
+          </div>
+
+          <div v-if="step3Error" class="text-red-600 text-sm">
+            {{ step3Error }}
+          </div>
+
+          <div v-if="paymentSuccess" class="bg-green-50 border border-green-200 rounded-md p-4">
+            <div class="flex items-center">
+              <svg class="w-5 h-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
+              </svg>
+              <span class="text-green-800 font-medium">Paiement effectué avec succès !</span>
+            </div>
+            <p class="text-green-700 text-sm mt-2">Votre réservation a été confirmée.</p>
+          </div>
+        </div>
+      </div>
+
+      <!-- Loading State -->
+      <div v-if="isLoading" class="flex items-center justify-center py-8">
+        <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
+        <span class="ml-2 text-gray-600">{{ loadingMessage }}</span>
+      </div>
+
+      <!-- Action Buttons -->
+      <div class="flex justify-between mt-6">
+        <button 
+          v-if="currentStep > 1 && !paymentSuccess" 
+          @click="previousStep" 
+          :disabled="isLoading"
+          class="px-4 py-2 text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50"
+        >
+          Précédent
+        </button>
+        <div v-else></div>
+        
+        <button 
+          v-if="!paymentSuccess"
+          @click="nextStep" 
+          :disabled="isLoading || !canProceed"
+          class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
+        >
+          {{ currentStep === 3 ? 'Payer' : 'Suivant' }}
+        </button>
+        
+        <button 
+          v-if="paymentSuccess"
+          @click="closeModal"
+          class="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
+        >
+          Fermer
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue'
+import UserService from '@/services/UserService'
+import ReservationService from '@/services/ReservationService'
+import TransactionService from '@/services/TransactionService'
+import type { User, Reservation } from '@/types'
+import { Role } from '@/Enum/Role'
+
+interface Passenger {
+  name: string
+  otherName: string
+  phone: string
+  email?: string
+}
+
+interface Props {
+  isOpen: boolean
+  passengers: Passenger[]
+  selectedSeats: number[]
+  totalAmount: number
+  tripId: number
+  tripDetails: string
+}
+
+interface Emits {
+  close: []
+  success: [reservationId: number]
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// State
+const currentStep = ref(1)
+const isLoading = ref(false)
+const loadingMessage = ref('')
+const selectedMainPassenger = ref(0)
+const createdUserId = ref<number | null>(null)
+const createdReservationId = ref<number | null>(null)
+const paymentSuccess = ref(false)
+
+// Error states
+const step1Error = ref('')
+const step2Error = ref('')
+const step3Error = ref('')
+
+// Computed
+const mainPassenger = computed(() => props.passengers[selectedMainPassenger.value])
+
+const generatedPassword = computed(() => {
+  console.log('Computed generatedPassword called', mainPassenger.value)
+  if (!mainPassenger.value) return ''
+  const phone = mainPassenger.value.phone
+  const lastTwoDigits = phone.slice(-2)
+  return `${mainPassenger.value.name.toLowerCase()}.${mainPassenger.value.otherName.toLowerCase()}${lastTwoDigits}`
+})
+
+const generatedEmail = computed(() => {
+  console.log('Computed generatedEmail called', mainPassenger.value)
+  if (!mainPassenger.value) return ''
+  if (mainPassenger.value.email) return mainPassenger.value.email
+  const phone = mainPassenger.value.phone
+  const lastTwoDigits = phone.slice(-2)
+  return `${mainPassenger.value.name.toLowerCase()}.${mainPassenger.value.otherName.toLowerCase()}${lastTwoDigits}@transport.ci`
+})
+
+const canProceed = computed(() => {
+  if (currentStep.value === 1) {
+    return mainPassenger.value && mainPassenger.value.name && mainPassenger.value.otherName && mainPassenger.value.phone
+  }
+  if (currentStep.value === 2) {
+    return createdUserId.value !== null
+  }
+  if (currentStep.value === 3) {
+    return createdReservationId.value !== null
+  }
+  return false
+})
+
+// Methods
+const stepClasses = (step: number) => {
+  if (currentStep.value > step) {
+    return 'bg-green-600 text-white'
+  } else if (currentStep.value === step) {
+    return 'bg-blue-600 text-white'
+  } else {
+    return 'bg-gray-300 text-gray-600'
+  }
+}
+
+const formatPrice = (price: number) => {
+  return new Intl.NumberFormat('fr-FR').format(price)
+}
+
+const closeModal = () => {
+  // Reset state
+  currentStep.value = 1
+  isLoading.value = false
+  selectedMainPassenger.value = 0
+  createdUserId.value = null
+  createdReservationId.value = null
+  paymentSuccess.value = false
+  step1Error.value = ''
+  step2Error.value = ''
+  step3Error.value = ''
+  
+  emit('close')
+}
+
+const previousStep = () => {
+  if (currentStep.value > 1) {
+    currentStep.value--
+  }
+}
+
+const nextStep = async () => {
+  if (!canProceed.value) return
+  
+  if (currentStep.value === 1) {
+    await createUser()
+  } else if (currentStep.value === 2) {
+    await createReservation()
+  } else if (currentStep.value === 3) {
+    await processPayment()
+  }
+}
+
+const createUser = async () => {
+  isLoading.value = true
+  loadingMessage.value = 'Création du compte utilisateur...'
+  step1Error.value = ''
+  
+  try {
+    const userData: Omit<User, 'id'> = {
+      name: `${mainPassenger.value.name} ${mainPassenger.value.otherName}`,
+      email: generatedEmail.value,
+      password: generatedPassword.value,
+      telephone: mainPassenger.value.phone,
+      role: Role.User
+    }
+
+    delete userData.role
+    const createdUser = await UserService.createUser(userData)
+    
+    if (createdUser && createdUser.id) {
+      createdUserId.value = createdUser.id
+      currentStep.value = 2
+    } else {
+      step1Error.value = 'Erreur lors de la création du compte utilisateur'
+    }
+  } catch (error) {
+    console.error('Erreur création utilisateur:', error)
+    step1Error.value = 'Erreur lors de la création du compte utilisateur'
+  } finally {
+    isLoading.value = false
+  }
+}
+
+const createReservation = async () => {
+  isLoading.value = true
+  loadingMessage.value = 'Création de la réservation...'
+  step2Error.value = ''
+  
+  try {
+    // Créer une réservation pour chaque passager/place
+    const reservations = []
+    
+    for (let i = 0; i < props.passengers.length; i++) {
+      const reservationData: Omit<Reservation, 'id'> = {
+        dateAt: new Date().toISOString(),
+        placeId: props.selectedSeats[i],
+        userId: createdUserId.value!,
+        trajetId: props.tripId
+      }
+      
+      const reservation = await ReservationService.createReservation(reservationData)
+      console.log('Réservations créées 1:', reservations)
+      if (reservation) {
+        reservations.push(reservation)
+      }
+    }
+    console.log('Réservations créées:', reservations) // Ajout de cette ligne pour débogage
+    if (reservations.length > 0) {
+      // Utiliser la première réservation comme référence principale
+      console.log('Je suis dedans', reservations) 
+      createdReservationId.value = reservations[0].data.id
+      currentStep.value = 3
+    } else {
+      step2Error.value = 'Erreur lors de la création de la réservation'
+    }
+  } catch (error) {
+    console.error('Erreur création réservation:', error)
+    step2Error.value = 'Erreur lors de la création de la réservation'
+  } finally {
+    isLoading.value = false
+  }
+}
+
+const processPayment = async () => {
+  isLoading.value = true
+  loadingMessage.value = 'Traitement du paiement...'
+  step3Error.value = ''
+  
+  try {
+    // Vérifier que nous avons un ID de réservation valide
+    if (!createdReservationId.value || typeof createdReservationId.value !== 'number') {
+      throw new Error('ID de réservation manquant ou invalide')
+    }
+    
+    // Données pour CinetPay selon la documentation
+    const cinetPayData = {
+      apikey: "174173934865f8799156f094.97916229", // À remplacer par votre vraie clé API
+      site_id: "105897483", // À remplacer par votre site ID
+      transaction_id: `TRANSPORT_${createdReservationId.value}_${Date.now()}`, // ID unique de transaction
+      amount: props.totalAmount,
+      currency: "XOF",
+      description: `Réservation transport - ${props.passengers.length} place(s)`,
+      customer_id: createdUserId.value?.toString() || "guest",
+      customer_name: mainPassenger.value.name || "",
+      customer_surname: mainPassenger.value.otherName || "",
+      customer_email: mainPassenger.value.email || generatedEmail.value,
+      customer_phone_number: mainPassenger.value.phone,
+      customer_address: "Côte d'Ivoire",
+      customer_city: "Abidjan",
+      customer_country: "CI",
+      customer_state: "CI",
+      customer_zip_code: "00225",
+      notify_url: `${window.location.origin}/api/payments/cinetpay/notify`,
+      return_url: `${window.location.origin}/payment/success?reservation=${createdReservationId.value}`,
+      channels: "MOBILE_MONEY", // Uniquement mobile money
+      metadata: JSON.stringify({ reservationId: createdReservationId.value }),
+      lang: "FR"
+    }
+    
+    // Appel à l'API CinetPay pour générer l'URL de paiement
+    const paymentResult = await fetch('https://api-checkout.cinetpay.com/v2/payment', {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(cinetPayData)
+    })
+    
+    if (!paymentResult.ok) {
+      throw new Error(`Erreur HTTP CinetPay: ${paymentResult.status}`)
+    }
+    
+    const result = await paymentResult.json()
+    
+    // Vérifier la réponse de CinetPay
+    if (result.code === '201' && result.data && result.data.payment_url) {
+      // Rediriger vers l'URL de paiement CinetPay
+      window.location.href = result.data.payment_url
+    } else {
+      throw new Error(result.message || 'Erreur lors de la génération du lien de paiement')
+    }
+    
+  } catch (error) {
+    console.error('Erreur paiement CinetPay:', error)
+    step3Error.value = error instanceof Error ? error.message : 'Erreur lors du traitement du paiement'
+  } finally {
+    isLoading.value = false
+  }
+}
+
+// Watch for modal opening to reset state
+watch(() => props.isOpen, (newValue) => {
+  if (newValue) {
+    currentStep.value = 1
+    selectedMainPassenger.value = 0
+    createdUserId.value = null
+    createdReservationId.value = null
+    paymentSuccess.value = false
+    step1Error.value = ''
+    step2Error.value = ''
+    step3Error.value = ''
+  }
+})
+</script>

+ 2 - 2
app_transport_front/src/services/CompanyService.ts

@@ -30,7 +30,7 @@ export default {
     }
   },
 
-  async getCompany(id: number): Promise<any | null> {
+  async getCompany(id: number): Promise<Company | null> {
     try {
       const response = await authenticatedFetch(`${API_URL}/companies/${id}`)
       if (!response.ok) {
@@ -113,7 +113,7 @@ export default {
     }
   },
 
-  async getCompanyTrips(id: number): Promise<Company[]> {
+  async getCompanyTrips(id: number): Promise<any[]> {
     try {
       const response = await authenticatedFetch(`${API_URL}/companies/${id}/trips`)
       if (!response.ok) {

+ 0 - 42
app_transport_front/src/stores/useUserStore.ts

@@ -1,16 +1,6 @@
 import { defineStore } from 'pinia'
 import type { PersistenceOptions } from 'pinia-plugin-persistedstate'
 
-// Interface pour le payload du JWT
-interface JWTPayload {
-  sub: string
-  email: string
-  role: string
-  companyId?: string
-  iat: number
-  exp: number
-}
-
 export const useUserStore = defineStore('user', {
   state: () => ({
     token: null as string | null,
@@ -23,33 +13,6 @@ export const useUserStore = defineStore('user', {
     otherName: null as string | null,
     companyId: null as string | null,
   }),
-  getters: {
-    // Getter pour décoder le token JWT et extraire le companyId
-    decodedCompanyId: (state) => {
-      if (!state.token) return null
-      
-      try {
-        // Décoder le JWT (partie payload)
-        const payload = JSON.parse(atob(state.token.split('.')[1])) as JWTPayload
-        return payload.companyId || null
-      } catch (error) {
-        console.error('Erreur lors du décodage du token JWT:', error)
-        return null
-      }
-    },
-    // Getter pour obtenir toutes les informations du token décodé
-    decodedToken: (state) => {
-      if (!state.token) return null
-      
-      try {
-        const payload = JSON.parse(atob(state.token.split('.')[1])) as JWTPayload
-        return payload
-      } catch (error) {
-        console.error('Erreur lors du décodage du token JWT:', error)
-        return null
-      }
-    }
-  },
   actions: {
     setUser(data: { 
       token: string, 
@@ -71,11 +34,6 @@ export const useUserStore = defineStore('user', {
       this.name = data.name
       this.otherName = data.otherName
       this.companyId = data.companyId || null
-      
-      // Mettre à jour le companyId depuis le token décodé si pas fourni
-      if (!this.companyId && this.decodedCompanyId) {
-        this.companyId = this.decodedCompanyId
-      }
     },
     clearUser() {
       this.$reset()

+ 1 - 1
app_transport_front/src/types/index.ts

@@ -167,7 +167,7 @@ export interface User {
   password?: string
   address?: string
   telephone?: string
-  role: Role
+  role?: Role
   createdAt?: string
   updatedAt?: string
 }

+ 2 - 5
app_transport_front/src/views/EmployeeView.vue

@@ -340,16 +340,13 @@ const loadEmployees = async (page: number = 1) => {
     loading.value = true
     const userStore = useUserStore()
     
-    // Récupérer le companyId depuis le token JWT décodé
-    const companyId = userStore.decodedCompanyId || userStore.companyId
-    
     // Vérifier si l'utilisateur a une entreprise associée
-    if (!companyId) {
+    if (!userStore.companyId) {
       error('Aucune entreprise associée à votre compte')
       return
     }
     
-    const result = await CompanyService.getCompanyEmployees(parseInt(companyId), page, pagination.value.limit)
+    const result = await CompanyService.getCompanyEmployees(parseInt(userStore.companyId), page, pagination.value.limit)
     
     employees.value = result.employees.map(user => ({
       ...user,

+ 60 - 40
app_transport_front/src/views/ReservationView.vue

@@ -140,23 +140,23 @@
                 <h4>Passager {{ index + 1 }}</h4>
                 <div class="form-row">
                   <div class="form-group">
-                    <label :for="`firstName-${index}`">Prénom</label>
+                    <label :for="`name-${index}`">Nom</label>
                     <input 
-                      :id="`firstName-${index}`"
+                      :id="`name-${index}`"
                       type="text" 
-                      v-model="passenger.firstName" 
+                      v-model="passenger.name" 
                       required 
-                      placeholder="Prénom"
+                      placeholder="Nom de Famille"
                     />
                   </div>
                   <div class="form-group">
-                    <label :for="`lastName-${index}`">Nom</label>
+                    <label :for="`otherName-${index}`">Prénoms</label>
                     <input 
-                      :id="`lastName-${index}`"
+                      :id="`otherName-${index}`"
                       type="text" 
-                      v-model="passenger.lastName" 
+                      v-model="passenger.otherName" 
                       required 
-                      placeholder="Nom"
+                      placeholder="Prénoms"
                     />
                   </div>
                 </div>
@@ -405,6 +405,18 @@
       </div>
     </div>
   </div>
+
+  <!-- Modal de workflow de réservation -->
+  <ReservationWorkflowModal
+    :is-open="showWorkflowModal"
+    :passengers="passengers"
+    :selected-seats="selectedSeats"
+    :total-amount="calculateTotal()"
+    :trip-id="trip?.id || 0"
+    :trip-details="getTripDetails()"
+    @close="showWorkflowModal = false"
+    @success="onReservationSuccess"
+  />
 </template>
 
 <script setup lang="ts">
@@ -413,11 +425,13 @@ import { useRoute, useRouter } from 'vue-router'
 import { useToast } from '@/composables/useToast'
 import { useStationStore } from '@/stores/useStationStore'
 import TripService from '@/services/TripService'
-import type { Trip, Station, Company } from '@/types'
+import type { Trip, Station, Company, Reservation } from '@/types'
 import 'leaflet/dist/leaflet.css'
 // @ts-ignore
 import * as L from 'leaflet'
 import CompanyService from '@/services/CompanyService'
+import ReservationWorkflowModal from '@/components/ReservationWorkflowModal.vue'
+import ReservationService from '@/services/ReservationService'
 
 const route = useRoute()
 const router = useRouter()
@@ -454,19 +468,23 @@ const mobileData = ref({
 })
 
 const passengers = ref<Array<{
-  firstName: string
-  lastName: string
+  name: string
+  otherName: string
   phone: string
   email: string
 }>>([]);
 
+// État du modal de workflow
+const showWorkflowModal = ref(false)
+const selectedSeats = ref<number[]>([])
+
 // Watchers
 watch(() => reservationData.value.passengerCount, (newCount) => {
   const count = parseInt(newCount)
   if (count > 0) {
     passengers.value = Array.from({ length: count }, () => ({
-      firstName: '',
-      lastName: '',
+      name: '',
+      otherName: '',
       phone: '',
       email: ''
     }))
@@ -482,8 +500,8 @@ const isFormValid = computed(() => {
   }
   
   const passengersValid = passengers.value.every(passenger => 
-    passenger.firstName.trim() && 
-    passenger.lastName.trim() && 
+    passenger.name.trim() && 
+    passenger.otherName.trim() && 
     passenger.phone.trim()
   )
   
@@ -712,37 +730,39 @@ const submitReservation = async () => {
       return
     }
 
-    const reservationPayload = {
-      tripId: trip.value?.id,
-      passengers: passengers.value,
-      paymentMethod: reservationData.value.paymentMethod,
-      totalAmount: calculateTotal(),
-      ...(reservationData.value.paymentMethod === 'card' && {
-        cardInfo: {
-          number: cardData.value.number.replace(/\s/g, ''),
-          name: cardData.value.name,
-          expiry: cardData.value.expiry,
-          cvv: cardData.value.cvv
-        }
-      })
-    }
-
-    console.log('Données de réservation:', reservationPayload)
+    // Générer des places aléatoirement pour la démo
+    // Dans un vrai système, ces places seraient sélectionnées par l'utilisateur
+    const passengerCount = parseInt(reservationData.value.passengerCount)
+    selectedSeats.value = Array.from({ length: passengerCount }, (_, i) => i + 1)
     
-    // Ici vous devriez appeler votre service de réservation
-    // await ReservationService.createReservation(reservationPayload)
-    
-    success('Réservation confirmée avec succès!')
-    
-    // Rediriger vers une page de confirmation ou le dashboard
-    //router.push({ name: 'home' })
+    // Ouvrir le modal de workflow
+    showWorkflowModal.value = true
     
   } catch (err) {
-    console.error('Erreur lors de la réservation:', err)
-    error('Erreur lors de la confirmation de la réservation')
+    console.error('Erreur lors de l\'ouverture du modal:', err)
+    error('Erreur lors de l\'ouverture du processus de réservation')
   }
 }
 
+// Méthodes pour le modal de workflow
+const getTripDetails = () => {
+  if (!trip.value || !departureStation.value || !arrivalStation.value) {
+    return 'Trajet non disponible'
+  }
+  return `${departureStation.value.name} → ${arrivalStation.value.name}`
+}
+
+const onReservationSuccess = (reservationId: number) => {
+  success('Réservation confirmée avec succès!')
+  showWorkflowModal.value = false
+  
+  // Rediriger vers une page de confirmation ou le dashboard
+  router.push({ 
+    name: 'home',
+    query: { reservationId: reservationId.toString() }
+  })
+}
+
 onMounted(() => {
   loadTripData()
 })

+ 33 - 0
node_app/.env.example

@@ -0,0 +1,33 @@
+# Database
+DATABASE_URL="postgresql://username:password@localhost:5432/transport_db?schema=public"
+
+# JWT
+JWT_SECRET="your-jwt-secret-key"
+JWT_EXPIRES_IN="24h"
+
+# Keycloak
+KEYCLOAK_URL="http://localhost:8080"
+KEYCLOAK_REALM="transport-realm"
+KEYCLOAK_CLIENT_ID="transport-client"
+KEYCLOAK_CLIENT_SECRET="your-keycloak-client-secret"
+
+# PayDunya Configuration
+PAYDUNYA_MASTER_KEY="U1wbVezW-k5VG-2JDW-l4Z6-IUlbFmig8Dqx"
+PAYDUNYA_PRIVATE_KEY="test_private_oicvJs3uU8SfJ9dtCFwCsvgfiYK"
+PAYDUNYA_TOKEN="JWFpxckPyzXoRg0vFwHx"
+PAYDUNYA_BASE_URL="https://app.paydunya.com/sandbox-api/v1"
+PAYDUNYA_MODE="sandbox"
+
+# Application
+PORT=3000
+NODE_ENV="development"
+
+# Notification Service
+SMTP_HOST="smtp.gmail.com"
+SMTP_PORT=587
+SMTP_USER="your-email@gmail.com"
+SMTP_PASS="your-email-password"
+
+# SMS Service
+SMS_API_KEY="your-sms-api-key"
+SMS_SENDER_ID="TRANSPORT"

+ 0 - 12
node_app/prisma/migrations/20250605154937_add_company_relation_and_place_availability/migration.sql

@@ -1,12 +0,0 @@
--- AlterTable
-ALTER TABLE `Place` ADD COLUMN `isAvailable` BOOLEAN NOT NULL DEFAULT true,
-    ADD COLUMN `occupiedAt` DATETIME(3) NULL;
-
--- AlterTable
-ALTER TABLE `Trajet` ADD COLUMN `companyId` INTEGER NULL;
-
--- CreateIndex
-CREATE INDEX `Trajet_companyId_fkey` ON `Trajet`(`companyId`);
-
--- AddForeignKey
-ALTER TABLE `Trajet` ADD CONSTRAINT `Trajet_companyId_fkey` FOREIGN KEY (`companyId`) REFERENCES `Company`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

+ 46 - 0
node_app/prisma/migrations/20250605182700_add_paydunya_fields_to_transaction/migration.sql

@@ -0,0 +1,46 @@
+/*
+  Warnings:
+
+  - A unique constraint covering the columns `[paydunya_token]` on the table `Transaction` will be added. If there are existing duplicate values, this will fail.
+  - A unique constraint covering the columns `[paydunya_transaction_id]` on the table `Transaction` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE `Place` ADD COLUMN `isAvailable` BOOLEAN NOT NULL DEFAULT true,
+    ADD COLUMN `occupiedAt` DATETIME(3) NULL;
+
+-- AlterTable
+ALTER TABLE `Trajet` ADD COLUMN `companyId` INTEGER NULL;
+
+-- AlterTable
+ALTER TABLE `Transaction` ADD COLUMN `paydunya_invoice_url` VARCHAR(191) NULL,
+    ADD COLUMN `paydunya_response` JSON NULL,
+    ADD COLUMN `paydunya_token` VARCHAR(191) NULL,
+    ADD COLUMN `paydunya_transaction_id` VARCHAR(191) NULL,
+    ADD COLUMN `payment_method` VARCHAR(191) NULL,
+    ADD COLUMN `payment_type` VARCHAR(191) NULL,
+    ADD COLUMN `webhook_data` JSON NULL;
+
+-- CreateIndex
+CREATE INDEX `Trajet_companyId_fkey` ON `Trajet`(`companyId`);
+
+-- CreateIndex
+CREATE UNIQUE INDEX `Transaction_paydunya_token_key` ON `Transaction`(`paydunya_token`);
+
+-- CreateIndex
+CREATE UNIQUE INDEX `Transaction_paydunya_transaction_id_key` ON `Transaction`(`paydunya_transaction_id`);
+
+-- CreateIndex
+CREATE INDEX `Transaction_paydunya_token_idx` ON `Transaction`(`paydunya_token`);
+
+-- CreateIndex
+CREATE INDEX `Transaction_paydunya_transaction_id_idx` ON `Transaction`(`paydunya_transaction_id`);
+
+-- CreateIndex
+CREATE INDEX `Transaction_payment_method_idx` ON `Transaction`(`payment_method`);
+
+-- CreateIndex
+CREATE INDEX `Transaction_payment_type_idx` ON `Transaction`(`payment_type`);
+
+-- AddForeignKey
+ALTER TABLE `Trajet` ADD CONSTRAINT `Trajet_companyId_fkey` FOREIGN KEY (`companyId`) REFERENCES `Company`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

+ 25 - 13
node_app/prisma/schema.prisma

@@ -156,21 +156,33 @@ model Reservation {
 }
 
 model Transaction {
-  id            String      @id @default(uuid())
-  amount        Float?
-  currency      String?
-  status        String?
-  provider      String?
-  reference     String?
-  metadata      Json?
-  createdAt     DateTime?    @default(now())
-  updatedAt     DateTime?    @updatedAt
-  userId        Int         @unique
-  reservationId Int         @unique
-  reservation   Reservation @relation("ReservationTransaction", fields: [reservationId], references: [id])
-  user          User        @relation("UserTransactions", fields: [userId], references: [id])
+  id                      String      @id @default(uuid())
+  amount                  Float?
+  currency                String?
+  status                  String?
+  provider                String?
+  reference               String?
+  metadata                Json?
+  createdAt               DateTime?    @default(now())
+  updatedAt               DateTime?    @updatedAt
+  userId                  Int         @unique
+  reservationId           Int         @unique
+  // Champs PayDunya
+  paydunya_token          String?     @unique
+  paydunya_transaction_id String?     @unique
+  paydunya_invoice_url    String?
+  payment_method          String?     // "mobile_money", "card", "bank_transfer"
+  payment_type            String?     // "invoice", "dmp", "softpay"
+  paydunya_response       Json?       // Réponse complète de PayDunya
+  webhook_data            Json?       // Données reçues via webhook
+  reservation             Reservation @relation("ReservationTransaction", fields: [reservationId], references: [id])
+  user                    User        @relation("UserTransactions", fields: [userId], references: [id])
 
   @@index([userId], map: "Transaction_userId_fkey")
+  @@index([paydunya_token], map: "Transaction_paydunya_token_idx")
+  @@index([paydunya_transaction_id], map: "Transaction_paydunya_transaction_id_idx")
+  @@index([payment_method], map: "Transaction_payment_method_idx")
+  @@index([payment_type], map: "Transaction_payment_type_idx")
 }
 
 model Qrcode {

+ 64 - 0
node_app/scripts/setup-paydunya.sh

@@ -0,0 +1,64 @@
+#!/bin/bash
+
+# Script de configuration PayDunya
+# Ce script applique les migrations et configure le module PayDunya
+
+echo "🚀 Configuration du module PayDunya..."
+
+# 1. Appliquer les migrations Prisma
+echo "📊 Application des migrations Prisma..."
+npx prisma migrate dev --name add_paydunya_fields
+
+# 2. Générer le client Prisma
+echo "🔧 Génération du client Prisma..."
+npx prisma generate
+
+# 3. Installer les dépendances si nécessaire
+echo "📦 Vérification des dépendances..."
+npm install
+
+# 4. Vérifier la configuration
+echo "✅ Vérification de la configuration..."
+if [ -f ".env" ]; then
+    echo "✓ Fichier .env trouvé"
+    
+    # Vérifier les variables PayDunya
+    if grep -q "PAYDUNYA_MASTER_KEY" .env; then
+        echo "✓ PAYDUNYA_MASTER_KEY configuré"
+    else
+        echo "⚠️  PAYDUNYA_MASTER_KEY manquant dans .env"
+    fi
+    
+    if grep -q "PAYDUNYA_PRIVATE_KEY" .env; then
+        echo "✓ PAYDUNYA_PRIVATE_KEY configuré"
+    else
+        echo "⚠️  PAYDUNYA_PRIVATE_KEY manquant dans .env"
+    fi
+    
+    if grep -q "PAYDUNYA_TOKEN" .env; then
+        echo "✓ PAYDUNYA_TOKEN configuré"
+    else
+        echo "⚠️  PAYDUNYA_TOKEN manquant dans .env"
+    fi
+else
+    echo "⚠️  Fichier .env non trouvé. Copiez .env.example vers .env et configurez vos clés PayDunya"
+fi
+
+# 5. Compiler le projet
+echo "🔨 Compilation du projet..."
+npm run build
+
+echo "✅ Configuration PayDunya terminée!"
+echo ""
+echo "📋 Prochaines étapes:"
+echo "1. Configurez vos clés PayDunya dans le fichier .env"
+echo "2. Testez les endpoints de paiement"
+echo "3. Configurez les webhooks PayDunya"
+echo ""
+echo "🔗 Endpoints disponibles:"
+echo "- POST /api/payments/reservation - Traiter un paiement de réservation"
+echo "- GET /api/payments/status/:transactionId - Vérifier le statut d'une transaction"
+echo "- POST /api/payments/invoice - Créer une facture PayDunya"
+echo "- POST /api/payments/dmp - Créer une demande de paiement mobile"
+echo "- POST /api/payments/softpay - Effectuer un paiement direct"
+echo "- GET /api/payments/methods - Lister les méthodes de paiement"

+ 4 - 0
node_app/src/app.module.ts

@@ -2,6 +2,7 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
 import { ConfigModule } from '@nestjs/config';
 import { PrismaModule } from './infrastructure/database/prisma.module';
 import { ServicesModule } from './infrastructure/services/services.module';
+import { PaydunyaModule } from './infrastructure/payment/paydunya.module';
 import { UserController } from './presentation/controllers/user.controller';
 import { CreateUserUseCase } from './application/use-cases/create-user.use-case';
 import { UserRepository } from './infrastructure/database/repositories/user.repository';
@@ -74,6 +75,7 @@ import { FindStationCompaniesByStationUseCase } from './application/use-cases/st
 import { DeleteStationCompanyUseCase } from './application/use-cases/station-company/delete-station-company.use-case';
 import { StationCompanyRepository } from './infrastructure/database/repositories/station-company.repository';
 import { StationCompanyMapper } from './infrastructure/mappers/station-company.mapper';
+import { ReservationModule } from './presentation/modules/reservation.module';
 
 @Module({
   imports: [
@@ -82,6 +84,8 @@ import { StationCompanyMapper } from './infrastructure/mappers/station-company.m
     }),
     PrismaModule,
     ServicesModule,
+    PaydunyaModule,
+    ReservationModule,
   ],
   controllers: [
     UserController,

+ 229 - 0
node_app/src/application/dto/paydunya-payment.dto.ts

@@ -0,0 +1,229 @@
+import { IsString, IsNumber, IsOptional, IsEmail, IsEnum, IsBoolean, ValidateNested, Min, Max } from 'class-validator';
+import { Type } from 'class-transformer';
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
+import { PaydunyaPaymentMethod } from '../../infrastructure/services/paydunya.service';
+
+export class CreatePaydunyaInvoiceDto {
+  @ApiProperty({ description: 'Montant total de la facture', minimum: 200, maximum: 3000000 })
+  @IsNumber()
+  @Min(200)
+  @Max(3000000)
+  total_amount: number;
+
+  @ApiProperty({ description: 'Description de la facture' })
+  @IsString()
+  description: string;
+
+  @ApiProperty({ description: 'Nom du magasin/entreprise' })
+  @IsString()
+  store_name: string;
+
+  @ApiPropertyOptional({ description: 'Slogan du magasin' })
+  @IsOptional()
+  @IsString()
+  store_tagline?: string;
+
+  @ApiPropertyOptional({ description: 'Adresse postale du magasin' })
+  @IsOptional()
+  @IsString()
+  store_postal_address?: string;
+
+  @ApiPropertyOptional({ description: 'Téléphone du magasin' })
+  @IsOptional()
+  @IsString()
+  store_phone?: string;
+
+  @ApiPropertyOptional({ description: 'URL du logo du magasin' })
+  @IsOptional()
+  @IsString()
+  store_logo_url?: string;
+
+  @ApiPropertyOptional({ description: 'Site web du magasin' })
+  @IsOptional()
+  @IsString()
+  store_website_url?: string;
+
+  @ApiPropertyOptional({ description: 'URL de retour après annulation' })
+  @IsOptional()
+  @IsString()
+  cancel_url?: string;
+
+  @ApiPropertyOptional({ description: 'URL de retour après succès' })
+  @IsOptional()
+  @IsString()
+  return_url?: string;
+
+  @ApiPropertyOptional({ description: 'URL de callback pour notifications' })
+  @IsOptional()
+  @IsString()
+  callback_url?: string;
+
+  @ApiPropertyOptional({ description: 'Données personnalisées' })
+  @IsOptional()
+  custom_data?: Record<string, any>;
+}
+
+export class CreatePaydunyaDMPDto {
+  @ApiPropertyOptional({ description: 'Email du destinataire' })
+  @IsOptional()
+  @IsEmail()
+  recipient_email?: string;
+
+  @ApiPropertyOptional({ description: 'Téléphone du destinataire' })
+  @IsOptional()
+  @IsString()
+  recipient_phone?: string;
+
+  @ApiProperty({ description: 'Montant de la demande de paiement', minimum: 200, maximum: 3000000 })
+  @IsNumber()
+  @Min(200)
+  @Max(3000000)
+  amount: number;
+
+  @ApiProperty({ description: 'Qui supporte les frais (0: client, 1: marchand)', enum: [0, 1] })
+  @IsNumber()
+  @IsEnum([0, 1])
+  support_fees: 0 | 1;
+
+  @ApiProperty({ description: 'Envoyer notification automatique (0: non, 1: oui)', enum: [0, 1] })
+  @IsNumber()
+  @IsEnum([0, 1])
+  send_notification: 0 | 1;
+
+  @ApiPropertyOptional({ description: 'Description de la demande de paiement' })
+  @IsOptional()
+  @IsString()
+  description?: string;
+}
+
+export class CreatePaydunyaSoftPayDto {
+  @ApiProperty({ description: 'Montant du paiement', minimum: 200, maximum: 3000000 })
+  @IsNumber()
+  @Min(200)
+  @Max(3000000)
+  amount: number;
+
+  @ApiProperty({ description: 'Devise (XOF, EUR, USD)', default: 'XOF' })
+  @IsString()
+  currency: string = 'XOF';
+
+  @ApiProperty({ description: 'Nom du client' })
+  @IsString()
+  customer_name: string;
+
+  @ApiPropertyOptional({ description: 'Email du client' })
+  @IsOptional()
+  @IsEmail()
+  customer_email?: string;
+
+  @ApiPropertyOptional({ description: 'Téléphone du client' })
+  @IsOptional()
+  @IsString()
+  customer_phone?: string;
+
+  @ApiProperty({ description: 'Méthode de paiement', enum: PaydunyaPaymentMethod })
+  @IsEnum(PaydunyaPaymentMethod)
+  payment_method: PaydunyaPaymentMethod;
+
+  @ApiPropertyOptional({ description: 'Description du paiement' })
+  @IsOptional()
+  @IsString()
+  description?: string;
+
+  @ApiPropertyOptional({ description: 'Données personnalisées' })
+  @IsOptional()
+  custom_data?: Record<string, any>;
+}
+
+export class CheckPaymentStatusDto {
+  @ApiProperty({ description: 'Numéro de référence de la demande de paiement' })
+  @IsString()
+  reference_number: string;
+}
+
+export class ProcessReservationPaymentDto {
+  @ApiProperty({ description: 'ID de la réservation' })
+  @IsNumber()
+  reservation_id: number;
+
+  @ApiProperty({ description: 'Méthode de paiement', enum: PaydunyaPaymentMethod })
+  @IsEnum(PaydunyaPaymentMethod)
+  payment_method: PaydunyaPaymentMethod;
+
+  @ApiPropertyOptional({ description: 'Email du client' })
+  @IsOptional()
+  @IsEmail()
+  customer_email?: string;
+
+  @ApiPropertyOptional({ description: 'Téléphone du client' })
+  @IsOptional()
+  @IsString()
+  customer_phone?: string;
+
+  @ApiProperty({ description: 'Qui supporte les frais (0: client, 1: marchand)', enum: [0, 1], default: 0 })
+  @IsNumber()
+  @IsEnum([0, 1])
+  support_fees: 0 | 1 = 0;
+
+  @ApiProperty({ description: 'Type de paiement (invoice, dmp, softpay)', enum: ['invoice', 'dmp', 'softpay'], default: 'invoice' })
+  @IsString()
+  @IsEnum(['invoice', 'dmp', 'softpay'])
+  payment_type: 'invoice' | 'dmp' | 'softpay' = 'invoice';
+}
+
+// DTOs de réponse
+export class PaydunyaInvoiceResponseDto {
+  @ApiProperty({ description: 'Code de réponse' })
+  response_code: string;
+
+  @ApiProperty({ description: 'URL de paiement' })
+  response_text: string;
+
+  @ApiProperty({ description: 'Description' })
+  description: string;
+
+  @ApiProperty({ description: 'Token de la facture' })
+  token: string;
+}
+
+export class PaydunyaDMPResponseDto {
+  @ApiPropertyOptional({ description: 'Succès de la requête' })
+  success?: boolean;
+
+  @ApiPropertyOptional({ description: 'Message de réponse' })
+  message?: string;
+
+  @ApiPropertyOptional({ description: 'Code de réponse' })
+  response_code?: string;
+
+  @ApiPropertyOptional({ description: 'Numéro de référence' })
+  reference_number?: string;
+
+  @ApiPropertyOptional({ description: 'URL de paiement' })
+  url?: string;
+
+  @ApiPropertyOptional({ description: 'Description' })
+  description?: string;
+}
+
+export class PaydunyaStatusResponseDto {
+  @ApiProperty({ description: 'Code de réponse' })
+  response_code: string;
+
+  @ApiProperty({ description: 'Statut du paiement', enum: ['pending', 'completed', 'failed'] })
+  status: 'pending' | 'completed' | 'failed';
+}
+
+export class PaydunyaSoftPayResponseDto {
+  @ApiProperty({ description: 'Code de réponse' })
+  response_code: string;
+
+  @ApiProperty({ description: 'ID de la transaction' })
+  transaction_id: string;
+
+  @ApiProperty({ description: 'Statut de la transaction' })
+  status: string;
+
+  @ApiProperty({ description: 'Message de réponse' })
+  message: string;
+}

+ 275 - 0
node_app/src/application/use-cases/payment/process-reservation-payment.use-case.ts

@@ -0,0 +1,275 @@
+import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
+import { PrismaService } from '../../../infrastructure/database/prisma.service';
+import { PaydunyaService, PaydunyaPaymentMethod } from '../../../infrastructure/services/paydunya.service';
+import { ProcessReservationPaymentDto } from '../../dto/paydunya-payment.dto';
+
+@Injectable()
+export class ProcessReservationPaymentUseCase {
+  private readonly logger = new Logger(ProcessReservationPaymentUseCase.name);
+
+  constructor(
+    private readonly prisma: PrismaService,
+    private readonly paydunyaService: PaydunyaService,
+  ) {}
+
+  async execute(dto: ProcessReservationPaymentDto) {
+    this.logger.log(`Processing payment for reservation ${dto.reservation_id}`);
+
+    // Vérifier que la réservation existe
+    const reservation = await this.prisma.reservation.findUnique({
+      where: { id: dto.reservation_id },
+      include: {
+        user: true,
+        trajet: {
+          include: {
+            departStation: true,
+            arriveStation: true,
+            company: true,
+          },
+        },
+        place: true,
+        transaction: true,
+      },
+    });
+
+    if (!reservation) {
+      throw new NotFoundException(`Réservation avec l'ID ${dto.reservation_id} introuvable`);
+    }
+
+    // Vérifier qu'il n'y a pas déjà une transaction en cours ou complétée
+    if (reservation.transaction) {
+      if (reservation.transaction.status === 'completed') {
+        throw new BadRequestException('Cette réservation a déjà été payée');
+      }
+      if (reservation.transaction.status === 'pending') {
+        throw new BadRequestException('Un paiement est déjà en cours pour cette réservation');
+      }
+    }
+
+    const amount = reservation.trajet.pricePerSeat;
+    if (amount <= 0) {
+      throw new BadRequestException('Le prix du trajet doit être supérieur à 0');
+    }
+
+    try {
+      let paymentResult;
+      let transactionReference: string;
+      let paymentUrl: string | null = null;
+
+      // Préparer les données communes
+      const customerEmail = dto.customer_email || reservation.user.email;
+      const customerPhone = dto.customer_phone || reservation.user.telephone;
+      const customerName = reservation.user.name || `${reservation.user.name} ${reservation.user.otherName || ''}`.trim();
+
+      switch (dto.payment_type) {
+        case 'invoice':
+          paymentResult = await this.paydunyaService.createInvoice({
+            invoice: {
+              total_amount: amount,
+              description: `Paiement pour réservation #${reservation.id} - Trajet ${reservation.trajet.departStation.name} → ${reservation.trajet.arriveStation.name}`,
+              custom_data: {
+                reservation_id: reservation.id,
+                user_id: reservation.userId,
+                trajet_id: reservation.trajetId,
+                place_number: reservation.place.number,
+              },
+              actions: {
+                callback_url: process.env.PAYDUNYA_CALLBACK_URL,
+                return_url: process.env.PAYDUNYA_RETURN_URL,
+                cancel_url: process.env.PAYDUNYA_CANCEL_URL,
+              },
+            },
+            store: {
+              name: reservation.trajet.company?.name || 'Transport Platform',
+              phone: reservation.trajet.company?.address || '',
+            },
+          });
+          transactionReference = paymentResult.token;
+          paymentUrl = paymentResult.response_text;
+          break;
+
+        case 'dmp':
+          if (!customerEmail && !customerPhone) {
+            throw new BadRequestException('Email ou téléphone requis pour DMP');
+          }
+          paymentResult = await this.paydunyaService.createPaymentRequest({
+            recipient_email: customerEmail,
+            recipient_phone: customerPhone || undefined,
+            amount: amount,
+            support_fees: dto.support_fees,
+            send_notification: 1,
+            description: `Paiement réservation #${reservation.id} - ${reservation.trajet.departStation.name} → ${reservation.trajet.arriveStation.name}`,
+          });
+          transactionReference = paymentResult.reference_number || paymentResult.response_code || '';
+          paymentUrl = paymentResult.url || null;
+          break;
+
+        case 'softpay':
+          if (!customerName) {
+            throw new BadRequestException('Nom du client requis pour SoftPay');
+          }
+          paymentResult = await this.paydunyaService.processSoftPayment({
+            amount: amount,
+            currency: 'XOF',
+            customer_name: customerName,
+            customer_email: customerEmail,
+            customer_phone: customerPhone || undefined,
+            payment_method: dto.payment_method,
+            description: `Réservation #${reservation.id} - ${reservation.trajet.departStation.name} → ${reservation.trajet.arriveStation.name}`,
+            custom_data: {
+              reservation_id: reservation.id,
+              user_id: reservation.userId,
+              trajet_id: reservation.trajetId,
+            },
+          });
+          transactionReference = paymentResult.transaction_id;
+          break;
+
+        default:
+          throw new BadRequestException('Type de paiement non supporté');
+      }
+
+      // Créer ou mettre à jour la transaction dans la base de données
+      const transactionData = {
+        amount: amount,
+        currency: 'XOF',
+        status: dto.payment_type === 'softpay' ? (paymentResult as any).status : 'pending',
+        provider: 'paydunya',
+        reference: transactionReference,
+        metadata: {
+          payment_type: dto.payment_type,
+          payment_method: dto.payment_method,
+          paydunya_response: paymentResult as any,
+          reservation_details: {
+            trajet: `${reservation.trajet.departStation.name} → ${reservation.trajet.arriveStation.name}`,
+            departure_time: reservation.trajet.departAt,
+            place_number: reservation.place.number,
+          },
+        } as any,
+        userId: reservation.userId,
+        reservationId: reservation.id,
+      };
+
+      let transaction;
+      if (reservation.transaction) {
+        // Mettre à jour la transaction existante
+        transaction = await this.prisma.transaction.update({
+          where: { id: reservation.transaction.id },
+          data: transactionData,
+        });
+      } else {
+        // Créer une nouvelle transaction
+        transaction = await this.prisma.transaction.create({
+          data: transactionData,
+        });
+      }
+
+      this.logger.log(`Transaction created/updated: ${transaction.id} for reservation ${reservation.id}`);
+
+      return {
+        transaction_id: transaction.id,
+        payment_reference: transactionReference,
+        payment_url: paymentUrl,
+        status: transaction.status,
+        amount: amount,
+        currency: 'XOF',
+        payment_type: dto.payment_type,
+        payment_method: dto.payment_method,
+        reservation: {
+          id: reservation.id,
+          trajet: `${reservation.trajet.departStation.name} → ${reservation.trajet.arriveStation.name}`,
+          departure_time: reservation.trajet.departAt,
+          place_number: reservation.place.number,
+        },
+      };
+    } catch (error) {
+      this.logger.error(`Payment processing failed for reservation ${dto.reservation_id}:`, error);
+      
+      // Enregistrer l'échec dans la base de données
+      if (reservation.transaction) {
+        await this.prisma.transaction.update({
+          where: { id: reservation.transaction.id },
+          data: {
+            status: 'failed',
+            metadata: {
+              ...(reservation.transaction.metadata && typeof reservation.transaction.metadata === 'object' ? reservation.transaction.metadata : {}),
+              error: (error as Error).message,
+              failed_at: new Date(),
+            },
+          },
+        });
+      }
+
+      throw error;
+    }
+  }
+
+  /**
+   * Vérifie le statut d'un paiement et met à jour la transaction
+   */
+  async checkPaymentStatus(transactionId: string) {
+    const transaction = await this.prisma.transaction.findUnique({
+      where: { id: transactionId },
+      include: {
+        reservation: {
+          include: {
+            trajet: {
+              include: {
+                departStation: true,
+                arriveStation: true,
+              },
+            },
+          },
+        },
+      },
+    });
+
+    if (!transaction) {
+      throw new NotFoundException(`Transaction ${transactionId} introuvable`);
+    }
+
+    if (!transaction.reference) {
+      throw new BadRequestException('Référence de transaction manquante');
+    }
+
+    try {
+      const statusResult = await this.paydunyaService.checkPaymentStatus(transaction.reference);
+      
+      // Mettre à jour le statut si nécessaire
+      if (statusResult.status !== transaction.status) {
+        const updatedTransaction = await this.prisma.transaction.update({
+          where: { id: transactionId },
+          data: {
+            status: statusResult.status,
+            updatedAt: new Date(),
+            metadata: {
+              ...(transaction.metadata && typeof transaction.metadata === 'object' ? transaction.metadata : {}),
+              last_status_check: new Date(),
+              paydunya_status_response: statusResult as any,
+            },
+          },
+        });
+
+        this.logger.log(`Transaction ${transactionId} status updated to: ${statusResult.status}`);
+        
+        return {
+          transaction_id: transactionId,
+          status: statusResult.status,
+          previous_status: transaction.status,
+          updated: true,
+          reservation: transaction.reservation,
+        };
+      }
+
+      return {
+        transaction_id: transactionId,
+        status: statusResult.status,
+        updated: false,
+        reservation: transaction.reservation,
+      };
+    } catch (error) {
+      this.logger.error(`Status check failed for transaction ${transactionId}:`, error);
+      throw error;
+    }
+  }
+}

+ 19 - 0
node_app/src/application/use-cases/reservation/delete-reservation.use-case.ts

@@ -0,0 +1,19 @@
+import { Injectable, Inject } from '@nestjs/common';
+import { ReservationRepository } from '../../../infrastructure/database/repositories/reservation.repository';
+import { ReservationNotFoundException } from '../../exceptions/reservation-not-found.exception';
+
+@Injectable()
+export class DeleteReservationUseCase {
+  constructor(
+    @Inject('ReservationRepository')
+    private readonly reservationRepository: ReservationRepository,
+  ) {}
+
+  async execute(id: number) {
+    const reservation = await this.reservationRepository.findById(id);
+    if (!reservation) {
+      throw new ReservationNotFoundException(id);
+    }
+    return this.reservationRepository.delete(id);
+  }
+}

+ 14 - 0
node_app/src/application/use-cases/reservation/find-all-reservations.use-case.ts

@@ -0,0 +1,14 @@
+import { Injectable, Inject } from '@nestjs/common';
+import { ReservationRepository } from '../../../infrastructure/database/repositories/reservation.repository';
+
+@Injectable()
+export class FindAllReservationsUseCase {
+  constructor(
+    @Inject('ReservationRepository')
+    private readonly reservationRepository: ReservationRepository,
+  ) {}
+
+  async execute(page = 1, limit = 10) {
+    return this.reservationRepository.findAll(page, limit);
+  }
+}

+ 19 - 0
node_app/src/application/use-cases/reservation/find-reservation-by-id.use-case.ts

@@ -0,0 +1,19 @@
+import { Injectable, Inject } from '@nestjs/common';
+import { ReservationRepository } from '../../../infrastructure/database/repositories/reservation.repository';
+import { ReservationNotFoundException } from '../../exceptions/reservation-not-found.exception';
+
+@Injectable()
+export class FindReservationByIdUseCase {
+  constructor(
+    @Inject('ReservationRepository')
+    private readonly reservationRepository: ReservationRepository,
+  ) {}
+
+  async execute(id: number) {
+    const reservation = await this.reservationRepository.findById(id);
+    if (!reservation) {
+      throw new ReservationNotFoundException(id);
+    }
+    return reservation;
+  }
+}

+ 9 - 5
node_app/src/infrastructure/database/repositories/transaction.repository.ts

@@ -51,13 +51,15 @@ export class TransactionRepository { // implements IBaseRepository<Transaction>
 
   async create(data: Transaction): Promise<Transaction> {
     const prismaData = this.transactionMapper.toPersistence(data);
-    // Exclude userId, reservationId and metadata from prismaData when using connect relations
-    const { userId, reservationId, metadata, ...dataWithoutRelations } = prismaData;
+    // Exclude userId, reservationId and JSON fields from prismaData when using connect relations
+    const { userId, reservationId, metadata, paydunya_response, webhook_data, ...dataWithoutRelations } = prismaData;
     const prismaTransaction = await this.prisma.transaction.create({
       data: {
         ...dataWithoutRelations, // Champs directs comme amount, currency, status, etc.
-        // Cast metadata to fix type compatibility
+        // Cast JSON fields to fix type compatibility
         metadata: metadata as any,
+        paydunya_response: paydunya_response as any,
+        webhook_data: webhook_data as any,
         // Assurez-vous que le mapper prépare correctement les données pour les relations
         // ou ajustez ici pour les `connect` si le mapper ne le fait pas.
         user: { connect: { id: data.userId } },
@@ -74,13 +76,15 @@ export class TransactionRepository { // implements IBaseRepository<Transaction>
   async update(id: string, entity: Transaction): Promise<Transaction> {
     const prismaData = this.transactionMapper.toPersistence(entity);
     // Exclude userId and reservationId from prismaData when using connect relations
-    const { userId, reservationId, metadata, ...dataWithoutRelations } = prismaData;
+    const { userId, reservationId, metadata, paydunya_response, webhook_data, ...dataWithoutRelations } = prismaData;
     const prismaTransaction = await this.prisma.transaction.update({
       where: { id },
       data: {
         ...dataWithoutRelations,
-        // Cast metadata to InputJsonValue to fix type compatibility
+        // Cast JSON fields to InputJsonValue to fix type compatibility
         metadata: metadata as any,
+        paydunya_response: paydunya_response as any,
+        webhook_data: webhook_data as any,
         user: entity.userId ? { connect: { id: entity.userId } } : undefined,
         reservation: entity.reservationId ? { connect: { id: entity.reservationId } } : undefined,
       },

+ 16 - 0
node_app/src/infrastructure/mappers/transaction.mapper.ts

@@ -37,6 +37,14 @@ export class TransactionMapper implements IPrismaMapper<PrismaTransaction, Trans
       provider: transaction.provider,
       reference: transaction.reference,
       metadata: transaction.metadata ?? null,
+      // PayDunya specific fields
+      payment_method: null,
+      payment_type: null,
+      paydunya_token: null,
+      paydunya_transaction_id: null,
+      paydunya_invoice_url: null,
+      paydunya_response: null,
+      webhook_data: null,
     };
   }
 
@@ -66,6 +74,14 @@ export class TransactionMapper implements IPrismaMapper<PrismaTransaction, Trans
       metadata: transaction.metadata ?? null,
       userId: transaction.userId,
       reservationId: transaction.reservationId ?? null,
+      // PayDunya specific fields
+      payment_method: null,
+      payment_type: null,
+      paydunya_token: null,
+      paydunya_transaction_id: null,
+      paydunya_invoice_url: null,
+      paydunya_response: null,
+      webhook_data: null,
     };
   }
 

+ 163 - 0
node_app/src/infrastructure/payment/README.md

@@ -0,0 +1,163 @@
+# Module de Paiement PayDunya
+
+Ce module implémente l'intégration avec l'API PayDunya pour gérer les paiements des réservations de transport.
+
+## Configuration
+
+### Variables d'environnement requises
+
+```env
+PAYDUNYA_MASTER_KEY=your-paydunya-master-key
+PAYDUNYA_PRIVATE_KEY=your-paydunya-private-key
+PAYDUNYA_TOKEN=your-paydunya-token
+PAYDUNYA_BASE_URL=https://app.paydunya.com/sandbox-api/v1
+PAYDUNYA_MODE=sandbox
+```
+
+### Obtenir les clés API PayDunya
+
+1. Créez un compte sur [PayDunya](https://paydunya.com/)
+2. Accédez à votre tableau de bord
+3. Récupérez vos clés API dans la section "API Keys"
+4. Pour les tests, utilisez l'environnement sandbox
+
+## Structure du Module
+
+### Services
+
+- **PaydunyaService** (`paydunya.service.ts`): Service principal pour interagir avec l'API PayDunya
+  - Création de factures
+  - Demandes de paiement (DMP)
+  - Paiements directs (SoftPay)
+  - Vérification du statut des paiements
+
+### Use Cases
+
+- **ProcessReservationPaymentUseCase**: Gère le processus de paiement des réservations
+  - Validation de la réservation
+  - Création/mise à jour des transactions
+  - Intégration avec PayDunya
+
+### DTOs
+
+- **PaydunyaPaymentDto**: DTOs pour les requêtes et réponses PayDunya
+  - CreateInvoiceDto
+  - CreateDmpDto
+  - CreateSoftPayDto
+  - PaymentStatusDto
+  - ProcessReservationPaymentDto
+
+### Contrôleur
+
+- **PaymentController**: Endpoints API pour les paiements
+  - `POST /api/payments/reservation` - Traiter un paiement de réservation
+  - `GET /api/payments/status/:transactionId` - Vérifier le statut d'une transaction
+  - `POST /api/payments/invoice` - Créer une facture PayDunya
+  - `POST /api/payments/dmp` - Créer une demande de paiement
+  - `POST /api/payments/softpay` - Effectuer un paiement direct
+  - `GET /api/payments/paydunya-status/:token` - Vérifier le statut PayDunya
+  - `POST /api/payments/webhook` - Webhook PayDunya (à implémenter)
+  - `GET /api/payments/methods` - Lister les méthodes de paiement
+
+## Types de Paiement Supportés
+
+### 1. Facture (Invoice)
+- Création d'une facture PayDunya
+- L'utilisateur est redirigé vers PayDunya pour payer
+- Idéal pour les paiements web
+
+### 2. Demande de Paiement Mobile (DMP)
+- Paiement via Mobile Money
+- Support pour Orange Money, MTN Mobile Money, Moov Money, etc.
+- Idéal pour les paiements mobiles
+
+### 3. Paiement Direct (SoftPay)
+- Paiement direct par carte bancaire
+- Traitement immédiat
+- Idéal pour les paiements par carte
+
+## Utilisation
+
+### Exemple de paiement de réservation
+
+```typescript
+// POST /api/payments/reservation
+{
+  "reservationId": "uuid-reservation-id",
+  "paymentType": "invoice", // ou "dmp" ou "softpay"
+  "amount": 5000,
+  "currency": "XOF",
+  "description": "Paiement réservation transport",
+  // Données spécifiques selon le type de paiement
+  "invoiceData": {
+    "items": [
+      {
+        "name": "Billet de transport",
+        "quantity": 1,
+        "unit_price": "5000",
+        "total_price": "5000",
+        "description": "Trajet Abidjan - Bouaké"
+      }
+    ],
+    "taxes": [],
+    "custom_data": {
+      "reservation_id": "uuid-reservation-id"
+    }
+  }
+}
+```
+
+### Réponse
+
+```typescript
+{
+  "success": true,
+  "transaction": {
+    "id": "transaction-uuid",
+    "status": "PENDING",
+    "amount": 5000,
+    "currency": "XOF"
+  },
+  "paydunyaResponse": {
+    "response_code": "00",
+    "token": "paydunya-token",
+    "invoice_url": "https://app.paydunya.com/sandbox-invoice/..."
+  }
+}
+```
+
+## Statuts des Transactions
+
+- **PENDING**: Transaction en attente
+- **COMPLETED**: Transaction complétée avec succès
+- **FAILED**: Transaction échouée
+- **CANCELLED**: Transaction annulée
+
+## Sécurité
+
+- Toutes les clés API sont stockées dans les variables d'environnement
+- Validation des données d'entrée avec les DTOs
+- Gestion des erreurs appropriée
+- Logs des transactions pour audit
+
+## Tests
+
+Pour tester le module :
+
+1. Configurez les variables d'environnement avec vos clés sandbox
+2. Utilisez les endpoints de test PayDunya
+3. Vérifiez les logs pour le débogage
+
+## Webhooks PayDunya
+
+Le webhook PayDunya est configuré sur l'endpoint `/api/payments/webhook` mais nécessite une implémentation complète pour :
+
+- Vérifier la signature du webhook
+- Mettre à jour le statut des transactions
+- Notifier les utilisateurs
+
+## Support
+
+Pour plus d'informations sur l'API PayDunya :
+- [Documentation officielle PayDunya](https://paydunya.com/developers)
+- [Guide d'intégration](https://paydunya.com/developers/api)

+ 27 - 0
node_app/src/infrastructure/payment/paydunya.module.ts

@@ -0,0 +1,27 @@
+import { Module } from '@nestjs/common';
+import { ConfigModule } from '@nestjs/config';
+import { PaydunyaService } from '../services/paydunya.service';
+import { ProcessReservationPaymentUseCase } from '../../application/use-cases/payment/process-reservation-payment.use-case';
+import { PaymentController } from '../../presentation/controllers/payment.controller';
+import { PrismaModule } from '../database/prisma.module';
+import { ServicesModule } from '../services/services.module';
+
+@Module({
+  imports: [
+    ConfigModule,
+    PrismaModule,
+    ServicesModule,
+  ],
+  providers: [
+    PaydunyaService,
+    ProcessReservationPaymentUseCase,
+  ],
+  controllers: [
+    PaymentController,
+  ],
+  exports: [
+    PaydunyaService,
+    ProcessReservationPaymentUseCase,
+  ],
+})
+export class PaydunyaModule {}

+ 225 - 0
node_app/src/infrastructure/services/paydunya.service.spec.ts

@@ -0,0 +1,225 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { ConfigService } from '@nestjs/config';
+import { PaydunyaService } from './paydunya.service';
+import { CreateInvoiceDto, CreateDmpDto, CreateSoftPayDto } from '../../application/dto/paydunya-payment.dto';
+
+describe('PaydunyaService', () => {
+  let service: PaydunyaService;
+  let configService: ConfigService;
+
+  const mockConfigService = {
+    get: jest.fn((key: string) => {
+      const config = {
+        PAYDUNYA_MASTER_KEY: 'test-master-key',
+        PAYDUNYA_PRIVATE_KEY: 'test-private-key',
+        PAYDUNYA_TOKEN: 'test-token',
+        PAYDUNYA_BASE_URL: 'https://app.paydunya.com/sandbox-api/v1',
+        PAYDUNYA_MODE: 'sandbox',
+      };
+      return config[key];
+    }),
+  };
+
+  beforeEach(async () => {
+    const module: TestingModule = await Test.createTestingModule({
+      providers: [
+        PaydunyaService,
+        {
+          provide: ConfigService,
+          useValue: mockConfigService,
+        },
+      ],
+    }).compile();
+
+    service = module.get<PaydunyaService>(PaydunyaService);
+    configService = module.get<ConfigService>(ConfigService);
+  });
+
+  it('should be defined', () => {
+    expect(service).toBeDefined();
+  });
+
+  describe('createInvoice', () => {
+    it('should create an invoice with valid data', async () => {
+      const createInvoiceDto: CreateInvoiceDto = {
+        invoice: {
+          total_amount: 5000,
+          description: 'Test invoice',
+        },
+        store: {
+          name: 'Transport Company',
+          tagline: 'Reliable transport service',
+          phone: '+225123456789',
+          email: 'contact@transport.ci',
+          website_url: 'https://transport.ci',
+          logo_url: 'https://transport.ci/logo.png',
+        },
+        actions: {
+          cancel_url: 'https://transport.ci/cancel',
+          return_url: 'https://transport.ci/success',
+        },
+        custom_data: {
+          reservation_id: 'test-reservation-id',
+        },
+      };
+
+      // Mock fetch response
+      global.fetch = jest.fn().mockResolvedValue({
+        ok: true,
+        json: async () => ({
+          response_code: '00',
+          token: 'test-token-123',
+          invoice_url: 'https://app.paydunya.com/sandbox-invoice/test-token-123',
+        }),
+      });
+
+      const result = await service.createInvoice(createInvoiceDto);
+
+      expect(result.response_code).toBe('00');
+      expect(result.token).toBe('test-token-123');
+      expect(result.invoice_url).toContain('test-token-123');
+    });
+
+    it('should handle API errors', async () => {
+      const createInvoiceDto: CreateInvoiceDto = {
+        invoice: {
+          total_amount: 5000,
+          description: 'Test invoice',
+        },
+        store: {
+          name: 'Transport Company',
+          tagline: 'Reliable transport service',
+          phone: '+225123456789',
+          email: 'contact@transport.ci',
+        },
+        actions: {
+          cancel_url: 'https://transport.ci/cancel',
+          return_url: 'https://transport.ci/success',
+        },
+      };
+
+      // Mock fetch error response
+      global.fetch = jest.fn().mockResolvedValue({
+        ok: false,
+        status: 400,
+        json: async () => ({
+          response_code: '01',
+          response_text: 'Invalid request',
+        }),
+      });
+
+      await expect(service.createInvoice(createInvoiceDto)).rejects.toThrow();
+    });
+  });
+
+  describe('createDmp', () => {
+    it('should create a DMP request with valid data', async () => {
+      const createDmpDto: CreateDmpDto = {
+        account_alias: '225123456789',
+        account_name: 'John Doe',
+        account_email: 'john@example.com',
+        amount: 5000,
+        description: 'Transport payment',
+      };
+
+      // Mock fetch response
+      global.fetch = jest.fn().mockResolvedValue({
+        ok: true,
+        json: async () => ({
+          response_code: '00',
+          token: 'dmp-token-123',
+          description: 'DMP request created successfully',
+        }),
+      });
+
+      const result = await service.createDmp(createDmpDto);
+
+      expect(result.response_code).toBe('00');
+      expect(result.token).toBe('dmp-token-123');
+    });
+  });
+
+  describe('createSoftPay', () => {
+    it('should process SoftPay with valid card data', async () => {
+      const createSoftPayDto: CreateSoftPayDto = {
+        account_alias: 'john@example.com',
+        account_name: 'John Doe',
+        account_email: 'john@example.com',
+        amount: 5000,
+        description: 'Transport payment',
+        card_number: '4111111111111111',
+        card_holder_name: 'John Doe',
+        card_expiry_month: '12',
+        card_expiry_year: '25',
+        card_cvc: '123',
+      };
+
+      // Mock fetch response
+      global.fetch = jest.fn().mockResolvedValue({
+        ok: true,
+        json: async () => ({
+          response_code: '00',
+          token: 'softpay-token-123',
+          transaction_id: 'txn-123',
+          description: 'Payment processed successfully',
+        }),
+      });
+
+      const result = await service.createSoftPay(createSoftPayDto);
+
+      expect(result.response_code).toBe('00');
+      expect(result.token).toBe('softpay-token-123');
+      expect(result.transaction_id).toBe('txn-123');
+    });
+  });
+
+  describe('checkPaymentStatus', () => {
+    it('should check payment status with valid token', async () => {
+      const token = 'test-token-123';
+
+      // Mock fetch response
+      global.fetch = jest.fn().mockResolvedValue({
+        ok: true,
+        json: async () => ({
+          response_code: '00',
+          status: 'completed',
+          transaction_id: 'txn-123',
+          amount: 5000,
+          currency: 'XOF',
+        }),
+      });
+
+      const result = await service.checkPaymentStatus(token);
+
+      expect(result.response_code).toBe('00');
+      expect(result.status).toBe('completed');
+      expect(result.transaction_id).toBe('txn-123');
+    });
+
+    it('should handle invalid token', async () => {
+      const token = 'invalid-token';
+
+      // Mock fetch error response
+      global.fetch = jest.fn().mockResolvedValue({
+        ok: false,
+        status: 404,
+        json: async () => ({
+          response_code: '01',
+          response_text: 'Transaction not found',
+        }),
+      });
+
+      await expect(service.checkPaymentStatus(token)).rejects.toThrow();
+    });
+  });
+
+  describe('configuration', () => {
+    it('should load configuration correctly', () => {
+      expect(configService.get('PAYDUNYA_MASTER_KEY')).toBe('test-master-key');
+      expect(configService.get('PAYDUNYA_PRIVATE_KEY')).toBe('test-private-key');
+      expect(configService.get('PAYDUNYA_TOKEN')).toBe('test-token');
+      expect(configService.get('PAYDUNYA_BASE_URL')).toBe('https://app.paydunya.com/sandbox-api/v1');
+      expect(configService.get('PAYDUNYA_MODE')).toBe('sandbox');
+    });
+  });
+});

+ 251 - 0
node_app/src/infrastructure/services/paydunya.service.ts

@@ -0,0 +1,251 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+
+@Injectable()
+export class PaydunyaService {
+  private readonly logger = new Logger(PaydunyaService.name);
+  private readonly baseUrl: string;
+  private readonly masterKey: string;
+  private readonly privateKey: string;
+  private readonly token: string;
+
+  constructor(private configService: ConfigService) {
+    this.baseUrl = this.configService.get<string>('PAYDUNYA_BASE_URL', 'https://app.paydunya.com/api/v1');
+    this.masterKey = this.configService.get<string>('PAYDUNYA_MASTER_KEY') || '';
+    this.privateKey = this.configService.get<string>('PAYDUNYA_PRIVATE_KEY') || '';
+    this.token = this.configService.get<string>('PAYDUNYA_TOKEN') || '';
+
+    if (!this.masterKey || !this.privateKey || !this.token) {
+      this.logger.error('PayDunya API keys are missing. Please check your environment variables.');
+      throw new Error('PayDunya configuration is incomplete. Missing required API keys.');
+    }
+  }
+
+  private getHeaders() {
+    return {
+      'Content-Type': 'application/json',
+      'PAYDUNYA-MASTER-KEY': this.masterKey,
+      'PAYDUNYA-PRIVATE-KEY': this.privateKey,
+      'PAYDUNYA-TOKEN': this.token,
+    };
+  }
+
+  /**
+   * Crée une facture de paiement PayDunya
+   * @param invoiceData - Données de la facture
+   * @returns Promise avec la réponse de PayDunya
+   */
+  async createInvoice(invoiceData: PaydunyaInvoiceRequest): Promise<PaydunyaInvoiceResponse> {
+    try {
+      const response = await fetch(`${this.baseUrl}/checkout-invoice/create`, {
+        method: 'POST',
+        headers: this.getHeaders(),
+        body: JSON.stringify(invoiceData),
+      });
+
+      const data = await response.json();
+
+      if (!response.ok) {
+        this.logger.error('PayDunya invoice creation failed:', data);
+        throw new Error(`PayDunya API Error: ${data.message || 'Unknown error'}`);
+      }
+
+      this.logger.log(`Invoice created successfully: ${data.token}`);
+      return data;
+    } catch (error) {
+      this.logger.error('Error creating PayDunya invoice:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * Crée une demande de paiement (DMP)
+   * @param dmpData - Données de la demande de paiement
+   * @returns Promise avec la réponse de PayDunya
+   */
+  async createPaymentRequest(dmpData: PaydunyaDMPRequest): Promise<PaydunyaDMPResponse> {
+    try {
+      const response = await fetch(`${this.baseUrl}/dmp-api`, {
+        method: 'POST',
+        headers: this.getHeaders(),
+        body: JSON.stringify(dmpData),
+      });
+
+      const data = await response.json();
+
+      if (!response.ok) {
+        this.logger.error('PayDunya DMP creation failed:', data);
+        throw new Error(`PayDunya API Error: ${data.message || 'Unknown error'}`);
+      }
+
+      this.logger.log(`Payment request created successfully: ${data.reference_number}`);
+      return data;
+    } catch (error) {
+      this.logger.error('Error creating PayDunya payment request:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * Vérifie le statut d'une demande de paiement
+   * @param referenceNumber - Numéro de référence de la demande
+   * @returns Promise avec le statut du paiement
+   */
+  async checkPaymentStatus(referenceNumber: string): Promise<PaydunyaStatusResponse> {
+    try {
+      const response = await fetch(`${this.baseUrl}/dmp-api/check-status`, {
+        method: 'POST',
+        headers: this.getHeaders(),
+        body: JSON.stringify({ reference_number: referenceNumber }),
+      });
+
+      const data = await response.json();
+
+      if (!response.ok) {
+        this.logger.error('PayDunya status check failed:', data);
+        throw new Error(`PayDunya API Error: ${data.message || 'Unknown error'}`);
+      }
+
+      return data;
+    } catch (error) {
+      this.logger.error('Error checking PayDunya payment status:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * Effectue un paiement direct via SoftPay
+   * @param softPayData - Données du paiement direct
+   * @returns Promise avec la réponse de PayDunya
+   */
+  async processSoftPayment(softPayData: PaydunyaSoftPayRequest): Promise<PaydunyaSoftPayResponse> {
+    try {
+      const response = await fetch(`${this.baseUrl}/softpay`, {
+        method: 'POST',
+        headers: this.getHeaders(),
+        body: JSON.stringify(softPayData),
+      });
+
+      const data = await response.json();
+
+      if (!response.ok) {
+        this.logger.error('PayDunya SoftPay failed:', data);
+        throw new Error(`PayDunya API Error: ${data.message || 'Unknown error'}`);
+      }
+
+      this.logger.log(`SoftPay transaction processed: ${data.transaction_id}`);
+      return data;
+    } catch (error) {
+      this.logger.error('Error processing PayDunya SoftPay:', error);
+      throw error;
+    }
+  }
+}
+
+// Types et interfaces pour PayDunya
+export interface PaydunyaInvoiceRequest {
+  invoice: {
+    total_amount: number;
+    description: string;
+    items?: Record<string, PaydunyaItem>;
+    taxes?: Record<string, PaydunyaTax>;
+    custom_data?: Record<string, any>;
+    actions?: {
+      cancel_url?: string;
+      return_url?: string;
+      callback_url?: string;
+    };
+  };
+  store: {
+    name: string;
+    tagline?: string;
+    postal_address?: string;
+    phone?: string;
+    logo_url?: string;
+    website_url?: string;
+  };
+}
+
+export interface PaydunyaItem {
+  name: string;
+  quantity: number;
+  unit_price: string;
+  total_price: string;
+  description?: string;
+}
+
+export interface PaydunyaTax {
+  name: string;
+  amount: number;
+}
+
+export interface PaydunyaInvoiceResponse {
+  response_code: string;
+  response_text: string;
+  description: string;
+  token: string;
+}
+
+export interface PaydunyaDMPRequest {
+  recipient_email?: string;
+  recipient_phone?: string;
+  amount: number;
+  support_fees: 0 | 1;
+  send_notification: 0 | 1;
+  description?: string;
+}
+
+export interface PaydunyaDMPResponse {
+  success?: boolean;
+  message?: string;
+  response_code?: string;
+  reference_number?: string;
+  url?: string;
+  description?: string;
+}
+
+export interface PaydunyaStatusResponse {
+  response_code: string;
+  status: 'pending' | 'completed' | 'failed';
+}
+
+export interface PaydunyaSoftPayRequest {
+  amount: number;
+  currency: string;
+  customer_name: string;
+  customer_email?: string;
+  customer_phone?: string;
+  payment_method: string;
+  description?: string;
+  custom_data?: Record<string, any>;
+}
+
+export interface PaydunyaSoftPayResponse {
+  response_code: string;
+  transaction_id: string;
+  status: string;
+  message: string;
+}
+
+// Énumération des méthodes de paiement supportées
+export enum PaydunyaPaymentMethod {
+  ORANGE_MONEY_CI = 'orange-money-ci',
+  MTN_CI = 'mtn-ci',
+  MOOV_CI = 'moov-ci',
+  WAVE_CI = 'wave-ci',
+  ORANGE_MONEY_SN = 'orange-money-sn',
+  FREE_MONEY_SN = 'free-money-sn',
+  EXPRESSO_SN = 'expresso-sn',
+  WAVE_SN = 'wave-sn',
+  WIZALL_SN = 'wizall-sn',
+  ORANGE_MONEY_BF = 'orange-money-bf',
+  MOOV_BF = 'moov-bf',
+  MOOV_BJ = 'moov-bj',
+  MTN_BJ = 'mtn-bj',
+  T_MONEY_TG = 't-money-tg',
+  MOOV_TG = 'moov-tg',
+  ORANGE_MONEY_ML = 'orange-money-ml',
+  MOOV_ML = 'moov-ml',
+  BANK_CARD = 'bank-card',
+  PAYDUNYA = 'paydunya'
+}

+ 281 - 0
node_app/src/presentation/controllers/payment.controller.ts

@@ -0,0 +1,281 @@
+import {
+  Controller,
+  Post,
+  Get,
+  Body,
+  Param,
+  HttpCode,
+  HttpStatus,
+  UseGuards,
+  Logger,
+  BadRequestException,
+} from '@nestjs/common';
+import {
+  ApiTags,
+  ApiOperation,
+  ApiResponse,
+  ApiBearerAuth,
+  ApiParam,
+} from '@nestjs/swagger';
+import { AuthGuard } from '../guards/auth.guard';
+import { ProcessReservationPaymentUseCase } from '../../application/use-cases/payment/process-reservation-payment.use-case';
+import { PaydunyaService } from '../../infrastructure/services/paydunya.service';
+import {
+  ProcessReservationPaymentDto,
+  CreatePaydunyaInvoiceDto,
+  CreatePaydunyaDMPDto,
+  CreatePaydunyaSoftPayDto,
+  CheckPaymentStatusDto,
+  PaydunyaInvoiceResponseDto,
+  PaydunyaDMPResponseDto,
+  PaydunyaSoftPayResponseDto,
+  PaydunyaStatusResponseDto,
+} from '../../application/dto/paydunya-payment.dto';
+import { Public } from '../decorators/public.decorator';
+
+@ApiTags('Paiements')
+@Controller('payments')
+@UseGuards(AuthGuard)
+@ApiBearerAuth()
+export class PaymentController {
+  private readonly logger = new Logger(PaymentController.name);
+
+  constructor(
+    private readonly processReservationPaymentUseCase: ProcessReservationPaymentUseCase,
+    private readonly paydunyaService: PaydunyaService,
+  ) {}
+
+  @Post('reservation')
+  @HttpCode(HttpStatus.OK)
+  @ApiOperation({ 
+    summary: 'Traiter le paiement d\'une réservation',
+    description: 'Initie le processus de paiement pour une réservation via PayDunya'
+  })
+  @ApiResponse({
+    status: 200,
+    description: 'Paiement initié avec succès',
+    schema: {
+      type: 'object',
+      properties: {
+        transaction_id: { type: 'string', description: 'ID de la transaction' },
+        payment_reference: { type: 'string', description: 'Référence PayDunya' },
+        payment_url: { type: 'string', description: 'URL de paiement (si applicable)' },
+        status: { type: 'string', description: 'Statut du paiement' },
+        amount: { type: 'number', description: 'Montant du paiement' },
+        currency: { type: 'string', description: 'Devise' },
+        payment_type: { type: 'string', description: 'Type de paiement' },
+        payment_method: { type: 'string', description: 'Méthode de paiement' },
+        reservation: {
+          type: 'object',
+          properties: {
+            id: { type: 'number' },
+            trajet: { type: 'string' },
+            departure_time: { type: 'string' },
+            place_number: { type: 'number' },
+          },
+        },
+      },
+    },
+  })
+  @ApiResponse({ status: 400, description: 'Données invalides' })
+  @ApiResponse({ status: 404, description: 'Réservation introuvable' })
+  @Public()
+  async processReservationPayment(@Body() dto: ProcessReservationPaymentDto) {
+    this.logger.log(`Processing payment for reservation: ${dto.reservation_id}`);
+    return await this.processReservationPaymentUseCase.execute(dto);
+  }
+
+  @Get('transaction/:transactionId/status')
+  @ApiOperation({ 
+    summary: 'Vérifier le statut d\'un paiement',
+    description: 'Vérifie le statut actuel d\'une transaction de paiement'
+  })
+  @ApiParam({ name: 'transactionId', description: 'ID de la transaction' })
+  @ApiResponse({
+    status: 200,
+    description: 'Statut récupéré avec succès',
+    schema: {
+      type: 'object',
+      properties: {
+        transaction_id: { type: 'string' },
+        status: { type: 'string' },
+        updated: { type: 'boolean' },
+        previous_status: { type: 'string' },
+        reservation: { type: 'object' },
+      },
+    },
+  })
+  @ApiResponse({ status: 404, description: 'Transaction introuvable' })
+  async checkTransactionStatus(@Param('transactionId') transactionId: string) {
+    this.logger.log(`Checking status for transaction: ${transactionId}`);
+    return await this.processReservationPaymentUseCase.checkPaymentStatus(transactionId);
+  }
+
+  @Post('paydunya/invoice')
+  @HttpCode(HttpStatus.OK)
+  @ApiOperation({ 
+    summary: 'Créer une facture PayDunya',
+    description: 'Crée une facture de paiement PayDunya directement'
+  })
+  @ApiResponse({
+    status: 200,
+    description: 'Facture créée avec succès',
+    type: PaydunyaInvoiceResponseDto,
+  })
+  @ApiResponse({ status: 400, description: 'Données invalides' })
+  async createPaydunyaInvoice(@Body() dto: CreatePaydunyaInvoiceDto) {
+    this.logger.log('Creating PayDunya invoice');
+    
+    const invoiceData = {
+      invoice: {
+        total_amount: dto.total_amount,
+        description: dto.description,
+        custom_data: dto.custom_data,
+        actions: {
+          cancel_url: dto.cancel_url,
+          return_url: dto.return_url,
+          callback_url: dto.callback_url,
+        },
+      },
+      store: {
+        name: dto.store_name,
+        tagline: dto.store_tagline,
+        postal_address: dto.store_postal_address,
+        phone: dto.store_phone,
+        logo_url: dto.store_logo_url,
+        website_url: dto.store_website_url,
+      },
+    };
+
+    return await this.paydunyaService.createInvoice(invoiceData);
+  }
+
+  @Post('paydunya/dmp')
+  @HttpCode(HttpStatus.OK)
+  @ApiOperation({ 
+    summary: 'Créer une demande de paiement (DMP)',
+    description: 'Crée une demande de paiement PayDunya via SMS/Email'
+  })
+  @ApiResponse({
+    status: 200,
+    description: 'Demande de paiement créée avec succès',
+    type: PaydunyaDMPResponseDto,
+  })
+  @ApiResponse({ status: 400, description: 'Données invalides' })
+  async createPaydunyaDMP(@Body() dto: CreatePaydunyaDMPDto) {
+    this.logger.log('Creating PayDunya DMP');
+    
+    if (!dto.recipient_email && !dto.recipient_phone) {
+      throw new BadRequestException('Email ou téléphone du destinataire requis');
+    }
+
+    return await this.paydunyaService.createPaymentRequest(dto);
+  }
+
+  @Post('paydunya/softpay')
+  @HttpCode(HttpStatus.OK)
+  @ApiOperation({ 
+    summary: 'Effectuer un paiement direct (SoftPay)',
+    description: 'Effectue un paiement direct via PayDunya SoftPay'
+  })
+  @ApiResponse({
+    status: 200,
+    description: 'Paiement traité avec succès',
+    type: PaydunyaSoftPayResponseDto,
+  })
+  @ApiResponse({ status: 400, description: 'Données invalides' })
+  async processPaydunyaSoftPay(@Body() dto: CreatePaydunyaSoftPayDto) {
+    this.logger.log('Processing PayDunya SoftPay');
+    return await this.paydunyaService.processSoftPayment(dto);
+  }
+
+  @Post('paydunya/status')
+  @HttpCode(HttpStatus.OK)
+  @ApiOperation({ 
+    summary: 'Vérifier le statut PayDunya',
+    description: 'Vérifie le statut d\'une demande de paiement PayDunya'
+  })
+  @ApiResponse({
+    status: 200,
+    description: 'Statut récupéré avec succès',
+    type: PaydunyaStatusResponseDto,
+  })
+  @ApiResponse({ status: 400, description: 'Données invalides' })
+  async checkPaydunyaStatus(@Body() dto: CheckPaymentStatusDto) {
+    this.logger.log(`Checking PayDunya status for reference: ${dto.reference_number}`);
+    return await this.paydunyaService.checkPaymentStatus(dto.reference_number);
+  }
+
+  @Post('paydunya/webhook')
+  @HttpCode(HttpStatus.OK)
+  @ApiOperation({ 
+    summary: 'Webhook PayDunya',
+    description: 'Endpoint pour recevoir les notifications de PayDunya'
+  })
+  @ApiResponse({ status: 200, description: 'Webhook traité avec succès' })
+  async handlePaydunyaWebhook(@Body() webhookData: any) {
+    this.logger.log('Received PayDunya webhook:', JSON.stringify(webhookData));
+    
+    // TODO: Implémenter la logique de traitement du webhook
+    // - Vérifier la signature du webhook
+    // - Mettre à jour le statut de la transaction
+    // - Envoyer des notifications
+    
+    return {
+      status: 'received',
+      message: 'Webhook processed successfully',
+    };
+  }
+
+  @Get('methods')
+  @ApiOperation({ 
+    summary: 'Lister les méthodes de paiement disponibles',
+    description: 'Retourne la liste des méthodes de paiement supportées par PayDunya'
+  })
+  @ApiResponse({
+    status: 200,
+    description: 'Liste des méthodes de paiement',
+    schema: {
+      type: 'object',
+      properties: {
+        payment_methods: {
+          type: 'array',
+          items: {
+            type: 'object',
+            properties: {
+              code: { type: 'string' },
+              name: { type: 'string' },
+              country: { type: 'string' },
+              type: { type: 'string' },
+            },
+          },
+        },
+      },
+    },
+  })
+  getPaymentMethods() {
+    return {
+      payment_methods: [
+        { code: 'orange-money-ci', name: 'Orange Money', country: 'Côte d\'Ivoire', type: 'mobile_money' },
+        { code: 'mtn-ci', name: 'MTN Mobile Money', country: 'Côte d\'Ivoire', type: 'mobile_money' },
+        { code: 'moov-ci', name: 'Moov Money', country: 'Côte d\'Ivoire', type: 'mobile_money' },
+        { code: 'wave-ci', name: 'Wave', country: 'Côte d\'Ivoire', type: 'mobile_money' },
+        { code: 'orange-money-sn', name: 'Orange Money', country: 'Sénégal', type: 'mobile_money' },
+        { code: 'free-money-sn', name: 'Free Money', country: 'Sénégal', type: 'mobile_money' },
+        { code: 'expresso-sn', name: 'Expresso', country: 'Sénégal', type: 'mobile_money' },
+        { code: 'wave-sn', name: 'Wave', country: 'Sénégal', type: 'mobile_money' },
+        { code: 'wizall-sn', name: 'Wizall', country: 'Sénégal', type: 'mobile_money' },
+        { code: 'orange-money-bf', name: 'Orange Money', country: 'Burkina Faso', type: 'mobile_money' },
+        { code: 'moov-bf', name: 'Moov Money', country: 'Burkina Faso', type: 'mobile_money' },
+        { code: 'moov-bj', name: 'Moov Money', country: 'Bénin', type: 'mobile_money' },
+        { code: 'mtn-bj', name: 'MTN Mobile Money', country: 'Bénin', type: 'mobile_money' },
+        { code: 't-money-tg', name: 'T-Money', country: 'Togo', type: 'mobile_money' },
+        { code: 'moov-tg', name: 'Moov Money', country: 'Togo', type: 'mobile_money' },
+        { code: 'orange-money-ml', name: 'Orange Money', country: 'Mali', type: 'mobile_money' },
+        { code: 'moov-ml', name: 'Moov Money', country: 'Mali', type: 'mobile_money' },
+        { code: 'bank-card', name: 'Carte Bancaire', country: 'International', type: 'card' },
+        { code: 'paydunya', name: 'PayDunya', country: 'Multi-pays', type: 'wallet' },
+      ],
+    };
+  }
+}

+ 106 - 12
node_app/src/presentation/controllers/reservation.controller.ts

@@ -6,37 +6,131 @@ import {
   Param,
   Put,
   Delete,
+  Query,
+  ParseIntPipe,
+  HttpStatus,
+  HttpCode,
 } from '@nestjs/common';
-import { ApiSecurity, ApiTags } from '@nestjs/swagger';
+import {
+  ApiSecurity,
+  ApiTags,
+  ApiOperation,
+  ApiResponse,
+  ApiQuery,
+  ApiParam,
+} from '@nestjs/swagger';
+import { CreateReservationDto } from '../../application/dto/create-reservation.dto';
+import { UpdateReservationDto } from '../../application/dto/update-reservation.dto';
+import { ReservationResponseDto } from '../../application/dto/responses/reservation-response.dto';
+import { CreateReservationUseCase } from '../../application/use-cases/reservation/create-reservation.use-case';
+import { FindAllReservationsUseCase } from '../../application/use-cases/reservation/find-all-reservations.use-case';
+import { FindReservationByIdUseCase } from '../../application/use-cases/reservation/find-reservation-by-id.use-case';
+import { UpdateReservationUseCase } from '../../application/use-cases/reservation/update-reservation.use-case';
+import { DeleteReservationUseCase } from '../../application/use-cases/reservation/delete-reservation.use-case';
+import { Public } from '../decorators/public.decorator';
 
 @ApiTags('reservations')
 @Controller('reservations')
 @ApiSecurity('access-token')
 export class ReservationController {
-  constructor() {}
+  constructor(
+    private readonly createReservationUseCase: CreateReservationUseCase,
+    private readonly findAllReservationsUseCase: FindAllReservationsUseCase,
+    private readonly findReservationByIdUseCase: FindReservationByIdUseCase,
+    private readonly updateReservationUseCase: UpdateReservationUseCase,
+    private readonly deleteReservationUseCase: DeleteReservationUseCase,
+  ) {}
 
   @Post()
-  async create(@Body() createReservationDto: any) {
-    // TODO: Implement create reservation use case
+  @ApiOperation({ summary: 'Créer une nouvelle réservation' })
+  @ApiResponse({
+    status: 201,
+    description: 'Réservation créée avec succès',
+    type: ReservationResponseDto,
+  })
+  @ApiResponse({ status: 400, description: 'Données invalides' })
+  @ApiResponse({ status: 401, description: 'Non autorisé' })
+  @Public()
+  async create(@Body() createReservationDto: CreateReservationDto) {
+    return this.createReservationUseCase.execute(createReservationDto);
   }
 
   @Get()
-  async findAll() {
-    // TODO: Implement get all reservations use case
+  @ApiOperation({ summary: 'Récupérer toutes les réservations' })
+  @ApiQuery({
+    name: 'page',
+    required: false,
+    type: Number,
+    description: 'Numéro de page (défaut: 1)',
+  })
+  @ApiQuery({
+    name: 'limit',
+    required: false,
+    type: Number,
+    description: 'Nombre d\'éléments par page (défaut: 10)',
+  })
+  @ApiResponse({
+    status: 200,
+    description: 'Liste des réservations récupérée avec succès',
+    schema: {
+      type: 'object',
+      properties: {
+        data: {
+          type: 'array',
+          items: { $ref: '#/components/schemas/ReservationResponseDto' },
+        },
+        total: { type: 'number' },
+      },
+    },
+  })
+  @ApiResponse({ status: 401, description: 'Non autorisé' })
+  async findAll(
+    @Query('page') page?: number,
+    @Query('limit') limit?: number,
+  ) {
+    return this.findAllReservationsUseCase.execute(page || 1, limit || 10);
   }
 
   @Get(':id')
-  async findOne(@Param('id') id: string) {
-    // TODO: Implement get reservation by id use case
+  @ApiOperation({ summary: 'Récupérer une réservation par ID' })
+  @ApiParam({ name: 'id', description: 'ID de la réservation' })
+  @ApiResponse({
+    status: 200,
+    description: 'Réservation trouvée',
+    type: ReservationResponseDto,
+  })
+  @ApiResponse({ status: 404, description: 'Réservation non trouvée' })
+  @ApiResponse({ status: 401, description: 'Non autorisé' })
+  async findOne(@Param('id', ParseIntPipe) id: number) {
+    return this.findReservationByIdUseCase.execute(id);
   }
 
   @Put(':id')
-  async update(@Param('id') id: string, @Body() updateReservationDto: any) {
-    // TODO: Implement update reservation use case
+  @ApiOperation({ summary: 'Mettre à jour une réservation' })
+  @ApiParam({ name: 'id', description: 'ID de la réservation' })
+  @ApiResponse({
+    status: 200,
+    description: 'Réservation mise à jour avec succès',
+    type: ReservationResponseDto,
+  })
+  @ApiResponse({ status: 400, description: 'Données invalides' })
+  @ApiResponse({ status: 404, description: 'Réservation non trouvée' })
+  @ApiResponse({ status: 401, description: 'Non autorisé' })
+  async update(
+    @Param('id', ParseIntPipe) id: number,
+    @Body() updateReservationDto: UpdateReservationDto,
+  ) {
+    return this.updateReservationUseCase.execute(id, updateReservationDto);
   }
 
   @Delete(':id')
-  async remove(@Param('id') id: string) {
-    // TODO: Implement delete reservation use case
+  @HttpCode(HttpStatus.NO_CONTENT)
+  @ApiOperation({ summary: 'Supprimer une réservation' })
+  @ApiParam({ name: 'id', description: 'ID de la réservation' })
+  @ApiResponse({ status: 204, description: 'Réservation supprimée avec succès' })
+  @ApiResponse({ status: 404, description: 'Réservation non trouvée' })
+  @ApiResponse({ status: 401, description: 'Non autorisé' })
+  async remove(@Param('id', ParseIntPipe) id: number) {
+    return this.deleteReservationUseCase.execute(id);
   }
 }

+ 37 - 0
node_app/src/presentation/modules/reservation.module.ts

@@ -0,0 +1,37 @@
+import { Module } from '@nestjs/common';
+import { ReservationController } from '../controllers/reservation.controller';
+import { CreateReservationUseCase } from '../../application/use-cases/reservation/create-reservation.use-case';
+import { FindAllReservationsUseCase } from '../../application/use-cases/reservation/find-all-reservations.use-case';
+import { FindReservationByIdUseCase } from '../../application/use-cases/reservation/find-reservation-by-id.use-case';
+import { UpdateReservationUseCase } from '../../application/use-cases/reservation/update-reservation.use-case';
+import { DeleteReservationUseCase } from '../../application/use-cases/reservation/delete-reservation.use-case';
+import { ReservationRepository } from '../../infrastructure/database/repositories/reservation.repository';
+import { ReservationMapper } from '../../infrastructure/mappers/reservation.mapper';
+import { PrismaModule } from '../../infrastructure/database/prisma.module';
+import { ServicesModule } from '../../infrastructure/services/services.module';
+
+@Module({
+  imports: [PrismaModule, ServicesModule],
+  controllers: [ReservationController],
+  providers: [
+    CreateReservationUseCase,
+    FindAllReservationsUseCase,
+    FindReservationByIdUseCase,
+    UpdateReservationUseCase,
+    DeleteReservationUseCase,
+    {
+      provide: 'ReservationRepository',
+      useClass: ReservationRepository,
+    },
+    ReservationMapper,
+  ],
+  exports: [
+    CreateReservationUseCase,
+    FindAllReservationsUseCase,
+    FindReservationByIdUseCase,
+    UpdateReservationUseCase,
+    DeleteReservationUseCase,
+    'ReservationRepository',
+  ],
+})
+export class ReservationModule {}