diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Commands/AddAppointmentCommandHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Commands/AddAppointmentCommandHandlerTests.cs new file mode 100644 index 0000000..e6312d7 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Commands/AddAppointmentCommandHandlerTests.cs @@ -0,0 +1,41 @@ +using Chipsoft.Assignments.EPDConsole.Application.Appointments.Commands; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Moq; +using Moq.EntityFrameworkCore; + + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Appointments.Commands; + +public class AddAppointmentCommandHandlerTests +{ + private readonly Mock _contextMock; + private readonly AddAppointmentCommandHandler _handler; + + public AddAppointmentCommandHandlerTests() + { + _contextMock = new Mock(); + _handler = new AddAppointmentCommandHandler(_contextMock.Object); + } + + [Fact] + public async Task Handle_Should_Add_Appointment_And_Save_Changes() + { + // Arrange + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(new List()); + + var command = new AddAppointmentCommand + { + PatientId = 1, + PhysicianId = 1, + AppointmentDateTime = DateTime.Now.AddHours(1) + }; + + // Act + await _handler.Handle(command, CancellationToken.None); + + // Assert + _contextMock.Verify(x => x.Appointments.Add(It.IsAny()), Times.Once); + _contextMock.Verify(x => x.SaveChangesAsync(CancellationToken.None), Times.Once); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Queries/GetAppointmentsByPatientQueryHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Queries/GetAppointmentsByPatientQueryHandlerTests.cs new file mode 100644 index 0000000..5e44135 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Queries/GetAppointmentsByPatientQueryHandlerTests.cs @@ -0,0 +1,48 @@ +using Chipsoft.Assignments.EPDConsole.Application.Appointments.Queries; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentAssertions; +using Moq; +using Moq.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Appointments.Queries; + +public class GetAppointmentsByPatientQueryHandlerTests +{ + private readonly Mock _contextMock; + private readonly GetAppointmentsByPatientQueryHandler _handler; + + public GetAppointmentsByPatientQueryHandlerTests() + { + _contextMock = new Mock(); + _handler = new GetAppointmentsByPatientQueryHandler(_contextMock.Object); + } + + [Fact] + public async Task Handle_Should_Return_Only_Appointments_For_Given_Patient() + { + // Arrange + var patient1 = new Patient { Id = 1, FirstName = "John", LastName = "Doe" }; + var patient2 = new Patient { Id = 2, FirstName = "Jane", LastName = "Doe" }; + var physician = new Physician { Id = 1, FirstName = "Dr.", LastName = "Smith" }; + + var appointments = new List + { + new Appointment { PatientId = 1, PhysicianId = 1, Patient = patient1, Physician = physician, AppointmentDateTime = DateTime.Now.AddDays(1) }, + new Appointment { PatientId = 2, PhysicianId = 1, Patient = patient2, Physician = physician, AppointmentDateTime = DateTime.Now.AddDays(2) }, + new Appointment { PatientId = 1, PhysicianId = 1, Patient = patient1, Physician = physician, AppointmentDateTime = DateTime.Now.AddDays(3) } + }; + + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(appointments); + + var query = new GetAppointmentsByPatientQuery { PatientId = 1 }; + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result.All(a => a.PatientName == "John Doe").Should().BeTrue(); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Queries/GetAppointmentsByPhysicianQueryHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Queries/GetAppointmentsByPhysicianQueryHandlerTests.cs new file mode 100644 index 0000000..2e95604 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Queries/GetAppointmentsByPhysicianQueryHandlerTests.cs @@ -0,0 +1,49 @@ +using Chipsoft.Assignments.EPDConsole.Application.Appointments.Queries; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentAssertions; +using Moq; +using Moq.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Appointments.Queries; + +public class GetAppointmentsByPhysicianQueryHandlerTests +{ + private readonly Mock _contextMock; + private readonly GetAppointmentsByPhysicianQueryHandler _handler; + + public GetAppointmentsByPhysicianQueryHandlerTests() + { + _contextMock = new Mock(); + _handler = new GetAppointmentsByPhysicianQueryHandler(_contextMock.Object); + } + + [Fact] + public async Task Handle_Should_Return_Only_Appointments_For_Given_Physician() + { + // Arrange + var patient = new Patient { Id = 1, FirstName = "John", LastName = "Doe" }; + var physician1 = new Physician { Id = 1, FirstName = "Dr.", LastName = "Smith" }; + var physician2 = new Physician { Id = 2, FirstName = "Dr.", LastName = "Who" }; + + + var appointments = new List + { + new Appointment { PatientId = 1, PhysicianId = 1, Patient = patient, Physician = physician1, AppointmentDateTime = DateTime.Now.AddDays(1) }, + new Appointment { PatientId = 1, PhysicianId = 2, Patient = patient, Physician = physician2, AppointmentDateTime = DateTime.Now.AddDays(2) }, + new Appointment { PatientId = 1, PhysicianId = 1, Patient = patient, Physician = physician1, AppointmentDateTime = DateTime.Now.AddDays(3) } + }; + + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(appointments); + + var query = new GetAppointmentsByPhysicianQuery { PhysicianId = 1 }; + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result.All(a => a.PhysicianName == "Dr. Smith").Should().BeTrue(); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Queries/GetAppointmentsListQueryHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Queries/GetAppointmentsListQueryHandlerTests.cs new file mode 100644 index 0000000..ca12214 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Queries/GetAppointmentsListQueryHandlerTests.cs @@ -0,0 +1,46 @@ +using Chipsoft.Assignments.EPDConsole.Application.Appointments.Queries; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentAssertions; +using Moq; +using Moq.EntityFrameworkCore; + + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Appointments.Queries; + +public class GetAppointmentsListQueryHandlerTests +{ + private readonly Mock _contextMock; + private readonly GetAppointmentsListQueryHandler _handler; + + public GetAppointmentsListQueryHandlerTests() + { + _contextMock = new Mock(); + _handler = new GetAppointmentsListQueryHandler(_contextMock.Object); + } + + [Fact] + public async Task Handle_Should_Return_All_Appointments_As_Dtos() + { + // Arrange + var patients = new List { new Patient { Id = 1, FirstName = "John", LastName = "Doe" } }; + var physicians = new List { new Physician { Id = 1, FirstName = "Jane", LastName = "Smith" } }; + var appointments = new List + { + new Appointment { Id = 1, PatientId = 1, PhysicianId = 1, AppointmentDateTime = DateTime.Now.AddDays(1), Patient = patients[0], Physician = physicians[0] } + }; + + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(appointments); + + var query = new GetAppointmentsListQuery(); + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + result[0].PatientName.Should().Be("John Doe"); + result[0].PhysicianName.Should().Be("Jane Smith"); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Validators/AddAppointmentCommandValidatorTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Validators/AddAppointmentCommandValidatorTests.cs new file mode 100644 index 0000000..b3014d9 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Appointments/Validators/AddAppointmentCommandValidatorTests.cs @@ -0,0 +1,138 @@ +using Chipsoft.Assignments.EPDConsole.Application.Appointments.Commands; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentValidation.TestHelper; +using Moq; +using Moq.EntityFrameworkCore; +using FluentAssertions; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Appointments.Validators; + +public class AddAppointmentCommandValidatorTests +{ + private readonly Mock _contextMock; + + public AddAppointmentCommandValidatorTests() + { + _contextMock = new Mock(); + } + + [Fact] + public async Task Should_Not_Have_Error_When_Appointment_Is_Valid() + { + // Arrange + _contextMock.Setup(x => x.Patients).ReturnsDbSet(new List { new Patient { Id = 1 } }); + _contextMock.Setup(x => x.Physicians).ReturnsDbSet(new List { new Physician { Id = 1 } }); + _contextMock.Setup(x => x.Appointments).ReturnsDbSet(new List()); + + var validator = new AddAppointmentCommandValidator(_contextMock.Object); + var command = new AddAppointmentCommand { PatientId = 1, PhysicianId = 1, AppointmentDateTime = DateTime.Now.AddDays(1) }; + + // Act + var result = await validator.TestValidateAsync(command); + + // Assert + result.ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public async Task Should_Have_Error_When_Physician_Has_Conflict() + { + // Arrange + var appointmentTime = DateTime.Now.AddDays(1); + var existingAppointments = new List + { + new Appointment { PhysicianId = 1, AppointmentDateTime = appointmentTime } + }; + + _contextMock.Setup(x => x.Patients).ReturnsDbSet(new List { new Patient { Id = 1 } }); + _contextMock.Setup(x => x.Physicians).ReturnsDbSet(new List { new Physician { Id = 1 } }); + _contextMock.Setup(x => x.Appointments).ReturnsDbSet(existingAppointments); + + var validator = new AddAppointmentCommandValidator(_contextMock.Object); + var command = new AddAppointmentCommand { PatientId = 1, PhysicianId = 1, AppointmentDateTime = appointmentTime }; + + // Act + var result = await validator.TestValidateAsync(command); + + // Assert + result.Errors.Should().Contain(e => e.ErrorMessage == "De arts of patiënt heeft op dit tijdstip al een andere afspraak."); + } + + [Fact] + public async Task Should_Have_Error_When_Patient_Has_Conflict() + { + // Arrange + var appointmentTime = DateTime.Now.AddDays(1); + var existingAppointments = new List + { + new Appointment { PatientId = 1, AppointmentDateTime = appointmentTime } + }; + + _contextMock.Setup(x => x.Patients).ReturnsDbSet(new List { new Patient { Id = 1 } }); + _contextMock.Setup(x => x.Physicians).ReturnsDbSet(new List { new Physician { Id = 1 } }); + _contextMock.Setup(x => x.Appointments).ReturnsDbSet(existingAppointments); + + var validator = new AddAppointmentCommandValidator(_contextMock.Object); + var command = new AddAppointmentCommand { PatientId = 1, PhysicianId = 1, AppointmentDateTime = appointmentTime }; + + // Act + var result = await validator.TestValidateAsync(command); + + // Assert + result.Errors.Should().Contain(e => e.ErrorMessage == "De arts of patiënt heeft op dit tijdstip al een andere afspraak."); + } + + [Fact] + public async Task Should_Have_Error_When_Date_Is_In_The_Past() + { + // Arrange + _contextMock.Setup(x => x.Patients).ReturnsDbSet(new List { new Patient { Id = 1 } }); + _contextMock.Setup(x => x.Physicians).ReturnsDbSet(new List { new Physician { Id = 1 } }); + _contextMock.Setup(x => x.Appointments).ReturnsDbSet(new List()); + var validator = new AddAppointmentCommandValidator(_contextMock.Object); + var command = new AddAppointmentCommand { PatientId = 1, PhysicianId = 1, AppointmentDateTime = DateTime.Now.AddDays(-1) }; + + // Act + var result = await validator.TestValidateAsync(command); + + // Assert + result.ShouldHaveValidationErrorFor(cmd => cmd.AppointmentDateTime); + } + + [Fact] + public async Task Should_Have_Error_When_PatientId_Does_Not_Exist() + { + // Arrange + _contextMock.Setup(x => x.Patients).ReturnsDbSet(new List()); + _contextMock.Setup(x => x.Physicians).ReturnsDbSet(new List { new Physician { Id = 1 } }); + _contextMock.Setup(x => x.Appointments).ReturnsDbSet(new List()); + var validator = new AddAppointmentCommandValidator(_contextMock.Object); + var command = new AddAppointmentCommand { PatientId = 99, PhysicianId = 1, AppointmentDateTime = DateTime.Now.AddDays(1) }; + + // Act + var result = await validator.TestValidateAsync(command); + + // Assert + result.ShouldHaveValidationErrorFor(cmd => cmd.PatientId) + .WithErrorMessage("De geselecteerde patiënt bestaat niet."); + } + + [Fact] + public async Task Should_Have_Error_When_PhysicianId_Does_Not_Exist() + { + // Arrange + _contextMock.Setup(x => x.Patients).ReturnsDbSet(new List { new Patient { Id = 1 } }); + _contextMock.Setup(x => x.Physicians).ReturnsDbSet(new List()); + _contextMock.Setup(x => x.Appointments).ReturnsDbSet(new List()); + var validator = new AddAppointmentCommandValidator(_contextMock.Object); + var command = new AddAppointmentCommand { PatientId = 1, PhysicianId = 99, AppointmentDateTime = DateTime.Now.AddDays(1) }; + + // Act + var result = await validator.TestValidateAsync(command); + + // Assert + result.ShouldHaveValidationErrorFor(cmd => cmd.PhysicianId) + .WithErrorMessage("De geselecteerde arts bestaat niet."); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Chipsoft.Assignments.EPDConsole.Application.Tests.csproj b/Chipsoft.Assignments.EPDConsole.Application.Tests/Chipsoft.Assignments.EPDConsole.Application.Tests.csproj new file mode 100644 index 0000000..545fd2e --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Chipsoft.Assignments.EPDConsole.Application.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Commands/AddPatientCommandHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Commands/AddPatientCommandHandlerTests.cs new file mode 100644 index 0000000..db75536 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Commands/AddPatientCommandHandlerTests.cs @@ -0,0 +1,55 @@ +using Chipsoft.Assignments.EPDConsole.Application.Patients.Commands; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Moq; +using Moq.EntityFrameworkCore; +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Patients.Commands; + +public class AddPatientCommandHandlerTests +{ + private readonly Mock _contextMock; + private readonly AddPatientCommandHandler _handler; + + public AddPatientCommandHandlerTests() + { + _contextMock = new Mock(); + _handler = new AddPatientCommandHandler(_contextMock.Object); + } + + [Fact] + public async Task Handle_Should_Add_Patient_And_Save_Changes() + { + // Arrange + var patients = new List(); + _contextMock.Setup(c => c.Patients).ReturnsDbSet(patients); + + var command = new AddPatientCommand + { + FirstName = "John", + LastName = "Doe", + BSN = "123456789", + Address = "123 Main St", + PhoneNumber = "555-1234", + Email = "john.doe@test.com", + DateOfBirth = new System.DateTime(1990, 1, 1) + }; + + // Act + await _handler.Handle(command, CancellationToken.None); + + // Assert + _contextMock.Verify( + x => x.Patients.Add(It.Is(p => + p.FirstName == command.FirstName && + p.LastName == command.LastName && + p.BSN == command.BSN && + p.Address == command.Address && + p.PhoneNumber == command.PhoneNumber && + p.Email == command.Email && + p.DateOfBirth == command.DateOfBirth + )), + Times.Once); + + _contextMock.Verify(x => x.SaveChangesAsync(CancellationToken.None), Times.Once); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Commands/DeletePatientCommandHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Commands/DeletePatientCommandHandlerTests.cs new file mode 100644 index 0000000..a437d7f --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Commands/DeletePatientCommandHandlerTests.cs @@ -0,0 +1,87 @@ +using Chipsoft.Assignments.EPDConsole.Application.Patients.Commands; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Moq; +using Moq.EntityFrameworkCore; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Patients.Commands; + +public class DeletePatientCommandHandlerTests +{ + private readonly Mock _contextMock; + + public DeletePatientCommandHandlerTests() + { + _contextMock = new Mock(); + } + + [Fact] + public async Task Handle_Should_Remove_Patient_When_Found() + { + // Arrange + var patient = new Patient { Id = 1, FirstName = "Test", LastName = "Patient" }; + var mockDbSet = new Mock>(); + mockDbSet.Setup(m => m.FindAsync(new object[] { 1 }, It.IsAny())) + .ReturnsAsync(patient); + + _contextMock.Setup(c => c.Patients).Returns(mockDbSet.Object); + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(new List()); + + var handler = new DeletePatientCommandHandler(_contextMock.Object); + var command = new DeletePatientCommand { Id = 1 }; + + // Act + await handler.Handle(command, CancellationToken.None); + + // Assert + mockDbSet.Verify(x => x.Remove(patient), Times.Once); + _contextMock.Verify(x => x.SaveChangesAsync(CancellationToken.None), Times.Once); + } + + [Fact] + public async Task Handle_Should_Throw_Exception_When_Patient_Not_Found() + { + // Arrange + var mockDbSet = new Mock>(); + mockDbSet.Setup(m => m.FindAsync(new object[] { 99 }, It.IsAny())) + .ReturnsAsync((Patient)null); + + _contextMock.Setup(c => c.Patients).Returns(mockDbSet.Object); + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(new List()); + + var handler = new DeletePatientCommandHandler(_contextMock.Object); + var command = new DeletePatientCommand { Id = 99 }; + + // Act + Func act = async () => await handler.Handle(command, CancellationToken.None); + + // Assert + await act.Should().ThrowAsync().WithMessage("Patient with id 99 not found"); + } + + [Fact] + public async Task Handle_Should_Throw_Exception_When_Patient_Has_Appointments() + { + // Arrange + var patient = new Patient { Id = 1, FirstName = "Test", LastName = "Patient" }; + var appointments = new List { new Appointment { PatientId = 1 } }; + + var mockPatientDbSet = new Mock>(); + mockPatientDbSet.Setup(m => m.FindAsync(new object[] { 1 }, It.IsAny())) + .ReturnsAsync(patient); + + _contextMock.Setup(c => c.Patients).Returns(mockPatientDbSet.Object); + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(appointments); + + var handler = new DeletePatientCommandHandler(_contextMock.Object); + var command = new DeletePatientCommand { Id = 1 }; + + // Act + Func act = async () => await handler.Handle(command, CancellationToken.None); + + // Assert + await act.Should().ThrowAsync().WithMessage("Cannot delete patient with active appointments."); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Queries/GetPatientsListQueryHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Queries/GetPatientsListQueryHandlerTests.cs new file mode 100644 index 0000000..ac6711f --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Queries/GetPatientsListQueryHandlerTests.cs @@ -0,0 +1,44 @@ +using Chipsoft.Assignments.EPDConsole.Application.Patients.Queries; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentAssertions; +using Moq; +using Moq.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Patients.Queries; + +public class GetPatientsListQueryHandlerTests +{ + private readonly Mock _contextMock; + private readonly GetPatientsListQueryHandler _handler; + + public GetPatientsListQueryHandlerTests() + { + _contextMock = new Mock(); + _handler = new GetPatientsListQueryHandler(_contextMock.Object); + } + + [Fact] + public async Task Handle_Should_Return_All_Patients_As_Dtos() + { + // Arrange + var patients = new List + { + new Patient { Id = 1, FirstName = "John", LastName = "Doe", BSN = "123456789" }, + new Patient { Id = 2, FirstName = "Jane", LastName = "Smith", BSN = "987654321" } + }; + + _contextMock.Setup(c => c.Patients).ReturnsDbSet(patients); + + var query = new GetPatientsListQuery(); + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result[0].Name.Should().Be("John Doe"); + result[1].BSN.Should().Be("987654321"); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Validators/AddPatientCommandValidatorTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Validators/AddPatientCommandValidatorTests.cs new file mode 100644 index 0000000..dfc7b68 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Patients/Validators/AddPatientCommandValidatorTests.cs @@ -0,0 +1,95 @@ +using Chipsoft.Assignments.EPDConsole.Application.Patients.Commands; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentValidation.TestHelper; +using Moq; +using Moq.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Patients.Validators; + +public class AddPatientCommandValidatorTests +{ + private readonly Mock _contextMock; + private readonly AddPatientCommandValidator _validator; + + public AddPatientCommandValidatorTests() + { + _contextMock = new Mock(); + // Setup the Patients DbSet mock + _contextMock.Setup(x => x.Patients).ReturnsDbSet(new List()); + _validator = new AddPatientCommandValidator(_contextMock.Object); + } + + [Fact] + public async Task Should_Have_Error_When_FirstName_Is_Empty() + { + var command = new AddPatientCommand { FirstName = string.Empty }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.FirstName); + } + + [Fact] + public async Task Should_Have_Error_When_LastName_Is_Empty() + { + var command = new AddPatientCommand { LastName = string.Empty }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.LastName); + } + + [Fact] + public async Task Should_Have_Error_When_BSN_Is_Invalid() + { + var command = new AddPatientCommand { BSN = "123" }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.BSN); + } + + [Fact] + public async Task Should_Have_Error_When_Email_Is_Invalid() + { + var command = new AddPatientCommand { Email = "invalid-email" }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.Email); + } + + [Fact] + public async Task Should_Have_Error_When_PhoneNumber_Is_Too_Short() + { + var command = new AddPatientCommand { PhoneNumber = "123" }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.PhoneNumber); + } + + [Fact] + public async Task Should_Have_Error_When_DateOfBirth_Is_In_Future() + { + var command = new AddPatientCommand { DateOfBirth = DateTime.Now.AddDays(1) }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.DateOfBirth); + } + + [Fact] + public async Task Should_Have_Error_When_Address_Is_Empty() + { + var command = new AddPatientCommand { Address = string.Empty }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.Address); + } + + [Fact] + public async Task Should_Not_Have_Error_When_Command_Is_Valid() + { + var command = new AddPatientCommand + { + FirstName = "John", + LastName = "Doe", + BSN = "123456789", + Address = "123 Main St", + Email = "john.doe@test.com", + DateOfBirth = new System.DateTime(1990, 1, 1), + PhoneNumber = "0612345678" + }; + var result = await _validator.TestValidateAsync(command); + result.ShouldNotHaveAnyValidationErrors(); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Commands/AddPhysicianCommandHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Commands/AddPhysicianCommandHandlerTests.cs new file mode 100644 index 0000000..773dc69 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Commands/AddPhysicianCommandHandlerTests.cs @@ -0,0 +1,40 @@ +using Chipsoft.Assignments.EPDConsole.Application.Physicians.Commands; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Moq; +using Moq.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Physicians.Commands; + +public class AddPhysicianCommandHandlerTests +{ + private readonly Mock _contextMock; + private readonly AddPhysicianCommandHandler _handler; + + public AddPhysicianCommandHandlerTests() + { + _contextMock = new Mock(); + _handler = new AddPhysicianCommandHandler(_contextMock.Object); + } + + [Fact] + public async Task Handle_Should_Add_Physician_And_Save_Changes() + { + // Arrange + var physicians = new List(); + _contextMock.Setup(c => c.Physicians).ReturnsDbSet(physicians); + + var command = new AddPhysicianCommand + { + FirstName = "Test", + LastName = "Physician" + }; + + // Act + await _handler.Handle(command, CancellationToken.None); + + // Assert + _contextMock.Verify(x => x.Physicians.Add(It.IsAny()), Times.Once); + _contextMock.Verify(x => x.SaveChangesAsync(CancellationToken.None), Times.Once); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Commands/DeletePhysicianCommandHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Commands/DeletePhysicianCommandHandlerTests.cs new file mode 100644 index 0000000..dc21b5b --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Commands/DeletePhysicianCommandHandlerTests.cs @@ -0,0 +1,88 @@ +using Chipsoft.Assignments.EPDConsole.Application.Physicians.Commands; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Moq; +using Moq.EntityFrameworkCore; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Physicians.Commands; + +public class DeletePhysicianCommandHandlerTests +{ + private readonly Mock _contextMock; + + public DeletePhysicianCommandHandlerTests() + { + _contextMock = new Mock(); + } + + [Fact] + public async Task Handle_Should_Remove_Physician_When_Found() + { + // Arrange + var physician = new Physician { Id = 1, FirstName = "Test", LastName = "Physician" }; + var physicians = new List { physician }; + var mockDbSet = new Mock>(); + mockDbSet.Setup(m => m.FindAsync(new object[] { 1 }, It.IsAny())) + .ReturnsAsync(physician); + + _contextMock.Setup(c => c.Physicians).Returns(mockDbSet.Object); + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(new List()); + + var handler = new DeletePhysicianCommandHandler(_contextMock.Object); + var command = new DeletePhysicianCommand { Id = 1 }; + + // Act + await handler.Handle(command, CancellationToken.None); + + // Assert + mockDbSet.Verify(x => x.Remove(physician), Times.Once); + _contextMock.Verify(x => x.SaveChangesAsync(CancellationToken.None), Times.Once); + } + + [Fact] + public async Task Handle_Should_Throw_Exception_When_Physician_Not_Found() + { + // Arrange + var mockDbSet = new Mock>(); + mockDbSet.Setup(m => m.FindAsync(new object[] { 99 }, It.IsAny())) + .ReturnsAsync((Physician)null); + + _contextMock.Setup(c => c.Physicians).Returns(mockDbSet.Object); + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(new List()); + + var handler = new DeletePhysicianCommandHandler(_contextMock.Object); + var command = new DeletePhysicianCommand { Id = 99 }; + + // Act + Func act = async () => await handler.Handle(command, CancellationToken.None); + + // Assert + await act.Should().ThrowAsync().WithMessage("Physician with id 99 not found"); + } + + [Fact] + public async Task Handle_Should_Throw_Exception_When_Physician_Has_Appointments() + { + // Arrange + var physician = new Physician { Id = 1, FirstName = "Test", LastName = "Physician" }; + var appointments = new List { new Appointment { PhysicianId = 1 } }; + + var mockPhysicianDbSet = new Mock>(); + mockPhysicianDbSet.Setup(m => m.FindAsync(new object[] { 1 }, It.IsAny())) + .ReturnsAsync(physician); + + _contextMock.Setup(c => c.Physicians).Returns(mockPhysicianDbSet.Object); + _contextMock.Setup(c => c.Appointments).ReturnsDbSet(appointments); + + var handler = new DeletePhysicianCommandHandler(_contextMock.Object); + var command = new DeletePhysicianCommand { Id = 1 }; + + // Act + Func act = async () => await handler.Handle(command, CancellationToken.None); + + // Assert + await act.Should().ThrowAsync().WithMessage("Cannot delete physician with active appointments."); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Queries/GetPhysiciansListQueryHandlerTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Queries/GetPhysiciansListQueryHandlerTests.cs new file mode 100644 index 0000000..7c5a50b --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Queries/GetPhysiciansListQueryHandlerTests.cs @@ -0,0 +1,44 @@ +using Chipsoft.Assignments.EPDConsole.Application.Physicians.Queries; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentAssertions; +using Moq; +using Moq.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Physicians.Queries; + +public class GetPhysiciansListQueryHandlerTests +{ + private readonly Mock _contextMock; + private readonly GetPhysiciansListQueryHandler _handler; + + public GetPhysiciansListQueryHandlerTests() + { + _contextMock = new Mock(); + _handler = new GetPhysiciansListQueryHandler(_contextMock.Object); + } + + [Fact] + public async Task Handle_Should_Return_All_Physicians_As_Dtos() + { + // Arrange + var physicians = new List + { + new Physician { Id = 1, FirstName = "Dr.", LastName = "Who" }, + new Physician { Id = 2, FirstName = "Dr.", LastName = "Strange" } + }; + + _contextMock.Setup(c => c.Physicians).ReturnsDbSet(physicians); + + var query = new GetPhysiciansListQuery(); + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result[0].Name.Should().Be("Dr. Who"); + result[1].Name.Should().Be("Dr. Strange"); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Validators/AddPhysicianCommandValidatorTests.cs b/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Validators/AddPhysicianCommandValidatorTests.cs new file mode 100644 index 0000000..601a0e7 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application.Tests/Physicians/Validators/AddPhysicianCommandValidatorTests.cs @@ -0,0 +1,81 @@ +using Chipsoft.Assignments.EPDConsole.Application.Physicians.Commands; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentValidation.TestHelper; +using Moq; +using Moq.EntityFrameworkCore; +using FluentAssertions; + +namespace Chipsoft.Assignments.EPDConsole.Application.Tests.Physicians.Validators; + +public class AddPhysicianCommandValidatorTests +{ + private readonly Mock _contextMock; + private readonly AddPhysicianCommandValidator _validator; + + public AddPhysicianCommandValidatorTests() + { + _contextMock = new Mock(); + _contextMock.Setup(x => x.Physicians).ReturnsDbSet(new List()); + _validator = new AddPhysicianCommandValidator(_contextMock.Object); + } + + [Fact] + public async Task Should_Have_Error_When_FirstName_Is_Empty() + { + var command = new AddPhysicianCommand { FirstName = string.Empty, LastName = "Test" }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.FirstName); + } + + [Fact] + public async Task Should_Have_Error_When_LastName_Is_Empty() + { + var command = new AddPhysicianCommand { FirstName = "Test", LastName = string.Empty }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.LastName); + } + + [Fact] + public async Task Should_Have_Error_When_FirstName_Exceeds_MaxLength() + { + var command = new AddPhysicianCommand { FirstName = new string('a', 201), LastName = "Test" }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.FirstName); + } + + [Fact] + public async Task Should_Have_Error_When_LastName_Exceeds_MaxLength() + { + var command = new AddPhysicianCommand { FirstName = "Test", LastName = new string('a', 201) }; + var result = await _validator.TestValidateAsync(command); + result.ShouldHaveValidationErrorFor(x => x.LastName); + } + + [Fact] + public async Task Should_Not_Have_Error_When_Name_Is_Unique() + { + var command = new AddPhysicianCommand { FirstName = "Test", LastName = "Physician" }; + var result = await _validator.TestValidateAsync(command); + result.ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public async Task Should_Have_Error_When_Name_Is_Not_Unique() + { + // Arrange + var existingPhysicians = new List + { + new Physician { FirstName = "Test", LastName = "Physician" } + }; + _contextMock.Setup(x => x.Physicians).ReturnsDbSet(existingPhysicians); + var validator = new AddPhysicianCommandValidator(_contextMock.Object); + var command = new AddPhysicianCommand { FirstName = "Test", LastName = "Physician" }; + + // Act + var result = await validator.TestValidateAsync(command); + + // Assert + result.Errors.Should().Contain(e => e.ErrorMessage == "Een arts met deze naam bestaat al."); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Appointments/Commands/AddAppointmentCommand.cs b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Commands/AddAppointmentCommand.cs new file mode 100644 index 0000000..ccb6ed9 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Commands/AddAppointmentCommand.cs @@ -0,0 +1,36 @@ +using MediatR; +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; + +namespace Chipsoft.Assignments.EPDConsole.Application.Appointments.Commands; + +public class AddAppointmentCommand : IRequest +{ + public int PatientId { get; set; } + public int PhysicianId { get; set; } + public DateTime AppointmentDateTime { get; set; } +} + +public class AddAppointmentCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public AddAppointmentCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(AddAppointmentCommand request, CancellationToken cancellationToken) + { + var appointment = new Appointment + { + PatientId = request.PatientId, + PhysicianId = request.PhysicianId, + AppointmentDateTime = request.AppointmentDateTime + }; + + _context.Appointments.Add(appointment); + await _context.SaveChangesAsync(cancellationToken); + return appointment.Id; + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Appointments/Commands/AddAppointmentCommandValidator.cs b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Commands/AddAppointmentCommandValidator.cs new file mode 100644 index 0000000..4085fc6 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Commands/AddAppointmentCommandValidator.cs @@ -0,0 +1,59 @@ +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Appointments.Commands; + +public class AddAppointmentCommandValidator : AbstractValidator +{ + private readonly IApplicationDbContext _context; + + public AddAppointmentCommandValidator(IApplicationDbContext context) + { + _context = context; + + RuleFor(v => v.AppointmentDateTime) + .GreaterThan(DateTime.Now).WithMessage("Een afspraak moet in de toekomst worden gepland."); + + RuleFor(v => v.PatientId) + .NotEmpty().WithMessage("Patiënt ID is verplicht.") + .MustAsync(PatientExists).WithMessage("De geselecteerde patiënt bestaat niet."); + + RuleFor(v => v.PhysicianId) + .NotEmpty().WithMessage("Arts ID is verplicht.") + .MustAsync(PhysicianExists).WithMessage("De geselecteerde arts bestaat niet."); + + RuleFor(v => v) + .MustAsync(BeAvailable).WithMessage("De arts of patiënt heeft op dit tijdstip al een andere afspraak."); + } + + private async Task PatientExists(int id, CancellationToken cancellationToken) + { + return await _context.Patients.AnyAsync(p => p.Id == id, cancellationToken); + } + + private async Task PhysicianExists(int id, CancellationToken cancellationToken) + { + return await _context.Physicians.AnyAsync(p => p.Id == id, cancellationToken); + } + + private async Task BeAvailable(AddAppointmentCommand command, CancellationToken cancellationToken) + { + var appointmentTime = command.AppointmentDateTime; + var appointmentEndTime = appointmentTime.AddMinutes(30); // Assuming 30 minute slots + + var physicianHasConflict = await _context.Appointments + .AnyAsync(a => a.PhysicianId == command.PhysicianId && + appointmentTime < a.AppointmentDateTime.AddMinutes(30) && + appointmentEndTime > a.AppointmentDateTime, cancellationToken); + + if (physicianHasConflict) return false; + + var patientHasConflict = await _context.Appointments + .AnyAsync(a => a.PatientId == command.PatientId && + appointmentTime < a.AppointmentDateTime.AddMinutes(30) && + appointmentEndTime > a.AppointmentDateTime, cancellationToken); + + return !patientHasConflict; + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Appointments/Dtos/AppointmentDto.cs b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Dtos/AppointmentDto.cs new file mode 100644 index 0000000..0908fe7 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Dtos/AppointmentDto.cs @@ -0,0 +1,8 @@ +namespace Chipsoft.Assignments.EPDConsole.Application.Appointments.Dtos; + +public class AppointmentDto +{ + public DateTime AppointmentDateTime { get; set; } + public string PatientName { get; set; } = string.Empty; + public string PhysicianName { get; set; } = string.Empty; +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Appointments/Queries/GetAppointmentsByPatientQuery.cs b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Queries/GetAppointmentsByPatientQuery.cs new file mode 100644 index 0000000..65a3d1e --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Queries/GetAppointmentsByPatientQuery.cs @@ -0,0 +1,37 @@ +using Chipsoft.Assignments.EPDConsole.Application.Appointments.Dtos; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Appointments.Queries; + +public class GetAppointmentsByPatientQuery : IRequest> +{ + public int PatientId { get; set; } +} + +public class GetAppointmentsByPatientQueryHandler : IRequestHandler> +{ + private readonly IApplicationDbContext _context; + + public GetAppointmentsByPatientQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle(GetAppointmentsByPatientQuery request, CancellationToken cancellationToken) + { + return await _context.Appointments + .Where(a => a.PatientId == request.PatientId) + .Include(a => a.Patient) + .Include(a => a.Physician) + .OrderBy(a => a.AppointmentDateTime) + .Select(a => new AppointmentDto + { + AppointmentDateTime = a.AppointmentDateTime, + PatientName = $"{a.Patient.FirstName} {a.Patient.LastName}", + PhysicianName = $"{a.Physician.FirstName} {a.Physician.LastName}" + }) + .ToListAsync(cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Appointments/Queries/GetAppointmentsByPhysicianQuery.cs b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Queries/GetAppointmentsByPhysicianQuery.cs new file mode 100644 index 0000000..6a6ef49 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Queries/GetAppointmentsByPhysicianQuery.cs @@ -0,0 +1,37 @@ +using Chipsoft.Assignments.EPDConsole.Application.Appointments.Dtos; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Appointments.Queries; + +public class GetAppointmentsByPhysicianQuery : IRequest> +{ + public int PhysicianId { get; set; } +} + +public class GetAppointmentsByPhysicianQueryHandler : IRequestHandler> +{ + private readonly IApplicationDbContext _context; + + public GetAppointmentsByPhysicianQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle(GetAppointmentsByPhysicianQuery request, CancellationToken cancellationToken) + { + return await _context.Appointments + .Where(a => a.PhysicianId == request.PhysicianId) + .Include(a => a.Patient) + .Include(a => a.Physician) + .OrderBy(a => a.AppointmentDateTime) + .Select(a => new AppointmentDto + { + AppointmentDateTime = a.AppointmentDateTime, + PatientName = $"{a.Patient.FirstName} {a.Patient.LastName}", + PhysicianName = $"{a.Physician.FirstName} {a.Physician.LastName}" + }) + .ToListAsync(cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Appointments/Queries/GetAppointmentsListQuery.cs b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Queries/GetAppointmentsListQuery.cs new file mode 100644 index 0000000..d39deef --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Appointments/Queries/GetAppointmentsListQuery.cs @@ -0,0 +1,35 @@ +using Chipsoft.Assignments.EPDConsole.Application.Appointments.Dtos; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Appointments.Queries; + +public class GetAppointmentsListQuery : IRequest> +{ +} + +public class GetAppointmentsListQueryHandler : IRequestHandler> +{ + private readonly IApplicationDbContext _context; + + public GetAppointmentsListQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle(GetAppointmentsListQuery request, CancellationToken cancellationToken) + { + return await _context.Appointments + .Include(a => a.Patient) + .Include(a => a.Physician) + .OrderBy(a => a.AppointmentDateTime) + .Select(a => new AppointmentDto + { + AppointmentDateTime = a.AppointmentDateTime, + PatientName = $"{a.Patient.FirstName} {a.Patient.LastName}", + PhysicianName = $"{a.Physician.FirstName} {a.Physician.LastName}" + }) + .ToListAsync(cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Chipsoft.Assignments.EPDConsole.Application.csproj b/Chipsoft.Assignments.EPDConsole.Application/Chipsoft.Assignments.EPDConsole.Application.csproj new file mode 100644 index 0000000..a08d803 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Chipsoft.Assignments.EPDConsole.Application.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/Chipsoft.Assignments.EPDConsole.Application/Common/Behaviors/ValidationBehavior.cs b/Chipsoft.Assignments.EPDConsole.Application/Common/Behaviors/ValidationBehavior.cs new file mode 100644 index 0000000..44cb4bd --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Common/Behaviors/ValidationBehavior.cs @@ -0,0 +1,33 @@ +using FluentValidation; +using MediatR; +using ValidationException = Chipsoft.Assignments.EPDConsole.Application.Common.Exceptions.ValidationException; + + +namespace Chipsoft.Assignments.EPDConsole.Application.Common.Behaviors; + +public class ValidationBehavior(IEnumerable> validators) : IPipelineBehavior + where TRequest : IRequest +{ + private readonly IEnumerable> _validators = validators; + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + if (_validators.Any()) + { + var context = new ValidationContext(request); + + var validationResults = await Task.WhenAll( + _validators.Select(v => + v.ValidateAsync(context, cancellationToken))); + + var failures = validationResults + .SelectMany(r => r.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Count != 0) + throw new ValidationException(failures); + } + return await next(cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Common/Exceptions/ValidationException.cs b/Chipsoft.Assignments.EPDConsole.Application/Common/Exceptions/ValidationException.cs new file mode 100644 index 0000000..7fad952 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Common/Exceptions/ValidationException.cs @@ -0,0 +1,22 @@ +using FluentValidation.Results; + +namespace Chipsoft.Assignments.EPDConsole.Application.Common.Exceptions; + +public class ValidationException : Exception +{ + public ValidationException() + : base("One or more validation failures have occurred.") + { + Errors = new Dictionary(); + } + + public ValidationException(IEnumerable failures) + : this() + { + Errors = failures + .GroupBy(e => e.PropertyName, e => e.ErrorMessage) + .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); + } + + public IDictionary Errors { get; } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/DependencyInjection.cs b/Chipsoft.Assignments.EPDConsole.Application/DependencyInjection.cs new file mode 100644 index 0000000..666a59f --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/DependencyInjection.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using MediatR; +using FluentValidation; +using System.Reflection; +using Chipsoft.Assignments.EPDConsole.Application.Common.Behaviors; + +namespace Chipsoft.Assignments.EPDConsole.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplicationServices(this IServiceCollection services) + { + var applicationAssembly = Assembly.GetExecutingAssembly(); + + services.AddValidatorsFromAssembly(applicationAssembly); + services.AddMediatR(cfg => { + cfg.RegisterServicesFromAssembly(applicationAssembly); + cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + }); + + return services; + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Patients/Commands/AddPatientCommand.cs b/Chipsoft.Assignments.EPDConsole.Application/Patients/Commands/AddPatientCommand.cs new file mode 100644 index 0000000..c473fbc --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Patients/Commands/AddPatientCommand.cs @@ -0,0 +1,44 @@ +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using MediatR; + +namespace Chipsoft.Assignments.EPDConsole.Application.Patients.Commands; + +public class AddPatientCommand : IRequest +{ + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string BSN { get; set; } = string.Empty; + public string Address { get; set; } = string.Empty; + public string PhoneNumber { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public DateTime DateOfBirth { get; set; } +} + +public class AddPatientCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public AddPatientCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(AddPatientCommand request, CancellationToken cancellationToken) + { + var patient = new Patient + { + FirstName = request.FirstName, + LastName = request.LastName, + BSN = request.BSN, + Address = request.Address, + PhoneNumber = request.PhoneNumber, + Email = request.Email, + DateOfBirth = request.DateOfBirth + }; + + _context.Patients.Add(patient); + await _context.SaveChangesAsync(cancellationToken); + return patient.Id; + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Patients/Commands/AddPatientCommandValidator.cs b/Chipsoft.Assignments.EPDConsole.Application/Patients/Commands/AddPatientCommandValidator.cs new file mode 100644 index 0000000..1e245b4 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Patients/Commands/AddPatientCommandValidator.cs @@ -0,0 +1,52 @@ +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using System.Text.RegularExpressions; + +namespace Chipsoft.Assignments.EPDConsole.Application.Patients.Commands; + +public class AddPatientCommandValidator : AbstractValidator +{ + private readonly IApplicationDbContext _context; + + public AddPatientCommandValidator(IApplicationDbContext context) + { + _context = context; + + RuleFor(v => v.FirstName) + .NotEmpty().WithMessage("Voornaam is een verplicht veld.") + .MaximumLength(200); + + RuleFor(v => v.LastName) + .NotEmpty().WithMessage("Achternaam is een verplicht veld.") + .MaximumLength(200); + + RuleFor(v => v.Address) + .NotEmpty().WithMessage("Adres is een verplicht veld."); + + RuleFor(v => v.BSN) + .NotEmpty().WithMessage("BSN is een verplicht veld.") + .Length(9).WithMessage("BSN moet exact 9 cijfers bevatten.") + .Matches("^[0-9]*$").WithMessage("BSN mag alleen cijfers bevatten.") + .MustAsync(BeUniqueBsn).WithMessage("Een patiënt met dit BSN bestaat al."); + + RuleFor(v => v.PhoneNumber) + .NotEmpty().WithMessage("Telefoonnummer is een verplicht veld.") + .MinimumLength(10).WithMessage("Telefoonnummer moet minimaal 10 tekens lang zijn.") + .Matches(new Regex(@"^[\d\s\(\)\+\-]+$")).WithMessage("Telefoonnummer mag alleen nummers en gebruikelijke tekens (+, -, (, )) bevatten."); + + RuleFor(v => v.Email) + .NotEmpty() + .EmailAddress(); + + RuleFor(v => v.DateOfBirth) + .NotEmpty() + .LessThan(DateTime.Now).WithMessage("Geboortedatum kan niet in de toekomst liggen."); + } + + private async Task BeUniqueBsn(string bsn, CancellationToken cancellationToken) + { + return await _context.Patients + .AllAsync(p => p.BSN != bsn, cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Patients/Commands/DeletePatientCommand.cs b/Chipsoft.Assignments.EPDConsole.Application/Patients/Commands/DeletePatientCommand.cs new file mode 100644 index 0000000..ef9f31b --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Patients/Commands/DeletePatientCommand.cs @@ -0,0 +1,30 @@ +using MediatR; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Patients.Commands; + +public class DeletePatientCommand : IRequest +{ + public int Id { get; set; } +} + +public class DeletePatientCommandHandler(IApplicationDbContext context) : IRequestHandler +{ + private readonly IApplicationDbContext _context = context; + + public async Task Handle(DeletePatientCommand request, CancellationToken cancellationToken) + { + var entity = await _context.Patients + .FindAsync([request.Id], cancellationToken) ?? throw new Exception($"Patient with id {request.Id} not found"); + var hasAppointments = await _context.Appointments.AnyAsync(a => a.PatientId == request.Id, cancellationToken); + if(hasAppointments) + { + throw new Exception("Cannot delete patient with active appointments."); + } + + _context.Patients.Remove(entity); + + await _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Patients/Queries/GetPatientsListQuery.cs b/Chipsoft.Assignments.EPDConsole.Application/Patients/Queries/GetPatientsListQuery.cs new file mode 100644 index 0000000..d4a7993 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Patients/Queries/GetPatientsListQuery.cs @@ -0,0 +1,33 @@ +using MediatR; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Patients.Queries; + +public class PatientDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string BSN { get; set; } = string.Empty; +} + +public class GetPatientsListQuery : IRequest> +{ +} + +public class GetPatientsListQueryHandler(IApplicationDbContext context) : IRequestHandler> +{ + private readonly IApplicationDbContext _context = context; + + public async Task> Handle(GetPatientsListQuery request, CancellationToken cancellationToken) + { + return await _context.Patients + .Select(p => new PatientDto + { + Id = p.Id, + Name = $"{p.FirstName} {p.LastName}", + BSN = p.BSN + }) + .ToListAsync(cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Physicians/Commands/AddPhysicianCommand.cs b/Chipsoft.Assignments.EPDConsole.Application/Physicians/Commands/AddPhysicianCommand.cs new file mode 100644 index 0000000..016bbdf --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Physicians/Commands/AddPhysicianCommand.cs @@ -0,0 +1,31 @@ +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using MediatR; + +namespace Chipsoft.Assignments.EPDConsole.Application.Physicians.Commands; + +public class AddPhysicianCommand : IRequest +{ + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; +} + +public class AddPhysicianCommandHandler(IApplicationDbContext context) : IRequestHandler +{ + private readonly IApplicationDbContext _context = context; + + public async Task Handle(AddPhysicianCommand request, CancellationToken cancellationToken) + { + var physician = new Physician + { + FirstName = request.FirstName, + LastName = request.LastName + }; + + _context.Physicians.Add(physician); + + await _context.SaveChangesAsync(cancellationToken); + + return physician.Id; + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Physicians/Commands/AddPhysicianCommandValidator.cs b/Chipsoft.Assignments.EPDConsole.Application/Physicians/Commands/AddPhysicianCommandValidator.cs new file mode 100644 index 0000000..8bdcc79 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Physicians/Commands/AddPhysicianCommandValidator.cs @@ -0,0 +1,32 @@ +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Physicians.Commands; + +public class AddPhysicianCommandValidator : AbstractValidator +{ + private readonly IApplicationDbContext _context; + + public AddPhysicianCommandValidator(IApplicationDbContext context) + { + _context = context; + + RuleFor(v => v.FirstName) + .NotEmpty().WithMessage("Voornaam is een verplicht veld.") + .MaximumLength(200).WithMessage("Voornaam mag niet meer dan 200 karakters bevatten."); + + RuleFor(v => v.LastName) + .NotEmpty().WithMessage("Achternaam is een verplicht veld.") + .MaximumLength(200).WithMessage("Achternaam mag niet meer dan 200 karakters bevatten."); + + RuleFor(x => x) + .MustAsync(BeUniqueName).WithMessage("Een arts met deze naam bestaat al."); + } + + private async Task BeUniqueName(AddPhysicianCommand command, CancellationToken cancellationToken) + { + return await _context.Physicians + .AllAsync(p => p.FirstName != command.FirstName || p.LastName != command.LastName, cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Physicians/Commands/DeletePhysicianCommand.cs b/Chipsoft.Assignments.EPDConsole.Application/Physicians/Commands/DeletePhysicianCommand.cs new file mode 100644 index 0000000..846044a --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Physicians/Commands/DeletePhysicianCommand.cs @@ -0,0 +1,32 @@ +using MediatR; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Physicians.Commands; + +public class DeletePhysicianCommand : IRequest +{ + public int Id { get; set; } +} + +public class DeletePhysicianCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public DeletePhysicianCommandHandler(IApplicationDbContext context) => _context = context; + + public async Task Handle(DeletePhysicianCommand request, CancellationToken cancellationToken) + { + var entity = await _context.Physicians + .FindAsync(new object[] { request.Id }, cancellationToken) ?? throw new Exception($"Physician with id {request.Id} not found"); + var hasAppointments = await _context.Appointments.AnyAsync(a => a.PhysicianId == request.Id, cancellationToken); + if(hasAppointments) + { + throw new Exception("Cannot delete physician with active appointments."); + } + + _context.Physicians.Remove(entity); + + await _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Application/Physicians/Queries/GetPhysiciansListQuery.cs b/Chipsoft.Assignments.EPDConsole.Application/Physicians/Queries/GetPhysiciansListQuery.cs new file mode 100644 index 0000000..ded8b1b --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Application/Physicians/Queries/GetPhysiciansListQuery.cs @@ -0,0 +1,31 @@ +using MediatR; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Application.Physicians.Queries; + +public class PhysicianDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +public class GetPhysiciansListQuery : IRequest> +{ +} + +public class GetPhysiciansListQueryHandler(IApplicationDbContext context) : IRequestHandler> +{ + private readonly IApplicationDbContext _context = context; + + public async Task> Handle(GetPhysiciansListQuery request, CancellationToken cancellationToken) + { + return await _context.Physicians + .Select(p => new PhysicianDto + { + Id = p.Id, + Name = $"{p.FirstName} {p.LastName}" + }) + .ToListAsync(cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Core/Chipsoft.Assignments.EPDConsole.Core.csproj b/Chipsoft.Assignments.EPDConsole.Core/Chipsoft.Assignments.EPDConsole.Core.csproj new file mode 100644 index 0000000..6603dda --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Core/Chipsoft.Assignments.EPDConsole.Core.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Chipsoft.Assignments.EPDConsole.Core/Entities/Appointment.cs b/Chipsoft.Assignments.EPDConsole.Core/Entities/Appointment.cs new file mode 100644 index 0000000..b58d027 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Core/Entities/Appointment.cs @@ -0,0 +1,13 @@ +namespace Chipsoft.Assignments.EPDConsole.Core.Entities; + +public class Appointment +{ + public int Id { get; set; } + public DateTime AppointmentDateTime { get; set; } + + public int PatientId { get; set; } + public Patient? Patient { get; set; } + + public int PhysicianId { get; set; } + public Physician? Physician { get; set; } +} diff --git a/Chipsoft.Assignments.EPDConsole.Core/Entities/Patient.cs b/Chipsoft.Assignments.EPDConsole.Core/Entities/Patient.cs new file mode 100644 index 0000000..bb12c6e --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Core/Entities/Patient.cs @@ -0,0 +1,14 @@ +namespace Chipsoft.Assignments.EPDConsole.Core.Entities; + +public class Patient +{ + public int Id { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? BSN { get; set; } // Burger Service Nummer + public string? Address { get; set; } + public string? PhoneNumber { get; set; } + public string? Email { get; set; } + public DateTime DateOfBirth { get; set; } + public ICollection Appointments { get; set; } = []; +} diff --git a/Chipsoft.Assignments.EPDConsole.Core/Entities/Physician.cs b/Chipsoft.Assignments.EPDConsole.Core/Entities/Physician.cs new file mode 100644 index 0000000..eb1d596 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Core/Entities/Physician.cs @@ -0,0 +1,9 @@ +namespace Chipsoft.Assignments.EPDConsole.Core.Entities; + +public class Physician +{ + public int Id { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public ICollection Appointments { get; set; } = []; +} diff --git a/Chipsoft.Assignments.EPDConsole.Core/Interfaces/IApplicationDbContext.cs b/Chipsoft.Assignments.EPDConsole.Core/Interfaces/IApplicationDbContext.cs new file mode 100644 index 0000000..1ddc11f --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Core/Interfaces/IApplicationDbContext.cs @@ -0,0 +1,14 @@ +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Microsoft.EntityFrameworkCore; + + +namespace Chipsoft.Assignments.EPDConsole.Core.Interfaces; + +public interface IApplicationDbContext +{ + DbSet Patients { get; set; } + DbSet Physicians { get; set; } + DbSet Appointments { get; set; } + + Task SaveChangesAsync(CancellationToken cancellationToken); +} diff --git a/Chipsoft.Assignments.EPDConsole.Infrastructure/Chipsoft.Assignments.EPDConsole.Infrastructure.csproj b/Chipsoft.Assignments.EPDConsole.Infrastructure/Chipsoft.Assignments.EPDConsole.Infrastructure.csproj new file mode 100644 index 0000000..8a3d95f --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Infrastructure/Chipsoft.Assignments.EPDConsole.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Chipsoft.Assignments.EPDConsole.Infrastructure/DependencyInjection.cs b/Chipsoft.Assignments.EPDConsole.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..971bd76 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Infrastructure/DependencyInjection.cs @@ -0,0 +1,19 @@ +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Chipsoft.Assignments.EPDConsole.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Chipsoft.Assignments.EPDConsole.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructureServices(this IServiceCollection services) + { + services.AddDbContext(options => + options.UseSqlite("Data Source=epd.db")); + + services.AddScoped(provider => provider.GetService()); + + return services; + } +} diff --git a/Chipsoft.Assignments.EPDConsole.Infrastructure/Persistence/EPDDbContext.cs b/Chipsoft.Assignments.EPDConsole.Infrastructure/Persistence/EPDDbContext.cs new file mode 100644 index 0000000..b7b2650 --- /dev/null +++ b/Chipsoft.Assignments.EPDConsole.Infrastructure/Persistence/EPDDbContext.cs @@ -0,0 +1,17 @@ +using Chipsoft.Assignments.EPDConsole.Core.Entities; +using Chipsoft.Assignments.EPDConsole.Core.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Chipsoft.Assignments.EPDConsole.Infrastructure.Persistence; + +public class EPDDbContext(DbContextOptions options) : DbContext(options), IApplicationDbContext +{ + public DbSet Patients { get; set; } + public DbSet Physicians { get; set; } + public DbSet Appointments { get; set; } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) + { + return await base.SaveChangesAsync(cancellationToken); + } +} diff --git a/Chipsoft.Assignments.EPDConsole.sln b/Chipsoft.Assignments.EPDConsole.sln index 01aeb4e..ee4e438 100644 --- a/Chipsoft.Assignments.EPDConsole.sln +++ b/Chipsoft.Assignments.EPDConsole.sln @@ -5,6 +5,14 @@ VisualStudioVersion = 17.3.32819.101 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chipsoft.Assignments.EPDConsole", "Chipsoft.Assignments.EPDConsole\Chipsoft.Assignments.EPDConsole.csproj", "{C3E36DFE-5F45-40C4-BCF8-CEACF7987B54}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chipsoft.Assignments.EPDConsole.Core", "Chipsoft.Assignments.EPDConsole.Core\Chipsoft.Assignments.EPDConsole.Core.csproj", "{13E9F04A-65F5-4805-A725-D449E249FDF6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chipsoft.Assignments.EPDConsole.Application", "Chipsoft.Assignments.EPDConsole.Application\Chipsoft.Assignments.EPDConsole.Application.csproj", "{2DE32729-1EF3-402A-816E-041CC4D4CB3C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chipsoft.Assignments.EPDConsole.Infrastructure", "Chipsoft.Assignments.EPDConsole.Infrastructure\Chipsoft.Assignments.EPDConsole.Infrastructure.csproj", "{CF90E1EF-00EA-48BD-8ED7-531A91BDD45F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chipsoft.Assignments.EPDConsole.Application.Tests", "Chipsoft.Assignments.EPDConsole.Application.Tests\Chipsoft.Assignments.EPDConsole.Application.Tests.csproj", "{04FF203F-12B0-40F2-BF3C-B83EA01F140B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +23,22 @@ Global {C3E36DFE-5F45-40C4-BCF8-CEACF7987B54}.Debug|Any CPU.Build.0 = Debug|Any CPU {C3E36DFE-5F45-40C4-BCF8-CEACF7987B54}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3E36DFE-5F45-40C4-BCF8-CEACF7987B54}.Release|Any CPU.Build.0 = Release|Any CPU + {13E9F04A-65F5-4805-A725-D449E249FDF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13E9F04A-65F5-4805-A725-D449E249FDF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13E9F04A-65F5-4805-A725-D449E249FDF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13E9F04A-65F5-4805-A725-D449E249FDF6}.Release|Any CPU.Build.0 = Release|Any CPU + {2DE32729-1EF3-402A-816E-041CC4D4CB3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DE32729-1EF3-402A-816E-041CC4D4CB3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DE32729-1EF3-402A-816E-041CC4D4CB3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DE32729-1EF3-402A-816E-041CC4D4CB3C}.Release|Any CPU.Build.0 = Release|Any CPU + {CF90E1EF-00EA-48BD-8ED7-531A91BDD45F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF90E1EF-00EA-48BD-8ED7-531A91BDD45F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF90E1EF-00EA-48BD-8ED7-531A91BDD45F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF90E1EF-00EA-48BD-8ED7-531A91BDD45F}.Release|Any CPU.Build.0 = Release|Any CPU + {04FF203F-12B0-40F2-BF3C-B83EA01F140B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04FF203F-12B0-40F2-BF3C-B83EA01F140B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04FF203F-12B0-40F2-BF3C-B83EA01F140B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04FF203F-12B0-40F2-BF3C-B83EA01F140B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Chipsoft.Assignments.EPDConsole/Chipsoft.Assignments.EPDConsole.csproj b/Chipsoft.Assignments.EPDConsole/Chipsoft.Assignments.EPDConsole.csproj index c7f7512..f348df4 100644 --- a/Chipsoft.Assignments.EPDConsole/Chipsoft.Assignments.EPDConsole.csproj +++ b/Chipsoft.Assignments.EPDConsole/Chipsoft.Assignments.EPDConsole.csproj @@ -2,13 +2,18 @@ Exe - net6.0 + net8.0 enable enable - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + @@ -17,4 +22,8 @@ + + + + diff --git a/Chipsoft.Assignments.EPDConsole/EPDDbContext.cs b/Chipsoft.Assignments.EPDConsole/EPDDbContext.cs deleted file mode 100644 index ec44f0c..0000000 --- a/Chipsoft.Assignments.EPDConsole/EPDDbContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Chipsoft.Assignments.EPDConsole -{ - public class EPDDbContext : DbContext - { - // The following configures EF to create a Sqlite database file in the - protected override void OnConfiguring(DbContextOptionsBuilder options) - => options.UseSqlite($"Data Source=epd.db"); - public DbSet Patients { get; set; } - } -} diff --git a/Chipsoft.Assignments.EPDConsole/Program.cs b/Chipsoft.Assignments.EPDConsole/Program.cs index 0f69d9d..5dd49c5 100644 --- a/Chipsoft.Assignments.EPDConsole/Program.cs +++ b/Chipsoft.Assignments.EPDConsole/Program.cs @@ -1,47 +1,53 @@ -namespace Chipsoft.Assignments.EPDConsole -{ - public class Program - { - //Don't create EF migrations, use the reset db option - //This deletes and recreates the db, this makes sure all tables exist +using Chipsoft.Assignments.EPDConsole.Application; +using Chipsoft.Assignments.EPDConsole.Infrastructure; +using Chipsoft.Assignments.EPDConsole.Infrastructure.Persistence; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using ValidationException = Chipsoft.Assignments.EPDConsole.Application.Common.Exceptions.ValidationException; - private static void AddPatient() - { - //Do action - //return to show menu again. - } +namespace Chipsoft.Assignments.EPDConsole; - private static void ShowAppointment() - { - } +public class Program +{ + private static IServiceProvider _serviceProvider; + private static IMediator _mediator; - private static void AddAppointment() - { - } + static async Task Main(string[] args) + { + RegisterServices(); + _mediator = _serviceProvider.GetRequiredService(); - private static void DeletePhysician() - { - } + await ShowMenu(); + + DisposeServices(); + } - private static void AddPhysician() - { - } + private static void RegisterServices() + { + var services = new ServiceCollection(); - private static void DeletePatient() + services.AddApplicationServices(); + services.AddInfrastructureServices(); + + _serviceProvider = services.BuildServiceProvider(); + } + + private static void DisposeServices() + { + if (_serviceProvider == null) { + return; } - - - #region FreeCodeForAssignment - static void Main(string[] args) + if (_serviceProvider is IDisposable) { - while (ShowMenu()) - { - //Continue - } + ((IDisposable)_serviceProvider).Dispose(); } + } - public static bool ShowMenu() + public static async Task ShowMenu() + { + bool continueRunning = true; + while (continueRunning) { Console.Clear(); foreach (var line in File.ReadAllLines("logo.txt")) @@ -63,37 +69,406 @@ public static bool ShowMenu() switch (option) { case 1: - AddPatient(); - return true; + await AddPatient(); + break; case 2: - DeletePatient(); - return true; + await DeletePatient(); + break; case 3: - AddPhysician(); - return true; + await AddPhysician(); + break; case 4: - DeletePhysician(); - return true; + await DeletePhysician(); + break; case 5: - AddAppointment(); - return true; + await AddAppointment(); + break; case 6: - ShowAppointment(); - return true; + await ShowAppointments(); + break; case 7: - return false; + continueRunning = false; + break; case 8: - EPDDbContext dbContext = new EPDDbContext(); - dbContext.Database.EnsureDeleted(); - dbContext.Database.EnsureCreated(); - return true; - default: - return true; + ResetDatabase(); + break; } } - return true; + } + } + + private static async Task AddPatient() + { + var command = new Application.Patients.Commands.AddPatientCommand(); + + Console.WriteLine("--- Nieuwe patiënt toevoegen ---"); + Console.Write("Voornaam: "); + command.FirstName = Console.ReadLine(); + Console.Write("Achternaam: "); + command.LastName = Console.ReadLine(); + Console.Write("BSN: "); + command.BSN = Console.ReadLine(); + Console.Write("Adres: "); + command.Address = Console.ReadLine(); + Console.Write("Telefoonnummer: "); + command.PhoneNumber = Console.ReadLine(); + Console.Write("E-mail: "); + command.Email = Console.ReadLine(); + + while (true) + { + Console.Write("Geboortedatum (dd-mm-jjjj): "); + if (DateTime.TryParseExact(Console.ReadLine(), "dd-MM-yyyy", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime dob)) + { + command.DateOfBirth = dob; + break; + } + Console.WriteLine("Ongeldige datum, probeer opnieuw."); } - #endregion + try + { + var patientId = await _mediator.Send(command); + Console.WriteLine($"Patiënt succesvol toegevoegd met ID: {patientId}."); + } + catch (ValidationException ex) + { + Console.WriteLine("\nValidatie mislukt:"); + foreach (var error in ex.Errors) + { + Console.WriteLine($"- {error.Key}: {string.Join(", ", error.Value)}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"\nEr is een onverwachte fout opgetreden: {ex.Message}"); + } + + WaitForKeyPress(); + } + + private static async Task DeletePatient() + { + Console.WriteLine("--- Patiënt verwijderen ---"); + + var patients = await _mediator.Send(new Application.Patients.Queries.GetPatientsListQuery()); + if (!patients.Any()) + { + Console.WriteLine("Er zijn geen patiënten om te verwijderen."); + WaitForKeyPress(); + return; + } + + Console.WriteLine("Huidige patiënten:"); + foreach (var p in patients) + { + Console.WriteLine($"ID: {p.Id}, Naam: {p.Name}, BSN: {p.BSN}"); + } + + Console.Write("\nVoer het ID in van de patiënt die u wilt verwijderen: "); + if (int.TryParse(Console.ReadLine(), out int id)) + { + try + { + var command = new Application.Patients.Commands.DeletePatientCommand { Id = id }; + await _mediator.Send(command); + Console.WriteLine("Patiënt succesvol verwijderd."); + } + catch (Exception ex) + { + Console.WriteLine($"Fout bij verwijderen: {ex.Message}"); + } + } + else + { + Console.WriteLine("Ongeldige invoer."); + } + WaitForKeyPress(); + } + + private static async Task AddPhysician() + { + Console.WriteLine("--- Nieuwe arts toevoegen ---"); + Console.Write("Voornaam: "); + var firstName = Console.ReadLine(); + + Console.Write("Achternaam: "); + var lastName = Console.ReadLine(); + + try + { + var command = new Application.Physicians.Commands.AddPhysicianCommand + { + FirstName = firstName, + LastName = lastName + }; + var physicianId = await _mediator.Send(command); + Console.WriteLine($"Arts succesvol toegevoegd met ID: {physicianId}."); + } + catch (ValidationException ex) + { + Console.WriteLine("\nValidatie mislukt:"); + foreach (var error in ex.Errors) + { + Console.WriteLine($"- {error.Key}: {string.Join(", ", error.Value)}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"\nEr is een onverwachte fout opgetreden: {ex.Message}"); + } + + WaitForKeyPress(); + } + + private static async Task DeletePhysician() + { + Console.WriteLine("--- Arts verwijderen ---"); + + var physicians = await _mediator.Send(new Application.Physicians.Queries.GetPhysiciansListQuery()); + if (!physicians.Any()) + { + Console.WriteLine("Er zijn geen artsen om te verwijderen."); + WaitForKeyPress(); + return; + } + + Console.WriteLine("Huidige artsen:"); + foreach (var p in physicians) + { + Console.WriteLine($"ID: {p.Id}, Naam: {p.Name}"); + } + + Console.Write("\nVoer het ID in van de arts die u wilt verwijderen: "); + if (int.TryParse(Console.ReadLine(), out int id)) + { + try + { + var command = new Application.Physicians.Commands.DeletePhysicianCommand { Id = id }; + await _mediator.Send(command); + Console.WriteLine("Arts succesvol verwijderd."); + } + catch (Exception ex) + { + Console.WriteLine($"Fout bij verwijderen: {ex.Message}"); + } + } + else + { + Console.WriteLine("Ongeldige invoer."); + } + WaitForKeyPress(); + } + + private static async Task AddAppointment() + { + Console.WriteLine("--- Nieuwe afspraak toevoegen ---"); + + var patients = await _mediator.Send(new Application.Patients.Queries.GetPatientsListQuery()); + if (!patients.Any()) + { + Console.WriteLine("Er zijn geen patiënten beschikbaar. Voeg eerst een patiënt toe."); + WaitForKeyPress(); + return; + } + + var physicians = await _mediator.Send(new Application.Physicians.Queries.GetPhysiciansListQuery()); + if (!physicians.Any()) + { + Console.WriteLine("Er zijn geen artsen beschikbaar. Voeg eerst een arts toe."); + WaitForKeyPress(); + return; + } + + Console.WriteLine("\nBeschikbare Patiënten:"); + foreach (var p in patients) + { + Console.WriteLine($"ID: {p.Id}, Naam: {p.Name}"); + } + Console.Write("Kies een patiënt ID: "); + if (!int.TryParse(Console.ReadLine(), out int patientId) || !patients.Any(p => p.Id == patientId)) + { + Console.WriteLine("Ongeldig patiënt ID."); + WaitForKeyPress(); + return; + } + + Console.WriteLine("\nBeschikbare Artsen:"); + foreach (var p in physicians) + { + Console.WriteLine($"ID: {p.Id}, Naam: {p.Name}"); + } + Console.Write("Kies een arts ID: "); + if (!int.TryParse(Console.ReadLine(), out int physicianId) || !physicians.Any(p => p.Id == physicianId)) + { + Console.WriteLine("Ongeldig arts ID."); + WaitForKeyPress(); + return; + } + + DateTime appointmentDateTime; + while (true) + { + Console.Write("Voer de datum en tijd in (dd-mm-jjjj uu:mm): "); + if (DateTime.TryParseExact(Console.ReadLine(), "dd-MM-yyyy HH:mm", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out appointmentDateTime)) + { + break; + } + Console.WriteLine("Ongeldige datum/tijd, probeer opnieuw."); + } + + try + { + var command = new Application.Appointments.Commands.AddAppointmentCommand + { + PatientId = patientId, + PhysicianId = physicianId, + AppointmentDateTime = appointmentDateTime + }; + + var appointmentId = await _mediator.Send(command); + Console.WriteLine($"Afspraak succesvol toegevoegd met ID: {appointmentId}."); + } + catch (ValidationException ex) + { + Console.WriteLine("\nValidatie mislukt:"); + foreach (var error in ex.Errors) + { + Console.WriteLine($"- {error.Key}: {string.Join(", ", error.Value)}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"\nEr is een onverwachte fout opgetreden: {ex.Message}"); + } + WaitForKeyPress(); + } + + private static async Task ShowAppointments() + { + Console.Clear(); + Console.WriteLine("--- Afspraken Inzien ---"); + Console.WriteLine("1 - Toon alle afspraken"); + Console.WriteLine("2 - Filter op patiënt"); + Console.WriteLine("3 - Filter op arts"); + Console.Write("Kies een optie: "); + + if (int.TryParse(Console.ReadLine(), out int option)) + { + switch (option) + { + case 1: + var allAppointments = await _mediator.Send(new Application.Appointments.Queries.GetAppointmentsListQuery()); + await DisplayAppointments(allAppointments); + break; + case 2: + await ShowAppointmentsByPatient(); + break; + case 3: + await ShowAppointmentsByPhysician(); + break; + default: + Console.WriteLine("Ongeldige optie."); + break; + } + } + else + { + Console.WriteLine("Ongeldige invoer."); + } + + WaitForKeyPress(); + } + + private static async Task ShowAppointmentsByPatient() + { + var patients = await _mediator.Send(new Application.Patients.Queries.GetPatientsListQuery()); + if (!patients.Any()) + { + Console.WriteLine("\nEr zijn geen patiënten om op te filteren."); + return; + } + + Console.WriteLine("\nKies een patiënt:"); + foreach (var p in patients) + { + Console.WriteLine($"ID: {p.Id}, Naam: {p.Name}"); + } + + Console.Write("Voer patiënt ID in: "); + if (int.TryParse(Console.ReadLine(), out int patientId) && patients.Any(p => p.Id == patientId)) + { + var query = new Application.Appointments.Queries.GetAppointmentsByPatientQuery { PatientId = patientId }; + var appointments = await _mediator.Send(query); + await DisplayAppointments(appointments); + } + else + { + Console.WriteLine("Ongeldig patiënt ID."); + } + } + + private static async Task ShowAppointmentsByPhysician() + { + var physicians = await _mediator.Send(new Application.Physicians.Queries.GetPhysiciansListQuery()); + if (!physicians.Any()) + { + Console.WriteLine("\nEr zijn geen artsen om op te filteren."); + return; + } + + Console.WriteLine("\nKies een arts:"); + foreach (var p in physicians) + { + Console.WriteLine($"ID: {p.Id}, Naam: {p.Name}"); + } + + Console.Write("Voer arts ID in: "); + if (int.TryParse(Console.ReadLine(), out int physicianId) && physicians.Any(p => p.Id == physicianId)) + { + var query = new Application.Appointments.Queries.GetAppointmentsByPhysicianQuery { PhysicianId = physicianId }; + var appointments = await _mediator.Send(query); + await DisplayAppointments(appointments); + } + else + { + Console.WriteLine("Ongeldig arts ID."); + } + } + + private static Task DisplayAppointments(List appointments) + { + Console.WriteLine("\n--- Overzicht afspraken ---"); + if (!appointments.Any()) + { + Console.WriteLine("Er zijn geen afspraken gevonden voor deze selectie."); + } + else + { + foreach (var app in appointments) + { + Console.WriteLine($"Datum/Tijd: {app.AppointmentDateTime:dd-MM-yyyy HH:mm}"); + Console.WriteLine($" Patiënt: {app.PatientName}"); + Console.WriteLine($" Arts: {app.PhysicianName}"); + Console.WriteLine(new string('-', 20)); + } + } + return Task.CompletedTask; + } + + private static void ResetDatabase() + { + using var scope = _serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.EnsureDeleted(); + dbContext.Database.EnsureCreated(); + Console.WriteLine("Database is gereset."); + WaitForKeyPress(); + } + + private static void WaitForKeyPress() + { + Console.WriteLine("\nDruk op een toets om terug te keren naar het menu..."); + Console.ReadKey(); } } \ No newline at end of file diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..3350421 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,36 @@ +# Designkeuzes EPDConsole + +## Clean Architecture +Het project is opgezet volgens het Clean Architecture-principe. Dit zorgt voor een duidelijke scheiding tussen domein, infrastructuur, applicatielogica en presentatie. Hierdoor is de code onderhoudbaar, testbaar en eenvoudig uitbreidbaar. + +- **Core**: bevat domein-entiteiten en interfaces. +- **Infrastructure**: bevat de database-implementatie (Entity Framework). +- **Application**: bevat businesslogica, CQRS-handlers en validatie. +- **EPDConsole**: bevat de gebruikersinterface (console). + +## CQRS & MediatR +Voor alle mutaties en queries wordt het CQRS-patroon toegepast, ondersteund door MediatR. Commands en Queries worden afgehandeld door aparte handlers. Dit maakt de logica overzichtelijk en testbaar. + +## Validatie met FluentValidation +Alle input wordt gevalideerd met FluentValidation. Validatieregels zijn per command gescheiden en worden automatisch uitgevoerd via een MediatR pipeline. Dit voorkomt duplicatie en zorgt voor consistente foutafhandeling. + +## Dependency Injection +Alle afhankelijkheden worden via Dependency Injection aangeboden. Dit maakt het mogelijk om eenvoudig te testen met mocks en zorgt voor een flexibele, uitbreidbare architectuur. + +## SOLID-principes +De code volgt de SOLID-principes: +- **Single Responsibility**: elke klasse heeft één duidelijke verantwoordelijkheid. +- **Open/Closed**: logica is uitbreidbaar via nieuwe handlers/validators zonder bestaande code te wijzigen. +- **Liskov Substitution**: alle implementaties van interfaces (zoals IApplicationDbContext) zijn uitwisselbaar en gedragen zich zoals verwacht, zowel in productie als in tests. +- **Interface Segregation**: interfaces zijn klein en doelgericht gehouden; er zijn geen onnodig brede interfaces. Voor deze schaal is IApplicationDbContext overzichtelijk en functioneel. +- **Dependency Inversion**: afhankelijkheden worden via interfaces aangeboden. + +## Testen +De Application-laag is volledig afgedekt met unittests (xUnit, Moq, FluentAssertions). Hierdoor is regressie snel zichtbaar en blijft de code betrouwbaar bij refactoring. + +## .NET 8 & Moderne Pakketten +Het project is geüpdatet naar .NET 8 en maakt gebruik van moderne, goed ondersteunde pakketten zoals MediatR, FluentValidation en Moq. + +--- + +Deze keuzes zorgen samen voor een toekomstbestendige, onderhoudbare en goed geteste applicatie. \ No newline at end of file