Files
crm_ecosplay/src/Entity/EFlex.php

414 lines
9.8 KiB
PHP
Raw Normal View History

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
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Attribute as Vich;
#[ORM\Entity]
#[Vich\Uploadable]
class EFlex
{
public const STATE_DRAFT = 'draft';
public const STATE_ACTIVE = 'active';
public const STATE_COMPLETED = 'completed';
public const STATE_CANCELLED = 'cancelled';
public const STATE_PENDING_SETUP = 'pending_setup';
public const METHOD_SEPA = 'sepa';
public const METHOD_CB = 'cb';
public const METHOD_VIREMENT = 'virement';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Customer::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Customer $customer;
#[ORM\Column(length: 500)]
private string $description;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
private string $totalAmount;
#[ORM\Column(length: 20, options: ['default' => 'draft'])]
private string $state = self::STATE_DRAFT;
#[ORM\Column(length: 20, options: ['default' => 'sepa'])]
private string $paymentMethod = self::METHOD_SEPA;
#[ORM\Column(length: 255, nullable: true)]
private ?string $stripeCustomerId = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $stripePaymentMethodId = null;
#[ORM\Column(length: 4, nullable: true)]
private ?string $stripeSepaLast4 = null;
#[ORM\Column(length: 100, nullable: true)]
private ?string $stripeSepaBankName = null;
#[ORM\Column(length: 2, nullable: true)]
private ?string $stripeSepaCountry = null;
#[ORM\Column(nullable: true)]
private ?string $submissionId = null;
// ── PDF Unsigned ──
#[ORM\Column(length: 255, nullable: true)]
private ?string $pdfUnsigned = null;
#[Vich\UploadableField(mapping: 'eflex_pdf', fileNameProperty: 'pdfUnsigned')]
private ?File $pdfUnsignedFile = null;
// ── PDF Signed ──
#[ORM\Column(length: 255, nullable: true)]
private ?string $pdfSigned = null;
#[Vich\UploadableField(mapping: 'eflex_signed_pdf', fileNameProperty: 'pdfSigned')]
private ?File $pdfSignedFile = null;
// ── PDF Audit ──
#[ORM\Column(length: 255, nullable: true)]
private ?string $pdfAudit = null;
#[Vich\UploadableField(mapping: 'eflex_audit_pdf', fileNameProperty: 'pdfAudit')]
private ?File $pdfAuditFile = null;
/** @var Collection<int, EFlexLine> */
#[ORM\OneToMany(targetEntity: EFlexLine::class, mappedBy: 'eflex', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['position' => 'ASC'])]
private Collection $lines;
#[ORM\Column]
private \DateTimeImmutable $createdAt;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
public function __construct(Customer $customer, string $description, string $totalAmount)
{
$this->customer = $customer;
$this->description = $description;
$this->totalAmount = $totalAmount;
$this->lines = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getCustomer(): Customer
{
return $this->customer;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): static
{
$this->description = $description;
return $this;
}
public function getTotalAmount(): string
{
return $this->totalAmount;
}
public function setTotalAmount(string $totalAmount): static
{
$this->totalAmount = $totalAmount;
return $this;
}
public function getState(): string
{
return $this->state;
}
public function setState(string $state): static
{
$this->state = $state;
return $this;
}
public function getPaymentMethod(): string
{
return $this->paymentMethod;
}
public function setPaymentMethod(string $paymentMethod): static
{
$this->paymentMethod = $paymentMethod;
return $this;
}
public function getStripeCustomerId(): ?string
{
return $this->stripeCustomerId;
}
public function setStripeCustomerId(?string $stripeCustomerId): static
{
$this->stripeCustomerId = $stripeCustomerId;
return $this;
}
public function getStripePaymentMethodId(): ?string
{
return $this->stripePaymentMethodId;
}
public function setStripePaymentMethodId(?string $stripePaymentMethodId): static
{
$this->stripePaymentMethodId = $stripePaymentMethodId;
return $this;
}
public function getStripeSepaLast4(): ?string
{
return $this->stripeSepaLast4;
}
public function setStripeSepaLast4(?string $stripeSepaLast4): static
{
$this->stripeSepaLast4 = $stripeSepaLast4;
return $this;
}
public function getStripeSepaBankName(): ?string
{
return $this->stripeSepaBankName;
}
public function setStripeSepaBankName(?string $stripeSepaBankName): static
{
$this->stripeSepaBankName = $stripeSepaBankName;
return $this;
}
public function getStripeSepaCountry(): ?string
{
return $this->stripeSepaCountry;
}
public function setStripeSepaCountry(?string $stripeSepaCountry): static
{
$this->stripeSepaCountry = $stripeSepaCountry;
return $this;
}
/** @return Collection<int, EFlexLine> */
public function getLines(): Collection
{
return $this->lines;
}
public function addLine(EFlexLine $line): static
{
if (!$this->lines->contains($line)) {
$this->lines->add($line);
}
return $this;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getSubmissionId(): ?string
{
return $this->submissionId;
}
public function setSubmissionId(?string $submissionId): static
{
$this->submissionId = $submissionId;
return $this;
}
public function getPdfUnsigned(): ?string
{
return $this->pdfUnsigned;
}
public function setPdfUnsigned(?string $pdfUnsigned): static
{
$this->pdfUnsigned = $pdfUnsigned;
return $this;
}
public function getPdfUnsignedFile(): ?File
{
return $this->pdfUnsignedFile;
}
public function setPdfUnsignedFile(?File $pdfUnsignedFile): static
{
$this->pdfUnsignedFile = $pdfUnsignedFile;
if (null !== $pdfUnsignedFile) {
$this->updatedAt = new \DateTimeImmutable();
}
return $this;
}
public function getPdfSigned(): ?string
{
return $this->pdfSigned;
}
public function setPdfSigned(?string $pdfSigned): static
{
$this->pdfSigned = $pdfSigned;
return $this;
}
public function setPdfSignedFile(?File $pdfSignedFile): static
{
$this->pdfSignedFile = $pdfSignedFile;
if (null !== $pdfSignedFile) {
$this->updatedAt = new \DateTimeImmutable();
}
return $this;
}
public function getPdfAudit(): ?string
{
return $this->pdfAudit;
}
public function setPdfAudit(?string $pdfAudit): static
{
$this->pdfAudit = $pdfAudit;
return $this;
}
public function setPdfAuditFile(?File $pdfAuditFile): static
{
$this->pdfAuditFile = $pdfAuditFile;
if (null !== $pdfAuditFile) {
$this->updatedAt = new \DateTimeImmutable();
}
return $this;
}
/**
* Reference unique (E_FLEX_00001).
*/
public function getReference(): string
{
return 'E_FLEX_'.str_pad((string) ($this->id ?? 0), 5, '0', \STR_PAD_LEFT);
}
public function getNbLines(): int
{
return $this->lines->count();
}
/**
* Montant mensuel (total / nb echeances).
*/
public function getMonthlyAmount(): float
{
$nb = $this->getNbLines();
return $nb > 0 ? round((float) $this->totalAmount / $nb, 2) : 0.0;
}
public function getNbPaid(): int
{
$count = 0;
foreach ($this->lines as $line) {
if (EFlexLine::STATE_OK === $line->getState()) {
++$count;
}
}
return $count;
}
public function getNbFailed(): int
{
$count = 0;
foreach ($this->lines as $line) {
if (EFlexLine::STATE_KO === $line->getState()) {
++$count;
}
}
return $count;
}
public function getTotalPaid(): float
{
$total = 0.0;
foreach ($this->lines as $line) {
if (EFlexLine::STATE_OK === $line->getState()) {
$total += (float) $line->getAmount();
}
}
return $total;
}
public function getProgress(): int
{
$nb = $this->getNbLines();
return $nb > 0 ? (int) round($this->getNbPaid() / $nb * 100) : 0;
}
public function getPaymentMethodLabel(): string
{
return match ($this->paymentMethod) {
self::METHOD_SEPA => 'Prelevement SEPA',
self::METHOD_CB => 'Carte bancaire',
self::METHOD_VIREMENT => 'Virement bancaire',
default => $this->paymentMethod,
};
}
}