Files
crm_ecosplay/src/Service/Pdf/ClientClosurePdf.php
Serreau Jovann 18daf096fa feat: systeme complet echeancier SEPA, E-Flex, attestations, avertissements clients
Echeancier - Webhooks DocuSeal:
- Webhook form.completed: telecharge PDF signe + audit, state SIGNED, prepare SEPA, notifie client + admin
- Webhook form.declined: state CANCELLED, notifie client + admin
- Reference EC_ECH_XXXXX affichee dans PDF, emails, pages client, admin
- Attestation fin de paiement auto via DocuSeal au completion

Echeancier - SEPA Direct Debit (remplace Subscriptions):
- Page /echeancier/setup-payment/{id}: formulaire IBAN Stripe Elements + mandat SEPA
- Confirmation SetupIntent -> stocke PaymentMethod -> state ACTIVE
- Commande cron app:echeancier:process-payments: preleve les echeances dues via PaymentIntent off_session
- Webhooks payment_intent.succeeded/failed: met a jour EcheancierLine, notifie client
- Regularisation CB via Stripe Checkout en cas d'echec prelevement
- Bouton "Forcer prelevement" par echeance dans admin
- Infos SEPA stockees (last4, bank_code, country) + affichees admin
- Page setup_payment_done quand SEPA deja configure
- Annulation auto apres 2 rejets + sync paiements vers Advert lie

Echeancier - Lien Advert:
- Champ advert (ManyToOne nullable) sur Echeancier
- Select "Avis lie" dans formulaire creation
- AdvertPayment cree a chaque echeance payee
- Advert passe en accepted quand echeancier completed

Comptabilite:
- Export echeanciers CSV/JSON/PDF/PDF signe dans /admin/comptabilite
- Colonnes: reference, client, creance, majoration, total, paye, restant, Stripe PI, avis lie

Stats:
- Case "Total impaye global" = factures impayees + echeances non payees
- Tableau echeanciers en cours avec restant du

Confiance client:
- Statut Confiant/Attention/Danger calcule dynamiquement
- Badge en haut a droite de la fiche client
- Integre warningLevel (1st=Attention, 2nd=Attention, last=Danger)
- Creation echeancier bloquee si Danger (template + controller)

Avertissements client (tab Controle, ROLE_ROOT):
- 3 niveaux: 1st, 2nd (procedure suspension preparee), last (48h)
- Motifs cochables: impayes, irrespect, hors horaires, services gratuits
- PDF signe DocuSeal pour chaque avertissement (ClientWarningPdf)
- PDF levee avertissement signe (ClientWarningResetPdf)
- Webhooks DocuSeal client_warning + client_warning_reset
- Barre progression 4 etapes dans admin
- Mentions legales: huis clos, contestation direction@e-cosplay.fr

Cloture compte:
- Bouton "Envoyer notification de cloture" apres dernier avertissement
- PDF signe DocuSeal (ClientClosurePdf): suppression 24h, recouvrement, commissaire justice, forces ordre
- Bouton "Suspendre le compte" (state suspended)
- Webhook DocuSeal client_closure: envoie PDF signe a client + admin + direction

Factures:
- Auto-generation PDF si absent lors de l'envoi
- Bouton "Envoyer" visible meme sans PDF pour factures payees

E-Flex (financement services):
- Entites EFlex + EFlexLine (reference E_FLEX_XXXXX)
- Methodes: SEPA, CB (Stripe Checkout), virement manuel
- PDF contrat avec 2 signatures DocuSeal (Company + Client)
- Controller admin CRUD + force payment + paiement manuel
- Pages client: verify, process, sign, signed, setup SEPA, paiement CB
- Webhook DocuSeal eflex: telecharge PDFs, prepare Stripe, notifie
- Webhooks Stripe payment_intent: gestion paiements E-Flex
- Cron traite aussi les E-Flex SEPA dans process-payments
- Tab E-Flex dans fiche client avec liste + modal creation
- Emails: signature, signed, verify_code, echeance_payee, echeance_echec

Attestations custom (ROLE_ROOT):
- Entite AttestationCustom avec items JSON + HMAC SHA-256
- Repeater dynamique pour ajouter elements a attester
- PDF avec phrase officielle "Je soussigne(e)..." + QR code verification
- Signature manuelle dans DocuSeal (redirection)
- Webhook attestation_custom: telecharge PDF signe + audit
- Page publique /attestation/verify/{id}/{hmac} avec validation HMAC
- Lien dans sidebar Super Admin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 07:45:22 +02:00

