From 4b78c173ca6384bc83df8c4689fca4df585f829f Mon Sep 17 00:00:00 2001 From: dahoud Date: Sun, 30 Nov 2025 11:26:37 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20PHASE=205.1=20-=20Entit=C3=A9s=20Gestio?= =?UTF-8?q?n=20Documentaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entités créées: - Document: Gestion sécurisée avec hash MD5/SHA256, vérification intégrité - PieceJointe: Association flexible avec relations multiples Enum créé (module API): - TypeDocument: IDENTITE, JUSTIFICATIF_DOMICILE, PHOTO, CONTRAT, FACTURE, RECU, RAPPORT, AUTRE Fonctionnalités: - Vérification intégrité avec MD5 et SHA256 - Formatage taille fichiers - Compteur téléchargements - Relations flexibles: Membre, Organisation, Cotisation, Adhesion, DemandeAide, TransactionWave - Validation qu'une seule relation est renseignée Respect strict DRY/WOU: - Patterns d'entité cohérents - Enum dans module API réutilisable --- .../api/enums/document/TypeDocument.java | 30 ++++ .../unionflow/server/entity/Document.java | 128 ++++++++++++++++++ .../unionflow/server/entity/PieceJointe.java | 103 ++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/enums/document/TypeDocument.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Document.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/PieceJointe.java diff --git a/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/enums/document/TypeDocument.java b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/enums/document/TypeDocument.java new file mode 100644 index 0000000..0a646be --- /dev/null +++ b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/enums/document/TypeDocument.java @@ -0,0 +1,30 @@ +package dev.lions.unionflow.server.api.enums.document; + +/** + * Énumération des types de documents + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +public enum TypeDocument { + IDENTITE("Pièce d'Identité"), + JUSTIFICATIF_DOMICILE("Justificatif de Domicile"), + PHOTO("Photo"), + CONTRAT("Contrat"), + FACTURE("Facture"), + RECU("Reçu"), + RAPPORT("Rapport"), + AUTRE("Autre"); + + private final String libelle; + + TypeDocument(String libelle) { + this.libelle = libelle; + } + + public String getLibelle() { + return libelle; + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Document.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Document.java new file mode 100644 index 0000000..063c69e --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Document.java @@ -0,0 +1,128 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.document.TypeDocument; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Document pour la gestion documentaire sécurisée + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "documents", + indexes = { + @Index(name = "idx_document_nom_fichier", columnList = "nom_fichier"), + @Index(name = "idx_document_type", columnList = "type_document"), + @Index(name = "idx_document_hash_md5", columnList = "hash_md5"), + @Index(name = "idx_document_hash_sha256", columnList = "hash_sha256") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Document extends BaseEntity { + + /** Nom du fichier original */ + @NotBlank + @Column(name = "nom_fichier", nullable = false, length = 255) + private String nomFichier; + + /** Nom original du fichier (tel que téléchargé) */ + @Column(name = "nom_original", length = 255) + private String nomOriginal; + + /** Chemin de stockage */ + @NotBlank + @Column(name = "chemin_stockage", nullable = false, length = 1000) + private String cheminStockage; + + /** Type MIME */ + @Column(name = "type_mime", length = 100) + private String typeMime; + + /** Taille du fichier en octets */ + @NotNull + @Min(value = 0, message = "La taille doit être positive") + @Column(name = "taille_octets", nullable = false) + private Long tailleOctets; + + /** Type de document */ + @Enumerated(EnumType.STRING) + @Column(name = "type_document", length = 50) + private TypeDocument typeDocument; + + /** Hash MD5 pour vérification d'intégrité */ + @Column(name = "hash_md5", length = 32) + private String hashMd5; + + /** Hash SHA256 pour vérification d'intégrité */ + @Column(name = "hash_sha256", length = 64) + private String hashSha256; + + /** Description du document */ + @Column(name = "description", length = 1000) + private String description; + + /** Nombre de téléchargements */ + @Builder.Default + @Column(name = "nombre_telechargements", nullable = false) + private Integer nombreTelechargements = 0; + + /** Date de dernier téléchargement */ + @Column(name = "date_dernier_telechargement") + private java.time.LocalDateTime dateDernierTelechargement; + + /** Pièces jointes associées */ + @OneToMany(mappedBy = "document", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List piecesJointes = new ArrayList<>(); + + /** Méthode métier pour vérifier l'intégrité avec MD5 */ + public boolean verifierIntegriteMd5(String hashAttendu) { + return hashMd5 != null && hashMd5.equalsIgnoreCase(hashAttendu); + } + + /** Méthode métier pour vérifier l'intégrité avec SHA256 */ + public boolean verifierIntegriteSha256(String hashAttendu) { + return hashSha256 != null && hashSha256.equalsIgnoreCase(hashAttendu); + } + + /** Méthode métier pour obtenir la taille formatée */ + public String getTailleFormatee() { + if (tailleOctets == null) { + return "0 B"; + } + if (tailleOctets < 1024) { + return tailleOctets + " B"; + } else if (tailleOctets < 1024 * 1024) { + return String.format("%.2f KB", tailleOctets / 1024.0); + } else { + return String.format("%.2f MB", tailleOctets / (1024.0 * 1024.0)); + } + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (nombreTelechargements == null) { + nombreTelechargements = 0; + } + if (typeDocument == null) { + typeDocument = TypeDocument.AUTRE; + } + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/PieceJointe.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/PieceJointe.java new file mode 100644 index 0000000..6d3155b --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/PieceJointe.java @@ -0,0 +1,103 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité PieceJointe pour l'association flexible de documents + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "pieces_jointes", + indexes = { + @Index(name = "idx_piece_jointe_document", columnList = "document_id"), + @Index(name = "idx_piece_jointe_membre", columnList = "membre_id"), + @Index(name = "idx_piece_jointe_organisation", columnList = "organisation_id"), + @Index(name = "idx_piece_jointe_cotisation", columnList = "cotisation_id"), + @Index(name = "idx_piece_jointe_adhesion", columnList = "adhesion_id"), + @Index(name = "idx_piece_jointe_demande_aide", columnList = "demande_aide_id"), + @Index(name = "idx_piece_jointe_transaction_wave", columnList = "transaction_wave_id") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class PieceJointe extends BaseEntity { + + /** Ordre d'affichage */ + @NotNull + @Min(value = 1, message = "L'ordre doit être positif") + @Column(name = "ordre", nullable = false) + private Integer ordre; + + /** Libellé de la pièce jointe */ + @Column(name = "libelle", length = 200) + private String libelle; + + /** Commentaire */ + @Column(name = "commentaire", length = 500) + private String commentaire; + + /** Document associé */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "document_id", nullable = false) + private Document document; + + // Relations flexibles (une seule doit être renseignée) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id") + private Membre membre; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id") + private Organisation organisation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cotisation_id") + private Cotisation cotisation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "adhesion_id") + private Adhesion adhesion; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "demande_aide_id") + private DemandeAide demandeAide; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "transaction_wave_id") + private TransactionWave transactionWave; + + /** Méthode métier pour vérifier qu'une seule relation est renseignée */ + public boolean isValide() { + int count = 0; + if (membre != null) count++; + if (organisation != null) count++; + if (cotisation != null) count++; + if (adhesion != null) count++; + if (demandeAide != null) count++; + if (transactionWave != null) count++; + return count == 1; // Exactement une relation doit être renseignée + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (ordre == null) { + ordre = 1; + } + } +} +