db = Database::getInstance(); $this->fileService = new FileService(); } /** * Génère un reçu pour un passage de type don avec email valide * * @param int $passageId ID du passage * @return bool True si le reçu a été généré avec succès */ public function generateReceiptForPassage(int $passageId): bool { try { // Récupérer les données du passage $passageData = $this->getPassageData($passageId); if (!$passageData) { LogService::log('Passage non trouvé pour génération de reçu', [ 'level' => 'warning', 'passageId' => $passageId ]); return false; } // Vérifier que c'est un don effectué (fk_type = 1) avec email valide if ((int)$passageData['fk_type'] !== 1) { return false; // Pas un don, pas de reçu } // Déchiffrer et vérifier l'email $email = ''; if (!empty($passageData['encrypted_email'])) { $email = ApiService::decryptSearchableData($passageData['encrypted_email']); } if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { LogService::log('Email invalide ou manquant pour le reçu', [ 'level' => 'info', 'passageId' => $passageId ]); return false; } // Récupérer les données de l'opération $operationData = $this->getOperationData($passageData['fk_operation']); if (!$operationData) { return false; } // Récupérer les données de l'entité $entiteData = $this->getEntiteData($operationData['fk_entite']); if (!$entiteData) { return false; } // Récupérer le logo de l'entité $logoPath = $this->getEntiteLogo($operationData['fk_entite']); // Préparer les données pour la génération du PDF $receiptData = $this->prepareReceiptData($passageData, $operationData, $entiteData, $email); // Générer le PDF optimisé $pdfContent = $this->generateOptimizedPDF($receiptData, $logoPath); // Créer le répertoire de stockage $uploadPath = "/entites/{$operationData['fk_entite']}/recus/{$operationData['id']}"; $fullPath = $this->fileService->createDirectory($operationData['fk_entite'], $uploadPath); // Nom du fichier $fileName = 'recu_' . $passageId . '.pdf'; $filePath = $fullPath . '/' . $fileName; // Sauvegarder le fichier if (file_put_contents($filePath, $pdfContent) === false) { throw new Exception('Impossible de sauvegarder le fichier PDF'); } // Appliquer les permissions $this->fileService->setFilePermissions($filePath); // Enregistrer dans la table medias $mediaId = $this->saveToMedias( $operationData['fk_entite'], $operationData['id'], $passageId, $fileName, $filePath, strlen($pdfContent) ); // Mettre à jour le passage avec les infos du reçu $this->updatePassageReceipt($passageId, $fileName); // Ajouter à la queue d'email $this->queueReceiptEmail($passageId, $email, $receiptData, $pdfContent); LogService::log('Reçu généré avec succès', [ 'level' => 'info', 'passageId' => $passageId, 'mediaId' => $mediaId, 'fileName' => $fileName, 'fileSize' => strlen($pdfContent) ]); return true; } catch (Exception $e) { LogService::log('Erreur lors de la génération du reçu', [ 'level' => 'error', 'error' => $e->getMessage(), 'passageId' => $passageId ]); return false; } } /** * Génère un PDF ultra-optimisé (< 20KB) * Utilise le format PDF natif pour minimiser la taille */ private function generateOptimizedPDF(array $data, ?string $logoPath): string { // Début du PDF $pdf = "%PDF-1.3\n"; $objects = []; $xref = []; // Object 1 - Catalog $objects[1] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"; // Object 2 - Pages $objects[2] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; // Object 3 - Page $objects[3] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n"; // Object 4 - Font (Helvetica) $objects[4] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"; // Contenu de la page (texte du reçu) $content = $this->generatePDFContent($data); // Object 5 - Content stream $contentLength = strlen($content); $objects[5] = "5 0 obj\n<< /Length $contentLength >>\nstream\n$content\nendstream\nendobj\n"; // Construction du PDF final $offset = strlen($pdf); foreach ($objects as $obj) { $xref[] = $offset; $pdf .= $obj; $offset += strlen($obj); } // Table xref $pdf .= "xref\n"; $pdf .= "0 " . (count($objects) + 1) . "\n"; $pdf .= "0000000000 65535 f \n"; foreach ($xref as $off) { $pdf .= sprintf("%010d 00000 n \n", $off); } // Trailer $pdf .= "trailer\n"; $pdf .= "<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\n"; $pdf .= "startxref\n"; $pdf .= "$offset\n"; $pdf .= "%%EOF\n"; return $pdf; } /** * Génère le contenu textuel du reçu pour le PDF */ private function generatePDFContent(array $data): string { $content = "BT\n"; $content .= "/F1 12 Tf\n"; $y = 750; // En-tête $content .= "50 $y Td\n"; $content .= "(" . $this->escapeString($data['entite_name']) . ") Tj\n"; $y -= 20; if (!empty($data['entite_address'])) { $content .= "0 -20 Td\n"; $content .= "(" . $this->escapeString($data['entite_address']) . ") Tj\n"; $y -= 20; } // Titre du reçu $y -= 40; $content .= "/F1 16 Tf\n"; $content .= "0 -40 Td\n"; $content .= "(RECU DE DON N° " . $data['receipt_number'] . ") Tj\n"; $content .= "/F1 10 Tf\n"; $content .= "0 -15 Td\n"; $content .= "(Article 200 du Code General des Impots) Tj\n"; // Informations du donateur $y -= 60; $content .= "/F1 12 Tf\n"; $content .= "0 -45 Td\n"; $content .= "(DONATEUR) Tj\n"; $content .= "/F1 11 Tf\n"; $content .= "0 -20 Td\n"; $content .= "(Nom : " . $this->escapeString($data['donor_name']) . ") Tj\n"; if (!empty($data['donor_address'])) { $content .= "0 -15 Td\n"; $content .= "(Adresse : " . $this->escapeString($data['donor_address']) . ") Tj\n"; } if (!empty($data['donor_email'])) { $content .= "0 -15 Td\n"; $content .= "(Email : " . $this->escapeString($data['donor_email']) . ") Tj\n"; } // Détails du don $content .= "0 -30 Td\n"; $content .= "/F1 12 Tf\n"; $content .= "(DETAILS DU DON) Tj\n"; $content .= "/F1 11 Tf\n"; $content .= "0 -20 Td\n"; $content .= "(Date : " . $data['donation_date'] . ") Tj\n"; $content .= "0 -15 Td\n"; $content .= "(Montant : " . $data['amount'] . " EUR) Tj\n"; $content .= "0 -15 Td\n"; $content .= "(Mode de reglement : " . $this->escapeString($data['payment_method']) . ") Tj\n"; if (!empty($data['operation_name'])) { $content .= "0 -15 Td\n"; $content .= "(Campagne : " . $this->escapeString($data['operation_name']) . ") Tj\n"; } // Mention légale $content .= "/F1 9 Tf\n"; $content .= "0 -40 Td\n"; $content .= "(Reduction d'impot egale a 66% du montant verse dans la limite de 20% du revenu imposable) Tj\n"; // Date et signature $content .= "/F1 11 Tf\n"; $content .= "0 -30 Td\n"; $content .= "(Fait a " . $this->escapeString($data['entite_city']) . ", le " . $data['signature_date'] . ") Tj\n"; $content .= "0 -20 Td\n"; $content .= "(Le President) Tj\n"; $content .= "ET\n"; return $content; } /** * Échappe les caractères spéciaux pour le PDF */ private function escapeString(string $str): string { // Échapper les caractères spéciaux PDF $str = str_replace('\\', '\\\\', $str); $str = str_replace('(', '\\(', $str); $str = str_replace(')', '\\)', $str); // Remplacer manuellement les caractères accentués les plus courants $accents = [ 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'Ñ' => 'N', 'ñ' => 'n', 'Ç' => 'C', 'ç' => 'c', 'Œ' => 'OE', 'œ' => 'oe', 'Æ' => 'AE', 'æ' => 'ae' ]; $str = strtr($str, $accents); // Supprimer tout caractère non-ASCII restant $str = preg_replace('/[^\x20-\x7E]/', '', $str); return $str; } /** * Récupère les données du passage */ private function getPassageData(int $passageId): ?array { $stmt = $this->db->prepare(' SELECT p.*, u.encrypted_name as user_encrypted_name, u.encrypted_email as user_encrypted_email, u.encrypted_phone as user_encrypted_phone FROM ope_pass p LEFT JOIN users u ON p.fk_user = u.id WHERE p.id = ? AND p.chk_active = 1 '); $stmt->execute([$passageId]); return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; } /** * Récupère les données de l'opération */ private function getOperationData(int $operationId): ?array { $stmt = $this->db->prepare(' SELECT * FROM operations WHERE id = ? AND chk_active = 1 '); $stmt->execute([$operationId]); return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; } /** * Récupère les données de l'entité */ private function getEntiteData(int $entiteId): ?array { $stmt = $this->db->prepare(' SELECT * FROM entites WHERE id = ? AND chk_active = 1 '); $stmt->execute([$entiteId]); $entite = $stmt->fetch(PDO::FETCH_ASSOC); if ($entite) { // Déchiffrer les données if (!empty($entite['encrypted_name'])) { $entite['name'] = ApiService::decryptData($entite['encrypted_name']); } if (!empty($entite['encrypted_email'])) { $entite['email'] = ApiService::decryptSearchableData($entite['encrypted_email']); } if (!empty($entite['encrypted_phone'])) { $entite['phone'] = ApiService::decryptData($entite['encrypted_phone']); } } return $entite ?: null; } /** * Récupère le chemin du logo de l'entité */ private function getEntiteLogo(int $entiteId): ?string { $stmt = $this->db->prepare(' SELECT file_path FROM medias WHERE support = ? AND support_id = ? AND file_category = ? ORDER BY created_at DESC LIMIT 1 '); $stmt->execute(['entite', $entiteId, 'logo']); $logo = $stmt->fetch(PDO::FETCH_ASSOC); if ($logo && !empty($logo['file_path']) && file_exists($logo['file_path'])) { return $logo['file_path']; } // Utiliser le logo par défaut si disponible if (file_exists(self::DEFAULT_LOGO_PATH)) { return self::DEFAULT_LOGO_PATH; } return null; } /** * Prépare les données pour le reçu */ private function prepareReceiptData(array $passage, array $operation, array $entite, string $email): array { // Déchiffrer le nom du donateur $donorName = ''; if (!empty($passage['encrypted_name'])) { $donorName = ApiService::decryptData($passage['encrypted_name']); } elseif (!empty($passage['user_encrypted_name'])) { $donorName = ApiService::decryptData($passage['user_encrypted_name']); } // Construire l'adresse du donateur $donorAddress = []; if (!empty($passage['numero'])) $donorAddress[] = $passage['numero']; if (!empty($passage['rue'])) $donorAddress[] = $passage['rue']; if (!empty($passage['rue_bis'])) $donorAddress[] = $passage['rue_bis']; if (!empty($passage['ville'])) $donorAddress[] = $passage['ville']; // Date du don $donationDate = ''; if (!empty($passage['passed_at'])) { $donationDate = date('d/m/Y', strtotime($passage['passed_at'])); } elseif (!empty($passage['created_at'])) { $donationDate = date('d/m/Y', strtotime($passage['created_at'])); } // Mode de règlement $paymentMethod = $this->getPaymentMethodLabel((int)($passage['fk_type_reglement'] ?? 1)); // Adresse de l'entité $entiteAddress = []; if (!empty($entite['adresse1'])) $entiteAddress[] = $entite['adresse1']; if (!empty($entite['adresse2'])) $entiteAddress[] = $entite['adresse2']; if (!empty($entite['code_postal']) || !empty($entite['ville'])) { $entiteAddress[] = trim($entite['code_postal'] . ' ' . $entite['ville']); } return [ 'receipt_number' => $passage['id'], 'entite_name' => $entite['name'] ?? 'Amicale des Sapeurs-Pompiers', 'entite_address' => implode(' ', $entiteAddress), 'entite_city' => $entite['ville'] ?? '', 'entite_email' => $entite['email'] ?? '', 'entite_phone' => $entite['phone'] ?? '', 'donor_name' => $donorName, 'donor_address' => implode(' ', $donorAddress), 'donor_email' => $email, 'donation_date' => $donationDate, 'amount' => number_format((float)($passage['montant'] ?? 0), 2, ',', ' '), 'payment_method' => $paymentMethod, 'operation_name' => $operation['libelle'] ?? '', 'signature_date' => date('d/m/Y') ]; } /** * Retourne le libellé du mode de règlement */ private function getPaymentMethodLabel(int $typeReglement): string { $stmt = $this->db->prepare('SELECT libelle FROM x_types_reglements WHERE id = ?'); $stmt->execute([$typeReglement]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result ? $result['libelle'] : 'Espèces'; } /** * Enregistre le fichier dans la table medias */ private function saveToMedias(int $entiteId, int $operationId, int $passageId, string $fileName, string $filePath, int $fileSize): int { $stmt = $this->db->prepare(' INSERT INTO medias ( support, support_id, fichier, file_type, file_category, file_size, mime_type, original_name, fk_entite, fk_operation, file_path, description, created_at, fk_user_creat ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?) '); $stmt->execute([ 'passage', // support $passageId, // support_id $fileName, // fichier 'pdf', // file_type 'recu', // file_category $fileSize, // file_size 'application/pdf', // mime_type $fileName, // original_name $entiteId, // fk_entite $operationId, // fk_operation $filePath, // file_path 'Reçu de don', // description 0 // fk_user_creat (système) ]); return (int)$this->db->lastInsertId(); } /** * Met à jour le passage avec les informations du reçu */ private function updatePassageReceipt(int $passageId, string $fileName): void { $stmt = $this->db->prepare(' UPDATE ope_pass SET nom_recu = ?, date_creat_recu = NOW() WHERE id = ? '); $stmt->execute([$fileName, $passageId]); } /** * Ajoute le reçu à la queue d'email */ private function queueReceiptEmail(int $passageId, string $email, array $receiptData, string $pdfContent): void { // Préparer le sujet $subject = "Votre reçu de don N°" . $receiptData['receipt_number']; // Préparer le corps de l'email $body = $this->generateEmailBody($receiptData); // Préparer les headers avec pièce jointe $boundary = md5((string)time()); $headers = "MIME-Version: 1.0\r\n"; $headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n"; // Corps complet avec pièce jointe $fullBody = "--$boundary\r\n"; $fullBody .= "Content-Type: text/html; charset=UTF-8\r\n"; $fullBody .= "Content-Transfer-Encoding: 7bit\r\n\r\n"; $fullBody .= $body . "\r\n\r\n"; // Pièce jointe PDF $fullBody .= "--$boundary\r\n"; $fullBody .= "Content-Type: application/pdf; name=\"recu_" . $receiptData['receipt_number'] . ".pdf\"\r\n"; $fullBody .= "Content-Transfer-Encoding: base64\r\n"; $fullBody .= "Content-Disposition: attachment; filename=\"recu_" . $receiptData['receipt_number'] . ".pdf\"\r\n\r\n"; $fullBody .= chunk_split(base64_encode($pdfContent)) . "\r\n"; $fullBody .= "--$boundary--"; // Insérer dans la queue $stmt = $this->db->prepare(' INSERT INTO email_queue ( fk_pass, to_email, subject, body, headers, created_at, status ) VALUES (?, ?, ?, ?, ?, NOW(), ?) '); $stmt->execute([ $passageId, $email, $subject, $fullBody, $headers, 'pending' ]); } /** * Génère le corps HTML de l'email */ private function generateEmailBody(array $data): string { // Convertir toutes les valeurs en string pour htmlspecialchars $safeData = array_map(function($value) { return is_string($value) ? $value : (string)$value; }, $data); $html = '

' . htmlspecialchars($safeData['entite_name']) . '

Bonjour ' . htmlspecialchars($safeData['donor_name']) . ',

Nous vous remercions chaleureusement pour votre don de ' . htmlspecialchars($safeData['amount']) . ' € effectué le ' . htmlspecialchars($safeData['donation_date']) . '.

Vous trouverez ci-joint votre reçu fiscal N°' . htmlspecialchars($safeData['receipt_number']) . ' qui vous permettra de bénéficier d\'une réduction d\'impôt égale à 66% du montant de votre don.

Votre soutien est précieux pour nous permettre de poursuivre nos actions.

Cordialement,
L\'équipe de ' . htmlspecialchars($safeData['entite_name']) . '

'; return $html; } /** * Met à jour la date d'envoi du reçu */ public function markReceiptAsSent(int $passageId): void { $stmt = $this->db->prepare(' UPDATE ope_pass SET date_sent_recu = NOW(), chk_email_sent = 1 WHERE id = ? '); $stmt->execute([$passageId]); } }