feat: PHASE 5.1 - Entités Gestion Documentaire

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
This commit is contained in:
dahoud
2025-11-30 11:26:37 +00:00
parent ac7b69e872
commit 4b78c173ca
3 changed files with 261 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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<PieceJointe> 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;
}
}
}

View File

@@ -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;
}
}
}