Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 238 additions & 0 deletions api/CourseRegistration.API/Controllers/CertificatesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
using Microsoft.AspNetCore.Mvc;
using CourseRegistration.Application.Services;
using CourseRegistration.Application.DTOs;
using Serilog;

namespace CourseRegistration.API.Controllers;

/// <summary>
/// Controller for managing certificates
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class CertificatesController : ControllerBase
{
private readonly ICertificateService _certificateService;

/// <summary>
/// Constructor
/// </summary>
/// <param name="certificateService">Certificate service</param>
public CertificatesController(ICertificateService certificateService)
{
_certificateService = certificateService;
}

/// <summary>
/// Search for certificates by various parameters
/// </summary>
/// <param name="studentId">Optional student ID</param>
/// <param name="studentName">Optional student name</param>
/// <param name="courseId">Optional course ID</param>
/// <param name="certificateNumber">Optional certificate number</param>
/// <returns>List of certificates matching the search criteria</returns>
/// <response code="200">Returns the list of certificates</response>
/// <response code="400">If the request parameters are invalid</response>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<IEnumerable<CertificateDto>>> SearchCertificates(
[FromQuery] Guid? studentId = null,
[FromQuery] string? studentName = null,
[FromQuery] Guid? courseId = null,
[FromQuery] string? certificateNumber = null)
{
try
{
// Search by certificate number first (most specific)
if (!string.IsNullOrWhiteSpace(certificateNumber))
{
var certificate = await _certificateService.GetCertificateByCertificateNumberAsync(certificateNumber);
return Ok(certificate != null ? new[] { certificate } : Array.Empty<CertificateDto>());
}

// Search by student ID
if (studentId.HasValue)
{
var certificates = await _certificateService.GetCertificatesByStudentIdAsync(studentId.Value);
return Ok(certificates);
}

// Search by student name
if (!string.IsNullOrWhiteSpace(studentName))
{
var certificates = await _certificateService.GetCertificatesByStudentNameAsync(studentName);
return Ok(certificates);
}

// Search by course ID
if (courseId.HasValue)
{
var certificates = await _certificateService.GetCertificatesByCourseIdAsync(courseId.Value);
return Ok(certificates);
}

// If no search parameters provided, return empty list
Log.Warning("Certificate search called without any search parameters");
return Ok(Array.Empty<CertificateDto>());
}
catch (Exception ex)
{
Log.Error(ex, "Error searching for certificates");
return StatusCode(500, new { message = "An error occurred while searching for certificates" });
}
Comment on lines +80 to +84
}

/// <summary>
/// Get a specific certificate by ID
/// </summary>
/// <param name="id">Certificate ID</param>
/// <returns>Certificate details</returns>
/// <response code="200">Returns the certificate</response>
/// <response code="404">If the certificate is not found</response>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CertificateDto>> GetCertificate(Guid id)
{
try
{
var certificate = await _certificateService.GetCertificateByIdAsync(id);

if (certificate == null)
{
Log.Information("Certificate not found: {CertificateId}", id);
return NotFound(new { message = $"Certificate with ID {id} not found" });
}

return Ok(certificate);
}
catch (Exception ex)
{
Log.Error(ex, "Error retrieving certificate {CertificateId}", id);
return StatusCode(500, new { message = "An error occurred while retrieving the certificate" });
}
Comment on lines +111 to +115
}

