diff --git a/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml new file mode 100644 index 000000000..1f55f21a3 --- /dev/null +++ b/.github/workflows/dotnet_tests.yml @@ -0,0 +1,28 @@ +name: .NET Tests + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore Polyclinic/Polyclinic.sln + + - name: Build + run: dotnet build Polyclinic/Polyclinic.sln --no-restore --configuration Release + + - name: Test + run: dotnet test Polyclinic/Polyclinic.sln --no-build --configuration Release --verbosity normal \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs b/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs new file mode 100644 index 000000000..627d57291 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs @@ -0,0 +1,47 @@ +namespace Polyclinic.Domain.Entities; + +/// +/// Запись пациента на прием к врачу +/// +public class Appointment +{ + /// + /// Уникальный идентификатор записи + /// + public int Id { get; set; } + + /// + /// Дата и время приема + /// + public DateTime AppointmentDateTime { get; set; } + + /// + /// Номер кабинета + /// + public required string RoomNumber { get; set; } + + /// + /// Флаг повторного приема + /// + public bool IsRepeat { get; set; } + + /// + /// Идентификатор пациента + /// + public int PatientId { get; set; } + + /// + /// Идентификатор врача + /// + public int DoctorId { get; set; } + + /// + /// Навигационное свойство: пациент + /// + public Patient? Patient { get; set; } + + /// + /// Навигационное свойство: врач + /// + public Doctor? Doctor { get; set; } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs b/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs new file mode 100644 index 000000000..94c610777 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs @@ -0,0 +1,57 @@ +namespace Polyclinic.Domain.Entities; + +/// +/// Врач поликлиники +/// +public class Doctor +{ + /// + /// Уникальный идентификатор врача + /// + public int Id { get; set; } + + /// + /// Номер паспорта (уникальный) + /// + public required string PassportNumber { get; set; } + + /// + /// ФИО врача + /// + public required string FullName { get; set; } + + /// + /// Дата рождения + /// + public DateTime BirthDate { get; set; } + + /// + /// Идентификатор специализации + /// + public int SpecializationId { get; set; } + + /// + /// Стаж работы (в годах) + /// + public int ExperienceYears { get; set; } + + /// + /// Навигационное свойство: специализация + /// + public Specialization? Specialization { get; set; } + + /// + /// Список приемов у этого врача + /// + public List Appointments { get; set; } = []; + + /// + /// Вычисление возраста врача на указанную дату + /// + public int GetAge(DateTime onDate) + { + var age = onDate.Year - BirthDate.Year; + if (BirthDate.Date > onDate.AddYears(-age)) age--; + return age; + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Domain/Entities/Patient.cs b/Polyclinic/Polyclinic.Domain/Entities/Patient.cs new file mode 100644 index 000000000..aab8ee069 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Entities/Patient.cs @@ -0,0 +1,69 @@ +using Polyclinic.Domain.Enums; + +namespace Polyclinic.Domain.Entities; + +/// +/// Пациент поликлиники +/// +public class Patient +{ + /// + /// Уникальный идентификатор пациента + /// + public int Id { get; set; } + + /// + /// Номер паспорта (уникальный) + /// + public required string PassportNumber { get; set; } + + /// + /// ФИО пациента + /// + public required string FullName { get; set; } + + /// + /// Пол пациента + /// + public Gender Gender { get; set; } + + /// + /// Дата рождения + /// + public DateTime BirthDate { get; set; } + + /// + /// Адрес проживания + /// + public required string Address { get; set; } + + /// + /// Группа крови + /// + public BloodGroup BloodGroup { get; set; } + + /// + /// Резус-фактор + /// + public RhFactor RhFactor { get; set; } + + /// + /// Контактный телефон + /// + public required string PhoneNumber { get; set; } + + /// + /// Список записей на прием этого пациента + /// + public List Appointments { get; set; } = []; + + /// + /// Вычисление возраста пациента на указанную дату + /// + public int GetAge(DateTime onDate) + { + var age = onDate.Year - BirthDate.Year; + if (BirthDate.Date > onDate.AddYears(-age)) age--; + return age; + } +} diff --git a/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs b/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs new file mode 100644 index 000000000..6f728be3f --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs @@ -0,0 +1,32 @@ +namespace Polyclinic.Domain.Entities; + +/// +/// Специализация врача (справочник) +/// +public class Specialization +{ + /// + /// Уникальный идентификатор специализации + /// + public int Id { get; set; } + + /// + /// Название специализации + /// + public required string Name { get; set; } + + /// + /// Описание специализации + /// + public required string Description { get; set; } + + /// + /// Код специализации + /// + public required string Code { get; set; } + + /// + /// Список врачей с этой специализацией + /// + public List Doctors { get; set; } = []; +} diff --git a/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs b/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs new file mode 100644 index 000000000..f9a6f0722 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs @@ -0,0 +1,27 @@ +namespace Polyclinic.Domain.Enums; + +/// +/// Группа крови пациента +/// +public enum BloodGroup +{ + /// + /// Первая (0) + /// + O, + + /// + /// Вторая (A) + /// + A, + + /// + /// Третья (B) + /// + B, + + /// + /// Четвертая (AB) + /// + Ab +} diff --git a/Polyclinic/Polyclinic.Domain/Enums/Gender.cs b/Polyclinic/Polyclinic.Domain/Enums/Gender.cs new file mode 100644 index 000000000..2f909a5c4 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Enums/Gender.cs @@ -0,0 +1,22 @@ +namespace Polyclinic.Domain.Enums; + +/// +/// Пол пациента +/// +public enum Gender +{ + /// + /// Не указано + /// + NotSet, + + /// + /// Мужской + /// + Male, + + /// + /// Женский + /// + Female +} diff --git a/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs b/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs new file mode 100644 index 000000000..0766c45e3 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs @@ -0,0 +1,17 @@ +namespace Polyclinic.Domain.Enums; + +/// +/// Резус-фактор пациента +/// +public enum RhFactor +{ + /// + /// Положительный + /// + Positive, + + /// + /// Отрицательный + /// + Negative +} diff --git a/Polyclinic/Polyclinic.Domain/Polyclinic.Domain.csproj b/Polyclinic/Polyclinic.Domain/Polyclinic.Domain.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Polyclinic.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Polyclinic/Polyclinic.Tests/Polyclinic.Tests.csproj b/Polyclinic/Polyclinic.Tests/Polyclinic.Tests.csproj new file mode 100644 index 000000000..bb1e0508d --- /dev/null +++ b/Polyclinic/Polyclinic.Tests/Polyclinic.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs b/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs new file mode 100644 index 000000000..0dd92b41b --- /dev/null +++ b/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs @@ -0,0 +1,422 @@ +using Polyclinic.Domain.Entities; +using Polyclinic.Domain.Enums; + +namespace Polyclinic.Tests; + +/// +/// Фикстура с тестовыми данными для поликлиники +/// +public class PolyclinicFixture +{ + public List Specializations { get; } + public List Doctors { get; } + public List Patients { get; } + public List Appointments { get; } + + public PolyclinicFixture() + { + Specializations = GetSpecializations(); + Doctors = GetDoctors(); + Patients = GetPatients(); + Appointments = GetAppointments(); + + LinkDoctorsWithSpecializations(); + LinkAppointmentsWithDoctorsAndPatients(); + } + + private static List GetSpecializations() => + [ + new() { Id = 1, Name = "Терапевт", Code = "THERAPIST", Description = "Врач общей практики" }, + new() { Id = 2, Name = "Хирург", Code = "SURGEON", Description = "Проведение операций" }, + new() { Id = 3, Name = "Кардиолог", Code = "CARDIOLOGIST", Description = "Заболевания сердца" }, + new() { Id = 4, Name = "Невролог", Code = "NEUROLOGIST", Description = "Заболевания нервной системы" }, + new() { Id = 5, Name = "Педиатр", Code = "PEDIATRICIAN", Description = "Детские болезни" }, + new() { Id = 6, Name = "Гинеколог", Code = "GYNECOLOGIST", Description = "Женское здоровье" }, + new() { Id = 7, Name = "Офтальмолог", Code = "OPHTHALMOLOGIST", Description = "Заболевания глаз" }, + new() { Id = 8, Name = "Отоларинголог", Code = "ENT", Description = "Ухо, горло, нос" }, + new() { Id = 9, Name = "Дерматолог", Code = "DERMATOLOGIST", Description = "Кожные заболевания" }, + new() { Id = 10, Name = "Эндокринолог", Code = "ENDOCRINOLOGIST", Description = "Гормональные нарушения" } + ]; + + private static List GetDoctors() => + [ + new() + { + Id = 1, + PassportNumber = "4501 123456", + FullName = "Иванов Иван Иванович", + BirthDate = new DateTime(1980, 5, 15), + SpecializationId = 1, + ExperienceYears = 15 + }, + new() + { + Id = 2, + PassportNumber = "4502 234567", + FullName = "Петров Петр Петрович", + BirthDate = new DateTime(1975, 8, 22), + SpecializationId = 2, + ExperienceYears = 20 + }, + new() + { + Id = 3, + PassportNumber = "4503 345678", + FullName = "Сидорова Анна Сергеевна", + BirthDate = new DateTime(1985, 3, 10), + SpecializationId = 3, + ExperienceYears = 12 + }, + new() + { + Id = 4, + PassportNumber = "4504 456789", + FullName = "Козлов Дмитрий Николаевич", + BirthDate = new DateTime(1990, 11, 30), + SpecializationId = 4, + ExperienceYears = 8 + }, + new() + { + Id = 5, + PassportNumber = "4505 567890", + FullName = "Морозова Елена Владимировна", + BirthDate = new DateTime(1982, 7, 18), + SpecializationId = 5, + ExperienceYears = 14 + }, + new() + { + Id = 6, + PassportNumber = "4506 678901", + FullName = "Волков Андрей Игоревич", + BirthDate = new DateTime(1978, 9, 25), + SpecializationId = 6, + ExperienceYears = 18 + }, + new() + { + Id = 7, + PassportNumber = "4507 789012", + FullName = "Соколова Татьяна Александровна", + BirthDate = new DateTime(1988, 2, 14), + SpecializationId = 7, + ExperienceYears = 10 + }, + new() + { + Id = 8, + PassportNumber = "4508 890123", + FullName = "Лебедев Михаил Сергеевич", + BirthDate = new DateTime(1992, 6, 5), + SpecializationId = 8, + ExperienceYears = 6 + }, + new() + { + Id = 9, + PassportNumber = "4509 901234", + FullName = "Николаева Ольга Викторовна", + BirthDate = new DateTime(1983, 12, 3), + SpecializationId = 9, + ExperienceYears = 13 + }, + new() + { + Id = 10, + PassportNumber = "4510 012345", + FullName = "Федоров Алексей Павлович", + BirthDate = new DateTime(1970, 4, 20), + SpecializationId = 10, + ExperienceYears = 25 + } + ]; + + private static List GetPatients() => + [ + new() + { + Id = 1, + PassportNumber = "6001 123456", + FullName = "Смирнов Алексей Викторович", + Gender = Gender.Male, + BirthDate = new DateTime(1990, 5, 15), + Address = "ул. Ленина, д. 10, кв. 25", + BloodGroup = BloodGroup.A, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 123-45-67" + }, + new() + { + Id = 2, + PassportNumber = "6002 234567", + FullName = "Кузнецова Елена Дмитриевна", + Gender = Gender.Female, + BirthDate = new DateTime(1985, 8, 22), + Address = "ул. Гагарина, д. 5, кв. 12", + BloodGroup = BloodGroup.O, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 234-56-78" + }, + new() + { + Id = 3, + PassportNumber = "6003 345678", + FullName = "Попов Сергей Иванович", + Gender = Gender.Male, + BirthDate = new DateTime(1978, 3, 10), + Address = "пр. Мира, д. 15, кв. 7", + BloodGroup = BloodGroup.B, + RhFactor = RhFactor.Negative, + PhoneNumber = "+7 (999) 345-67-89" + }, + new() + { + Id = 4, + PassportNumber = "6004 456789", + FullName = "Васильева Мария Петровна", + Gender = Gender.Female, + BirthDate = new DateTime(1995, 11, 30), + Address = "ул. Советская, д. 8, кв. 42", + BloodGroup = BloodGroup.Ab, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 456-78-90" + }, + new() + { + Id = 5, + PassportNumber = "6005 567890", + FullName = "Соколов Андрей Николаевич", + Gender = Gender.Male, + BirthDate = new DateTime(1982, 7, 18), + Address = "ул. Пушкина, д. 3, кв. 56", + BloodGroup = BloodGroup.A, + RhFactor = RhFactor.Negative, + PhoneNumber = "+7 (999) 567-89-01" + }, + new() + { + Id = 6, + PassportNumber = "6006 678901", + FullName = "Михайлова Анна Сергеевна", + Gender = Gender.Female, + BirthDate = new DateTime(1975, 9, 25), + Address = "пр. Ленинградский, д. 22, кв. 15", + BloodGroup = BloodGroup.O, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 678-90-12" + }, + new() + { + Id = 7, + PassportNumber = "6007 789012", + FullName = "Новиков Денис Александрович", + Gender = Gender.Male, + BirthDate = new DateTime(1988, 2, 14), + Address = "ул. Кирова, д. 12, кв. 8", + BloodGroup = BloodGroup.B, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 789-01-23" + }, + new() + { + Id = 8, + PassportNumber = "6008 890123", + FullName = "Морозова Татьяна Владимировна", + Gender = Gender.Female, + BirthDate = new DateTime(1992, 6, 5), + Address = "ул. Садовая, д. 7, кв. 31", + BloodGroup = BloodGroup.Ab, + RhFactor = RhFactor.Negative, + PhoneNumber = "+7 (999) 890-12-34" + }, + new() + { + Id = 9, + PassportNumber = "6009 901234", + FullName = "Зайцев Игорь Павлович", + Gender = Gender.Male, + BirthDate = new DateTime(1970, 12, 3), + Address = "пр. Невский, д. 45, кв. 19", + BloodGroup = BloodGroup.A, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 901-23-45" + }, + new() + { + Id = 10, + PassportNumber = "6010 012345", + FullName = "Волкова Ольга Игоревна", + Gender = Gender.Female, + BirthDate = new DateTime(1965, 4, 20), + Address = "ул. Комсомольская, д. 6, кв. 23", + BloodGroup = BloodGroup.O, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 012-34-56" + }, + new() + { + Id = 11, + PassportNumber = "6011 123456", + FullName = "Белова Наталья Сергеевна", + Gender = Gender.Female, + BirthDate = new DateTime(1998, 1, 8), + Address = "ул. Мичурина, д. 18, кв. 67", + BloodGroup = BloodGroup.B, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 123-56-78" + }, + new() + { + Id = 12, + PassportNumber = "6012 234567", + FullName = "Карпов Евгений Владимирович", + Gender = Gender.Male, + BirthDate = new DateTime(1983, 9, 12), + Address = "ул. Лермонтова, д. 9, кв. 14", + BloodGroup = BloodGroup.Ab, + RhFactor = RhFactor.Negative, + PhoneNumber = "+7 (999) 234-67-89" + } + ]; + + private static List GetAppointments() + { + var appointments = new List(); + var appointmentId = 1; + + // Записи на текущий месяц (февраль 2026) + appointments.AddRange([ + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 5, 10, 0, 0), + RoomNumber = "101", + IsRepeat = false, + PatientId = 1, + DoctorId = 1 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 5, 11, 0, 0), + RoomNumber = "101", + IsRepeat = true, + PatientId = 2, + DoctorId = 1 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 10, 14, 30, 0), + RoomNumber = "202", + IsRepeat = false, + PatientId = 3, + DoctorId = 2 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 15, 9, 15, 0), + RoomNumber = "303", + IsRepeat = false, + PatientId = 4, + DoctorId = 3 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 12, 13, 30, 0), + RoomNumber = "101", + IsRepeat = false, + PatientId = 8, + DoctorId = 1 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 16, 9, 30, 0), + RoomNumber = "101", + IsRepeat = false, + PatientId = 11, + DoctorId = 1 + } + ]); + + // Записи на прошлый месяц (январь 2026) + appointments.AddRange([ + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 1, 15, 10, 0, 0), + RoomNumber = "101", + IsRepeat = true, + PatientId = 6, + DoctorId = 2 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 1, 20, 11, 0, 0), + RoomNumber = "202", + IsRepeat = true, + PatientId = 7, + DoctorId = 2 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 1, 5, 9, 0, 0), + RoomNumber = "303", + IsRepeat = false, + PatientId = 8, + DoctorId = 3 + } + ]); + + // Пациент Соколов (Id=5) записан к нескольким врачам + appointments.AddRange([ + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 8, 9, 0, 0), + RoomNumber = "102", + IsRepeat = false, + PatientId = 5, + DoctorId = 2 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 22, 11, 30, 0), + RoomNumber = "606", + IsRepeat = false, + PatientId = 5, + DoctorId = 6 + } + ]); + + return appointments; + } + + private void LinkDoctorsWithSpecializations() + { + foreach (var doctor in Doctors) + { + doctor.Specialization = Specializations.First(s => s.Id == doctor.SpecializationId); + doctor.Specialization.Doctors.Add(doctor); + } + } + + private void LinkAppointmentsWithDoctorsAndPatients() + { + foreach (var appointment in Appointments) + { + appointment.Doctor = Doctors.First(d => d.Id == appointment.DoctorId); + appointment.Patient = Patients.First(p => p.Id == appointment.PatientId); + + appointment.Doctor.Appointments.Add(appointment); + appointment.Patient.Appointments.Add(appointment); + } + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs b/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs new file mode 100644 index 000000000..d9c3a412e --- /dev/null +++ b/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs @@ -0,0 +1,173 @@ +namespace Polyclinic.Tests; + +/// +/// Тесты для поликлиники с использованием фикстуры +/// +public class PolyclinicTests(PolyclinicFixture fixture) : IClassFixture +{ + /// + /// ТЕСТ 1: Вывести информацию о всех врачах, стаж работы которых не менее 10 лет + /// + [Fact] + public void GetDoctorsWithExperienceMoreThan10Years() + { + var actual = ( + from doctor in fixture.Doctors + where doctor.ExperienceYears >= TestConstants.MinExperienceYears + orderby doctor.FullName + select doctor + ).ToList(); + + Assert.NotEmpty(actual); + Assert.All(actual, doctor => + Assert.True(doctor.ExperienceYears >= TestConstants.MinExperienceYears)); + + var excludedDoctors = fixture.Doctors.Except(actual); + Assert.All(excludedDoctors, doctor => + Assert.True(doctor.ExperienceYears < TestConstants.MinExperienceYears)); + + Assert.Equal([.. actual.OrderBy(d => d.FullName)], actual); + } + + /// + /// ТЕСТ 2: Вывести информацию о всех пациентах, записанных к указанному врачу, упорядочить по ФИО + /// + [Fact] + public void GetPatientsByDoctorOrderedByFullName() + { + var testDoctor = fixture.Doctors.First(d => fixture.Appointments.Any(a => a.DoctorId == d.Id)); + + var actual = ( + from appointment in fixture.Appointments + where appointment.DoctorId == testDoctor.Id + join patient in fixture.Patients on appointment.PatientId equals patient.Id + orderby patient.FullName + select patient + ).Distinct().ToList(); + + Assert.NotEmpty(actual); + Assert.All(actual, patient => + { + var hasAppointmentWithDoctor = fixture.Appointments + .Any(a => a.PatientId == patient.Id && a.DoctorId == testDoctor.Id); + Assert.True(hasAppointmentWithDoctor); + }); + + Assert.Equal([.. actual.OrderBy(p => p.FullName)], actual); + } + + /// + /// ТЕСТ 3: Вывести информацию о количестве повторных приемов пациентов за последний месяц + /// + [Fact] + public void CountRepeatAppointmentsLastMonth() + { + var actual = ( + from appointment in fixture.Appointments + where appointment.AppointmentDateTime >= TestConstants.StartOfLastMonth + where appointment.AppointmentDateTime < TestConstants.StartOfCurrentMonth + where appointment.IsRepeat + select appointment + ).ToList(); + + Assert.All(actual, appointment => + { + Assert.True(appointment.IsRepeat); + Assert.True(appointment.AppointmentDateTime >= TestConstants.StartOfLastMonth); + Assert.True(appointment.AppointmentDateTime < TestConstants.StartOfCurrentMonth); + }); + + var nonRepeatInLastMonth = fixture.Appointments + .Where(a => a.AppointmentDateTime >= TestConstants.StartOfLastMonth) + .Where(a => a.AppointmentDateTime < TestConstants.StartOfCurrentMonth) + .Where(a => !a.IsRepeat); + + Assert.All(nonRepeatInLastMonth, a => Assert.False(a.IsRepeat)); + } + + /// + /// ТЕСТ 4: Вывести информацию о пациентах старше 30 лет, + /// которые записаны на прием к нескольким врачам, упорядочить по дате рождения + /// + [Fact] + public void GetPatientsOver30WithMultipleDoctorsOrderedByBirthDate() + { + var actual = ( + from patient in fixture.Patients + where patient.GetAge(TestConstants.Today) >= TestConstants.MinPatientAge + let doctorsCount = ( + from appointment in fixture.Appointments + where appointment.PatientId == patient.Id + select appointment.DoctorId + ).Distinct().Count() + where doctorsCount >= 2 + orderby patient.BirthDate + select patient + ).ToList(); + + Assert.All(actual, patient => + { + Assert.True(patient.GetAge(TestConstants.Today) >= TestConstants.MinPatientAge); + + var doctorsCount = fixture.Appointments + .Where(a => a.PatientId == patient.Id) + .Select(a => a.DoctorId) + .Distinct() + .Count(); + Assert.True(doctorsCount >= 2); + }); + + var excludedPatients = fixture.Patients.Except(actual); + Assert.All(excludedPatients, patient => + { + if (patient.GetAge(TestConstants.Today) >= TestConstants.MinPatientAge) + { + var doctorsCount = fixture.Appointments + .Where(a => a.PatientId == patient.Id) + .Select(a => a.DoctorId) + .Distinct() + .Count(); + Assert.True(doctorsCount < 2); + } + }); + + Assert.Equal([.. actual.OrderBy(p => p.BirthDate)], actual); + } + + /// + /// ТЕСТ 5: Вывести информацию о приемах за текущий месяц, проходящих в выбранном кабинете + /// + [Fact] + public void GetAppointmentsCurrentMonthInRoom() + { + var testRoom = fixture.Appointments + .First(a => a.AppointmentDateTime >= TestConstants.StartOfCurrentMonth && + a.AppointmentDateTime < TestConstants.StartOfCurrentMonth.AddMonths(1)) + .RoomNumber; + + var actual = ( + from appointment in fixture.Appointments + where appointment.RoomNumber == testRoom + where appointment.AppointmentDateTime >= TestConstants.StartOfCurrentMonth + where appointment.AppointmentDateTime < TestConstants.StartOfCurrentMonth.AddMonths(1) + orderby appointment.AppointmentDateTime + select appointment + ).ToList(); + + Assert.NotEmpty(actual); + Assert.All(actual, appointment => + { + Assert.Equal(testRoom, appointment.RoomNumber); + Assert.True(appointment.AppointmentDateTime >= TestConstants.StartOfCurrentMonth); + Assert.True(appointment.AppointmentDateTime < TestConstants.StartOfCurrentMonth.AddMonths(1)); + }); + + var otherRoomAppointments = fixture.Appointments + .Where(a => a.AppointmentDateTime >= TestConstants.StartOfCurrentMonth) + .Where(a => a.AppointmentDateTime < TestConstants.StartOfCurrentMonth.AddMonths(1)) + .Where(a => a.RoomNumber != testRoom); + + Assert.All(otherRoomAppointments, a => Assert.NotEqual(testRoom, a.RoomNumber)); + Assert.Equal([.. actual.OrderBy(a => a.AppointmentDateTime)], actual); + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Tests/TestConstants.cs b/Polyclinic/Polyclinic.Tests/TestConstants.cs new file mode 100644 index 000000000..53541a3f7 --- /dev/null +++ b/Polyclinic/Polyclinic.Tests/TestConstants.cs @@ -0,0 +1,47 @@ +namespace Polyclinic.Tests; + +/// +/// Константы для тестов +/// +public static class TestConstants +{ + /// + /// Фиксированная дата для тестов (15 февраля 2026) + /// + public static readonly DateTime Today = new(2026, 2, 15); + + /// + /// Начало текущего месяца + /// + public static readonly DateTime StartOfCurrentMonth = new(Today.Year, Today.Month, 1); + + /// + /// Начало прошлого месяца + /// + public static readonly DateTime StartOfLastMonth = StartOfCurrentMonth.AddMonths(-1); + + /// + /// Минимальный стаж для опытных врачей + /// + public const int MinExperienceYears = 10; + + /// + /// Минимальный возраст для "пациенты старше 30 лет" + /// + public const int MinPatientAge = 30; + + /// + /// Номер кабинета терапевта + /// + public const string TherapyRoom = "101"; + + /// + /// Номер кабинета хирурга + /// + public const string SurgeryRoom = "202"; + + /// + /// Номер кабинета кардиолога + /// + public const string CardiologyRoom = "303"; +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.sln b/Polyclinic/Polyclinic.sln new file mode 100644 index 000000000..28573c5ce --- /dev/null +++ b/Polyclinic/Polyclinic.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36915.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Domain", "Polyclinic.Domain\Polyclinic.Domain.csproj", "{6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Tests", "Polyclinic.Tests\Polyclinic.Tests.csproj", "{0C24BBBB-C432-4776-9968-5DAD9432AB5A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}.Release|Any CPU.Build.0 = Release|Any CPU + {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1F0773E0-B943-4E7B-A2FB-1B30E6ADA139} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 76afcbfdd..5b1298a8d 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,51 @@ -# Разработка корпоративных приложений -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1JD6aiOG6r7GrA79oJncjgUHWtfeW4g_YZ9ayNgxb_w0/edit?usp=sharing) +# Разработка корпоративных приложений. Лабораторная работа №1. -## Задание -### Цель -Реализация проекта сервисно-ориентированного приложения. +### Цель - Реализация объектной модели данных и unit-тестов -### Задачи -* Реализация объектно-ориентированной модели данных, -* Изучение реализации серверных приложений на базе WebAPI/OpenAPI, -* Изучение работы с брокерами сообщений, -* Изучение паттернов проектирования, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Unit-тестирование. +Необходимо подготовить структуру классов, описывающих предметную область. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -### Лабораторные работы -
-1. «Классы» - Реализация объектной модели данных и unit-тестов -
-В рамках первой лабораторной работы необходимо подготовить структуру классов, описывающих предметную область, определяемую в задании. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. +### Предметная область - Поликлиника -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -Необходимо включить **как минимум 10** экземпляров каждого класса в датасид. - -
-
-2. «Сервер» - Реализация серверного приложения с использованием REST API -
-Во второй лабораторной работе необходимо реализовать серверное приложение, которое должно: -- Осуществлять базовые CRUD-операции с реализованными в первой лабораторной сущностями -- Предоставлять результаты аналитических запросов (раздел «Unit-тесты» задания) +В базе данных поликлиники содержится информация о записях пациентов на прием к врачам. -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -
-
-
-3. «ORM» - Реализация объектно-реляционной модели. Подключение к базе данных и настройка оркестрации -
-В третьей лабораторной работе хранение должно быть переделано c инмемори коллекций на базу данных. -Должны быть созданы миграции для создания таблиц в бд и их первоначального заполнения. -
-Также необходимо настроить оркестратор Aspire на запуск сервера и базы данных. -
-
-
-4. «Инфраструктура» - Реализация сервиса генерации данных и его интеграция с сервером -
-В четвертой лабораторной работе необходимо имплементировать сервис, который генерировал бы контракты. Контракты далее передаются в сервер и сохраняются в бд. -Сервис должен представлять из себя отдельное приложение без референсов к серверным проектам за исключением библиотеки с контрактами. -Отправка контрактов при помощи gRPC должна выполняться в потоковом виде. -При использовании брокеров сообщений, необходимо предусмотреть ретраи при подключении к брокеру. +Пациент характеризуется: номером паспорта, ФИО, полом, датой рождения, адресом, группой крови, резус фактором и контактным телефоном. +Пол пациента является перечислением. +Группа крови пациента является перечислением. +Резус фактор пациента является перечислением. -Также необходимо добавить в конфигурацию Aspire запуск генератора и (если того требует вариант) брокера сообщений. -
-
-
-5. «Клиент» - Интеграция клиентского приложения с оркестратором -
-В пятой лабораторной необходимо добавить в конфигурацию Aspire запуск клиентского приложения для написанного ранее сервера. Клиент создается в рамках курса "Веб разработка". -
-
+Информация о враче включает номер паспорта, ФИО, год рождения, специализацию, стаж работы. Специализация врача является справочником. -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Реализация серверной части на [ASP.NET](https://dotnet.microsoft.com/ru-ru/apps/aspnet). -* Реализация unit-тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Использование хранения данных в базе данных согласно варианту задания. -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview) -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus) и его взаимодейсвие с сервером согласно варианту задания. -* Автоматизация тестирования на уровне репозитория через [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. +При записи на прием пациента в базе данных фиксируется дата и время приема, номер кабинета, а также индикатор того, является ли прием повторным. +Используется в качестве контракта. -**Факультативно**: -* Реализация авторизации/аутентификации. -* Реализация atomic batch publishing/atomic batch consumption для брокеров, поддерживающих такой функционал. -* Реализация интеграционных тестов при помощи .NET Aspire. -* Реализация клиента на Blazor WASM. +### Юнит-тесты -Внимательно прочитайте [дискуссии](https://github.com/itsecd/enterprise-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. + - Вывести информацию о всех врачах, стаж работы которых не менее 10 лет. + - Вывести информацию о всех пациентах, записанных на прием к указанному врачу, упорядочить по ФИО. + - Вывести информацию о количестве повторных приемов пациентов за последний месяц. + - Вывести информацию о пациентах старше 30 лет, которые записаны на прием к нескольким врачам, упорядочить по дате рождения. + - Вывести информацию о приемах за текущий месяц, проходящих в выбранном кабинете. -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма +### Структура проекта -image1 +Polyclinic (Solution) +│ +├── Polyclinic.Domain (Class Library) +│ ├── Entities/ +│ │ ├── Appointment.cs +│ │ ├── Doctor.cs +│ │ ├── Patient.cs +│ │ └── Specialization.cs +│ │ +│ └── Enums/ +│ ├── BloodGroup.cs +│ ├── Gender.cs +│ └── RhFactor.cs +│ +└── Polyclinic.Tests (xUnit) + ├── PolyclinicFixture.cs + ├── PolyclinicTests.cs + └── TestConstants.cs -
-## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. -[Список вариантов](https://docs.google.com/document/d/1Wc8AvsKS_1JptpsxHO-cwfAxz2ghxvQRQ0fy4el2ZOc/edit?usp=sharing) -[Список предметных областей](https://docs.google.com/document/d/15jWhXMwd2K8giFMKku_yrY_s2uQNEu4ugJXLYPvYJAE/edit?usp=sharing) -[Вопросы к экзамену](https://docs.google.com/document/d/1bjfvtzjyMljJbcu8YCvC8DzDegDUAmDeNtBz9M6FQes/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve -6. Прийти на занятие и защитить работу - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. - -### Шкала оценивания - -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл -- **3 балла** за защиту: при сдаче лабораторной работы вам задается 3 вопроса, за каждый правильный ответ - 1 балл - -У вас 2 попытки пройти ревью (первичное ревью, ревью по результатам исправления). Если замечания по итогу не исправлены, то снимается один балл за код лабораторной работы. - -## Вопросы и обратная связь по курсу - -Чтобы задать вопрос по лабораторной, воспользуйтесь [соотвествующим разделом дискуссий](https://github.com/itsecd/enterprise-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/enterprise-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/enterprise-development/discussions/categories/ideas).