Skip to content
Merged
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
21 changes: 21 additions & 0 deletions src/Booking/Booking.API/Controllers/DoctorsController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Booking.API.Dtos.Doctor;
using Booking.Application.Doctors.Command.CreateDoctor;
using Booking.Application.Doctors.Command.DeleteDoctor;
using Booking.Application.Doctors.Command.UpdateDoctor;
using Booking.Application.Doctors.Command.UpdateProfilePhoto;
using Booking.Application.Doctors.Command.UpdateScheduleConfig;
Expand Down Expand Up @@ -223,5 +224,25 @@ public async Task<ActionResult<List<DoctorTimeSlotDto>>> GetDoctorSlots(
var slots = await _mediator.Send(query);
return Ok(slots);
}

/// <summary>
/// Deletes the doctor with the specified unique identifier.
/// </summary>
/// <remarks>Requires the caller to have the Admin role. This operation is not
/// reversible.</remarks>
/// <param name="id">The unique identifier of the doctor to delete.</param>
/// <returns>A 204 No Content response if the doctor was successfully deleted; a 404 Not Found response if no doctor with
/// the specified identifier exists; or a 400 Bad Request response if the identifier is invalid.</returns>
[HttpDelete("{id}")]
[Authorize(Roles = Roles.Admin)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Delete(Guid id)
{
await _mediator.Send(new DeleteDoctorCommand(id));

return NoContent();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@ namespace Booking.Application.Appointments.Dtos
public record AppointmentDetailDto : AppointmentBaseDto
{
public List<AttachmentDto> Attachments { get; init; } = [];

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Booking.Application.Common.Models;
using Microsoft.AspNetCore.Identity;

namespace Booking.Application.Common.Extension
{
public static class IdentityResultExtensions
{
public static Result ToApplicationResult(this IdentityResult result)
{
return result.Succeeded
? Result.Success()
: Result.Failure(result.Errors.Select(e => e.Description));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
namespace Booking.Application.Common.Interfaces
using Booking.Application.Common.Models;
using MediatR;

namespace Booking.Application.Common.Interfaces
{
public interface IIdentityService
{
Task<string?> GetUserNameAsync(string userId);
Task<bool> IsInRoleAsync(string userId, string role);
Task<bool> AuthorizeAsync(string userId, string policyName);
Task<Result> DisableUserAsync(string userId);
}
}
25 changes: 25 additions & 0 deletions src/Booking/Booking.Application/Common/Models/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Booking.Application.Common.Models
{
public class Result
{
internal Result(bool succeeded, IEnumerable<string> errors)
{
Succeeded = succeeded;
Errors = errors.ToArray();
}

public bool Succeeded { get; init; }

public string[] Errors { get; init; }

public static Result Success()
{
return new Result(true, Array.Empty<string>());
}

public static Result Failure(IEnumerable<string> errors)
{
return new Result(false, errors);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using MediatR;

namespace Booking.Application.Doctors.Command.DeleteDoctor
{
public record DeleteDoctorCommand(Guid DoctorId) : IRequest;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Booking.Application.Common.Exceptions;
using Booking.Application.Common.Interfaces;
using Booking.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Booking.Application.Doctors.Command.DeleteDoctor
{
public class DeleteDoctorCommandHandler(
IBookingDbContext context,
IIdentityService identityService)
: IRequestHandler<DeleteDoctorCommand>
{
private readonly IBookingDbContext _context = context;
private readonly IIdentityService _identityService = identityService;

public async Task Handle(
DeleteDoctorCommand request,
CancellationToken cancellationToken)
{
var doctor = await _context.Doctors
.Include(d => d.Appointments)
.FirstOrDefaultAsync(d => d.Id == request.DoctorId, cancellationToken)
?? throw new NotFoundException(nameof(Doctor), request.DoctorId);


var hasActiveAppointments = doctor.Appointments
.Any(a => a.StartTime > DateTime.UtcNow && a.Status != AppointmentStatus.Canceled);

if (hasActiveAppointments)
throw new Exception("Cannot delete doctor with active future appointments. Please cancel or reschedule them first.");

var identityResult = await _identityService.DisableUserAsync(doctor.ApplicationUserId);

if (!identityResult.Succeeded)
{
var errors = string.Join(", ", identityResult.Errors);
throw new Exception($"Failed to disable user account: {errors}");
}

doctor.Deactivate();
await _context.SaveChangesAsync(cancellationToken);
}
}
}
2 changes: 2 additions & 0 deletions src/Booking/Booking.Domain/Entities/Doctor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ public class Doctor
public string? Bio { get; private set; }
public int ExperienceYears { get; private set; }
public string? ImageUrl { get; private set; }
public void Deactivate() { IsActive = false; }
public virtual DoctorScheduleConfig? ScheduleConfig { get; private set; }
public bool IsActive { get; private set; }
[Column(TypeName = "decimal(18,2)")]
public decimal ConsultationFee { get; private set; }
private readonly List<Review> _reviews = new();
public List<Appointment> Appointments { get; set; } = new();
public IReadOnlyCollection<Review> Reviews => _reviews.AsReadOnly();

private Doctor() { }
Expand Down
Loading
Loading