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 диаграмма
+### Структура проекта
-
+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).