226 lines
8.1 KiB
PHP

<?php
namespace App\Service\Pdf;
use App\Entity\Customer;
use setasign\Fpdi\Fpdi;
use Symfony\Component\HttpKernel\KernelInterface;
class ClientClosurePdf extends Fpdi
{
public function __construct(
private readonly KernelInterface $kernel,
private readonly Customer $customer,
) {
parent::__construct();
$this->SetTitle($this->enc('Notification de cloture - '.$this->customer->getFullName()));
$this->SetAuthor($this->enc('Association E-Cosplay'));
}
public function generate(): void
{
$this->AliasNbPages();
$this->AddPage();
$this->writeHeader();
$this->writeContent();
$this->writeLegal();
$this->writeSignature();
}
/** @codeCoverageIgnore */
public function Header(): void
{
}
/** @codeCoverageIgnore */
public function Footer(): void
{
$this->SetY(-22);
$this->SetDrawColor(253, 140, 4);
$this->Line(15, $this->GetY(), 195, $this->GetY());
$this->Ln(3);
$this->SetFont('Arial', '', 7);
$this->SetTextColor(0, 0, 0);
$this->Cell(190, 3, $this->enc('42 rue de Saint-Quentin - 02800 BEAUTOR - Tel: 06 79 34 88 02 - contact@e-cosplay.fr'), 0, 1, 'C');
$this->Cell(190, 3, $this->enc('Association E-Cosplay - N SIRET 943 121 517 00011 - CODE APE 9329Z - RNA W022006988'), 0, 1, 'C');
$this->SetFont('Arial', 'I', 7);
$this->SetTextColor(150, 150, 150);
$this->Cell(190, 3, $this->enc('Page ').$this->PageNo().' / {nb}', 0, 0, 'C');
}
/** @codeCoverageIgnore */
private function writeHeader(): void
{
$logo = $this->kernel->getProjectDir().'/public/logo.jpg';
if (file_exists($logo)) {
$this->Image($logo, 10, 8, 45);
}
$this->SetFont('Arial', 'B', 16);
$this->SetXY(60, 10);
$this->Cell(0, 8, $this->enc('NOTIFICATION DE CLOTURE'), 0, 1, 'L');
$formatter = new \IntlDateFormatter(
'fr_FR',
\IntlDateFormatter::FULL,
\IntlDateFormatter::NONE,
'Europe/Paris',
\IntlDateFormatter::GREGORIAN
);
$this->SetFont('Arial', '', 10);
$this->SetXY(60, 19);
$this->Cell(0, 5, $this->enc('Emise a Beautor, le '.$formatter->format(new \DateTime())), 0, 1, 'L');
$this->SetFont('Arial', 'I', 9);
$this->SetXY(60, 25);
$this->SetTextColor(220, 38, 38);
$this->Cell(0, 5, $this->enc('LETTRE RECOMMANDEE AVEC ACCUSE DE RECEPTION'), 0, 1, 'L');
$this->SetTextColor(0, 0, 0);
// Destinataire
$y = 35;
$this->SetFont('Arial', 'B', 11);
$this->SetXY(120, $y);
$name = $this->customer->getRaisonSociale() ?: $this->customer->getFullName();
$this->Cell(0, 5, $this->enc($name), 0, 1, 'L');
if ($address = $this->customer->getAddress()) {
$y += 5;
$this->SetXY(120, $y);
$this->SetFont('Arial', '', 10);
$this->Cell(0, 5, $this->enc($address), 0, 1, 'L');
}
$y += 5;
$this->SetXY(120, $y);
$this->SetFont('Arial', '', 10);
$cityLine = ($this->customer->getZipCode() ?? '').' '.($this->customer->getCity() ?? '');
$this->Cell(0, 5, $this->enc(trim($cityLine)), 0, 1, 'L');
if ($this->customer->getEmail()) {
$y += 5;
$this->SetXY(120, $y);
$this->Cell(0, 5, $this->enc($this->customer->getEmail()), 0, 1, 'L');
}
$this->Ln(10);
}
/** @codeCoverageIgnore */
private function writeContent(): void
{
$this->SetY(65);
// Bandeau rouge
$this->SetFillColor(127, 29, 29);
$this->SetTextColor(255, 255, 255);
$this->SetFont('Arial', 'B', 12);
$this->Cell(0, 10, $this->enc(' CLOTURE DEFINITIVE DU COMPTE'), 0, 1, 'L', true);
$this->SetTextColor(0, 0, 0);
$this->Ln(5);
$greeting = $this->customer->getRaisonSociale()
? 'Chez '.$this->customer->getRaisonSociale()
: ($this->customer->getFirstName() ?? $this->customer->getFullName());
$this->SetFont('Arial', '', 10);
$this->MultiCell(0, 5, $this->enc($greeting.','), 0, 'L');
$this->Ln(3);
$this->MultiCell(0, 5, $this->enc(
'Malgre les avertissements qui vous ont ete adresses et en l\'absence de changement de votre part, '
.'le bureau de l\'Association E-Cosplay, reuni a huis clos, a decide d\'effectuer la procedure '
.'de cloture definitive de votre compte.'
), 0, 'L');
$this->Ln(3);
$this->SetFont('Arial', 'B', 10);
$this->SetTextColor(220, 38, 38);
$this->MultiCell(0, 5, $this->enc('Les mesures suivantes seront appliquees :'), 0, 'L');
$this->SetTextColor(0, 0, 0);
$this->Ln(2);
$this->SetFont('Arial', '', 10);
$items = [
'La totalite de vos services (sites internet, emails, noms de domaine) seront supprimes et detruits dans un delai de 24 heures, sans possibilite de recuperation.',
'Un depot aupres d\'une societe de recouvrement sera effectue pour les factures restant dues.',
'Une mise en demeure sera deposee aupres d\'un commissaire de justice.',
'En cas de manque de respect ou d\'insultes constatees, un depot aupres des forces de l\'ordre sera effectue.',
'L\'ensemble de vos donnees sera supprime conformement au RGPD, a l\'exception des donnees necessaires aux procedures de recouvrement.',
];
foreach ($items as $item) {
$this->Cell(5, 5, '', 0, 0);
$x = $this->GetX();
$this->Cell(5, 5, '-', 0, 0);
$this->MultiCell(0, 5, $this->enc($item), 0, 'L');
$this->Ln(1);
}
$this->Ln(3);
$this->SetFont('Arial', 'B', 10);
$this->SetTextColor(220, 38, 38);
$this->MultiCell(0, 5, $this->enc('Cette decision est definitive et irrevocable.'), 0, 'L');
$this->SetTextColor(0, 0, 0);
$this->Ln(5);
}
/** @codeCoverageIgnore */
private function writeLegal(): void
{
$this->SetDrawColor(200, 200, 200);
$this->Cell(0, 0.5, '', 'T', 1, 'L');
$this->Ln(3);
$this->SetFont('Arial', 'I', 8);
$this->SetTextColor(150, 150, 150);
$this->MultiCell(0, 4, $this->enc(
'Le present document constitue une notification officielle de cloture de compte de l\'Association E-Cosplay. '
.'Il fait foi en cas de litige. '
.'Toutes les decisions relatives aux avertissements et clotures sont prises par le bureau de l\'association a huis clos. '
.'Toute contestation devra etre adressee a direction@e-cosplay.fr dans un delai de 24 heures suivant la reception de ce document.'
), 0, 'L');
$this->SetTextColor(0, 0, 0);
$this->Ln(5);
}
/** @codeCoverageIgnore */
private function writeSignature(): void
{
if ($this->GetY() + 35 > $this->GetPageHeight() - 25) {
$this->AddPage();
}
$formatter = new \IntlDateFormatter(
'fr_FR',
\IntlDateFormatter::LONG,
\IntlDateFormatter::NONE,
'Europe/Paris',
\IntlDateFormatter::GREGORIAN
);
$this->SetFont('Arial', '', 9);
$this->Cell(0, 5, $this->enc('Fait a Beautor, le '.$formatter->format(new \DateTime())), 0, 1, 'L');
$this->Ln(3);
$this->SetFont('Arial', 'B', 9);
$this->Cell(0, 5, $this->enc('Pour le bureau de l\'Association E-Cosplay :'), 0, 1, 'L');
$this->Ln(2);
$this->SetFont('Arial', '', 10);
$this->Cell(85, 20, '{{Sign;type=signature;role=First Party}}', 0, 1, 'L');
$this->Ln(3);
$this->SetFont('Arial', 'I', 8);
$this->SetTextColor(150, 150, 150);
$this->Cell(0, 4, $this->enc('Signature electronique via DocuSeal - Valeur juridique (reglement eIDAS, art. 1367 Code civil)'), 0, 1, 'C');
$this->SetTextColor(0, 0, 0);
}
private function enc(string $text): string
{
return mb_convert_encoding($text, 'Windows-1252', 'UTF-8');
}
}