diff --git a/src/RealCV.Application/Interfaces/ICVCheckService.cs b/src/RealCV.Application/Interfaces/ICVCheckService.cs index 4308c39..84ea21a 100644 --- a/src/RealCV.Application/Interfaces/ICVCheckService.cs +++ b/src/RealCV.Application/Interfaces/ICVCheckService.cs @@ -11,4 +11,9 @@ public interface ICVCheckService Task> GetUserChecksAsync(Guid userId); Task GetReportAsync(Guid checkId, Guid userId); Task DeleteCheckAsync(Guid checkId, Guid userId); + + /// + /// GDPR: Delete all CV checks and associated data for a user (right to erasure). + /// + Task DeleteAllUserDataAsync(Guid userId); } diff --git a/src/RealCV.Infrastructure/Jobs/DataRetentionJob.cs b/src/RealCV.Infrastructure/Jobs/DataRetentionJob.cs new file mode 100644 index 0000000..78dbc18 --- /dev/null +++ b/src/RealCV.Infrastructure/Jobs/DataRetentionJob.cs @@ -0,0 +1,106 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using RealCV.Application.Interfaces; +using RealCV.Domain.Enums; +using RealCV.Infrastructure.Data; + +namespace RealCV.Infrastructure.Jobs; + +/// +/// GDPR compliance job that automatically deletes old CV check data +/// based on configured retention period. +/// +public sealed class DataRetentionJob +{ + private readonly ApplicationDbContext _dbContext; + private readonly IFileStorageService _fileStorageService; + private readonly ILogger _logger; + private readonly int _retentionDays; + + public DataRetentionJob( + ApplicationDbContext dbContext, + IFileStorageService fileStorageService, + IConfiguration configuration, + ILogger logger) + { + _dbContext = dbContext; + _fileStorageService = fileStorageService; + _logger = logger; + _retentionDays = configuration.GetValue("DataRetention:CVCheckRetentionDays", 30); + } + + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Starting GDPR data retention job (retention: {Days} days)", _retentionDays); + + try + { + var cutoffDate = DateTime.UtcNow.AddDays(-_retentionDays); + + // Find old completed CV checks that should be deleted + var oldChecks = await _dbContext.CVChecks + .Include(c => c.Flags) + .Where(c => c.CompletedAt != null && c.CompletedAt < cutoffDate) + .Where(c => c.Status == CheckStatus.Completed || c.Status == CheckStatus.Failed) + .ToListAsync(cancellationToken); + + if (oldChecks.Count == 0) + { + _logger.LogInformation("No CV checks older than {Days} days found for deletion", _retentionDays); + return; + } + + _logger.LogInformation("Found {Count} CV checks older than {Days} days for deletion", oldChecks.Count, _retentionDays); + + var deletedCount = 0; + var fileDeletedCount = 0; + + foreach (var check in oldChecks) + { + try + { + // Delete any remaining files (should already be deleted after processing, but be thorough) + if (!string.IsNullOrWhiteSpace(check.BlobUrl)) + { + try + { + await _fileStorageService.DeleteAsync(check.BlobUrl); + fileDeletedCount++; + _logger.LogDebug("Deleted orphaned file for CV check {CheckId}", check.Id); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete file for CV check {CheckId}", check.Id); + } + } + + // Delete associated flags + _dbContext.CVFlags.RemoveRange(check.Flags); + + // Delete the CV check record + _dbContext.CVChecks.Remove(check); + deletedCount++; + + _logger.LogDebug("Marked CV check {CheckId} for deletion (created: {Created})", + check.Id, check.CreatedAt); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing CV check {CheckId} for deletion", check.Id); + } + } + + await _dbContext.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "GDPR data retention job completed. Deleted {DeletedCount} CV checks and {FileCount} orphaned files", + deletedCount, fileDeletedCount); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in GDPR data retention job"); + throw; + } + } +} diff --git a/src/RealCV.Infrastructure/Jobs/ProcessCVCheckJob.cs b/src/RealCV.Infrastructure/Jobs/ProcessCVCheckJob.cs index f8127e9..3d206b6 100644 --- a/src/RealCV.Infrastructure/Jobs/ProcessCVCheckJob.cs +++ b/src/RealCV.Infrastructure/Jobs/ProcessCVCheckJob.cs @@ -265,6 +265,12 @@ public sealed class ProcessCVCheckJob cvCheckId, score); await _auditService.LogAsync(cvCheck.UserId, AuditActions.CVProcessed, "CVCheck", cvCheckId, $"Score: {score}"); + + // GDPR: Delete the uploaded CV file immediately after processing + // We only need the extracted data and report, not the original file + await DeleteCVFileAsync(cvCheck.BlobUrl, cvCheckId); + cvCheck.BlobUrl = string.Empty; // Clear the URL as file no longer exists + await _dbContext.SaveChangesAsync(cancellationToken); } catch (Exception ex) { @@ -287,6 +293,29 @@ public sealed class ProcessCVCheckJob } } + /// + /// GDPR: Safely delete the uploaded CV file after processing. + /// + private async Task DeleteCVFileAsync(string blobUrl, Guid cvCheckId) + { + if (string.IsNullOrWhiteSpace(blobUrl)) + { + _logger.LogDebug("No file to delete for CV check {CheckId}", cvCheckId); + return; + } + + try + { + await _fileStorageService.DeleteAsync(blobUrl); + _logger.LogInformation("GDPR: Deleted CV file for check {CheckId}", cvCheckId); + } + catch (Exception ex) + { + // Log but don't fail the job - file deletion is important but shouldn't break processing + _logger.LogWarning(ex, "Failed to delete CV file for check {CheckId}: {BlobUrl}", cvCheckId, blobUrl); + } + } + private static (int Score, List Flags) CalculateVeracityScore( List verifications, List educationResults, diff --git a/src/RealCV.Infrastructure/Services/CVCheckService.cs b/src/RealCV.Infrastructure/Services/CVCheckService.cs index 7c676bb..f9d4dd1 100644 --- a/src/RealCV.Infrastructure/Services/CVCheckService.cs +++ b/src/RealCV.Infrastructure/Services/CVCheckService.cs @@ -185,17 +185,78 @@ public sealed class CVCheckService : ICVCheckService var fileName = cvCheck.OriginalFileName; + // GDPR: Delete the uploaded file if it still exists + if (!string.IsNullOrWhiteSpace(cvCheck.BlobUrl)) + { + try + { + await _fileStorageService.DeleteAsync(cvCheck.BlobUrl); + _logger.LogDebug("Deleted file for CV check {CheckId}", checkId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete file for CV check {CheckId}", checkId); + // Continue with deletion even if file deletion fails + } + } + _dbContext.CVFlags.RemoveRange(cvCheck.Flags); _dbContext.CVChecks.Remove(cvCheck); await _dbContext.SaveChangesAsync(); - _logger.LogInformation("Deleted CV check {CheckId} for user {UserId}", checkId, userId); + _logger.LogInformation("GDPR: Deleted CV check {CheckId} and associated data for user {UserId}", checkId, userId); await _auditService.LogAsync(userId, AuditActions.CVDeleted, "CVCheck", checkId, $"File: {fileName}"); return true; } + public async Task DeleteAllUserDataAsync(Guid userId) + { + _logger.LogInformation("GDPR: Deleting all CV data for user {UserId}", userId); + + var userChecks = await _dbContext.CVChecks + .Include(c => c.Flags) + .Where(c => c.UserId == userId) + .ToListAsync(); + + if (userChecks.Count == 0) + { + _logger.LogDebug("No CV checks found for user {UserId}", userId); + return 0; + } + + var deletedCount = 0; + + foreach (var check in userChecks) + { + // Delete the file if it exists + if (!string.IsNullOrWhiteSpace(check.BlobUrl)) + { + try + { + await _fileStorageService.DeleteAsync(check.BlobUrl); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete file for CV check {CheckId}", check.Id); + } + } + + _dbContext.CVFlags.RemoveRange(check.Flags); + _dbContext.CVChecks.Remove(check); + deletedCount++; + } + + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation("GDPR: Deleted {Count} CV checks for user {UserId}", deletedCount, userId); + + await _auditService.LogAsync(userId, AuditActions.CVDeleted, null, null, $"Deleted all data: {deletedCount} checks"); + + return deletedCount; + } + private static CVCheckDto MapToDto(CVCheck cvCheck) { return new CVCheckDto diff --git a/src/RealCV.Web/Components/Layout/MainLayout.razor b/src/RealCV.Web/Components/Layout/MainLayout.razor index 1834fe0..2467bf0 100644 --- a/src/RealCV.Web/Components/Layout/MainLayout.razor +++ b/src/RealCV.Web/Components/Layout/MainLayout.razor @@ -78,8 +78,16 @@
-
-