/// <summary>
/// Get certificates for a specific student
/// </summary>
/// <param name="studentId">Student ID</param>
/// <returns>List of certificates for the student</returns>
/// <response code="200">Returns the list of certificates</response>
[HttpGet("student/{studentId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CertificateDto>>> GetCertificatesByStudent(Guid studentId)
{
try
{
var certificates = await _certificateService.GetCertificatesByStudentIdAsync(studentId);
return Ok(certificates);
}
catch (Exception ex)
{
Log.Error(ex, "Error retrieving certificates for student {StudentId}", studentId);
return StatusCode(500, new { message = "An error occurred while retrieving certificates" });
}
Comment on lines +133 to +137
}

/// <summary>
/// Get certificates for a specific course
/// </summary>
/// <param name="courseId">Course ID</param>
/// <returns>List of certificates for the course</returns>
/// <response code="200">Returns the list of certificates</response>
[HttpGet("course/{courseId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CertificateDto>>> GetCertificatesByCourse(Guid courseId)
{
try
{
var certificates = await _certificateService.GetCertificatesByCourseIdAsync(courseId);
return Ok(certificates);
}
catch (Exception ex)
{
Log.Error(ex, "Error retrieving certificates for course {CourseId}", courseId);
return StatusCode(500, new { message = "An error occurred while retrieving certificates" });
}
Comment on lines +155 to +159
}

/// <summary>
/// Verify a certificate by its ID or certificate number
/// </summary>
/// <param name="identifier">Certificate ID or certificate number</param>
/// <returns>Certificate verification result</returns>
/// <response code="200">Returns the certificate if valid</response>
/// <response code="404">If the certificate is not found</response>
[HttpGet("verify/{identifier}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CertificateDto>> VerifyCertificate(string identifier)
{
try
{
// Try to parse as GUID first
if (Guid.TryParse(identifier, out Guid certificateId))
{
var certificate = await _certificateService.GetCertificateByIdAsync(certificateId);
if (certificate != null)
{
return Ok(certificate);
}
}

// Try as certificate number
var certByNumber = await _certificateService.GetCertificateByCertificateNumberAsync(identifier);
if (certByNumber != null)
{
return Ok(certByNumber);
}

Log.Information("Certificate verification failed for identifier: {Identifier}", identifier);
return NotFound(new { message = "Certificate not found or invalid" });
}
catch (Exception ex)
{
Log.Error(ex, "Error verifying certificate {Identifier}", identifier);
return StatusCode(500, new { message = "An error occurred while verifying the certificate" });
}
Comment on lines +196 to +200
}

/// <summary>
/// Create a new certificate for a completed course
/// </summary>
/// <param name="createCertificateDto">Certificate creation data</param>
/// <returns>The created certificate</returns>
/// <response code="201">Returns the created certificate</response>
/// <response code="400">If the request data is invalid</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<CertificateDto>> CreateCertificate([FromBody] CreateCertificateDto createCertificateDto)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var certificate = await _certificateService.CreateCertificateAsync(createCertificateDto);

Log.Information("Certificate created: {CertificateId} for student {StudentId}",
certificate.CertificateId, certificate.StudentId);

return CreatedAtAction(
nameof(GetCertificate),
new { id = certificate.CertificateId },
certificate);
}
catch (Exception ex)
{
Log.Error(ex, "Error creating certificate");
return StatusCode(500, new { message = "An error occurred while creating the certificate" });
}
Comment on lines +232 to +236
}
}
1 change: 1 addition & 0 deletions api/CourseRegistration.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
builder.Services.AddScoped<IStudentService, StudentService>();
builder.Services.AddScoped<ICourseService, CourseService>();
builder.Services.AddScoped<IRegistrationService, RegistrationService>();
builder.Services.AddScoped<ICertificateService, CertificateService>();

// Register authorization services
builder.Services.AddScoped<AuthorizationService>();
Expand Down
2 changes: 2 additions & 0 deletions api/CourseRegistration.Application/DTOs/CertificateDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class CertificateDto
public DateTime CourseEndDate { get; set; }
public string? Remarks { get; set; }
public string? DigitalSignature { get; set; }
public string? VerificationUrl { get; set; }
public string? QRCodeData { get; set; }
}

/// <summary>
Expand Down
43 changes: 38 additions & 5 deletions api/CourseRegistration.Application/Services/CertificateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ namespace CourseRegistration.Application.Services;
/// </summary>
public class CertificateService : ICertificateService
{
// Thread-safe lock for certificate number generation
private static readonly object _lock = new object();

// In a real application, this would use a repository pattern with Entity Framework
// For demo purposes, I'll use in-memory data
private static readonly List<Certificate> _certificates = new();
Expand Down Expand Up @@ -121,6 +124,23 @@ public async Task<IEnumerable<CertificateDto>> GetCertificatesByStudentNameAsync
return certificates.Select(MapToDto);
}

public async Task<IEnumerable<CertificateDto>> GetCertificatesByCourseIdAsync(Guid courseId)
{
await Task.CompletedTask; // Simulate async operation

var certificates = _certificates.Where(c => c.CourseId == courseId);
return certificates.Select(MapToDto);
}

public async Task<CertificateDto?> GetCertificateByCertificateNumberAsync(string certificateNumber)
{
await Task.CompletedTask; // Simulate async operation

var certificate = _certificates.FirstOrDefault(c =>
c.CertificateNumber.Equals(certificateNumber, StringComparison.OrdinalIgnoreCase));
return certificate != null ? MapToDto(certificate) : null;
}

public async Task<CertificateDto> CreateCertificateAsync(CreateCertificateDto createCertificateDto)
{
await Task.CompletedTask; // Simulate async operation
Expand All @@ -137,22 +157,33 @@ public async Task<CertificateDto> CreateCertificateAsync(CreateCertificateDto cr
DigitalSignature = "DS-" + Guid.NewGuid().ToString()[..8]
};

_certificates.Add(certificate);
lock (_lock)
{
_certificates.Add(certificate);
}

return MapToDto(certificate);
}

public string GenerateCertificateNumber()
{
var year = DateTime.Now.Year;
var sequence = _certificates.Count + 1;
return $"CERT-{year}-{sequence:D3}";
lock (_lock)
{
var year = DateTime.Now.Year;
var sequence = _certificates.Count + 1;
return $"CERT-{year}-{sequence:D3}";
}
}

private CertificateDto MapToDto(Certificate certificate)
{
var student = _students.FirstOrDefault(s => s.StudentId == certificate.StudentId);
var course = _courses.FirstOrDefault(c => c.CourseId == certificate.CourseId);

// Generate verification URL and QR code data
var verificationUrl = $"https://courseregistration.app/api/certificates/verify/{certificate.CertificateNumber}";
var qrCodeData = $"{verificationUrl}|{certificate.CertificateId}|{certificate.DigitalSignature}";

return new CertificateDto
{
CertificateId = certificate.CertificateId,
Expand All @@ -167,7 +198,9 @@ private CertificateDto MapToDto(Certificate certificate)
CourseStartDate = course?.StartDate ?? DateTime.MinValue,
CourseEndDate = course?.EndDate ?? DateTime.MinValue,
Remarks = certificate.Remarks,
DigitalSignature = certificate.DigitalSignature
DigitalSignature = certificate.DigitalSignature,
VerificationUrl = verificationUrl,
QRCodeData = qrCodeData
};
}
}
14 changes: 14 additions & 0 deletions api/CourseRegistration.Application/Services/ICertificateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ public interface ICertificateService
/// <returns>List of certificates</returns>
Task<IEnumerable<CertificateDto>> GetCertificatesByStudentNameAsync(string studentName);

/// <summary>
/// Get certificates by course ID
/// </summary>
/// <param name="courseId">Course ID</param>
/// <returns>List of certificates</returns>
Task<IEnumerable<CertificateDto>> GetCertificatesByCourseIdAsync(Guid courseId);

/// <summary>
/// Get certificate by certificate number
/// </summary>
/// <param name="certificateNumber">Certificate number</param>
/// <returns>Certificate details</returns>
Task<CertificateDto?> GetCertificateByCertificateNumberAsync(string certificateNumber);

/// <summary>
/// Create a new certificate
/// </summary>
Expand Down
Loading