CCDigital manages the complete lifecycle of citizen documents from upload through review approval to file storage and versioning.
Document Model
Documents in CCDigital follow a two-tier model:
Document Definition Catalog of available document types (e.g., “Cedula de Ciudadania”, “Diploma”) Entity: DocumentDefinition.java
Person Document Instance of a document type associated with a specific person Entity: PersonDocument.javaTable: person_documents
Entity Structure
The PersonDocument entity represents a document instance with full review workflow support:
// PersonDocument.java
@ Entity
@ Table ( name = "person_documents" )
public class PersonDocument {
@ Id
@ GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id ;
@ ManyToOne
@ JoinColumn ( name = "person_id" , nullable = false )
private Person person ;
@ ManyToOne
@ JoinColumn ( name = "document_id" , nullable = false )
private DocumentDefinition documentDefinition ;
@ Enumerated ( EnumType . STRING )
@ Column ( name = "status" )
private PersonDocumentStatus status ; // VIGENTE, VENCIDO, REVOCADO
@ Column ( name = "issued_at" )
private LocalDate issueDate ;
@ Column ( name = "expires_at" )
private LocalDate expiryDate ;
@ ManyToOne
@ JoinColumn ( name = "issuer_entity_id" )
private IssuingEntity issuerEntity ;
// Review workflow fields
@ Enumerated ( EnumType . STRING )
@ Column ( name = "review_status" )
private ReviewStatus reviewStatus ; // PENDING, APPROVED, REJECTED
@ Column ( name = "reviewed_by_user" )
private Long reviewedByUserId ;
@ Column ( name = "reviewed_at" )
private LocalDateTime reviewedAt ;
@ Column ( name = "review_notes" )
private String reviewNotes ;
@ OneToMany ( mappedBy = "personDocument" , cascade = CascadeType . ALL )
private List < FileRecord > files ;
}
Document Upload Workflows
CCDigital supports three distinct upload workflows based on user role:
1. Admin Upload
Government administrators can upload any document type for any citizen.
Select Person and Document Type
Admin navigates to /admin/persons/{id}/upload and selects the document type from the catalog.
Upload File
File is uploaded and stored in the person’s folder with cryptographic hash. // PersonDocumentService.java:316
@ Transactional
public PersonDocument uploadForPerson ( Long personId, Long documentId,
PersonDocumentStatus status,
LocalDate issueDate, LocalDate expiryDate,
MultipartFile file) {
Person person = personRepository . findById (personId)
. orElseThrow (() -> new IllegalArgumentException ( "Persona no encontrada" ));
DocumentDefinition def = documentDefinitionService . findById (documentId)
. orElseThrow (() -> new IllegalArgumentException ( "Documento no encontrado" ));
PersonDocument pd = new PersonDocument ();
pd . setPerson (person);
pd . setDocumentDefinition (def);
pd . setReviewStatus ( ReviewStatus . PENDING );
PersonDocument savedPd = personDocumentRepository . save (pd);
// Store file with SHA256 hash
FileStorageService . StoredFileInfo info = fileStorageService . storePersonFile (person, file);
FileRecord fr = new FileRecord ();
fr . setPersonDocument (savedPd);
fr . setHashSha256 ( info . getSha256 ());
fr . setStoragePath ( info . getRelativePath ());
fileRecordRepository . save (fr);
return savedPd;
}
Review Queue
Document enters PENDING state awaiting review at /admin/person-documents/{id}/review.
2. Issuer Upload
Authorized entities (universities, hospitals, etc.) can upload documents they are permitted to issue.
Search Person
Issuer searches for person by id_type and id_number at /issuer.
Validate PDF
System enforces strict PDF validation: // PersonDocumentService.java:268
private boolean isPdfFile ( MultipartFile file) {
// Check extension
boolean hasPdfExtension = originalName . toLowerCase (). endsWith ( ".pdf" );
// Check MIME type
boolean hasPdfMime = contentType . toLowerCase (). contains ( "pdf" );
// Check file signature
byte [] header = file . getInputStream (). readNBytes ( 4 );
boolean hasPdfSignature = header[ 0 ] == '%'
&& header[ 1 ] == 'P'
&& header[ 2 ] == 'D'
&& header[ 3 ] == 'F' ;
return hasPdfSignature && (hasPdfExtension || hasPdfMime);
}
Permission Check
System validates issuer has permission for the document type: // PersonDocumentService.java:187
@ Transactional
public PersonDocument uploadFromIssuer ( Long issuerId, Long personId,
Long documentId, /* ... */ ) {
// Validate issuer permission
boolean allowed = documentDefinitionService . findAllowedByIssuer (issuerId)
. stream ()
. anyMatch (d -> d . getId (). equals (documentId));
if ( ! allowed) {
throw new IllegalArgumentException (
"Este emisor no tiene permitido cargar ese tipo de documento."
);
}
// ... continue with upload
}
Store with Metadata
Document stored with fixed MIME type application/pdf and SHA256 hash for integrity verification.
Issuer Restriction: Only PDF files are accepted from issuer uploads. Extension, MIME type, and file signature (%PDF) are all validated.
3. API Upload
Programmatic upload via REST API using PersonDocumentRequest DTO.
// PersonDocumentService.java:107
@ Transactional
public PersonDocument create ( PersonDocumentRequest request) {
Person person = personRepository . findById ( request . getPersonId ())
. orElseThrow (() -> new IllegalArgumentException ( "Persona no encontrada" ));
DocumentDefinition def = documentDefinitionService . findById ( request . getDocumentId ())
. orElseThrow (() -> new IllegalArgumentException ( "Documento no encontrado" ));
PersonDocument pd = new PersonDocument ();
pd . setPerson (person);
pd . setDocumentDefinition (def);
pd . setReviewStatus ( ReviewStatus . PENDING );
PersonDocument saved = personDocumentRepository . save (pd);
// If storagePath provided, create FileRecord
if ( request . getStoragePath () != null ) {
FileRecord fr = new FileRecord ();
fr . setPersonDocument (saved);
fr . setStoragePath ( request . getStoragePath ());
fr . setHashSha256 ( request . getHashSha256 ());
fr . setStoredAs ( FileStoredAs . PATH );
fileRecordRepository . save (fr);
}
return saved;
}
Review Workflow
All uploaded documents must be reviewed by government administrators before they can be accessed through access requests.
Pending Review
New documents have review_status = PENDING and are listed at /admin/persons/{id}.
Administrator Review
Admin opens document at /admin/person-documents/{id}/review and chooses:
APPROVED - Document is valid and can be shared
REJECTED - Document has issues and cannot be shared
PENDING - Return to queue for later review
// PersonDocumentService.java:387
@ Transactional
public void review ( Long personDocumentId, ReviewStatus status, String notes) {
PersonDocument pd = personDocumentRepository . findById (personDocumentId)
. orElseThrow (() -> new IllegalArgumentException ( "PersonDocument no encontrado" ));
pd . setReviewStatus (status != null ? status : ReviewStatus . PENDING );
pd . setReviewNotes (notes != null && ! notes . isBlank () ? notes . trim () : null );
pd . setReviewedAt ( LocalDateTime . now ());
pd . setReviewedByUserId ( null ); // Set to actual admin ID when auth is implemented
personDocumentRepository . save (pd);
}
Access Control
Only documents with review_status = APPROVED can be included in access requests. Validation: AccessRequestService.java:168
File Storage Architecture
Files are stored on the local filesystem with metadata tracked in the database.
Storage Service
// FileStorageService.java
@ Service
public class FileStorageService {
@ Value ( "${ccdigital.fs.base-path}" )
private String basePath ;
public StoredFileInfo storePersonFile ( Person person , MultipartFile file ) {
// Create person folder with normalized name
String folderName = normalizePersonFolderName (person);
Path personDir = Paths . get (basePath, folderName);
Files . createDirectories (personDir);
// Generate unique filename
String originalName = file . getOriginalFilename ();
String fileName = generateUniqueFileName (originalName);
Path targetPath = personDir . resolve (fileName);
// Copy file and calculate SHA256
file . transferTo (targetPath);
String sha256 = calculateSHA256 (targetPath);
// Return relative path from base
String relativePath = basePath . relativize (targetPath). toString ();
return new StoredFileInfo (
originalName,
relativePath,
file . getSize (),
sha256
);
}
}
Configuration:
CCDIGITAL_FS_BASE_PATH - Root directory for document storage
Files organized in person-specific folders
Folder names normalized from person identification
File details are stored in the files table via FileRecord entity:
@ Entity
@ Table ( name = "files" )
public class FileRecord {
@ Id
@ GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id ;
@ ManyToOne
@ JoinColumn ( name = "person_document_id" )
private PersonDocument personDocument ;
@ Column ( name = "original_name" )
private String originalName ;
@ Column ( name = "mime_type" )
private String mimeType ;
@ Column ( name = "byte_size" )
private Long byteSize ;
@ Column ( name = "hash_sha256" )
private String hashSha256 ;
@ Enumerated ( EnumType . STRING )
@ Column ( name = "stored_as" )
private FileStoredAs storedAs ; // PATH or BLOB
@ Column ( name = "storage_path" )
private String storagePath ;
@ Column ( name = "version" )
private Integer version ;
}
Version Control
File versions are automatically managed through a database trigger.
Trigger: trg_files_autoversion
When a new file is inserted for an existing person_document_id:
Existing file versions are incremented
New file receives version = 1
Version history is maintained for audit purposes
When loading documents for access requests, the system selects the file with the highest version number to serve the most recent revision. Implementation: AccessRequestService.java:619
Document Status Lifecycle
Documents have a functional status separate from review status:
VIGENTE - Document is currently valid
VENCIDO - Document has expired (based on expires_at)
REVOCADO - Document has been revoked by issuer
SUSPENDIDO - Document is temporarily suspended
The review status controls access permission:
PENDING - Awaiting admin review (default)
APPROVED - Validated by admin, can be shared
REJECTED - Invalid, cannot be shared
Key Services
PersonDocumentService.java
Location: src/main/java/co/edu/unbosque/ccdigital/service/PersonDocumentService.javaCentral service for document lifecycle management including upload, review, and retrieval operations.
Location: src/main/java/co/edu/unbosque/ccdigital/service/FileStorageService.javaHandles physical file storage, path validation, and resource loading for document access.
DocumentDefinitionService.java
Location: src/main/java/co/edu/unbosque/ccdigital/service/DocumentDefinitionService.javaManages the document catalog and issuer permissions for document types.
Database Schema
Core Tables
person_documents - Document instances with review workflow
Unique constraint: (person_id, document_id, issued_at)
files - File metadata and version history
Auto-versioning via trg_files_autoversion trigger
documents (via DocumentDefinition) - Document type catalog
Links to categories for organization
entity_document_definitions - Junction table for issuer permissions
Stored Procedures
sp_add_person_document - Helper for document creation
sp_upload_file_path - File path registration
sp_upload_pdf_blob - PDF blob storage (legacy)
Security Features
Path Validation: All file access operations validate that the requested path is within the allowed base directory to prevent path traversal attacks.Service: FileStorageService.java
Integrity Verification: Every file is hashed with SHA256 upon storage. The hash can be used to verify file integrity before serving or during sync to Hyperledger Fabric.