© @DateTime.Now.Year RealCV. All rights reserved.

+
+
+
+

© @DateTime.Now.Year RealCV. All rights reserved.

+
+
+ Privacy Policy + GDPR Compliant +
+
diff --git a/src/RealCV.Web/Components/Pages/Account/Login.razor b/src/RealCV.Web/Components/Pages/Account/Login.razor index ee8742d..ed297f8 100644 --- a/src/RealCV.Web/Components/Pages/Account/Login.razor +++ b/src/RealCV.Web/Components/Pages/Account/Login.razor @@ -123,7 +123,7 @@
- "RealCV has transformed our hiring process. We catch discrepancies we would have missed before." + "RealCV has transformed our recruitment process. We catch discrepancies we would have missed before."
- HR Director, Tech Company
diff --git a/src/RealCV.Web/Components/Pages/Account/Register.razor b/src/RealCV.Web/Components/Pages/Account/Register.razor index cf21b96..72ea299 100644 --- a/src/RealCV.Web/Components/Pages/Account/Register.razor +++ b/src/RealCV.Web/Components/Pages/Account/Register.razor @@ -97,9 +97,9 @@

By creating an account, you agree to our - Terms of Service + Terms of Service and - Privacy Policy + Privacy Policy

@@ -123,9 +123,9 @@
-

