feat: Release version 3.1.4 - Mode terrain et génération PDF
✨ Nouvelles fonctionnalités: - Ajout du mode terrain pour utilisation mobile hors connexion - Génération automatique de reçus PDF avec template personnalisé - Révision complète du système de cartes avec amélioration des performances 🔧 Améliorations techniques: - Refactoring du module chat avec architecture simplifiée - Optimisation du système de sécurité NIST SP 800-63B - Amélioration de la gestion des secteurs géographiques - Support UTF-8 étendu pour les noms d'utilisateurs 📱 Application mobile: - Nouveau mode terrain dans user_field_mode_page - Interface utilisateur adaptative pour conditions difficiles - Synchronisation offline améliorée 🗺️ Cartographie: - Optimisation des performances MapBox - Meilleure gestion des tuiles hors ligne - Amélioration de l'affichage des secteurs 📄 Documentation: - Ajout guide Android (ANDROID-GUIDE.md) - Documentation sécurité API (API-SECURITY.md) - Guide module chat (CHAT_MODULE.md) 🐛 Corrections: - Résolution des erreurs 400 lors de la création d'utilisateurs - Correction de la validation des noms d'utilisateurs - Fix des problèmes de synchronisation chat 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
622
api/src/Services/ReceiptService.php
Normal file
622
api/src/Services/ReceiptService.php
Normal file
@@ -0,0 +1,622 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
require_once __DIR__ . '/ApiService.php';
|
||||
require_once __DIR__ . '/FileService.php';
|
||||
|
||||
use PDO;
|
||||
use Database;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use FileService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Service de gestion des reçus pour les passages de type don (fk_type=1)
|
||||
* Optimisé pour générer des PDF très légers (< 20KB)
|
||||
*/
|
||||
class ReceiptService {
|
||||
private PDO $db;
|
||||
private FileService $fileService;
|
||||
private const DEFAULT_LOGO_PATH = __DIR__ . '/../../docs/_logo_recu.png';
|
||||
|
||||
public function __construct() {
|
||||
$this->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 = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background-color: #f4f4f4; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; }
|
||||
.footer { background-color: #f4f4f4; padding: 10px; text-align: center; font-size: 12px; }
|
||||
.amount { font-size: 24px; font-weight: bold; color: #2c5aa0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>' . htmlspecialchars($safeData['entite_name']) . '</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Bonjour ' . htmlspecialchars($safeData['donor_name']) . ',</p>
|
||||
|
||||
<p>Nous vous remercions chaleureusement pour votre don de <span class="amount">' .
|
||||
htmlspecialchars($safeData['amount']) . ' €</span> effectué le ' .
|
||||
htmlspecialchars($safeData['donation_date']) . '.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>Votre soutien est précieux pour nous permettre de poursuivre nos actions.</p>
|
||||
|
||||
<p>Cordialement,<br>
|
||||
L\'équipe de ' . htmlspecialchars($safeData['entite_name']) . '</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Conservez ce reçu pour votre déclaration fiscale</p>
|
||||
<p>' . htmlspecialchars($safeData['entite_name']) . '<br>
|
||||
' . htmlspecialchars($safeData['entite_address']) . '<br>
|
||||
' . htmlspecialchars($safeData['entite_email']) . '</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user