Start Your Free Trial

+

Create Your Free Account

- Get 3 free CV verifications to experience the power of AI-driven credential analysis. + Get 3 free CV verifications per month. No credit card required.

@@ -157,7 +157,7 @@
- "We reduced bad hires by 40% in the first quarter using RealCV." + "We reduced unsuitable appointments by 40% in the first quarter using RealCV."
- Recruitment Manager, Financial Services
diff --git a/src/RealCV.Web/Components/Pages/Home.razor b/src/RealCV.Web/Components/Pages/Home.razor index e7a9f8f..3f15348 100644 --- a/src/RealCV.Web/Components/Pages/Home.razor +++ b/src/RealCV.Web/Components/Pages/Home.razor @@ -182,6 +182,126 @@
+ +
+
+
+

Why Choose RealCV?

+

Make better recruitment decisions with verified candidate information

+
+ +
+
+
+
+
+ + + + +
+
Reduce Poor Appointments
+
+

+ Studies show 30-40% of CVs contain inaccuracies. Catch embellishments and false claims before they become costly recruitment mistakes. +

+
+
+ +
+
+
+
+ + + +
+
Save Time
+
+

+ Get comprehensive verification reports in minutes, not days. No more manual reference checking or waiting for background check results. +

+
+
+ +
+
+
+
+ + + + +
+
Official Data Sources
+
+

+ Verify against Companies House records, cross-reference incorporation dates, check company status, and validate director claims. +

+
+
+ +
+
+
+
+ + + + +
+
Detailed Reports
+
+

+ Get actionable insights with employment verification scores, timeline analysis, education checks, and specific flags for areas of concern. +

+
+
+ +
+
+
+
+ + + +
+
GDPR Compliant
+
+

+ CVs are deleted immediately after processing. Data is automatically purged after 30 days. Your candidates' privacy is protected. +

+
+
+ +
+
+
+
+ + + +
+
UK Specialist
+
+

+ Purpose-built for UK recruitment with support for NHS, councils, public sector employers, charities, and Companies House registered businesses. +

+
+
+
+ + +
+
+
diff --git a/src/RealCV.Web/Components/Pages/Pricing.razor b/src/RealCV.Web/Components/Pages/Pricing.razor index 4d51aa6..37ec2c9 100644 --- a/src/RealCV.Web/Components/Pages/Pricing.razor +++ b/src/RealCV.Web/Components/Pages/Pricing.razor @@ -11,7 +11,7 @@

Simple, Transparent Pricing

- Choose the plan that fits your hiring needs. All plans include our core CV verification technology. + Choose the plan that fits your recruitment needs. All plans include our core CV verification technology.

@@ -69,9 +69,7 @@ @if (_currentPlan == "Free") { - + Your current plan } else { @@ -85,7 +83,7 @@
-
+
@if (_currentPlan == "Professional") {
@@ -209,15 +207,13 @@ @if (_currentPlan == "Enterprise") { - + Your current plan } else {
-